Angular and Vue sites are readable by Google and AI crawlers when public pages are rendered on the server or pre-rendered at build time, use real URL paths instead of hash-based routes, set titles, descriptions and canonicals per route in the server output, and use real links for navigation. Both frameworks support this: Angular through its built-in server-side rendering and prerendering, and Vue through Nuxt or a prerendering setup. A client-only single-page app sends crawlers an almost empty page.
Why client-only rendering is a problem
A client-only Angular or Vue app sends an HTML shell with an empty root element and a JavaScript bundle. Googlebot renders JavaScript, sometimes with a delay; most AI crawlers do not render it at all. In our render test, a client-only React app sent 2 visible words to a crawler. The same applies to Angular and Vue apps built the same way.
Angular
Enable server-side rendering or prerendering
# new project with SSR $ ng new my-site --ssr # add SSR to an existing project $ ng add @angular/ssr
Recent Angular versions include server-side rendering and build-time prerendering through @angular/ssr, with hydration so the client takes over the server-rendered page without rebuilding it. Prerender marketing pages that rarely change, and render dynamic pages on the server.
Set metadata per route
import { Component, inject } from "@angular/core";
import { Meta, Title } from "@angular/platform-browser";
@Component({ selector: "app-service", templateUrl: "./service.html" })
export class ServicePage {
private title = inject(Title);
private meta = inject(Meta);
ngOnInit() {
this.title.setTitle("Technical SEO Audit | Example");
this.meta.updateTag({ name: "description", content: "What the audit covers and how long it takes." });
}
}
With server rendering, these tags are in the HTML the server sends. Without it, they only exist after JavaScript runs.
Vue
Use Nuxt for public sites
Nuxt renders pages on the server by default and can prerender static routes. It is the most direct way to make a Vue site crawlable. Set metadata with its composables:
<!-- pages/services/[slug].vue -->
<script setup>
const route = useRoute();
const { data: service } = await useFetch(`/api/services/${route.params.slug}`);
useSeoMeta({
title: () => `${service.value.name} | Example`,
description: () => service.value.summary,
});
useHead({
link: [{ rel: "canonical", href: `https://www.example.com/services/${route.params.slug}/` }],
});
</script>
useFetch runs on the server during rendering, so the content and metadata are in the initial HTML.
If you must keep a Vue single-page app
Pre-render the public routes to static HTML at build time with a prerendering tool for your build setup, and keep the app behaviour for logged-in areas.
Routing
- Use path-based routing (history mode in Vue Router, the default in Angular). URLs with # fragments, such as example.com/#/services, are treated as one page by search engines.
- Return real HTTP status codes from the server: 404 for unknown routes, 301 for redirects. A single-page app that answers every URL with 200 creates soft 404s.
- Use anchor elements with href for navigation: Angular’s routerLink and Vue’s RouterLink render real links.
Common mistakes
- Fetching page content in a lifecycle hook that only runs in the browser
- Setting the canonical or title after an API call in the browser
- Blocking JavaScript, CSS or API paths in robots.txt, which stops Google rendering pages
- Guarding public pages behind client-side checks that show a loading state to crawlers
Test
- View the page source or run curl, and search for a sentence from the middle of the page.
- Run our AI crawler access checker on each template.
- Use Search Console’s URL Inspection to see Google’s rendered HTML.
Frequently asked questions
Is Angular or Vue bad for SEO?
No. Client-only rendering is the problem. With server rendering or prerendering, both work well.
Does dynamic rendering still make sense?
Google describes dynamic rendering, serving pre-rendered pages only to bots, as a workaround rather than a long-term solution. Server rendering or prerendering for everyone is simpler and serves users too.
What about Svelte and other frameworks?
The same principle applies. SvelteKit, Astro and similar frameworks render on the server or at build time by default.
Sources
- Angular documentation, “Server-side and hybrid rendering”
- Nuxt documentation, rendering modes, useFetch and useSeoMeta
- Google Search Central, “Understand the JavaScript SEO basics” and “Dynamic rendering as a workaround”