模組五|閱讀端與規模化(Day 26–29)
模組四講完 3D。剩下四天回到比較日常的東西:200 回的閱讀端。
我有 200 個 Markdown 檔,需要 200 個可以在瀏覽器讀的頁面。今天講這個轉換怎麼做的,以及一個我寫這篇時才發現的自打嘴巴。
如果每一頁長得一樣,這件事很無聊,套個模板就好。
但我的每一章頁面有自己的視覺識別:
每一頁的結構相同,內容全部不同。
這種「一個骨架、N 組資料」的東西,標準答案是模板引擎。我沒有用。
tools/gen_ch_pages.py,99 行:
TPL = pathlib.Path('novel/第十三章.html').read_text(encoding='utf-8')
模板不是一個 .template 檔,是第十三章的 HTML 本身。
第十三章是我手工做到滿意的那一頁。它有完整的排版、動畫、響應式處理、字體設定。腳本讀它,然後把裡面屬於第十三章的東西換成目標章節的。
因為 Day 20 那條限制:零 build step。
引入模板引擎意味著:多一個依賴、多一個要學的語法、多一個「模板檔跟實際頁面會不一致」的可能。
而拿現成頁面當模板有一個很實在的好處——模板永遠是可以直接打開看的。
你不需要跑任何東西就知道它長什麼樣。它就是一個網頁。
我改動排版的時候,是在第十三章這一頁上改,改到滿意,再重跑腳本讓其他章跟上。模板和成品是同一種東西,這消除了一整類「模板改了但沒生效」的困惑。

CHAPTERS = [
dict(md='novel/第十四章_下潛.md', out='novel/第十四章.html',
no='第十四章 · CH. 14',
title='下<em>潛</em>',
accent='#c9564f', acc_cmt='緋紅裂縫 crimson crack — 底下那個「看」',
glow='rgba(201,86,79,', sel='#1a0705',
sub='不是往下走。往下走誰都會。是走到一個地方,深到你身上每一樣東西⋯⋯',
desc='矽墟 SILICA HOLLOW 概念小說 · 第十四章。⋯⋯',
img='../assets/novel-ch14-descent.png',
alt='深不見底的垂直裂隙,一條掛著銅鈴的繩垂進黑暗,最底下透出一道緋紅的光',
pos='50% 50%', relic='下潛 · THE DESCENT',
labels=['LEAVE WORD ABOVE','LAYER SIX','THE CALLING',
'WHERE THE MAP ENDS','THE BOTTOM','THE WEIGHING'],
prev=('第十三章.html','← 第十三章 · 香的底火'),
nxt=('第十五章.html','第十五章 · 留白 →'),
fin='第十四章'),
...
]
一章一個 dict,十七個欄位。
有一個欄位我要特別提:acc_cmt。
accent='#c9564f', acc_cmt='緋紅裂縫 crimson crack — 底下那個「看」'
它是註解,會被寫進生成出來的 CSS 裡。它記錄的是「為什麼這一章是這個顏色」。
#c9564f 這個十六進位數字本身不帶任何意義。三個月後我看到它,只知道它是暗紅色。但 緋紅裂縫 — 底下那個「看」 告訴我這個顏色對應的是這一章的核心意象。
魔術數字要帶著它的來由一起存放。 這在寫程式上是老生常談,但在設計參數上很少人做。
寫到這裡我意識到一件事。
Day 12 我花了一整篇講:把 prompt 從 Python list 搬出來,變成一角一個檔案,因為「改內容不該要改程式」。
而這支腳本:
CHAPTERS = [
dict(md=..., title=..., accent=..., sub=..., desc=..., labels=[...]),
...
]
一個 Python list,每章一個 dict,全部寫死在 .py 裡。
跟我在 Day 12 批評的那個第一版生圖腳本,是一模一樣的結構。
我想改第十六章的副標(那是一個純內容的決定),要打開 .py,在一堆引號和逗號中間編輯多行中文字串。

因為這兩件事在我的認知裡是分開的。生圖腳本那次的痛點很明確(20 個角色、prompt 很長、要反覆調),所以我做了重構。
章節頁這支腳本我只跑過幾次,每次加幾章,痛感沒有累積到讓我停下來看結構。
同一個錯誤在不同檔案裡,不會自動被辨認出來。 你需要主動去問「我在別的地方學到的教訓,這裡適用嗎」。而這件事幾乎不會自然發生。
我是因為要寫這篇文章、逐檔重讀,才把兩件事放在一起看。
跟 Day 12 一樣:把 CHAPTERS 抽出來變成 chapters.json 或一章一個資料檔。
但我要誠實:我可能不會修。 因為 20 章已經全部生完了,這支腳本大概不會再跑第三次。
而這正是我在 Day 16 講過的那個問題:沒有痛感就沒有動力。差別在於這次的代價很小(沒有人會因此損失錢),所以我判斷它可以放著。
知道一個東西寫得不好,跟它值不值得修,是兩個獨立的判斷。
代價一:拿現成頁面當模板,模板會漂移。
TPL 指向第十三章。如果哪天我手動改了第十三章的 HTML(修個錯字、調個間距),那個改動會在下次跑腳本時傳染給所有章節。
反過來,如果我手動改了第十四章,下次重跑就會被蓋掉。
沒有任何機制標示「第十三章是模板,不要手動改」。 它跟其他 19 章長得一模一樣。
一行註解可以解決,我沒寫。
代價二:字串替換沒有結構保護。
腳本靠的是文字比對與替換。如果模板裡某個要被替換的字串剛好也出現在別的地方,就會誤傷。
模板引擎有 {{變數}} 這種明確的標記,字串替換沒有。我用可讀性換掉了安全性。
目前 20 章都正常,但那是運氣加上規模小。
代價三:99 行裡有一大半是資料。
那個 CHAPTERS list 佔了腳本的大部分。真正的邏輯(讀檔、替換、寫檔)大概只有二三十行。
一個「99 行的腳本」聽起來比它實際的複雜度嚇人,而這會讓未來的我不太想打開它。
一、用「已經做好的成品」當模板,在小規模下是合理的選擇。
好處:模板可以直接打開看、不需要學新語法、不需要新依賴、模板和成品不會有認知落差。
適用條件:數量少(幾十個以內)、你自己維護、替換的標記不容易誤傷。
超過這個規模就該換模板引擎——但不要因為「正統做法是模板引擎」而在只有 5 個頁面的時候引入它。
二、魔術數字要跟它的來由存在一起。
accent='#c9564f', acc_cmt='緋紅裂縫 crimson crack — 底下那個「看」'
多一個欄位,換來三個月後看得懂。
這條在設定值、閾值、色碼、magic number 上都成立。判斷句:這個值如果我明天忘記為什麼是它,我還能推導出來嗎? 不能的話就要寫下來,而且要寫在值的旁邊,不是寫在別的文件裡。
三、定期把你的教訓拿去別的檔案對照一次。
這是我今天最大的收穫。
我在 A 檔案學到「內容不該寫死在程式裡」,然後在 B 檔案犯了一模一樣的錯,而且過了幾週都沒發現。
因為教訓是綁在情境上的,不會自動遷移。
可以做的事:每次做完一個重構,花五分鐘問一句:「這個問題,專案裡還有哪裡有?」 然後真的去 grep 一次。
我如果那時候搜一下 = [ 開頭的長 list,就會當場看到 CHAPTERS。
明天 Day 27,講首頁:945 行 JavaScript、Three.js 從 CDN 載入、零 build step 的互動網站。以及這個選擇在兩年尺度下的實際代價——包括一個我 pin 死的版本號。