GEO for Angular: Complete Implementation Guide
advancedPublished April 18, 2026 · Updated May 13, 2026
Angular 19 introduced Incremental Hydration — @defer blocks hydrate on demand, reducing initial JavaScript and improving LCP. Angular 18+ includes @angular/ssr built-in without Angular Universal. Enable withIncrementalHydration() in provideClientHydration() and withEventReplay() for INP improvements. JSON-LD for GEO is injected server-side via Renderer2.
GEO for Angular: Complete Implementation Guide
Angular requires SSR (Angular Universal) for GEO — client-side rendering is invisible to AI crawlers. Use Angular’s Meta service for meta tags, Title service for titles, and DOCUMENT injection token to create JSON-LD script elements. Configure @angular/ssr for server-side rendering.
Angular’s default client-side rendering (CSR) produces HTML that AI crawlers cannot read — they receive a near-empty shell and no page content. SSR is mandatory for any Angular site that wants to be cited by AI engines.
Installing Angular SSR
# Add SSR to existing project
ng add @angular/ssr
# Or create new project with SSR
ng new my-app --ssr
Component Implementation
// geo-guide.component.ts
import { Component, OnInit, inject, PLATFORM_ID } from '@angular/core'
import { Meta, Title } from '@angular/platform-browser'
import { DOCUMENT, isPlatformBrowser } from '@angular/common'
@Component({
selector: 'app-geo-guide',
templateUrl: './geo-guide.component.html',
})
export class GeoGuideComponent implements OnInit {
private meta = inject(Meta)
private title = inject(Title)
private doc = inject(DOCUMENT)
private platformId = inject(PLATFORM_ID)
private readonly publishedTime = '2026-04-18T00:00:00Z'
private readonly url = 'https://yoursite.com/geo-guide'
private readonly description = 'GEO in Angular with SSR, Meta service, and JSON-LD. Complete guide.'
ngOnInit() {
// Set title
this.title.setTitle('How to Implement GEO in Angular | My Site')
// Set all meta tags
const tags = [
{ name: 'description', content: this.description },
{ name: 'author', content: 'My Company' },
{ name: 'robots', content: 'index, follow' },
// Open Graph
{ property: 'og:type', content: 'article' },
{ property: 'og:title', content: 'How to Implement GEO in Angular' },
{ property: 'og:description', content: this.description },
{ property: 'og:url', content: this.url },
{ property: 'og:site_name', content: 'My Site' },
{ property: 'og:image', content: 'https://yoursite.com/og/geo-guide.jpg' },
{ property: 'og:locale', content: 'en_US' },
// Article dates (recency signal)
{ property: 'article:published_time', content: this.publishedTime },
{ property: 'article:modified_time', content: this.publishedTime },
{ property: 'article:author', content: 'https://yoursite.com/author/my-company' },
{ property: 'article:section', content: 'Technical Guides' },
{ property: 'article:tag', content: 'GEO' },
]
tags.forEach(tag => this.meta.updateTag(tag))
// Add canonical link
const existing = this.doc.querySelector('link[rel="canonical"]')
if (existing) {
existing.setAttribute('href', this.url)
} else {
const canonical = this.doc.createElement('link')
canonical.rel = 'canonical'
canonical.href = this.url
this.doc.head.appendChild(canonical)
}
// Add JSON-LD schema
this.addJsonLd({
'@context': 'https://schema.org',
'@type': 'Article',
headline: 'How to Implement GEO in Angular',
description: this.description,
author: { '@type': 'Organization', name: 'My Company' },
publisher: {
'@type': 'Organization',
name: 'My Site',
logo: { '@type': 'ImageObject', url: 'https://yoursite.com/logo.png' },
},
datePublished: this.publishedTime,
dateModified: this.publishedTime,
mainEntityOfPage: { '@type': 'WebPage', '@id': this.url },
})
}
private addJsonLd(schema: object) {
// Remove existing schema of this type if present
const existing = this.doc.querySelector('script[type="application/ld+json"]')
if (existing) existing.remove()
const script = this.doc.createElement('script')
script.type = 'application/ld+json'
script.text = JSON.stringify(schema)
this.doc.head.appendChild(script)
}
}
Reusable SEO Service
For larger applications, extract meta tag logic into a shared service:
// seo.service.ts
import { Injectable, inject } from '@angular/core'
import { Meta, Title } from '@angular/platform-browser'
import { DOCUMENT } from '@angular/common'
export interface PageSeoConfig {
title: string
description: string
url: string
imageUrl?: string
publishedTime: string
modifiedTime: string
authorUrl?: string
section?: string
tags?: string[]
schema?: object
}
@Injectable({ providedIn: 'root' })
export class SeoService {
private meta = inject(Meta)
private title = inject(Title)
private doc = inject(DOCUMENT)
setSeo(config: PageSeoConfig) {
this.title.setTitle(`${config.title} | My Site`)
const metaTags = [
{ name: 'description', content: config.description },
{ name: 'robots', content: 'index, follow' },
{ property: 'og:type', content: 'article' },
{ property: 'og:title', content: config.title },
{ property: 'og:description', content: config.description },
{ property: 'og:url', content: config.url },
{ property: 'og:site_name', content: 'My Site' },
{ property: 'article:published_time', content: config.publishedTime },
{ property: 'article:modified_time', content: config.modifiedTime },
]
if (config.imageUrl) {
metaTags.push({ property: 'og:image', content: config.imageUrl })
}
if (config.authorUrl) {
metaTags.push({ property: 'article:author', content: config.authorUrl })
}
metaTags.forEach(tag => this.meta.updateTag(tag))
this.setCanonical(config.url)
if (config.schema) {
this.addJsonLd(config.schema)
}
}
private setCanonical(url: string) {
let link = this.doc.querySelector('link[rel="canonical"]') as HTMLLinkElement
if (!link) {
link = this.doc.createElement('link')
link.rel = 'canonical'
this.doc.head.appendChild(link)
}
link.href = url
}
private addJsonLd(schema: object) {
const script = this.doc.createElement('script')
script.type = 'application/ld+json'
script.text = JSON.stringify(schema)
this.doc.head.appendChild(script)
}
}
robots.txt
Place robots.txt in the src/ directory and configure angular.json to copy it to the dist/ folder:
// angular.json — in architect.build.options.assets
{
"assets": [
"src/favicon.ico",
"src/assets",
{ "glob": "robots.txt", "input": "src/", "output": "/" },
{ "glob": "llms.txt", "input": "src/", "output": "/" },
{ "glob": "sitemap.xml", "input": "src/", "output": "/" }
]
}
# src/robots.txt
User-agent: GPTBot
Allow: /
User-agent: OAI-SearchBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: Claude-User
Allow: /
User-agent: Claude-SearchBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: BingBot
Allow: /
Sitemap: https://yoursite.com/sitemap.xml
Sitemap: https://yoursite.com/llms.txt
llms.txt
Create src/llms.txt:
# My Site Name
> Description of what your site does and who it serves.
## Main Content
- [GEO Guide](https://yoursite.com/geo-guide): Complete GEO implementation for Angular
- [Technical Reference](https://yoursite.com/technical): SSR configuration and meta service usage
## About
- [About](https://yoursite.com/about): Team and credentials
Server-Side Route for Dynamic Sitemap
// server.ts (Express server for Angular SSR)
import * as express from 'express'
const app = express()
app.get('/sitemap.xml', async (req, res) => {
const pages = await getAllPages()
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://yoursite.com/</loc>
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
</url>
${pages.map(p => `
<url>
<loc>https://yoursite.com/${p.slug}</loc>
<lastmod>${p.updatedAt}</lastmod>
</url>`).join('')}
</urlset>`
res.set('Content-Type', 'application/xml')
res.send(sitemap)
})
GEO Checklist for Angular
- @angular/ssr installed and configured
- server.ts with Express SSR server running in production
- Title service: setTitle() called in ngOnInit
- Meta service: updateTag() for description, og:*, article:published_time, article:modified_time
- Canonical link element created via DOCUMENT injection
- JSON-LD script element created via DOCUMENT injection
- src/robots.txt with all 8 AI crawlers, copied to dist via angular.json
- src/llms.txt with site description, copied to dist
- Sitemap with lastmod dates
- Inverted pyramid content in component templates
- Core Web Vitals: LCP < 2.5s, INP < 200ms, CLS < 0.1
Angular 18-19: SSR and Hydration Improvements
@angular/ssr (Angular 18+)
Angular 18+ includes SSR directly in Angular CLI. No separate Angular Universal installation required:
ng add @angular/ssr
This generates server.ts and configures SSR automatically.
Event Replay — INP Improvement (Angular 18)
Angular 18 introduced Event Replay — captures user interactions before hydration completes, eliminating the dead zone where clicks are lost. This directly improves INP (Interaction to Next Paint), a Core Web Vital:
// app.config.ts
import { provideClientHydration, withEventReplay } from '@angular/platform-browser'
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withEventReplay()),
]
}
Incremental Hydration (Angular 19)
Angular 19 introduced Incremental Hydration — the most significant GEO improvement since Angular Universal. @defer blocks hydrate on demand (scroll, interaction, timer):
// app.config.ts
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser'
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withIncrementalHydration()),
]
}
<!-- Component template -->
@defer (hydrate on viewport) {
<heavy-interactive-component />
}
GEO impact: The server-rendered HTML (what AI crawlers see) contains all the content. JavaScript payload is reduced, improving LCP. AI crawlers receive full content without hydration.
i18n with Hydration (Angular 18)
Angular 18 enabled hydration for internationalized apps — previously impossible. Multi-language sites can now use full SSR with hydration, which means both language versions are fully indexable by AI crawlers.