🔴

GEO con Angular: Guía completa de implementación

advanced

Angular implementa GEO con @angular/ssr obligatorio, Meta service para Open Graph y article dates, Title service para el title tag, y inyección manual de JSON-LD via DOCUMENT token. Sin SSR Angular sirve HTML vacío y es invisible para todos los crawlers de IA. Este es el requisito más crítico.

GEO con Angular: Guía completa de implementación

Angular implementa GEO con @angular/ssr obligatorio, Meta service para Open Graph y article dates, Title service para el title tag, y inyección manual de JSON-LD via token DOCUMENT. Sin SSR Angular sirve HTML vacío y es invisible para todos los crawlers de IA.

Obligatorio: @angular/ssr instalado y configurado. Sin SSR, los crawlers de IA reciben HTML vacío y el sitio es invisible para todos los sistemas generativos.

Instalación de Angular SSR

ng add @angular/ssr

Esto configura automáticamente el server.ts y las entradas necesarias para SSR.

Implementación del componente

// guia-geo.component.ts
import { Component, OnInit, inject } from '@angular/core'
import { Meta, Title } from '@angular/platform-browser'
import { DOCUMENT } from '@angular/common'

@Component({
  selector: 'app-guia-geo',
  templateUrl: './guia-geo.component.html',
})
export class GuiaGeoComponent implements OnInit {
  private meta = inject(Meta)
  private title = inject(Title)
  private doc = inject(DOCUMENT)

  private readonly publishedTime = '2026-04-18T00:00:00Z'
  private readonly url = 'https://misitio.com/guia-geo'

  ngOnInit() {
    this.title.setTitle('Cómo implementar GEO en Angular | Mi Sitio')

    const tags = [
      { name: 'description', content: 'GEO en Angular con SSR, meta service y JSON-LD. Guía completa.' },
      { name: 'author', content: 'Mi Empresa' },
      { name: 'robots', content: 'index, follow' },
      { property: 'og:type', content: 'article' },
      { property: 'og:title', content: 'Cómo implementar GEO en Angular' },
      { property: 'og:description', content: 'Guía técnica de GEO para Angular' },
      { property: 'og:url', content: this.url },
      { property: 'og:site_name', content: 'Mi Sitio' },
      { property: 'og:image', content: 'https://misitio.com/og/guia-geo.jpg' },
      { property: 'og:locale', content: 'es_ES' },
      { property: 'article:published_time', content: this.publishedTime },
      { property: 'article:modified_time', content: this.publishedTime },
      { property: 'article:author', content: 'https://misitio.com/author/mi-empresa' },
      { property: 'article:section', content: 'Guías técnicas' },
      { property: 'article:tag', content: 'GEO' },
    ]

    tags.forEach(tag => this.meta.updateTag(tag))

    // URL canónica
    const canonical = this.doc.createElement('link')
    canonical.rel = 'canonical'
    canonical.href = this.url
    this.doc.head.appendChild(canonical)

    // JSON-LD
    const script = this.doc.createElement('script')
    script.type = 'application/ld+json'
    script.text = JSON.stringify({
      '@context': 'https://schema.org',
      '@type': 'Article',
      headline: 'Cómo implementar GEO en Angular',
      description: 'Guía técnica de GEO para Angular',
      author: { '@type': 'Organization', name: 'Mi Empresa' },
      publisher: {
        '@type': 'Organization',
        name: 'Mi Sitio',
        logo: { '@type': 'ImageObject', url: 'https://misitio.com/logo.png' },
      },
      datePublished: this.publishedTime,
      dateModified: this.publishedTime,
      mainEntityOfPage: { '@type': 'WebPage', '@id': this.url },
    })
    this.doc.head.appendChild(script)
  }
}

Servicio SeoService reutilizable

Para no repetir la lógica en cada componente, crea un servicio centralizado:

// services/seo.service.ts
import { Injectable, inject } from '@angular/core'
import { Meta, Title } from '@angular/platform-browser'
import { DOCUMENT } from '@angular/common'

export interface SeoConfig {
  title: string
  description: string
  url: string
  publishedTime: string
  modifiedTime: string
  author: string
  section?: string
  image?: string
}

@Injectable({ providedIn: 'root' })
export class SeoService {
  private meta = inject(Meta)
  private title = inject(Title)
  private doc = inject(DOCUMENT)

  setPageSeo(config: SeoConfig) {
    this.title.setTitle(`${config.title} | Mi Sitio`)

    this.meta.updateTag({ name: 'description', content: config.description })
    this.meta.updateTag({ name: 'robots', content: 'index, follow' })
    this.meta.updateTag({ property: 'og:type', content: 'article' })
    this.meta.updateTag({ property: 'og:title', content: config.title })
    this.meta.updateTag({ property: 'og:description', content: config.description })
    this.meta.updateTag({ property: 'og:url', content: config.url })
    this.meta.updateTag({ property: 'article:published_time', content: config.publishedTime })
    this.meta.updateTag({ property: 'article:modified_time', content: config.modifiedTime })
    this.meta.updateTag({ property: 'article:author', content: config.author })

    if (config.image) {
      this.meta.updateTag({ property: 'og:image', content: config.image })
    }

    this.setCanonical(config.url)
    this.setJsonLd(config)
  }

  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 setJsonLd(config: SeoConfig) {
    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({
      '@context': 'https://schema.org',
      '@type': 'Article',
      headline: config.title,
      description: config.description,
      datePublished: config.publishedTime,
      dateModified: config.modifiedTime,
      author: { '@type': 'Person', name: config.author },
      publisher: { '@type': 'Organization', name: 'Mi Sitio' },
      mainEntityOfPage: { '@type': 'WebPage', '@id': config.url },
    })
    this.doc.head.appendChild(script)
  }
}

robots.txt

Coloca robots.txt en la carpeta src/ (se sirve como asset estático):

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: /

User-agent: *
Allow: /

Sitemap: https://misitio.com/sitemap.xml
Sitemap: https://misitio.com/llms.txt

Asegúrate de incluirlo en assets en angular.json:

{
  "assets": [
    "src/favicon.ico",
    "src/assets",
    "src/robots.txt",
    "src/llms.txt"
  ]
}

Checklist GEO para Angular

  • @angular/ssr instalado y configurado — obligatorio para GEO
  • Title service: setTitle() en ngOnInit de cada componente de página
  • Meta service: description, robots, og:type, og:title, og:description, og:url
  • Meta service: article:published_time y article:modified_time
  • DOCUMENT token: canonical URL añadida al head
  • DOCUMENT token: JSON-LD Article schema añadido al head
  • SeoService reutilizable (recomendado para proyectos grandes)
  • src/robots.txt con los 8 crawlers de IA permitidos
  • src/llms.txt con descripción del sitio
  • angular.json: robots.txt y llms.txt en assets
  • Core Web Vitals: LCP < 2.5s, INP < 200ms, CLS < 0.1