🏗️

GEO for SSGs: Hugo, Jekyll, and Astro Guide

intermediate

Static site generators (Hugo, Jekyll, Astro) are ideal for GEO: they output pure HTML with no JavaScript required for AI crawlers. Implement GEO in the base template for site-wide meta tags and JSON-LD. SSGs generate XML sitemaps with lastmod automatically. Astro adds zero JavaScript overhead by default.

GEO for SSGs: Hugo, Jekyll, and Astro Guide

Static site generators (Hugo, Jekyll, Astro) are ideal for GEO: they output pure HTML with no JavaScript required for AI crawlers. Implement GEO in the base template for site-wide meta tags and JSON-LD. SSGs generate XML sitemaps with lastmod automatically. Astro adds zero JavaScript overhead by default.

SSGs have the best possible starting position for GEO: every page is pre-rendered static HTML, served directly from a CDN. AI crawlers get full content without needing to execute JavaScript or wait for hydration.

Hugo

Base Template (layouts/_default/baseof.html)

<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- TITLE -->
  <title>
    {{- if .IsHome -}}
      {{ .Site.Title }}
    {{- else -}}
      {{ .Title }} | {{ .Site.Title }}
    {{- end -}}
  </title>

  <!-- DESCRIPTION -->
  {{ with .Description }}
    <meta name="description" content="{{ . }}">
  {{ else }}
    <meta name="description" content="{{ .Site.Params.description }}">
  {{ end }}

  <meta name="author" content="{{ .Params.author | default .Site.Params.author }}">
  <link rel="canonical" href="{{ .Permalink }}">
  <meta name="robots" content="index, follow">

  <!-- OPEN GRAPH -->
  {{ if .IsPage }}
    <meta property="og:type" content="article">
    <meta property="article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">
    <meta property="article:modified_time" content="{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}">
    {{ with .Params.author }}
      <meta property="article:author" content="{{ . }}">
    {{ end }}
    {{ range .Params.tags }}
      <meta property="article:tag" content="{{ . }}">
    {{ end }}
  {{ else }}
    <meta property="og:type" content="website">
  {{ end }}

  <meta property="og:title" content="{{ .Title }}">
  <meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}">
  <meta property="og:url" content="{{ .Permalink }}">
  <meta property="og:site_name" content="{{ .Site.Title }}">
  <meta property="og:locale" content="{{ .Site.Language.Lang | replace "-" "_" }}">
  {{ with .Params.image }}
    <meta property="og:image" content="{{ . | absURL }}">
  {{ end }}

  <!-- JSON-LD -->
  {{ if .IsPage }}
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": {{ .Title | jsonify }},
    "description": {{ with .Description }}{{ . | jsonify }}{{ else }}{{ .Summary | jsonify }}{{ end }},
    "author": {
      "@type": "Person",
      "name": {{ .Params.author | default .Site.Params.author | jsonify }}
    },
    "publisher": {
      "@type": "Organization",
      "name": {{ .Site.Title | jsonify }},
      "logo": {
        "@type": "ImageObject",
        "url": {{ .Site.Params.logoUrl | absURL | jsonify }}
      }
    },
    "datePublished": {{ .Date.Format "2006-01-02T15:04:05Z07:00" | jsonify }},
    "dateModified": {{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" | jsonify }},
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": {{ .Permalink | jsonify }}
    }
  }
  </script>
  {{ end }}
</head>
<body>
  {{ block "main" . }}{{ end }}
</body>
</html>

robots.txt (static/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: {{ .Site.BaseURL }}sitemap.xml
Sitemap: {{ .Site.BaseURL }}llms.txt

llms.txt Template (layouts/llms.txt)

Create content/llms.md with layout: llms and outputs: [txt]:

---
layout: llms
outputs:
  - txt
---
# {{ .Site.Title }}
> {{ .Site.Params.description }}

## Main Content
{{ range where .Site.RegularPages "Section" "guides" -}}
- [{{ .Title }}]({{ .Permalink }}): {{ .Description }}
{{ end }}

Hugo Sitemap

Hugo generates sitemap.xml automatically with <lastmod> from the page’s lastmod front matter or file modification time. Set enableRobotsTXT = true in config.toml.

Jekyll

_layouts/default.html

<!DOCTYPE html>
<html lang="{{ site.lang | default: 'en' }}">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>
    {%- if page.title -%}
      {{ page.title }} | {{ site.title }}
    {%- else -%}
      {{ site.title }}
    {%- endif -%}
  </title>

  <meta name="description" content="{{ page.description | default: site.description | escape }}">
  <meta name="author" content="{{ page.author | default: site.author }}">
  <link rel="canonical" href="{{ page.url | absolute_url }}">
  <meta name="robots" content="index, follow">

  <meta property="og:type" content="article">
  <meta property="og:title" content="{{ page.title | escape }}">
  <meta property="og:description" content="{{ page.description | default: site.description | escape }}">
  <meta property="og:url" content="{{ page.url | absolute_url }}">
  <meta property="og:site_name" content="{{ site.title }}">

  {% if page.date %}
  <meta property="article:published_time" content="{{ page.date | date_to_xmlschema }}">
  {% endif %}
  {% if page.last_modified_at %}
  <meta property="article:modified_time" content="{{ page.last_modified_at | date_to_xmlschema }}">
  {% endif %}

  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": {{ page.title | jsonify }},
    "description": {{ page.description | default: site.description | jsonify }},
    "author": { "@type": "Person", "name": {{ page.author | default: site.author | jsonify }} },
    "publisher": {
      "@type": "Organization",
      "name": {{ site.title | jsonify }},
      "logo": { "@type": "ImageObject", "url": {{ "/assets/logo.png" | absolute_url | jsonify }} }
    },
    "datePublished": {{ page.date | date_to_xmlschema | jsonify }},
    "dateModified": {{ page.last_modified_at | default: page.date | date_to_xmlschema | jsonify }},
    "mainEntityOfPage": { "@type": "WebPage", "@id": {{ page.url | absolute_url | jsonify }} }
  }
  </script>
</head>
<body>
  {{ content }}
</body>
</html>

Jekyll robots.txt

Create robots.txt in the project root (Jekyll copies it to _site/):

User-agent: GPTBot
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: Google-Extended
Allow: /

User-agent: *
Allow: /

Sitemap: {{ site.url }}/sitemap.xml

Jekyll’s jekyll-sitemap gem generates sitemap.xml with <lastmod> from last_modified_at front matter.

Astro

src/layouts/BaseLayout.astro

---
interface Props {
  title: string
  description: string
  datePublished: string
  dateModified: string
  author?: string
  image?: string
}

const {
  title,
  description,
  datePublished,
  dateModified,
  author = 'My Company',
  image,
} = Astro.props

const canonicalURL = new URL(Astro.url.pathname, Astro.site)

const schema = {
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: title,
  description,
  author: { '@type': 'Organization', name: author },
  publisher: {
    '@type': 'Organization',
    name: 'My Site',
    logo: { '@type': 'ImageObject', url: new URL('/logo.png', Astro.site).href },
  },
  datePublished,
  dateModified,
  mainEntityOfPage: { '@type': 'WebPage', '@id': canonicalURL.href },
}
---

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{title} | My Site</title>
  <meta name="description" content={description}>
  <meta name="author" content={author}>
  <link rel="canonical" href={canonicalURL}>
  <meta name="robots" content="index, follow">

  <meta property="og:type" content="article">
  <meta property="og:title" content={title}>
  <meta property="og:description" content={description}>
  <meta property="og:url" content={canonicalURL}>
  <meta property="og:site_name" content="My Site">
  {image && <meta property="og:image" content={image}>}

  <meta property="article:published_time" content={datePublished}>
  <meta property="article:modified_time" content={dateModified}>

  <script type="application/ld+json" set:html={JSON.stringify(schema)} />
</head>
<body>
  <slot />
</body>
</html>

Astro robots.txt

Create public/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: *
Allow: /

Sitemap: https://yoursite.com/sitemap-index.xml
Sitemap: https://yoursite.com/llms.txt

Astro Sitemap

Use @astrojs/sitemap integration:

npx astro add sitemap
// astro.config.mjs
import { defineConfig } from 'astro/config'
import sitemap from '@astrojs/sitemap'

export default defineConfig({
  site: 'https://yoursite.com',
  integrations: [sitemap()],
})

The integration generates sitemap-index.xml with <lastmod> dates automatically from page modification times.

GEO Checklist for SSGs

  • Base template: title, description, canonical, author, robots
  • Base template: Open Graph with og:type=article, og:title, og:description, og:url
  • Base template: article:published_time and article:modified_time from front matter
  • Base template: JSON-LD Article schema with publisher and dates
  • public/ or static/: robots.txt with all 8 AI crawlers
  • public/ or static/: llms.txt with site description and page listing
  • Sitemap: generated with lastmod dates (built-in for Hugo/Astro, plugin for Jekyll)
  • Front matter: date and lastmod/last_modified_at on all pages
  • Content: inverted pyramid structure in Markdown files
  • Core Web Vitals: LCP < 2.5s, INP < 200ms, CLS < 0.1