🔴

GEO for Angular: Complete Implementation Guide

advanced

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 @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