ssrchecker
HomeCheckFrameworksAngular

Is your Angular app server-rendered, or a client-only SPA?

Short answer
A default Angular app does not render on the server. It ships an empty <app-root> that JavaScript fills in, so crawlers see nothing on the first pass. Angular does have first-class server rendering — added with @angular/ssr — but it is opt-in, so most unconfigured apps come back client-rendered.

Angular sits between the two extremes. Plain React never renders on the server; Next.js always does by default. Angular ships client-only out of the box like React, but unlike React it has an official, well-supported server-rendering path built into the framework. So the real question for an Angular app is not "is SSR possible" — it is "did anyone turn it on".

Check your Angular app
https://

The empty app-root signature

A client-only Angular app has a distinctive fingerprint in the raw HTML. Stripped down, the body is essentially this:

<app-root></app-root> <script src="runtime.js" type="module"></script> <script src="polyfills.js" type="module"></script> <script src="main.js" type="module"></script>

That is the whole page to a crawler. The checker reports a content ratio near zero, few or no links, and the default title from index.html rather than the per-route title you set in code. The rendered screenshot shows your full application, which is why this is so hard to catch by eye — in a browser everything looks correct.

The tag name is Angular's tell. React leaves an empty <div id="root">; Angular leaves a custom <app-root> element (or whatever selector the root component declares). The signature is the emptiness either way.

Two-wave indexing is why it fails quietly

This is the mechanism specific to how search engines handle JavaScript-heavy Angular apps, and it explains why the failure is so easy to miss: nothing errors, pages just never show up.

Wave one is immediate. The crawler fetches the raw HTML and finds the empty <app-root> — one element and some script tags. No headings, no copy, no internal links to follow. There is nothing to index and, worse, nothing to tell the crawler this page is worth coming back for.

Wave two is the render queue. The URL is set aside for a headless browser to execute the JavaScript later — sometimes minutes, sometimes days, sometimes never for pages that looked empty the first time. On a large or frequently updated Angular app, that gap is where rankings quietly leak away. The tool shows you wave one directly: the raw fetch is exactly what the first pass receives.

Side-by-side comparison of an Angular page's raw HTML showing an empty app-root and the fully rendered DOM showing the complete application
Wave one on the left, wave two on the right. Crawlers act on the left first.

The hash-routing trap

This one is unique to Angular among the common frameworks, and it silently destroys the indexability of an entire site regardless of rendering.

Angular can run its router in hash mode, producing URLs like site.com/#/about and site.com/#/products. To a user these are separate pages. To a crawler they are not: everything after the # is a fragment, and fragments are not distinct URLs. Every route on the site resolves to the same base URL — site.com/ — so the crawler sees one page where you have hundreds.

How to spot it
Look at your own address bar as you navigate the site. If the URLs contain #/, you are in hash mode. The fix is switching to path-location strategy (real paths), which needs a server rewrite so deep links resolve — the same catch-all pattern SPAs use. Do this before worrying about SSR; a hash-routed site cannot be indexed per-page even if it renders perfectly.

Angular's built-in server rendering

The good news that makes Angular different from plain React: server rendering is an official, first-class feature, not a third-party bolt-on. Modern Angular ships it under @angular/ssr (this is what used to be called Angular Universal in older projects).

Adding it — via ng new --ssr on a new project or ng add @angular/ssr on an existing one — makes each route prerender to complete HTML on the server, then hydrate in the browser. That is the state that returns a green verdict here. There is also incremental hydration in recent versions, which changes when interactivity attaches but not what the crawler receives: the HTML is still complete.

The subtlety once SSR is on: data fetched in ngOnInitwithout using Angular's transfer-state mechanism can still be fetched twice — once discarded on the server, once in the browser — and in some setups the server render finishes before the data arrives, leaving that content out of the HTML. That produces a HYBRID verdict on an app you believed was fully server-rendered.

Your setup and the verdict to expect

Find your configuration. A result that differs from the expectation is the thing to investigate.

SetupExpectWhy
Default `ng new` app (no SSR flag)CSRClient-only. Ships an empty <app-root> that boots in the browser.
`ng new --ssr` / SSR added via `ng add @angular/ssr`SSRPrerenders each route to HTML on the server, then hydrates.
SSR configured with prerendering (SSG) for static routesSSRRoutes built to HTML at build time. Crawlers get everything.
Legacy Angular Universal (older projects)SSRThe predecessor of the built-in SSR. Same crawler outcome when working.
Hash routing enabled (URLs contain #/)CSREvery route collapses to one URL for crawlers. The fragment is ignored.
SSR on, but main data fetched in ngOnInit without transfer stateHYBRIDShell renders server-side; the API data can arrive only after hydration.
Behind a third-party prerendering serviceSSRCrawlers receive a cached rendered snapshot. Works, adds drift risk.
Any route whose content depends on a client-only guardCSR or HYBRIDIf a route guard resolves in the browser, its content isn't in the HTML.

Why an Angular page comes back CSR or HYBRID

In rough order of how often each turns out to be the cause:

01
SSR was never added
The single most common cause. A default `ng new` app is client-only and stays that way until someone runs `ng add @angular/ssr`. If you never configured it, the empty app-root is expected, not a bug.
02
Hash routing collapses every URL
URLs containing #/ mean the crawler sees one page for the whole site. This is independent of rendering — fix the routing strategy before anything else, or per-page indexing is impossible.
03
ngOnInit fetches without transfer state
With SSR on, content loaded in a component's init hook can be excluded from the server HTML unless Angular's transfer-state carries it across. The shell renders; the data does not.
04
A client-only route guard
If a guard resolves in the browser to decide what a route shows, that decision — and its content — happens after the HTML is captured.
05
Content injected by a third-party widget
Reviews, chat, listings or search widgets that inject from another origin. That content isn't in your HTML regardless of whether Angular renders on the server.

Angular rendering FAQ

No. A default Angular application is client-rendered: it ships an empty app-root element and boots in the browser, so crawlers receive no content on the first pass. Angular has first-class server rendering under @angular/ssr, but it is opt-in and has to be added to the project.