GEO for Angular: Complete Implementation Guide
advancedAngular 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 @nguniversal/express-engine for server-side rendering.
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