Hugo 靜態網站 SEO + AEO 完整實作筆記:從 schema 到 llms.txt

把部落格從「只給人看」升級到「也給 AI 看」,這篇文章記錄了整個過程——包括踩到的坑。

為什麼現在要做 AEO

2025 年之後,流量的入口已經分裂成兩條路:

  1. 傳統搜尋引擎(Google、Bing)→ 依然重要,排名決定曝光
  2. AI 答案引擎(Perplexity、ChatGPT Search、Google AI Overviews)→ 快速成長,直接引用內容回答問題

如果你的網站只做了 SEO,AI 引擎在回答相關問題時可能完全不引用你,即使你的內容是最完整的。AEO 的核心是讓 AI 系統能夠「讀懂」你的內容並信任你是可靠來源。


現況盤點:Hugo 靜態網站 SEO 的基礎

開始動工前先盤點現有基礎。一個典型的 Hugo 部落格通常已經有:

  • <meta name="description"> 與 Open Graph 基礎標籤
  • <link rel="canonical">
  • RSS Feed
  • Hugo 自動產生的 sitemap.xml
  • 基本的 JSON-LD Article schema

但缺少的項目往往更關鍵:

缺少的項目影響
robots.txtGooglebot 行為不可控,AI 爬蟲沒有明確許可
og:image社群分享沒有預覽圖,點擊率偏低
meta name="robots"無法告訴 Google 允許大圖摘要與長片段
BreadcrumbList schema缺少 Google 麵包屑富摘要
wordCountarticleSectionArticle schema 不完整
speakable schema無法被語音搜尋與 AI 朗讀摘要
llms.txtAI 引擎沒有結構化的站點入口
FAQPage schemaGoogle AI Overviews 無法直接抽取 Q&A
E-E-A-T 作者信號AI 引擎無法判斷內容可信度

實作一:robots.txt

沒有 robots.txt 並不代表所有爬蟲都被允許,只是讓爬蟲行為變得不可預測。更重要的是,現在需要明確告訴 AI 爬蟲「你們可以來」。

把以下內容放到 static/robots.txt

User-agent: *
Allow: /

# AI crawlers — allow for AEO
User-agent: GPTBot
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: anthropic-ai
Allow: /

Sitemap: https://your-domain.com/sitemap.xml

坑一:Cloudflare Managed robots.txt

部署之後用 curl 測試,發現回傳的不是我們的檔案,而是 Cloudflare 的「Content Signals Framework」:

curl -s https://your-domain.com/robots.txt
# As a condition of accessing this website, you agree to abide by the following
# content signals:
# search: building a search index...
# ai-input: inputting content into one or more AI models...
# ai-train: training or fine-tuning AI models.

這是 Cloudflare 的「Managed robots.txt」功能,會直接覆蓋掉你的靜態檔案。

解決方法:

Cloudflare Dashboard → Scrape Shield → Managed robots.txt → 關閉

關閉後 curl 就能拿到自己的 robots.txt 了。


坑二:Cloudflare 預設封鎖 AI 爬蟲

即使 robots.txt 正確,Cloudflare 從 2025 年 7 月起對新加入網域預設封鎖所有 AI bot。這個封鎖發生在網路層,在請求到達你的 server 之前就被 drop 掉,所以 robots.txt 完全沒有機會被讀到。

被封鎖的包含:

  • PerplexityBot
  • GPTBot(ChatGPT Search)
  • ClaudeBot(Anthropic)
  • anthropic-ai

解決方法:

Cloudflare Dashboard → Security → Bots → AI Scrapers and Crawlers → Off

重要區別:這裡設成 Off 是讓所有 AI bot 通過,然後靠 robots.txt 自己精細控制。如果你想繼續封鎖用來「訓練模型」的爬蟲(如 CCBot、Common Crawl),可以用 Cloudflare WAF 規則針對特定 User-Agent 封鎖,而不是全部擋掉。

關閉後,可以在 Cloudflare Dashboard 的 AI Crawl Control 頁面看到 AI 爬蟲的請求統計,確認爬蟲是否已順利抵達你的網站:

Cloudflare AI Crawl Control 統計頁面,顯示 74 次 AI 爬蟲請求,76.2% 較前期增加


實作二:og:image 與 Twitter Card

og:image 是社群分享時顯示預覽圖的關鍵。沒有這個標籤,文章分享到 Facebook、LINE、Twitter 時只會顯示一個空白方塊。

layouts/partials/head.html 加入:

{{/* OG Image:優先用文章自訂圖,否則用站點預設 */}}
{{ $ogImage := "" }}
{{ if .Params.image }}
  {{ $ogImage = .Params.image | absURL }}
{{ else }}
  {{ $ogImage = site.Params.og_image | absURL }}
{{ end }}

<meta property="og:image" content="{{ $ogImage }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="{{ $ogImage }}">

同時在 config/_default/params.toml 加入預設圖路徑:

og_image = "/images/og-default.png"

文章 front matter 可以個別指定:

image: "/images/posts/my-article-cover.png"

實作三:meta name="robots" 完整指令

這個標籤告訴 Google 允許哪些摘要方式。max-image-preview:large 讓 Google 在搜尋結果顯示大圖;max-snippet:-1 允許顯示任意長度的文字摘要。

<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">

沒有這行,Google 可能只顯示小圖或截短摘要,直接影響點擊率。


實作四:完整的 Article schema

基本的 Article schema 只有標題、日期、作者,但完整版還要加上 wordCountarticleSectionspeakable

layouts/partials/schema.html

{{ if .IsPage }}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "{{ .Params.schema_type | default "Article" }}",
  "headline": {{ .Title | jsonify }},
  "description": {{ with .Params.description }}{{ . | jsonify }}{{ else }}{{ .Summary | plainify | truncate 160 | jsonify }}{{ end }},
  "datePublished": "{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}",
  "dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}",
  "url": "{{ .Permalink }}",
  "wordCount": {{ .WordCount }},
  {{ with .Params.categories }}"articleSection": {{ index . 0 | jsonify }},{{ end }}
  "author": {
    "@type": "Person",
    "name": "{{ site.Params.schema.person_name }}",
    "url": "{{ site.Params.schema.person_url }}"
  },
  "image": {{ with .Params.image }}{{ . | absURL | jsonify }}{{ else }}{{ site.Params.og_image | absURL | jsonify }}{{ end }}
  {{ with .Params.tags }},
  "keywords": [{{ range $i, $v := . }}{{ if $i }}, {{ end }}{{ $v | jsonify }}{{ end }}]
  {{ end }},
  "speakable": {
    "@type": "SpeakableSpecification",
    "cssSelector": [".post-header h1", ".post-summary", ".content > p:first-child"]
  }
}
</script>
{{ end }}

speakable 標記的 CSS selector 告訴 Google 哪些段落「適合被朗讀」,這是語音搜尋和 AI 口語摘要的依據。


實作五:BreadcrumbList schema

BreadcrumbList 讓 Google 在搜尋結果中顯示路徑導覽(「首頁 › 文章 › 文章標題」),提升點擊意願。

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首頁",
      "item": "{{ site.BaseURL }}"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "{{ "posts/" | absURL }}"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": {{ .Title | jsonify }},
      "item": "{{ .Permalink }}"
    }
  ]
}
</script>

實作六:llms.txt(AEO 的核心)

llms.txtllmstxt.org 推動的新興標準。概念類似 robots.txt,但目標是幫助 LLM 理解網站,而不是控制爬蟲行為。

格式是 Markdown,放在網站根目錄 /llms.txt

# Stan Wu 吳信典

> 拆解被包裝過的東西、數位自主權實踐、非典型理財觀

## 重要頁面

- [首頁](https://your-domain.com/): 最新文章列表
- [關於](https://your-domain.com/about/): 關於作者

## 文章

- [文章標題](https://your-domain.com/posts/slug/): 摘要描述
...

## 關於作者

專長領域:DevOps、Python、Linux、資訊安全...

在 Hugo 中用 custom output format 動態產生,確保文章清單永遠是最新的。

config/_default/hugo.toml 加入輸出格式:

[outputs]
  home = ["HTML", "RSS", "JSON", "LLMS"]

[outputFormats.LLMS]
  mediatype = "text/plain"
  baseName = "llms"
  isPlainText = true
  notAlternative = true

layouts/index.llms.txt 模板:

# {{ site.Title }}

> {{ site.Params.description }}

## 文章

{{ range where .Site.RegularPages "Section" "posts" }}{{ if not .Params.draft }}- [{{ .Title }}]({{ .Permalink }}){{ with .Summary }}: {{ . | plainify | truncate 120 }}{{ end }}
{{ end }}{{ end }}

實作七:FAQPage schema

FAQPage 是 AI Overviews 和 Google 精選摘要最喜歡的格式,AI 引擎會直接從 FAQ 提取問題和答案作為回覆。

在 Hugo 中透過 front matter 的 faq 陣列驅動,schema partial 自動輸出:

# 文章 front matter
faq:
  - q: "問題一?"
    a: "詳細的答案..."
  - q: "問題二?"
    a: "詳細的答案..."

schema.html 對應的輸出:

{{ with .Params.faq }}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {{ range $i, $item := . }}{{ if $i }},{{ end }}
    {
      "@type": "Question",
      "name": {{ $item.q | jsonify }},
      "acceptedAnswer": {
        "@type": "Answer",
        "text": {{ $item.a | jsonify }}
      }
    }
    {{ end }}
  ]
}
</script>
{{ end }}

實作八:E-E-A-T 作者信號(knowsAbout

Google 和 AI 引擎判斷內容可信度的方式之一是 E-E-A-T(Experience、Expertise、Authoritativeness、Trustworthiness)。在 Person schema 中加入 knowsAbout 欄位,明確宣告作者的專長領域。

config/_default/params.toml

[schema]
  person_name = "你的名字"
  person_url = "https://your-personal-site.com"
  person_same_as = [
    "https://github.com/your-handle",
  ]
  knows_about = [
    "Linux 系統管理",
    "DevOps",
    "Python",
    "nginx",
    "資訊安全",
  ]

head.html 的 Person schema:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Person",
  "name": "{{ site.Params.schema.person_name }}",
  "url": "{{ site.Params.schema.person_url }}"
  {{ with site.Params.schema.person_same_as }},
  "sameAs": [{{ range $i, $v := . }}{{ if $i }}, {{ end }}"{{ $v }}"{{ end }}]
  {{ end }}
  {{ with site.Params.schema.knows_about }},
  "knowsAbout": [{{ range $i, $v := . }}{{ if $i }}, {{ end }}"{{ $v }}"{{ end }}]
  {{ end }}
}
</script>

sameAs 把你的 GitHub、社群媒體帳號連結起來,幫助 AI 引擎將「散落在各處的你」識別為同一個人,強化權威性。


實作九:閱讀時間顯示

閱讀時間對 UX 有直接幫助——讓讀者在點進文章前知道需要花多少時間,降低跳出率。Hugo 內建 .ReadingTime 變數,直接在文章 meta 中使用:

<span class="reading-time">{{ .ReadingTime }} 分鐘閱讀</span>

驗證方式

快速驗證:curl

最直接的方式,用不同 User-Agent 模擬不同爬蟲:

# 確認 robots.txt 是自己的內容
curl -s https://your-domain.com/robots.txt

# 確認 llms.txt 存在
curl -s https://your-domain.com/llms.txt

# 用 Googlebot 身份抓頁面,確認 schema 存在
curl -s -A "Googlebot" https://your-domain.com/posts/your-post/ | grep -o 'application/ld+json'

完整驗證:Python 模擬爬蟲

寫一支 Python 腳本,模擬 PerplexityBot 爬蟲,逐項檢查所有 SEO/AEO 信號:

import urllib.request, re, json

BASE = "https://your-domain.com"
HEADERS = {
    "User-Agent": "Mozilla/5.0 (compatible; PerplexityBot/1.0; +https://perplexity.ai/bot)",
}

def fetch(url):
    req = urllib.request.Request(url, headers=HEADERS)
    with urllib.request.urlopen(req, timeout=10) as r:
        return r.read().decode("utf-8")

def check(label, condition):
    print(f"  {'✅' if condition else '❌'} {label}")

# 抓文章頁
html = fetch(f"{BASE}/posts/your-post/")

# Hugo --minify 會把 <script type="application/ld+json"> 的引號移除
# 必須用無引號的 regex 才能正確比對
blocks = re.findall(
    r'<script type=application/ld\+json>(.*?)</script>', html, re.DOTALL
)

types = []
has_breadcrumb = has_speakable = has_wordcount = has_faqpage = has_knowsabout = False

for block in blocks:
    data = json.loads(block)
    t = data.get("@type", "")
    types.append(t)
    if t == "BreadcrumbList": has_breadcrumb = True
    if t in ("Article", "BlogPosting"):
        has_speakable = "speakable" in data
        has_wordcount = "wordCount" in data
    if t == "FAQPage": has_faqpage = True
    if t == "Person" and "knowsAbout" in data: has_knowsabout = True

check(f"JSON-LD 區塊數量({len(blocks)})", len(blocks) >= 3)
check("Article schema", any(t in ("Article", "BlogPosting") for t in types))
check("BreadcrumbList", has_breadcrumb)
check("speakable", has_speakable)
check("wordCount", has_wordcount)
check("FAQPage", has_faqpage)
check("Person knowsAbout", has_knowsabout)
check("og:image", "og:image" in html)
check("twitter:card large_image", "summary_large_image" in html)
check("meta robots", "name=robots" in html)
check("閱讀時間", "分鐘閱讀" in html)

注意:Hugo 的 --minify 會把 <script type="application/ld+json"> 的引號去掉,變成 <script type=application/ld+json>。如果 regex 還在找有引號的版本,就會全部找不到——這是一個很隱蔽的驗證陷阱。

Google 官方工具

https://search.google.com/test/rich-results

貼入文章 URL,可以視覺化確認 Article、BreadcrumbList、FAQPage 三個 schema 是否都通過驗證。


成果

跑完完整驗證後的結果:

[1] robots.txt
  ✅ 非 Cloudflare managed
  ✅ Allow PerplexityBot / ClaudeBot / GPTBot
  ✅ Sitemap 指向

[2] llms.txt
  ✅ 存在並包含文章清單與作者專長

[3] Article schema
  ✅ JSON-LD 3 個區塊(Person / Article / BreadcrumbList)
  ✅ BreadcrumbList
  ✅ speakable
  ✅ wordCount
  ✅ Person knowsAbout
  ✅ FAQPage(5 個 Q&A)

[4] Meta tags
  ✅ og:image
  ✅ twitter:card summary_large_image
  ✅ meta robots
  ✅ canonical

[5] UX
  ✅ 閱讀時間顯示

小結

SEO 是讓人找到你,AEO 是讓 AI 引用你。兩件事現在都要做。

對 Hugo 靜態網站來說,實作成本不高,大多數改動都在 layouts 和 config,不需要動到文章內容本身。最麻煩的反而是 Cloudflare 那兩個預設行為——Managed robots.txt 和 AI bot 封鎖——如果不知道,可能花很多時間找不到原因。

這篇文章記錄的所有改動,歡迎直接參考或借用。