Vibe Coding 初體驗:AI 協作打造公廁尋找器
利用 AI 協作開發,快速打造一個實用的附近公廁尋找器。從政府開放資料到 Flask 後端、Tailwind CSS 前端,一次 Vibe Coding 體驗。
在研究 Agent Discovery Protocol 時,我發現一個有趣的現象:雖然有各種「AI Agents 專用」的複雜協議(ANP、A2A 等),但實際上已經有 12+ 企業(包含 Shopify、Snowflake、O’Reilly Media)採用了一個更務實的方案 — NLWeb 語意層。
這個方案的核心理念很簡單:
不需要全新的協議,只需要讓現有的網站結構更適合 AI 閱讀。
就像人類瀏覽網站時,我們會先看首頁導覽、RSS 訂閱、或網站地圖。AI agents 也需要類似的「入口」來快速理解網站內容。NLWeb 就是為此而生的標準。
NLWeb (Natural Language Web) 是由 Microsoft 在 Build 2025(2025-05-19)正式宣布的規範,主要包含四個組件:
| 組件 | 用途 | 網址範例 |
|---|---|---|
| llms.txt | 網站結構導覽入口 | /llms.txt |
| llms-full.txt | 完整內容索引(可選) | /llms-full.txt |
| RSS/Atom Feed | 文章訂閱與完整內容 | /rss.xml |
| Schema.org JSON-LD | 結構化資料標記 | 各頁面 <script type="application/ld+json"> |
這些組件互補運作,形成一個完整的「語意層」:
檔案位置: site/public/llms.txt
這是一個靜態的純文字檔案,類似「人類可讀的網站地圖」。我的實作包含:
# Powei Lee - AI & Automation Blog
> 從金門出發的學習紀錄...
## 📚 內容導覽
### 部落格文章
- 所有文章: https://pwlee.dev/blog
- RSS Feed (Atom): https://pwlee.dev/rss.xml
### 專案紀錄
- 專案列表: https://pwlee.dev/projects
## 🏷️ 重點主題
### AI 與自動化
- AI 開發與應用: https://pwlee.dev/tags/ai
- 自動化工作流程: https://pwlee.dev/tags/automation
...(其他主題分類)
## 👤 關於作者
- 個人簡介: https://pwlee.dev/about
- GitHub: https://github.com/pwlee
設計原則:
檔案位置: site/public/llms-full.txt(build 時自動生成)
llms.txt 提供「目錄」,llms-full.txt 提供「完整清單」。這個檔案包含所有文章的詳細資訊:
## 📝 部落格文章
共 6 篇文章
### 1. 實作 NLWeb 語意層:讓 AI Agents 能夠閱讀你的網站
- URL: https://pwlee.dev/blog/nlweb-semantic-layer
- 發布日期: 2025-11-22
- 類型: tutorial
- 作者: Claude Code
- 標籤: AI, NLWeb, SEO, Schema.org, RSS, Astro
- 摘要: 完整實作 NLWeb 語意層...
### 2. [下一篇文章]
...
自動生成腳本 (site/scripts/generate-llms-full.ts):
這個 TypeScript 腳本會:
site/src/content/blog/ 和 site/src/content/projects/ 所有 markdown 檔案gray-matter 解析 frontmatterdraft: true 的內容整合到 build 流程 (package.json):
{
"scripts": {
"prebuild": "tsx scripts/generate-llms-full.ts",
"build": "astro build"
}
}
每次執行 npm run build 前,prebuild 會自動執行,確保 llms-full.txt 永遠是最新的。
檔案位置: site/src/pages/rss.xml.ts
從原本的 RSS 2.0 升級到 Atom 1.0 格式,並加入完整 HTML 內容:
主要改進:
| 項目 | RSS 2.0 (舊) | Atom 1.0 (新) |
|---|---|---|
| 內容 | 僅 summary | 完整 HTML (<content type="html">) |
| 作者 | ❌ 無 | ✅ <author><name> |
| 標籤 | ❌ 無 | ✅ <category term="..."> |
| 更新時間 | ❌ 無 | ✅ <published> + <updated> |
| 文章數量 | 20 篇 | 50 篇 |
技術挑戰:Markdown → HTML 轉換
Astro Content Collections 的 render() 返回的是 Astro Component,無法直接取得 HTML string。解決方案:
import { readFileSync } from "fs";
import matter from "gray-matter";
import { marked } from "marked";
function getPostHtmlContent(slug: string): string {
// 1. 讀取 markdown 檔案
const contentDir = join(process.cwd(), 'src', 'content', 'blog');
const files = readdirSync(contentDir);
const file = files.find((f: string) => {
const fileContent = readFileSync(join(contentDir, f), 'utf-8');
const { data } = matter(fileContent);
return data.slug === slug;
});
// 2. 解析 frontmatter
const fileContent = readFileSync(join(contentDir, file), 'utf-8');
const { content } = matter(fileContent);
// 3. 使用 marked 轉換為 HTML
return marked.parse(content, { async: false });
}
Atom 1.0 格式範例:
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-Hant">
<title>水溝神先知日記</title>
<subtitle>從金門出發的學習紀錄...</subtitle>
<link href="https://pwlee.dev" />
<link href="https://pwlee.dev/rss.xml" rel="self" type="application/atom+xml" />
<updated>2025-11-22T16:00:00.000Z</updated>
<author>
<name>Powei Lee</name>
<uri>https://pwlee.dev</uri>
</author>
<entry>
<title>實作 NLWeb 語意層</title>
<link href="https://pwlee.dev/blog/nlweb-semantic-layer" />
<id>https://pwlee.dev/blog/nlweb-semantic-layer</id>
<published>2025-11-22T00:00:00.000Z</published>
<updated>2025-11-22T00:00:00.000Z</updated>
<author>
<name>Claude Code</name>
</author>
<category term="AI" />
<category term="NLWeb" />
<summary>完整實作 NLWeb 語意層...</summary>
<content type="html"><![CDATA[
<!-- 完整的 HTML 內容 -->
<h2>為什麼需要 NLWeb?</h2>
<p>在研究 Agent Discovery Protocol 時...</p>
]]></content>
</entry>
</feed>
已實作的 Schema:
site/src/layouts/Layout.astro)const webSiteSchema = siteUrl ? {
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Powei Lee",
"url": siteUrl,
"inLanguage": "zh-Hant",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": `${siteUrl}/blog?q={search_term_string}`,
},
"query-input": "required name=search_term_string",
},
} : null;
新增項目:
inLanguage: "zh-Hant" - 語言標註potentialAction - SearchAction(告訴搜尋引擎網站有搜尋功能)const organizationSchema = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Powei Lee",
"url": siteUrl,
"founder": {
"@type": "Person",
"name": "Powei Lee",
"url": `${siteUrl}/about`,
},
"foundingDate": "2024-01-01",
"sameAs": [
"https://github.com/pwlee",
"https://linkedin.com/in/powei-lee",
],
};
新增項目:
founder - 創辦人資訊foundingDate - 網站創立日期site/src/pages/blog/[slug].astro)const articleStructuredData = {
"@context": "https://schema.org",
"@type": "BlogPosting",
"url": canonicalUrl,
"headline": post.title,
"description": post.summary,
"datePublished": publishedTimeIso,
"dateModified": modifiedTimeIso,
"inLanguage": "zh-Hant", // ✅ 新增
"wordCount": post.readingTime * 200, // ✅ 新增(估算)
"author": {
"@type": "Person",
"name": post.author ?? "Powei Lee",
"url": `${siteUrl}/about`, // ✅ 新增
},
"publisher": {
"@type": "Person",
"name": "Powei Lee",
"url": siteUrl, // ✅ 新增
},
"image": [post.coverImage],
"keywords": post.tags.join(", "),
"mainEntityOfPage": canonicalUrl,
};
新增項目:
inLanguage - 文章語言wordCount - 字數(透過 readingTime 估算,200字/分鐘)完成實作後,使用以下工具驗證:
工具: W3C Feed Validator
https://pwlee.dev/rss.xml檢查項目:
<content type="html"> 包含完整 HTML<category> tags 正確<author> 資訊完整<updated> 時間格式正確(ISO 8601)常見錯誤修正:
<content> 未使用 <![CDATA[...]]> 包裹 HTML<![CDATA[ 包裹所有 HTML 內容工具 1: Google Rich Results Test
https://pwlee.dev/blog/nlweb-semantic-layer)預期結果:
工具 2: Schema.org Validator
檢查項目:
@type: "BlogPosting" 正確inLanguage 符合 BCP 47 標準(zh-Hant)wordCount 為數字型別手動檢查:
https://pwlee.dev/llms.txtAI Agent 測試:
你可以直接詢問 AI(如 Claude、ChatGPT):
“請閱讀 https://pwlee.dev/llms.txt 並告訴我這個網站的主要內容”
如果 AI 能夠正確理解並摘要網站結構,代表 llms.txt 實作成功。
# 測試 llms-full.txt 生成
cd site
npm run generate:llms
# 檢查輸出
cat public/llms-full.txt | head -50
# 完整 build 測試
npm run build
# 檢查 build 輸出
ls -lh dist/
ls -lh .vercel/output/static/
預期輸出:
🤖 Generating llms-full.txt...
✅ llms-full.txt generated successfully
Output: /path/to/site/public/llms-full.txt
Content length: 2145 characters
問題: Astro 的 render() 返回 Astro Component,無法在 Node.js script 中使用。
解決方案: 直接讀取 markdown 檔案,使用 marked 套件轉換:
import { marked } from "marked";
import matter from "gray-matter";
const { content } = matter(markdownFile);
const html = marked.parse(content, { async: false });
問題: 手動更新會遺漏新文章。
解決方案: 使用 npm prebuild hook:
{
"scripts": {
"prebuild": "tsx scripts/generate-llms-full.ts",
"build": "astro build"
}
}
每次 npm run build 會自動執行 prebuild,確保 llms-full.txt 最新。
解決方案: 建立檢查清單並使用 TypeScript 型別檢查:
interface BlogPostingSchema {
"@context": "https://schema.org";
"@type": "BlogPosting";
url: string;
headline: string;
inLanguage: string; // ✅ 必填
wordCount?: number; // ✅ 建議
author: {
"@type": "Person";
name: string;
url: string; // ✅ 必填
};
// ...其他欄位
}
SEO 提升:
AI Agents 友善:
內容分發:
docs/agent-discovery-research-2025.mdNLWeb 語意層的核心價值在於:不需要複雜的新協議,只需要讓現有的網站結構更適合 AI 閱讀。
這次實作讓我深刻體會到,技術標準的成功不在於「多先進」,而在於「多實用」。ANP、A2A 等協議雖然技術完整,但沒有企業採用;反觀 NLWeb,因為簡單實用,已經有 12+ 企業在生產環境使用。
如果你的網站想要對 AI agents 友善,不妨從實作 llms.txt 開始 — 只需要 30 分鐘,就能讓你的網站加入「AI 可讀」的行列。
實作程式碼: GitHub - pw-astro 相關研究: Agent Discovery Protocol 研究報告
<content type="html">)。相較於 RSS 2.0,Atom 更適合 AI agents 解析完整文章內容。