Reading time: 12 minutes
Angular and Vue work for SEO when each page arrives ready in the HTML, with SSR in Angular and Nuxt in Vue. Without that, however, every URL returns the same nearly empty HTML. Googlebot runs the JavaScript later, in a queue, but an AI crawler that reads only the initial HTML misses the text.
In short
- Angular renders in the browser by default. So Angular SEO starts with
ng add @angular/ssr, which turns on SSR and prerendering per route. - For Vue SEO, the most direct path is Nuxt, which renders on the server by default and accepts per-route rules.
- However, even with SSR, two things stay out of the HTML:
@deferwithout ahydratetrigger in Angular and<ClientOnly>in Nuxt. - To test Angular or Vue SEO, download the page with the GPTBot user agent and look for the text and the title. An empty root, such as
<app-root></app-root>, means the browser builds the page.
This guide follows the documentation for Angular 22.2, Vue 3.5 and Nuxt 4.6.
Are Angular and Vue without SSR bad for SEO?
For Google, not necessarily, but there is delay and risk. For a crawler that reads only the initial HTML, however, the answer is yes. That’s because, without SSR, Angular and Vue serve the same nearly empty HTML on every URL. So the text, each page’s title and the links only appear after the JavaScript runs.
According to the Angular guide to rendering strategies, CSR is the framework’s default. For SEO, the same guide rates CSR in Angular as poor, because crawlers can’t see the content until the JavaScript runs. In addition, in an Angular CLI project without SSR, every route returns the same index.html. It has an empty <app-root></app-root> tag and a title with the project name.
Similarly, the Vue documentation warns that client-only SPAs are a problem when SEO matters. That’s because the browser receives a nearly empty page. For example, in an app created with create-vue, every route returns an empty <div id="app"></div> and the same title, Vite App. The Vue SSR guide explains the limit for Google and Bing:
“As of now, Google and Bing can index synchronous JavaScript applications just fine. Synchronous being the key word there. If your app starts with a loading spinner, then fetches content via Ajax, the crawler will not wait for you to finish.”
Vue documentation, SSR guide (2026)
For AI crawlers, however, the picture is less clear. In fact, the OpenAI, Anthropic and Perplexity pages about their crawlers don’t say whether they run JavaScript. Vercel, which sells hosting, ran a 2024 study with the consultancy MERJ. In it, the tested crawlers from OpenAI, Anthropic and Perplexity did not run JavaScript. In September 2026, however, a report suggested a change in OpenAI’s crawlers, still unconfirmed. So the rule from the SSR vs CSR comparison for SEO and AI still applies: the content must be in the initial HTML.
Can you do SEO in Angular and Vue without SSR?
Yes, with prerendering, which generates the pages at build time and needs no server running the app. In fact, the Vue SSR guide points to this path, SSG, for the SEO of a few marketing pages, such as home, about and contact. In Angular, the same works with RenderMode.Prerender and outputMode: "static". On the other hand, when rewriting the front end doesn’t fit the deadline, prerendering as a service, in the Prerender.io model, buys time.

How do you turn on SSR in Angular?
In a new project, create the app with ng new --ssr. In an existing project, run ng add @angular/ssr. The command creates the server and, by default, prerenders every route at build time. Then the app.routes.server.ts file sets, route by route, whether the HTML comes from the build, from the server on each request or only from the browser.
By the way, @angular/ssr brings together the former Angular Universal, which moved into the Angular CLI in version 17, according to the Universal repository. So a tutorial that tells you to install Universal covers an older version.
ng add @angular/ssr
// src/app/app.routes.server.ts
import {RenderMode, ServerRoute} from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{path: '', renderMode: RenderMode.Prerender}, // home: HTML generated at build time
{path: 'products/:id', renderMode: RenderMode.Server}, // HTML generated on each request
{path: 'account/**', renderMode: RenderMode.Client}, // logged-in area: browser only
{path: '**', renderMode: RenderMode.Server},
];
The choice follows the Angular SEO matrix, with SSG for static content and SSR for dynamic content. CSR stays where SEO doesn’t matter, such as logged-in areas. According to the Angular SSR guide, a few details change the result:
- Routes with parameters: to prerender
products/:id, thegetPrerenderParamsfunction defines which URLs the build generates. - Hosting without Node: the
outputMode: "static"option inangular.jsongenerates the HTML for each route at build time, with no server file. - Redirects: in SSR,
redirectTobecomes an HTTP redirect. According to the code of the Angular SSR package, the default is 302, and the server route acceptsstatus: 301. In prerendering, however, it becomes a meta refresh in the HTML. - Missing page: on the error page, set the 404 status with the
RESPONSE_INITtoken, so a missing URL doesn’t return 200.
Does Nuxt solve SEO for a Vue site?
Mostly, yes. For SEO, Nuxt uses universal rendering by default. In other words, the server runs Vue and returns the full page, and the browser takes over during hydration. So text, links and meta tags arrive in the initial HTML, which any crawler reads, including AI crawlers that don’t run JavaScript.
According to the Nuxt documentation, universal rendering is the default, among other reasons, to optimize search engine indexing. With ssr: false, on the other hand, Nuxt builds the page in the browser, and the same documentation warns that indexing that content takes longer. In addition, the Vue documentation recommends a framework only when the site needs SSR and lists Nuxt, Vike, Astro and Quasar.
Nuxt also lets you choose the mode for each route, with routeRules:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // HTML generated at build time
'/blog/**': { swr: 3600 }, // 1-hour cache, rebuilt in the background
'/account/**': { ssr: false }, // logged-in area: browser only
},
})
isr works like swr, but it keeps the page on the CDN, currently only on Netlify and Vercel. For static hosting, nuxt generate prerenders the site. The Nitro crawler starts from the home page and the routes without parameters, then follows the <a href> links. So a page with parameters that no other page links to gets no HTML.
| Mode | Angular | Nuxt |
|---|---|---|
| HTML on each request (SSR) | RenderMode.Server | Default |
| HTML at build time (SSG) | RenderMode.Prerender | prerender: true |
| Browser only (CSR) | RenderMode.Client | ssr: false |
| Fully static site | outputMode: "static" | nuxt generate |
Moving a Vue SPA to Nuxt pays off when the public pages need to show up in search and AI. Meanwhile, the logged-in area can stay in the browser, with ssr: false. Nuxt, by the way, plays the role in Vue that Next.js plays in React.
Is the Nuxt SEO module worth it?
It’s worth it when the site needs a sitemap, robots.txt, Schema.org and Open Graph images without building each piece. The Nuxt SEO module, @nuxtjs/seo, installs nine modules in one step, including those and a link checker. Still, it isn’t required, since Nuxt creates SEO tags with useSeoMeta and useHead.
It’s a third-party module, maintained by Harlan Wilton and listed in the Nuxt modules directory. Currently, version 6 requires Nuxt 4.6.1 or later.
What stays out of the HTML in Angular and Nuxt, even with SSR?
Everything the framework leaves for the browser stays out. In Angular, for example, that includes the content of @defer without a hydrate trigger and code in afterNextRender. In Vue and Nuxt, it includes the content of <ClientOnly>, data fetched with server: false and anything created in onMounted. Therefore, a crawler without JavaScript sees none of this.
| Pattern in the code | In the server HTML? | What to do |
|---|---|---|
Angular: @defer without hydrate | No, only the @placeholder | Add hydrate on viewport |
Angular: afterNextRender | No, it runs only in the browser | Keep only non-content code there |
Nuxt: <ClientOnly> | No, only the fallback | Use it for widgets without key text |
Nuxt: useFetch with server: false | No, it arrives after hydration | Fetch on the server, the default |
Vue and Nuxt: fetching in onMounted | No, the hook doesn’t run in SSR | In Nuxt, switch to useFetch |
According to the Angular @defer guide, in SSR and prerendering the block renders only its @placeholder, or nothing. To put the content in the HTML, add a hydrate trigger. It works because incremental hydration is on by default when the app uses provideClientHydration().
@defer (hydrate on viewport) {
<app-reviews />
} @placeholder {
<p>Customer reviews</p>
}
In our own test with Angular 22.2, the server rendered the same block without the hydrate trigger as just the placeholder. With hydrate on viewport, however, it rendered the text of the reviews.
How do you generate meta tags for each page in Angular and Vue?
In Angular, use each route’s title and the Title and Meta services. Likewise, for SEO in Nuxt, use useSeoMeta and useHead. With SSR or prerendering, the tags appear in the <head> of each URL’s HTML. Without that, they only exist after the JavaScript runs, and a crawler without JavaScript sees the default title from index.html.
// src/app/product.ts
import {Component, inject} from '@angular/core';
import {Meta, Title} from '@angular/platform-browser';
@Component({
selector: 'app-product',
template: `<h1>Blue running shoes</h1>`,
})
export class Product {
constructor() {
inject(Title).setTitle('Blue running shoes | Example Store');
inject(Meta).updateTag({name: 'description', content: 'Lightweight shoes for 5 to 21 km runs.'});
}
}
For Angular SEO with SSR, what counts is that the server writes the tag. In fact, in our SSR test, the title and the description came out in the server HTML. For the canonical, the Angular SSR guide shows a service that creates the link with the DOCUMENT token.
<!-- app/pages/products/[id].vue -->
<script setup lang="ts">
const route = useRoute()
useSeoMeta({
title: 'Blue running shoes | Example Store',
description: 'Lightweight shoes for 5 to 21 km runs.',
})
useHead({
link: [{ rel: 'canonical', href: `https://www.example.com${route.path}` }],
})
</script>
For SEO meta tags, the Nuxt documentation recommends useSeoMeta, which has types for more than 100 tags. With CMS data, for example, pass functions, such as title: () => product.value?.name. In a Vue app made with Vite, however, SEO tags that JavaScript creates only exist for whoever renders the page.
What about Nuxt’s experimental streaming?
In Nuxt 4.5 or later, SSR streaming is experimental and off by default. When it’s on, however, it sends the <head> before rendering ends. So tags registered after an await become scripts that patch the page in the browser, according to the experimental features documentation. Therefore, clients that get the stream without running JavaScript don’t see them.
Crawlers that match botRegex, on the other hand, get the full page without streaming, and the documentation only names Googlebot and Bingbot as examples. In the Nuxt 4.6 code, the default pattern matches names ending in bot, such as GPTBot, ClaudeBot and PerplexityBot. In our own test with the official user agent, however, the pattern left out Perplexity-User. This crawler opens pages when a user asks, as the guide to Perplexity and Claude crawlers shows. So call useSeoMeta before any await, or turn off streaming on public routes with streaming: false. Another option is to add Perplexity-User to botRegex, which replaces the whole default pattern.
How do you test what AI crawlers get?
Download the page with an AI crawler’s user agent and look for the main text, the title and the meta description in the HTML. If the response only has the app’s empty root, such as <app-root></app-root> in Angular or <div id="app"></div> in Vue, the page depends on JavaScript.
URL="https://www.example.com/products/blue-shoes"
UA="Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.4; +https://openai.com/gptbot"
curl -sL -A "$UA" "$URL" > gptbot.html
grep -c "Blue running shoes" gptbot.html # main text: 0 = missing
grep -o '<title>[^<]*</title>' gptbot.html # this page's title, not the app default
grep -c 'name="description"' gptbot.html # meta description: 0 = missing
grep -oE '<app-root></app-root>|<div id="app"></div>' gptbot.html # empty root = CSR
Run it in a Linux or macOS terminal or in Git Bash on Windows. The user agent comes from OpenAI’s documentation. Also, in Nuxt with streaming, repeat the test with the Perplexity-User user agent and compare the responses.
For Angular and Vue SEO, the test checks the server’s first response, which is what a crawler without JavaScript reads. The test runs from your IP, so it doesn’t reproduce firewall blocks based on IP. For Google, the live test in the URL Inspection tool in Search Console shows a screenshot of the rendered page. The full checklist, with logs and Search Console, is in the guide to JavaScript SEO for AI crawlers.
The first three checks also work for sites in React and Next.js. After all, ready-made HTML is the foundation of the SEO that supports brand visibility on Google and in AI.
Frequently asked questions
Not under that name. In version 17, Angular Universal moved into the Angular CLI, with the code in the @angular/ssr package, and the Angular team archived the old repository. To turn on SSR in Angular, use ng add @angular/ssr in an existing project or ng new --ssr in a new one.
It can, because the server runs Angular on every request, as the documentation warns. Prerendering avoids that cost: the HTML comes ready from the build and can sit on a CDN. With outputMode: "static", Angular generates each route’s HTML at build time, without a Node server.
Neither has an edge by default, because Angular and Vue render in the browser and give crawlers nearly empty HTML. With SSR or prerendering, both deliver the full page for SEO: Angular through the @angular/ssr package, and Vue through Nuxt. So the choice can follow your team and your project.
Main sources
- Angular, Server and hybrid rendering, guide to SSR and prerendering (2026).
- Angular, Rendering strategies in Angular (2026).
- Angular, @defer and how it behaves in SSR (2026).
- Vue.js, Server-Side Rendering (2026).
- Vue.js, Ways of Using Vue (2026).
- Nuxt, Rendering Modes (2026).
- Nuxt, SEO and Meta (2026).
- Nuxt, experimental SSR streaming (2026).


