「公司形象站 SEO 最大的痛是 — 沒人有時間每天寫文章。」
這是我們做完 gomylife.tw 的痛點。網站架構做完、Schema.org / OpenGraph / Sitemap 都調好,剩下「Google News / 搜尋引擎需要看到頻繁更新」這個 SEO 必要條件。
人工每天寫一篇 — 沒人有時間。沒文章 — SEO 排名不會起來。
我們做了一條全自動 daily news pipeline:每天 09:30 觸發、抓外部 AI 新聞、Claude 篩選改寫成繁中、加我們編輯部觀點、寫成 markdown、rsync 到 production、docker rebuild。人工介入 = 0。
2 週後被 Google News 收錄。
這篇拆完整流程跟踩過的坑。
整體流程 #
[crontab @09:30]
↓
news-cron.sh
├─ npm run news:generate
│ ├─ fetch AIHOT /api/public/daily(外部 AI 新聞聚合)
│ ├─ POST claude-proxy /chat(系統提示 + 整份 JSON 丟給 Claude)
│ ├─ Claude 回傳結構化 JSON(5-8 條精選 + 編輯觀點)
│ └─ 寫 src/content/news/YYYY-MM-DD.md(frontmatter + body)
├─ 偵測:今天的 .md 是新檔嗎?
│ └─ 是 → 繼續;否 → exit 0(不重 deploy)
├─ rsync src/ → production host:/opt/app/
└─ ssh production-host: docker compose up -d --build
為什麼這樣設計 #
幾個關鍵設計決策跟踩過的坑:
1. 外部 API 抓題不自己刷題 #
自己刷網路太花時間。用現成的 AI 新聞聚合服務(市面上幾家公開 REST API 可選):
// src/lib/news-client.ts
const BASE = "<https://news-aggregator.example>";
const UA = "Mozilla/5.0 (...) Chrome/124.0.0.0 Safari/537.36"; // ← 重要!
export async function fetchDaily(dateISO?: string) {
const url = dateISO
? `${BASE}/api/public/daily/${dateISO}`
: `${BASE}/api/public/daily`;
const res = await fetch(url, {
headers: { "User-Agent": UA, Accept: "application/json" },
});
if (!res.ok) throw new Error(`news API ${url} → ${res.status}`);
return res.json();
}
坑 1:很多公開新聞 API 用瀏覽器 UA 過濾(防 bot 濫用)。curl 預設 UA → 403。fetch Node.js 預設 UA → 403。必須帶完整 Chrome UA。
挑 API 時優先選「已分版塊的 daily summary」(每塊精選若干條),不要選 raw RSS — 後續用 Claude 處理會更乾淨。
2. Claude 用系統提示寫死「目標讀者」 #
關鍵是 system prompt 設定你站台的客戶輪廓 + 篩選原則,這樣 Claude 知道要挑哪些題目寫:
const SYSTEM_PROMPT = `你是 [站台名] 的 AI 新聞編輯。
[一句話描述站台業務]
客戶輪廓:[一句話描述目標讀者]
# 篩選原則
要收:
- [你的領域 A,例:企業 AI 整合]
- [你的領域 B]
- [你的領域 C]
不收:
- [不相關領域 1]
- [不相關領域 2]
# 輸出 JSON Schema
{
"items": [
{
"title": "繁中標題(30 字內)",
"summary": "100-200 字摘要",
"perspective": "50-100 字編輯部觀點(為什麼客戶該看)",
"sourceUrl": "...",
"sourceName": "...",
"category": "..."
}
]
}
`;
坑 2:必須強制 JSON Schema 輸出。不然 Claude 會輸出自由 markdown,後續解析痛苦。
坑 3:原始來源 JSON 整份塞給 Claude(不要自己 pre-filter)— Claude 篩選比正則更聰明。
關於成本:system prompt 通常 200-500 字、來源 JSON 視 API 而異(幾 KB ~ 10 KB tokens)。Claude Sonnet 跑一次大約 幾美分等級 — 每月 cost 對小站來說完全在合理範圍。
3. Markdown render(自動 frontmatter) #
function renderMarkdown({ dateISO, items }) {
const fm = matter.stringify("", {
slug: dateISO,
title: `${dateISO} AI 動態`,
short: items.slice(0, 3).map(i => i.title).join("、"),
publishedAt: dateISO,
author: "編輯部",
tags: [...new Set(items.flatMap(i => [i.category]))],
});
const body = items.map(i => `
## ${i.title}
${i.summary}
> **編輯部觀點**:${i.perspective}
[來源:${i.sourceName}](${i.sourceUrl})
`).join("\n");
return fm + body;
}
Next.js 的 [slug] route 用 getAllNews() glob src/content/news/*.md,新檔案自動有自己的 URL(如 /news/2026-05-20),自動進 sitemap.xml。零手動設定。
4. 冪等性:用「檔案是否存在」決定 skip #
async function main(dateISO) {
const file = path.join(NEWS_DIR, `${dateISO}.md`);
if (await fileExists(file)) {
console.log(`[skip] ${dateISO}.md 已存在,不重產`);
process.exit(0);
}
// ... 產文
}
坑 4:cron 一定要冪等。同天跑多次只會產一次,遇到失敗 retry 也安全。
5. cron wrapper 加「沒新檔就不 deploy」短路 #
#!/bin/bash
set -e
PROJECT_DIR="/opt/projects/your-app"
NEWS_DIR="$PROJECT_DIR/src/content/news"
REMOTE_HOST="user@production-host"
REMOTE_DIR="/opt/your-app"
cd "$PROJECT_DIR"
# 1. 跑 generator
BEFORE=$(ls "$NEWS_DIR" 2>/dev/null | wc -l)
npm run news:generate
AFTER=$(ls "$NEWS_DIR" 2>/dev/null | wc -l)
if [ "$AFTER" -le "$BEFORE" ]; then
echo "[cron] 沒新文章,不重 deploy"
exit 0
fi
# 2. rsync + 重建容器
echo "[cron] 偵測到新文章,部署中..."
rsync -az --delete \
--exclude=node_modules --exclude=.next --exclude=.git \
-e "ssh -o BatchMode=yes -o ConnectTimeout=10" \
./ "$REMOTE_HOST:$REMOTE_DIR/"
ssh -o BatchMode=yes "$REMOTE_HOST" \
"cd $REMOTE_DIR && docker compose up -d --build" 2>&1 | tail -5
echo "[cron $(date '+%F %T')] done"
為什麼這樣設計:
- 沒新文章 → 不重 build,避免無意義 container restart
- 有新文章 → rsync 全 source(不只 markdown,因為 sitemap.xml 是 build-time 產的,需要 rebuild)
- container restart 中斷服務 1-2 秒(healthcheck 過後恢復)
6. crontab 設定(cron 環境的坑) #
crontab -e
# 09:30 跑(早報時段,前一天的 AIHOT daily 已經結算完)
30 9 * * * PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin /opt/projects/your-app/scripts/news-cron.sh >> /var/log/your-app-news.log 2>&1
坑 5:cron 環境變數沒有 user 的 PATH!要明確設 PATH=...,否則 npm rsync ssh 都會 command not found。
坑 6:log 要 redirect 到檔案,不然 cron 跑失敗時你會收到 root mail 不會看。
SEO 效果 #
上線 2 週後 Google Search Console 觀察到的趨勢:
| 指標 | 變化 |
|---|---|
| Indexed pages | 明顯增加 |
| Google News 收錄 | 從未收錄 → 開始收錄 |
| Daily impressions | 數倍成長 |
| 國外搜尋曝光 | 大幅增加(特別是 AI 圈關注的英文技術詞) |
意外發現:daily news 文章內容是 AI 圈關注的主題,SEO 長尾自然打中 AI 工程師這個流量段 — 比直接寫專業文章便宜很多。
成本 #
整套 pipeline 一個月的固定成本:
- 新聞聚合 API:通常免費(或低月費)
- Claude Sonnet API:每天 1 次,每月 cost 對小站來說屬於零頭等級
- cron host:用既有開發機跑、無額外成本
對應的是「網站每天自動有新文章 + SEO 流量明顯成長」— ROI 完全成立。
變化型 / 擴充 #
| 想加什麼 | 怎麼擴 |
|---|---|
| 多語版本 | system prompt 加「同時輸出英文版」→ generator 寫兩個 .md(zh / en) |
| 多站台共用 | 同個 generator 給多站跑,差別在 system prompt + NEWS_DIR |
| 早/晚 兩次 cron | 08:00 早報 / 18:00 晚報 → cron 加一條、dateISO 帶不同段 |
| 失敗通知 | news-cron.sh 加 ` |
| 不同題材專業度 | system prompt 加「先列 5 個題材分類,再從每類挑 1 條」 |
| 圖片 | Claude API 加 generate hero image 或抓來源網站 OG image |
常見陷阱 #
| 陷阱 | 解法 |
|---|---|
| 新聞 API 回 403 | UA 一定要帶完整 Chrome string |
Claude 回 JSON 卡在 json ... block |
regex 抓 ``` 區塊解析 |
| cron 找不到 npm / node | cron 環境變數空,要 PATH=... 明確寫 |
| docker rebuild 拖到隔天 | 加 --no-cache=false 利用 cache、Dockerfile 縮層 |
| 同一天跑多次重複產文 | 檔案存在就 skip(冪等) |
| sitemap.xml 沒更新 | Next.js sitemap.ts 用 getAllNews() glob 自動掃即可 |
重點 #
| 設計 | 為什麼 |
|---|---|
| 用外部 API 抓題 | 不要自己刷網路抓 RSS,浪費時間 |
| Claude 用 system prompt 寫死讀者輪廓 | 篩選結果跟客戶相關度高 |
| 強制 JSON Schema 輸出 | 結構化資料、後續好處理 |
| 冪等設計(檔案存在 = skip) | cron 失敗 retry 安全 |
| 沒新文章 = 不 deploy | 避免無意義 container restart |
| sitemap 自動帶 | Next.js getAllNews() glob 解決 |
| cron 明確設 PATH | cron 環境變數預設空 |
整套設計上線後幾乎完全不用管。SEO 效果好、成本低、維運 0。
想做類似的自動內容引擎 #
我們做 Web 應用整合 AI / 自動化內容 pipeline:
- 公司形象站 daily news / blog 自動產文
- 電商網站商品描述 / SEO 文案自動生成
- 內部知識庫 → 對外白皮書自動轉換
- 多語站點機翻 + 編輯 pipeline
聯絡:0912852835 / henryccy@icloud.com / LINE @3q3tw / 線上表單
延伸閱讀: