開發日誌
記錄小島神話的開發歷程:功能上線、內容里程碑、資料模型演進,也包含踩雷與撤回。每則附工程細節可展開。
-
文章網址搬家——/articles/ 一次性遷入知識庫,舊連結自動轉址
文章的網址從 /articles/文章名 搬到 /knowledge/分類/文章名,網址本身現在就說得出這篇文章屬於哪個主題;24 篇舊網址全部設好自動轉址,外部既有連結一個都不會斷。同時每篇文章補上發布日期,「最新文章」從此是真的最新,訂閱 RSS 也看得到發布時間;新增「全部文章」一頁可以從新到舊一次瀏覽。
工程細節
這是 2026-06-12 知識庫規格裡就埋下的「URL 分歧記錄」的結案:規格書 4.1 一直定義文章頁該住在
/knowledge/{category}/{slug}/,但 milestone-1 實作在/articles/{slug}/,當時決定等正式網域上線再一次性遷移、避免破壞連結兩次。myths.tw 上線後這個前提到期,而且每多等一天,舊址被搜尋引擎索引、被外部分享的量就多一天——轉址債只會變厚,所以趁早結案。轉址是這次的核心工程。Pages
_redirects的:slug佔位符查不到 frontmatter 的分類,一條 splat 規則組不出目的地,只能逐篇列舉。好在集合天然封閉:遷移日之後出生的文章(content-loop 每天還在產)從未存在過 /articles/ 網址,不需要轉址——所以產出的是「凍結快照」:當日 24 篇 × 含/不含斜線兩行(保證一跳到位、不吃雙重轉址)+列表頁 2 行,共 50 條規則,產生器scripts/migrations/gen-articles-redirects.mjs入版控留痕,區塊本身勿再增修。全站組址收攏成一個函式。原本文章連結是五、六處各自手拼
/articles/${id}/字串;現在統一走articleUrl(id, category),住在src/lib/knowledge-categories.mjs——原本是 .ts,改成 .mjs+d.ts(仿 feast-parse 慣例)是因為 remark-wikilinks 在 build 期跑在 astro.config 的載入鏈裡,也要組文章網址。未知分類直接 throw,build 期 fail-fast,抓「enum 加了新分類但固定表沒跟上」的漂移。清單頁採乙案:/articles/升格為/knowledge/all/全部文章平面視圖,文章宇宙從此收攏在單一 namespace。順手補齊了拖很久的日期缺口。articles schema 一直沒有日期欄位,「最新兩篇」其實是檔名字母序。這次加
publishedAt(optional——必填會炸 content-loop 既有草稿契約),24 篇舊文以 git 首次 commit 日回填(scripts/migrations/backfill-published-at.mjs);總覽預覽、分類 hub、all 頁、RSS 全部改吃此欄。RSS 同時把 guid 改成文章 id(isPermaLink=false):識別與網址從此脫鉤,這次訂閱者會看到整批重複一次(link 全換無可避免),但下次再改址就不會了。架構代價要記住:分類進了網址,改 category 等於改永久連結。對策寫進 EDITORIAL——已發布文章不改分類;真要改的個案,
_redirects補一條舊址轉新址。驗證方式也記一筆:本機 VM 沙箱跑不了完整 build(better-sqlite3 是 Mac 二進位),這次改在雲端 Linux 沙箱npm ci原生編譯後跑完整npm run build——dist 24 篇新路徑齊、殘留舊連結 0(html/js/pagefind 全掃)、sitemap/RSS 皆新址,回寫後 48 檔 md5 與驗證樹逐位元一致。互動分析零影響:計數鍵是 (type, id) 不是路徑,累計瀏覽數完整保留,推薦欄存的舊 url 會隨瀏覽 upsert 自癒。 -
「雙嶼日出」上任——品牌標記進了導覽列與瀏覽器分頁
小島神話終於有了自己的圖案。設計系統裡定稿的標記「雙嶼日出」——朱砂的雙島、金色的日與海線——正式放進網站左上角的站名旁邊,也做成了 favicon,加入手機主畫面時不再是一個空白預設圖示。首頁的分頁標題同時改成完整的「小島神話 Mythsland - 紀錄島嶼的眾神諸佛」。
工程細節
站名從 miao 改成小島神話/Mythsland 是 7 月中的事,但改名之後有一段時間,網站的視覺識別只剩下文字——導覽列是純文字站名,瀏覽器分頁是預設的白紙圖示,加入手機主畫面出來一個灰塊。設計系統規格書(
docs/design-system/)這邊已經把標記定稿了,缺的只是落地。BrandMark 元件。新增
src/components/BrandMark.astro,幾何完全依規格書。配色用的是深底版本——朱砂提亮一階到cinnabar-400、金日gold-300、海線gold-400,因為導覽列是深色底,直接套淺底版的朱砂會糊掉。尺寸 30px(規格書指定的頁首尺寸),置於站名左側。順手把 brand 區塊改成 flex 置中對齊,同時讓站名文字與 tagline 維持 baseline 對齊——這兩件事要同時成立需要一點克制,圖標垂直置中、文字水平基線對齊,不能只用一個align-items打發。favicon 與 apple-touch-icon。依規格書第柒節(宣紙底、圓角、標記佔畫布 72%)產出
public/favicon.svg與 180×180 的apple-touch-icon.png。PNG 是用 Playwright 從 SVG 轉出來的——已經為了手機版檢查裝了 Playwright headless Chromium,拿它當一次性的 SVG 光柵化器,比為此再引入一個圖形處理相依划算。BaseLayout與靜態的public/404.html兩處 head 都補上 icon 與 apple-touch-icon link;404 頁是純靜態不走 layout,很容易漏。首頁標題的一個小 prop。首頁的
<title>原本是「首頁|小島神話」,作為 SEO 與社群分享的標題太弱。想改成完整站名加標語,但BaseLayout一律會補「|小島神話」尾綴,直接傳完整標語會變成疊字。做法是給 BaseLayout 加一個titleSuffixprop(預設true,其餘頁面行為不變),首頁傳false時直接使用傳入的 title。這種「預設維持舊行為、例外才 opt out」的加法,是改共用 layout 時最不容易誤傷的形狀。 -
知識庫自動互連——文章裡提到哪位神明,就自動連過去
讀一篇講中元普渡的文章,內文提到「地藏王菩薩」,現在會自動變成連結,點一下就到祂的條目。這件事以前要靠人一篇一篇手動標記,239 篇裡只標到 135 篇,漏標的地方就是斷掉的路。改成渲染時自動判斷之後,既有內容與未來所有內容都一體適用。
工程細節
知識庫的價值有一半在互連——讀者從一位神明走到祂的節慶、再走到主祀的廟宇,這條路走得通,站才像一個網而不是一疊文件。原本的做法是手動
[[wikilink]],問題是覆蓋率:239 個內容檔裡只有 135 個標過,而且「有標」不等於「標全」,同一篇裡第二次提到同一位神明多半就漏了。手動標記的覆蓋率只會隨著內容增加而繼續稀釋。定案方案是渲染層自動連結,不改寫 content md。這個取捨很關鍵:如果選擇批次改寫既有檔案塞 wikilink,等於把一次性的工具產出永久寫進 SSOT,日後詞典變了要再改一次,而且 diff 會髒到看不出人為修改。改在渲染層做,content md 保持乾淨,未來寫的每一篇也自動涵蓋,不需要作者記得標。
實作是一支新的 remark plugin
src/lib/remark-autolink-entities.mjs,掛在remarkWikilinks之後——順序有意義:先讓手動標的實體成為 link,自動層才會把它們視為「已連」而不重複處理。plugin 在 build 時 fs 掃 deities(name加titles全部聖號別名)與策展 temples(name)建詞典,對純文字節點做 alternation 比對,最長優先(避免「玄天上帝」被「上帝」先吃掉)。防呆規則是這支 plugin 的主體,因為自動連結最大的風險是連錯:
- 歧義排除:像「閻羅天子」這種 key 直接除名,同名指涉不只一位。
- STOPLIST:龍王、王公、羅漢、飛熊、代天巡狩、世尊——這些詞在中文行文裡太常作為通稱或泛指,連了反而誤導。
- 每頁每實體只連首次:一篇文章裡「媽祖」出現三十次,連三十個藍字沒有人讀得下去。
- 頁內已有手動連結則整頁不補:作者若已經自己標過,代表他有意識地在控制連結密度,機器不插手。
- 自我排除:媽祖的條目頁不會連到自己。
- 建物後綴防呆:「保安宮」「行天宮」這類——當實體名後面接的是宮/廟/寺⋯⋯爐等建物後綴,那是在講一座廟不是在講一位神,不連。
- 書名號內不連:《》〈〉裡的是經典篇名,不是實體提及。
範圍限定在 articles/deities/temples/saga 四個 collection,devlog 排除在外——站務敘事裡提到神明是在講工程,不是在講信仰,連過去沒有意義。非目標也明列了兩項:全量廟宇庫(1000 多筆裡同名歧義太多,詞典會失控)與文章標題(同理)。
順帶做了一件枯燥但該做的事:
remark-wikilinks與新 plugin 有重複的 frontmatter 掃描邏輯,抽成共用的src/lib/content-scan.mjs,行為不變。兩支 plugin 各自維護一份掃描碼,遲早會漂移。 -
站務儀表板改頁籤,並在頁尾埋一道只有站長看得見的門
站內有一頁 /ops/ 是給站長看的「工程進度」——哪些內容該補、哪些神像卡在哪一關、哪些查核還沒做。這次把它從一路捲到底改成四個頁籤,並新增一張神像產製的總覽表。同時在每一頁的頁尾預埋了一個入口,只有站長自己的網路連進來時才會顯現,其他訪客與爬蟲完全看不到。
工程細節
/ops/是自治閉環的觀測窗:機器每天自己取題、產內容、開 PR,人只做終審,那麼「現在整體長什麼樣」就得有一頁看得到。它原本是四大區塊縱向堆疊,內容一多,看第四塊要捲很久。頁籤化。指標 strip 之下拆成四頁籤——優先序建議、backlog 進度、內容查核佇列、神像產製進度。實作用 ARIA
tablist,配 hash 深連結(#tab=<key>)讓某一籤可以直接分享或加書籤,方向鍵可循環切換,並監聽hashchange讓同文件內的導航(不重載頁面的那種)也能正確切籤。順手拿掉 lead 裡硬編的「對照」句——backlog.updated本來就自帶,先前是重複顯示。神像產製表。這是新增的第四籤,解的是「每一尊神像現在卡在哪」沒有總覽視角的問題。神像閉環的狀態不是單一欄位能表達的:
work-queue裡的build-deity-image任務有六種狀態,但同一個in-review底下,實際可能是「等使用者產圖」「母檔已收件等後製」「後製報告卡人工」「已開 PR 等終審」好幾種處境。src/lib/image-pipeline.ts的做法是拿佇列任務跟 deities collection 交叉比對,再依落點檔案存在與否推導子階段——pending-tg、image-intake底下的 original/cleaned/POSTPROCESS-REPORT、pending-pr、public 下的 webp,逐層往下推。排序刻意不按字母,按「該理誰」的順序:escalated → 待產圖 → 待終審 → 後製卡關 → 機器在途 → open → 未入佇列 → dismissed → done → 既有圖。佇列檔缺席時回null顯示「無佇列資料」,沿用雲端 build 缺檔降級的先例,不讓一個缺檔炸掉整頁。手機上這張表在容器內橫捲。頁尾的隱形入口。全站是 SSG,頁尾是 build 時就定死的靜態 HTML,沒辦法在伺服器端按訪客 IP 出不同內容。所以做法是一個執行期的判斷點:新增
GET /api/ops-gate(SSR,prerender=false),讀CF-Connecting-IP比對站長 IP 白名單,回{ops: boolean},並帶Cache-Control: no-store防止這種因人而異的回應被快取住。頁尾在「開發日誌」旁邊預埋一個hidden的「工程進度」連結——初始 HTML 就是隱藏的,非白名單的 client 從頭到尾不會被揭示,爬蟲也抓不到。client script 先查sessionStorage快取(mt:ops-gate),沒快取才打端點,回 true 才揭示;任何失敗一律靜默,沿用互動分析 track-client 的旁路原則——輔助功能壞掉不該讓使用者感覺得到。dev 環境一律回 true,與/ops/頁本身 dev 一律可見保持一致。要說清楚的是:這個入口是便利功能,不是防線。IP 白名單能被偽造、
hidden屬性能被翻出來,/ops/真正的保護仍然是 Cloudflare Access。規格 §3 把這個定位寫進去了,免得日後有人以為這道門擋得住誰。IPv6 的邊角情況(站長從 v6 位址連入不在白名單內)也一併記了。 -
自治閉環進入滿速——四天併入 19 筆自動 PR
站上的內容現在有一條穩定的產線:機器每天自己找出缺口、查資料、寫草稿、開一個待審的 PR,人只做最後一道把關。這四天累積併入 19 筆——8 篇文章、7 尊神像、3 筆節慶行事曆,外加 1 篇開發日誌。同期也把產線上兩處還要人工介入的地方補成自動。
工程細節
自治閉環(ADR 0007)從 7 月 16 日上線,前一週還在補管線;每日取題上限在 7 月 23 日從 2 筆調到 4 筆之後,產出量才真正跑起來。7/23 到 7/26 這四天,併入 main 的自動 PR 共 19 筆:
- 文章 8 篇:釋迦牟尼佛、讀懂媽祖神像、讀懂觀音神像、讀懂玄天上帝神像、讀懂關帝神像、五府千歲怎麼認、瑤池金母與台灣母娘信仰、讀懂保生大帝神像
- 神像 7 尊:五福大帝、五路財神、五年千歲、三官大帝、三府王爺、五顯大帝、伏羲
- 節慶行事曆 3 筆:有應公、西方三聖(大勢至菩薩聖誕)、五福大帝五靈公五列
- 開發日誌 1 篇
每一筆都經過 AI 草稿、AI 稽核、人工終審三道。要強調的是第三道沒有被自動化,也不打算自動化——發布權留在人手上是這個閉環的前提,機器省掉的是查資料與寫初稿的重複勞動,不是判斷。
Stage 4b:母檔歸檔補成自動。神像閉環有一段一直是人工的:使用者產出的圖檔落在 Downloads,收件處理完之後要手動搬到
~/Pictures/miao-神像母檔/閉環產出歸檔。7/21 一次性搬遷過一輪,然後立刻又積壓三張——人工步驟在每日節奏下不可能不積。這件事有實質風險:macOS 的 Downloads 有 30 天自動清除,而 R2 備份只涵蓋 Pictures 目錄,等於母檔放在一個會自己消失又沒被備份的地方。image-bridge.mjs補上的做法:收件成功後原檔自動搬入閉環產出,並 best-effort 追加一列 provenance README 索引(讀 PNG 的 IHDR 取尺寸、md5 去重);已經收件但仍留在收件匣的同內容舊檔也補歸檔。跨磁碟搬移失敗時退回 copy 加 md5 校驗。歸檔遇到同名舊母檔,改名為<名>.v<日期>.png保留——永不覆寫,母檔是不可再生的東西。母檔目錄不存在時只警告不歸檔,不中斷收件。同時修掉一個靜默的坑:原本的
hasIntake規則會讓已收件的檔案永遠留在收件匣,而重新產圖的同名新檔會被永遠略過——也就是「這尊圖重畫了」這件事管線根本看不見。判準改成:同名但 md5 不同、且任務在in-review,即認定為重收件,把舊的 intake 目錄整包移到data/image-intake/.superseded/<slug>-<時戳>/再重走一次收件。要連 POSTPROCESS-REPORT 一起移走,否則沙箱的 Step 0 會讀到舊報告,判斷「已處理過」就跳過不重做。這種「舊產物讓新輸入被靜默略過」的形狀,在任何有中間檔的管線裡都會反覆出現。通知分流。閉環跑滿速之後,Telegram 上部署通知與內容通知混在同一個 topic 裡,部署狀態被內容訊息淹掉。
notify.mjs加了deployThreadId設定位:TG_THREAD_ID的語意改為「deploy 以外所有通知的預設 topic」,deploy 自帶TG_DEPLOY_THREAD_ID覆蓋留在原 topic。deploy.mjs四個通知點(kill-switch/前置失敗/完成/失敗)全部帶上覆蓋。產圖通知則反過來併回一般 topic,image-bridge裡「見產圖 thread」的文案跟著失真,一併拿掉。deity-image-loop.md規格裡「發布位置=獨立 thread 5」的舊條目用劃線保留沿革、下面補記現況——規格改動留下歷史痕跡,比直接覆寫更能解釋「為什麼會變成現在這樣」。 -
自動開 PR 橋接補上節慶資料型別
站點的內容自動化再進一步——節慶行事曆(民俗節日)的資料現在也能由 AI 產出、自動開 PR 送人工審核,成為繼神祇、文章之後第三種走自動流程的內容。同時審核通知改成每筆直接附上 PR 連結,終審者少一次翻查。
工程細節
自動開 PR 橋接(
open-prs)是把 AI 在沙箱裡產出的草稿,變成一個待人工終審 PR 的那一段機器。它原本只認兩種產出:內容草稿(神祇、文章的 md 檔)與圖像題。節慶建檔(calendar-feast)的產出型態不一樣——它不是新建一個 md 檔,而是往festivals.yaml追加一段 YAML 條目。橋接沒有它的處理路徑,於是沙箱 run 只能把結果寫成.json.ready積在原地等人工,當時已積了 2 筆,佇列後方還排著同型任務。補上的做法是新增一條
kind:'yaml-append'分岔:從草稿裡抽出唯一一個 YAML 區塊,用 js-yaml 先驗「追加之後整份檔案仍可解析、且name沒有重複」,通過才追加到目標檔,接著開分支、commit、push、印出 PR 連結。有兩點刻意跟內容草稿流程不同——草稿保留不刪(裡面存著稽核表與內文修正建議),以及 merge 之後post-merge沒有這型別的自動收尾(沒有 frontmatter 可翻狀態),佇列任務得人工標 done。多個分支追加到同一個檔尾,merge 第二筆時可能要 rebase 解尾端衝突——這個限制照實記在契約裡。上線前在 scratch clone 端對端跑過兩筆真實記錄(--no-push),確認分支、驗證、佇列流轉都對,才把契約寫進content-loop.md的 Stage 5。同一天順手補了審核通知的體驗:待終審的 Telegram 通知原本只列出 slug 與分支名,終審的人得自己翻本機 log 或手動拼網址,才拿得到開 PR 的連結。抽出一個
prUrl()統一組連結,三處通知文字與 console log 都改成直接附上,少一段不必要的查找。這兩筆都不是華麗的新功能,而是自治閉環的基礎建設補強。但橋接要接的縫,正是「AI 產出」與「人工終審」之間那一段——每少一次人工查找、每多接住一種產出型別,這條把發布權留在人手上、把重複勞動交給機器的流水線就更順一分。
-
全站錯誤回報上線——每一頁都有一顆香火圓鈕
站上的內容有錯字、記載可議、或哪裡壞掉了,現在讀者可以直接說。每一頁右下角都有一顆香火色的圓鈕,點開就能填問題、拖一張截圖進去送出,不必註冊、不必寫信。三天後補上選填的稱呼與 Email,願意留的人我們才有辦法回覆。
工程細節
一個以事實查核為賣點的站,最怕的是錯誤沒有回頭路。神明聖誕差一天、敕封朝代寫錯、廟宇地址搬過家——這些只有真正在拜的人會發現,而他們發現的當下若沒有一個「兩秒鐘就能講」的入口,這個發現就流失了。錯誤回報 V1 要解的就是這個。
收單管線。
POST /api/report的處理順序是刻意排的,攻擊面在入口一次性擋掉:Origin 白名單(只收 myths.tw、miao.test、127.0.0.1,pages.dev 與 preview 刻意不收)→ bot UA 過濾 → 總量粗篩 → Turnstile siteverify → 欄位消毒 → 截圖 magic bytes 複驗 → 生 ULID → 寫 R2(副檔名依實際嗅探結果重寫,不信任使用者給的檔名)→ 寫 D1 → 發 Telegram(best-effort,通知掛掉不影響收單)。資料落在兩個獨立資源:D1miao-feedback存單、R2 私有桶miao-feedback存截圖,都不與正式內容庫混用。前端的成本控制。BaseLayout 注入的是純 HTML/CSS 的圓鈕,首載零 JavaScript、不動渲染路徑;點擊當下才動態 import modal 與 Turnstile。表單支援三種上傳途徑(拖曳/選檔/貼上),前端先做一次 magic bytes 初驗擋掉明顯偽檔,後端再驗一次——前端那層是省流量,不是防線。
三天後的規格放寬。V0.1 刻意 YAGNI 不收聯絡資訊,實際上線後發現:收到「這裡怪怪的」但無法追問細節,等於收到一半的情報。於是 v0.3 補上稱呼與 Email 兩個選填欄,配套是三態消毒語意——
null代表未填(照收)、有值代表格式合格、undefined代表填了但不合格(拒單)。選填但填了就要對,避免資料庫裡躺著一堆打不通的地址。回信流程本身推遲到 V3 再議。兩個踩雷,照實記。上線三天後修掉兩個都是自己造成的問題。其一,短頁面的回報鈕永久隱形:頁尾淡出用 IntersectionObserver 判斷 footer 是否進入視窗,但像大甲鎮瀾宮、
/knowledge/這類內容不足一個視窗高的頁面,footer 從一開始就在視窗裡,鈕一載入就被判定該淡出——修法是淡出邏輯只在可捲動距離超過 240px 的頁面啟用。其二,神明頁的回報鈕懸在半空中:首頁的行事曆浮牌會把鈕往上抬,抬升邏輯用.feast類名找浮牌,結果神明頁的聖誕資訊行也叫.feast,抬升量拿內文元素去算,桌機硬是被推高 602px——修法是選擇器改鎖.feast[data-feast],用浮牌本體獨有的屬性。兩個都是「靠類名找元素」的老問題,在一個元件散落全站的功能上會放大。還有兩項是機器做不了的人工待辦,登記在
docs/MAINTENANCE.md裡等處理:Turnstile widget 的正式設定,以及 WAF 規則要擴到回報路徑。V2 的分流與 V3 的審核台都還沒動——目前收到的單是靠 Telegram 通知人工看。這是刻意的順序:先讓話有地方講,再想怎麼把話整理好。 -
頁面開始記得誰來過——瀏覽數與「其他人也讀了什麼」
廟宇、神祇、故事、專題與知識庫文章的頁尾,現在會累計這一頁被閱讀的次數;等足夠多讀者走過同一條路徑,還會浮現「其他人也讀了什麼」的同類推薦。全程匿名——不發 cookie、不存 IP、不追蹤任何人。剛上線時的安靜是預期中的醞釀:數字與推薦,會隨香火自己長出來。
工程細節
一個月前的規劃(spec v0.1)原本要把分析後端放到自有主機的 PHP+MySQL 上;這次覆閱時整個翻案(ADR 0008):自治營運落地後 launchd 運行時已就緒、「Pages 不能 cron」不再成立,更關鍵的是發現推薦根本不需要每日聚合——讀取當下對單一實體做索引點查即可。於是整套系統縮進站本體:獨立 D1
miao-analytics、同源/api/track與/api/stats,零第二主機、零 CORS、零 cron,對一人營運是最小的維運面。攝取走旁路,渲染路徑一根手指都不碰。計數靠頁面載入後 idle 時機的
sendBeacon——fire-and-forget,後端慢了掛了使用者都無感,靜態頁的 cache 命中與 TTFB 原封不動。哪些頁要計數由 BaseLayout 的analyticsprop 點名:五類內容頁傳入即啟用,hub、列表與工具頁沒傳就是零輸出,不需要維護任何黑名單。推薦是即時算的。同一位訪客近期讀過的同類頁面,會以對稱共現對(字典序去重)累計進
covisit;讀取時兩條索引各掃一段、取 top-K,一次查詢就是一份推薦。共現次數不到門檻(MIN_CNT=3)不顯示——既擋雜訊,也避免「只有一個人走過的路徑」洩漏個人足跡。冷啟動期間整個區塊優雅隱藏,不出現空框。防刷與消毒是分層的。JS beacon 天然濾掉不跑 JS 的爬蟲;zone 層 Rate Limiting 掐住腳本洪流;端點驗來源白名單、丟 bot UA;前端 30 分鐘節流擋狂刷重整。標題與網址來自 beacon、屬攻擊者可控,入口 strip tags+僅收站內相對路徑,輸出端一律
textContent組 DOM——雙重防護。定位誠實:這是熱度訊號,不是審計計票。維運掛進既有的骨架。分析庫併入每日 D1 匯出 → R2 備份輪替(庫未建也只警告、不擋部署);額度天花板誠實量化過——最緊的 D1 寫入約當日均一萬四千次瀏覽,離現況兩個數量級,超標的路是五美元月費,最後的退路是那台已確認可用的老 PHP 主機。願它永遠用不上。
-
首頁多了一扇「今日聖誕」曆牌——把神明行事曆迎到最顯眼的地方
過去神明行事曆只藏在頁尾與內文連結裡,訪客不容易遇見。這次替首頁加了一扇黃曆曆牌浮窗:左頁報今天農曆幾號、什麼干支,右頁列出近日輪到哪幾位神明的聖誕。信眾一進站就知道「這幾天該向誰上香」,把日常時序和神明重新繫在一起。
工程細節
行事曆一直都在,但它只掛在頁尾與文章內連結——訪客得先想到要找、才找得到。神明與信眾的連結,本該由「時序」牽起:初一十五、某位神明的聖誕,是信仰在生活裡的節拍。把行事曆迎到首頁,等於把這條節拍還給每一位進站的人。
黃曆曆牌浮窗(DeityFeastWidget)。定調成一張對開曆牌:左頁是今日農曆大字+干支,順帶答了「今天農曆幾號」這個最常被問、卻最難即時查的問題;右頁是近日聖誕清單,寫到誰、就能一鍵連到那位神明的百科與供奉廟宇。它是首頁一個顯眼但不擾人的入口,也是行事曆的縮影。
淡季不留空窗。神明聖誕在曆上分佈不均,硬性「只看近五日」會在淡季開天窗、反而顯得冷清。
upcomingFeasts加了第 4 個參數minCount=3:近五日不足三筆就無限往後追到補滿,標題隨之在「近五日聖誕/近期聖誕」之間切換,遠日的相對時間顯示為「N 天後」。無論哪一天進站,曆牌上都有神明在等你。行事曆改成 18 個月西元滾動視窗。原本要靠人工逐年延展資料;改成「當月一日起 18 個月」過濾、
buildCalendar取[今年, +1, +2]三年現算農曆,配合每日 04:30 的 deploy 重 build,日期層每天自動往前滾一格,再也不會有「明年的初一查不到」。農曆解析抽成 SSOT(feast-parse)。曆牌與行事曆缺口自治都要把「農曆某月某日」的聖誕字串解析成日期,原本 regex 散在兩處、容易漂移。抽出
parseFeastDay為純 JS 模組(先切尾註再比對格式),Node 與 Vite 兩端共載一份,來源唯一。缺口自治(calendar-gap)。有些神明香火鼎盛、卻因聖誕字串格式不齊而無法在行事曆現身。新增
calendar-gap.mjs掛進每週scan.mjs,把「有香火卻上不了行事曆」的神祇撈進人工佇列補列,香火門檻(廟數)由 20 下修為 12,讓更多地方神也能被時序接住。當晚緊接著一輪體感潤飾(fix 94b0a2c):收合/關閉鈕改成等大 26px 方框+SVG 線條做幾何對齊;收合態把左曆牌縮成一行日期 chip、整張壓成約 78px 矮 bar,消掉右側留白;電腦版依回饋從底部置中改到右下角靠邊,手機版維持貼底滿版。一扇曆牌,要夠顯眼、也要夠安分。
-
社群分享圖上線——接進神像閉環
把網站連結分享到社群平台,現在會亮出漂亮的預覽圖了。首頁與神祇頁先行,而且神像定稿時會自動連分享圖一起產好。
工程細節
Open Graph/Twitter 卡從首頁與神祇頁做起——神祇頁是最常被分享的頁型,且神像素材現成。
重點不在圖而在管線:分享圖產製直接掛進 deity-image 閉環,神像定稿的同時自動產出該尊的 OG 圖,不另設人工步驟。新神像上頁即帶分享圖,存量再逐步回補。
失敗路徑也補了通知:神像 PR 產分享圖失敗時發 TG,不再只寫 console——自動化管線裡沉默的失敗最貴,這條原則在整個自治體系裡一體適用。
-
神像產圖閉環 v0.2——免費半自動產線
神像圖也有了產線:AI 產圖、Telegram 前置審核、定稿自動上頁。既有十尊神像同步更新為去浮水印版本,並新增雷震子、東海龍王、地藏王菩薩、玉皇上帝四尊。
工程細節
6 月手工做神像的痛(產圖、去浮水印、逐尊上頁)收斂成半自動閉環:產圖 → TG 發圖前置審 → 人在 TG 裡定奪 → 定稿自動進站。走免費產圖方案,v0.2 版本號誠實反映它還在演進(規格定案是 Gemini API 路線,現階段先用免費半自動頂著)。
TG 通知按主題分流:產圖摘要發產圖 thread(TG_IMAGE_THREAD_ID),不跟內容閉環的通知混流。存量整理同步做掉:十尊舊神像換去浮水印版,再補四尊新神像。
缺口統計:全神祇庫尚缺 94 張神像——這條產線的存在意義就是把這個數字磨到零。
-
手機版跑版總清理與 Playwright 掃描
修掉手機版四處中文逐字直排的跑版與導覽列破版,並建立自動掃描腳本——之後每次改版都能一鍵檢查 22 個頁面模板有沒有跑版。
工程細節
中文跑版的病灶很典型:flex/grid 擠壓下中文逐字直排。一次掃出四處修掉,並把「病灶模式」寫成自動檢測:Playwright headless 掃頁面橫向溢出、逐字直排、元素超出視窗三種症狀,命中即 exit 1。
改用 Playwright 也是工作流決策:瀏覽器擴充對
.test本機網域無法記住授權,每個動作都跳權限視窗。腳本直打 dev server(繞過 https 代理),無參數掃 22 個代表頁模板,--shot存全頁截圖,--viewport換尺寸。順帶記一條當天踩的雷:
npm install改寫 package.json 觸發 dev server 設定重載,可能讀到寫一半的 JSON 卡成半殘(程序活著但 port 沒聽)——裝完套件要確認 port,沒聽就 kickstart。 -
AI 自治營運落地——通知、備份、護欄、儀表板
網站開始「自己照顧自己」:每日排程自動運作,所有動作發 Telegram 通知,資料每日備份輪替,進度儀表板公開透明。人只在關鍵處把關。
工程細節
ADR 0007 從規格變成現實的兩天。通知線:所有自動化動作發 TG(支援主題群組、格式定版),配速率護欄與 T3 觸發詞攔截——高風險動作直接擋下待人工。備份線:R2 備份輪替,近 30 天每日檔+月初檔留 12 個月。儀表板:公開
/stats/覆蓋率頁+Cloudflare Access 保護的/ops/工程進度頁。護欄是重頭戲:token 預算 ledger(月度取題上限 60,逾限停題+TG 通知)、work-queue 檔案鎖防排程重疊互蓋(claim 原子化,還得繞過沙箱禁刪檔的 EPERM)、終審收尾自動化(post-merge 腳本接 deploy 流程)。
實體機的雷也一併處理:launchd 排程以 caffeinate 包裹防執行中睡眠、04:25 wake-window 撐兩小時涵蓋部署與內容閉環。7/17 常態排程開跑,每日 05:30 自動產出。
-
神祇自動建檔閉環——AI 寫、AI 稽、人終審
神祇建檔進入半自動時代:AI 起草、AI 稽核、開 PR、人工終審後自動收尾。普庵祖師、三府王爺、西方三聖是頭三尊走完全流程的神祇。
工程細節
內容閉環的完整路徑:排程取題(照主祀覆蓋率缺口排序)→ AI 草稿 → AI 稽核 → 自動分支開 PR → TG 通知 → 人工終審 merge → post-merge 自動收尾(reviewStatus→reviewed、補 lastVerified、回推部署)。
頭三尊試金石:普庵祖師(7/16 首發)、三府王爺、西方三聖(7/17)。三篇皆以
auto/deity-*分支走 PR 流程,7/17 終審收尾一次補上狀態欄。設計原則是「AI 可以做很多,但發布權在人」:PR 是強制關卡,reviewStatus 生命週期(draft→ai-audited→reviewed→published)讓每篇內容的查核狀態可稽核。取題量由 token 預算 ledger 節流,不因為自動化就無限產出。
-
品牌正名「小島神話」與正式網域 myths.tw
站名從「神島誌」正名為「小島神話」,正式網域 myths.tw 上線,並裝上 Google Analytics 開始看見讀者。
工程細節
「神島誌」是 6/11 定的站名,用了一個月後正名「小島神話」——更口語、更接近內容本體(島嶼的神話),英文域名 myths.tw 也能對上。改名是全站 refactor:站名散落首頁、頁尾、meta、通知前綴,一次收攏。
網域架構:myths.tw zone 同帳號代管,root CNAME 指向 Pages(proxied);pages.dev 預設網域保留可存取,全站加 self-canonical 指向正式網域,SEO 權重不分裂。
GA4 同日全站安裝——結果隔天才發現追蹤碼從未觸發:inline script 被誤用 JSX 模板包裹,Astro 把它當成需要 hydration 的內容處理掉了。7/17 修正,教訓是「裝了」和「動了」是兩件事,第三方腳本上線要看得到資料才算數。
-
神像上頁與流光特效
神祇頁開始有「臉」了:關聖帝君、觀世音菩薩、七星娘娘、二郎神、中壇元帥的神像陸續上頁,搭配四種模式隨機的流光特效,列表卡片也加上神像背景。
工程細節
點雲撤回後的新路線:AI 產製平面神像圖+CSS/JS 流光特效。流光做了四種模式隨機(含香火),每次進頁氛圍略異;特效中途改組過一次架構以便掛更多神像。
一個有意識的決策:移除了 prefers-reduced-motion 關閉特效的機制。特效是神像呈現的一部分而非裝飾,且強度溫和;這個取捨記錄在案,若有使用者反映再檢討。
此時神像仍是手動一尊一尊做——產圖、去浮水印、上頁。這個流程的痛感直接催生了 7 月的神像產圖半自動閉環(見〈神像產圖閉環〉)。
-
《神島演義》85 章上線——神祇庫擴至 101 尊
站的第四根柱子:一部從開天闢地寫到眾神渡海來台的故事主線,序卷到終卷共 85 章。為了寫這部故事,神祇庫一口氣擴充到 101 尊。
工程細節
神祇百科是查閱層,《神島演義》是閱讀層——把整個神譜用一條世界觀時序串起來:開天、洪荒、天庭、封神、佛陀、西遊、地府、成神、人間九個敘事弧。每章標注 canonLevel(典據/演義/敷演),讀者分得清哪些有經典依據、哪些是民間演義、哪些是本站敷演。
saga collection 沿用字串 fallback 策略連結神祇與廟宇,章內 featuredDeities 反向帶動神祇庫擴充:故事寫到誰、誰就建檔,單日從 75 尊擴到 101 尊。
同日導覽列改版(搜尋改圖示、行事曆移頁尾),並新增了
/devlog/佔位頁——就是現在這個頁面,佔位將近一個月後終於回填。 -
規格醞釀日——籤詩、自治營運、互動分析
沒有新功能上線的一天,卻是後來一個月方向的源頭:六十甲子籤詩、AI 自治營運、互動分析三份規格同時開工。
工程細節
三條線同日立規格:六十甲子籤詩(ADR 0006 定編號穩定契約——籤詩編號一旦發布永不變動,外部連結才敢引用);AI 自治營運(ADR 0007 自治模型——哪些事 AI 可以自己做、哪些要人審,後來 7 月整套落地);互動分析(瀏覽數與「其他人也讀了什麼」,隔日補 XSS 防護設計與 18 個 task 的實作計畫)。
配套的治理文件也在這兩天成形:內容版本紀錄與回溯 spec、維運登記簿(MAINTENANCE)。
事後看,這是全案從「衝功能」轉向「建體制」的轉折點:之後的大事幾乎都是這幾份規格的執行。規格先行雖然當下沒有可見產出,但 7/16–7/17 自治營運能兩天全面落地,靠的就是這時打的底。
-
撤回立體點雲聖像——一次誠實的退場
6/11 上線的 3D 點雲神像功能(關帝、媽祖首發)正式移除。做了、上線了、然後承認它不夠好——撤回也是開發的一部分。
工程細節
點雲聖像的構想:神祇個頁放一座由粒子構成的立體神像,呼應「神像是香火凝成的」意象。技術上做到了(three.js 點雲,關帝、媽祖兩尊首發),但存在九天後決定撤回、ADR 0003 標記撤回。
撤回理由:視覺辨識度不足(點雲看不出是誰)、載入成本高(每尊都要點雲資料)、與後來 AI 產製神像圖的路線重疊——平面神像圖+流光特效(見〈神像與流光特效〉)在辨識度和擴充成本上全面勝出。
留下 ADR 而不是默默刪碼,是刻意的:決策紀錄要包含「做錯的決策」,否則下次還會在同一個地方心動。
-
奉祀四分法——主祀、配祀、從祀、同祀
廟裡拜的不只一尊神。資料模型升級為主祀/配祀/從祀/同祀四分法,廟宇頁與神祇頁能完整呈現一間廟奉祀的神明結構。
工程細節
原本 temples 只有 mainDeity 一欄,塞不下真實廟宇的奉祀結構(霞海城隍廟的月老怎麼放?)。ADR 0004 定案四分法:主祀維持獨立欄位,其餘以
enshrines陣列帶角色(配祀/從祀/同祀)+粗略殿位。雙軌落地:策展廟(md)走 schema 的 enshrineSchema;全量廟(D1)新增
temple_deitiesjunction table。種子案例台北霞海城隍廟,第一版顯示直接吐 slug,隨即修成聖號。神祇參照沿用「優先 slug、未建檔填原名字串、純文字 fallback」的過渡策略——不用
reference()硬約束,內容擴充不被 schema 卡死。同日順手開了 ADR 0005 廟宇空間配置的坑(見次篇)。 -
廟宇空間配置模型——把一間廟畫進資料
開始把廟宇的空間格局(哪個殿、哪個龕、拜哪尊)建成結構化資料,以艋舺龍山寺做第一個試點。
工程細節
ADR 0004 的
hall只是粗略殿位文字,ADR 0005 把野心放大:殿—龕—神的立體配置模型,未來能渲染成廟宇平面導覽。神龕的 role enum 與四分法共用,兩個模型咬合。試點選艋舺龍山寺——殿宇結構複雜度夠(前殿、正殿、後殿多龕)、資料公開充分。YAML 試點檔完成,驗證了模型裝得下真實廟宇。
刻意只做到「規格+ADR+試點資料」就停:渲染層(Zod collection 化+P1 平面圖)另排期程,不跟四分法搶同一個檔期。大模型分段落地,每段都有可驗收的產出。
-
神祇覆蓋率衝刺與事實查核機制
神祇百科單日新增 26 尊,全台主祀神明的建檔覆蓋率從 47% 衝到 75%。同時建立事實查核追蹤表,發現錯誤就修——當天就修掉了五年千歲科期等錯誤。
工程細節
覆蓋率是從全量廟宇資料反推的:D1 裡全台主祀神明統計出來後,「哪些神最多廟拜、但百科還沒建檔」一目瞭然,衝刺清單就是照這個排的。兩批共 26 檔(47%→70%→75%)。
量衝上去之後馬上補質的機制:26 檔事實查核追蹤表,逐檔覆閱。當天抓到並修正的包括五年千歲科期與孚佑帝君封號,覆閱定案後再修開台聖王、濟公、五福大帝、瑤池金母四檔。「AI 草稿 → 事實查核 → 發布」的產製流程在這次衝刺中第一次真正跑完整輪,後來演化成 reviewStatus 生命週期欄位。
-
全量廟宇 P1b——一萬兩千頁與一張地圖
全台一萬兩千多間登記寺廟每間都有自己的頁面了,還能在互動地圖上找廟。缺座標的廟宇也補上了定位。
工程細節
12,414 筆個頁不可能全部 SSG(build 時間與產物體積都爆),定案混合渲染:
/temples/t/[id]/走 SSR+Cloudflare D1,其餘維持靜態。SQLite 同步到 D1 的管線同日打通,本地 dev 用 miniflare 模擬 D1(.wrangler/state/),ETL 重跑後要sync:d1:local重灌,忘了就是本地 500——這條雷之後踩過不只一次,最後寫進 CLAUDE.md。/temples/all/提供全量名錄的排序與搜尋(D1 LIKE,之後和 Pagefind 靜態搜尋分工)。缺座標廟宇跑 geocoding 補值,然後互動地圖上線;地圖後來(同日稍晚)併入廟宇頁作為檢視模式,點廟彈基本資料卡,配色調成站內暖色深色風。SSR 路由的 sitemap 是額外功課:
sitemap-temples.xml.ts於 build 自產,這也定下了「新增 SSR 路由要手動納 sitemap」的守則。 -
首頁 Hero 重建——捲動香煙
首頁門面砍掉重練:拿掉創站日的星圖節點與燈籠,換成一縷隨捲動流轉的香煙。
工程細節
創站日的星圖 hero 問題不斷:節點標籤和副標重疊修了一次、窄視窗又和主標題重疊再修一次,視覺密度也太高。與其繼續補,不如承認方向錯了。
重建走 D 版方案「捲動香煙」:單一意象、隨捲動有機流轉,把「進廟先聞到香」的體感放進首頁。移除節點圖與燈籠後,首屏資訊密度大降,站名與標語反而立起來。
留下的教訓:hero 這種高風險視覺,先在草稿頁比稿再上正頁比較省——這也是後來
/labs/沙盒頁誕生的原因之一(搜尋頁版型就是先在 labs 打稿)。 -
基礎建設一日四發——搜尋、SEO、行事曆、知識庫
一天之內補齊四樣基礎配備:全站搜尋、搜尋引擎與 AI 友善設定、宗教節慶行事曆(含農曆換算)、知識庫分類入口。
工程細節
搜尋選 Pagefind:build 後索引靜態 HTML,零 runtime 後端,中文 CJK 分段可用。已知限制誠實記錄在規格:中文複合詞查詢端與索引端斷詞不一致(「媽祖」查不到「媽祖遶境」連寫的文章),v1 接受,導流 D1 名錄搜尋補位。搜尋頁版型先在
/labs/沙盒打草稿再定稿,資料層用 Pagefind JS API 自繪 DOM 而非官方 UI。SEO/AI-friendly 全套:結構化資料、RSS、
llms.txt——把「AI 引用友善」當成一級公民,這站本來就打算讓 AI 讀得懂。行事曆做了農曆換算,宗教節慶以農曆為準,這是繞不開的功課。知識庫 hub 支援 wikilink 解析,讓知識條目之間互相引用不用寫完整路徑。 -
全量廟宇名錄 P1a——政府資料進站
不只介紹精選廟宇——把政府登記的全台上萬間寺廟資料接進站內,可以依 22 縣市瀏覽,神祇頁也長出全台主祀統計。
工程細節
ADR 0002 定調資料層雙軌制:策展內容永遠是 Markdown(人寫的、要查核的),機器量級資料(政府寺廟登記逾萬筆)走 ETL 管線——CSV 快照+YAML 修正層 → SQLite。修正層是關鍵設計:政府資料有錯不改快照本體,錯誤修正寫進 YAML 疊加,快照可隨時換新版重跑。
P1a 範圍守在靜態可承受的部分:22 縣市 hub 頁(SSG)、策展廟宇與登記資料的對應、神祇頁全台主祀統計。上萬筆的個頁留給 P1b,因為那需要 SSR。
這是全站第一次「策展層」與「資料層」正面整合,雙軌各自演進、接點明確的架構從這裡定下。
-
創站日——從零到視覺骨架
網站誕生的第一天。用 Astro 搭起整個站的骨架,定下暖色深色的視覺基調——星空、燈火、廟埕的氛圍,從第一天就是現在這個樣子。
工程細節
技術選型 Astro v5:內容型網站、SSG 為主,動態需求(後來的全量廟宇個頁)再用混合渲染補。先寫規格再動工的守則也是這天定的——里程碑一「視覺骨架」規格定案後才開 scaffold。
Design tokens 起手:色彩、字距、間距全部進 CSS variables,之後所有頁面共用同一套
--c-*變數,改版基調只動一處。首頁 hero 首版是「星圖節點+燈火動態」——這版後來在 6/12 被整個重做(見〈首頁 Hero 重建〉),但星空的意象留了下來。同日踩雷:本機 dev 網域
miao.test的 https 代理設定折騰了兩輪,最後定案 Herd secure proxy;dev server 隔天進一步改由 launchd 常駐,crash 自動重拉。 -
神明位階圖——從 2D 到 3D
把神明之間的位階與統轄關係畫成一張可以互動的圖:2D 版人人可用,桌機預設 3D 版可以旋轉俯瞰整個神譜。
工程細節
資料先行:rank-tiers 位階序列+perspectives 逐視角出處,先把「誰管誰、依據是什麼」建成資料,圖只是渲染。位階圖資料 JSON 於 build 時預生成,前端不用重算。
一天內從 2D 疊到 3D(three.js),桌機預設 3D、行動裝置退回 2D。神祇個頁另做 ego 特寫——以該神為中心的局部位階圖。後續踩雷兩則:3D 場景在背景分頁切回來時鏡頭會貼臉(6/12 修,順手加了 Z 軸統轄縱深);dev 模式下圖學依賴載入會 504(6/21 改預打包解掉)。
入口位置也調整過:一開始掛主選單,6/12 移到神祇頁入口卡——位階圖是神祇百科的延伸,不是獨立頻道。
-
地府專題上線
第一個主題式專題:地府。從城隍、東嶽大帝到十殿閻羅,五篇文章加三間廟宇,把民間信仰裡的冥界行政體系整理成一條完整的閱讀線。
工程細節
Phase B 的驗證題目:三大內容主體(廟宇、文章、神祇)能不能被一條敘事線串起來。選地府是因為它結構最清楚——統轄鏈明確(城隍隸東嶽、十殿閻羅分工),跨廟宇(城隍廟、東嶽殿)、跨神祇、跨文章都有得寫。
實作是內容工程多於程式:城隍與東嶽體系廟宇三間、地府內容線五篇、一個專題 hub 頁把線收攏。專題 hub 這個版型後來成為「主題策展」的模板。
Phase B 完工時神祇規格驗收六條全過,證明了 collection schema+關係資料層撐得起主題策展,才敢往下接全量廟宇名錄這個更大的坑。
-
三大內容主體首發——廟宇、文章、神祇百科
站的三根柱子同日立起:策展廟宇介紹、三層深度的文章、以及神祇百科。神祇從首批 6 尊當天擴到 22 尊,補齊地府統轄鏈。
工程細節
Content collections 是內容層的地基:temples、articles、deities 三個 collection 各配 Zod schema,Markdown 為單一事實來源(SSOT),仿 taiwan.md 的產製架構。
神祇百科的設計重點在「關係」:另立 relations 資料層(統轄、師徒、化身、配偶、對立⋯⋯),神祇個頁直接長出神譜關係區塊。首批就刻意收了「矛盾並存」案例——同一尊神在不同體系有不同位階,不硬扣單一真相,逐視角列出處。這個原則成了之後整個神譜資料模型的基調。
文章走三層深度(30 秒/5 分鐘/深讀),對應不同讀者的耐心預算。當天內容量:22 尊神祇、含地府統轄鏈(城隍—東嶽—十殿閻羅)。