跳到主要內容
W
科技發布於 約 5 分鐘

我是怎麼把這個部落格架起來的

用 Astro 加 Cloudflare Pages 搭一個零成本的靜態站,記錄過程中九個只有真的部署才會暴露的問題。

這個站從零到上線大約花了一小時。技術選擇很單純:Astro 產生靜態頁面,Cloudflare Pages 負責全球散佈,文章是 Git 倉庫裡的 Markdown 檔

有趣的不是這個架構本身——網路上已經有幾百篇教學了——而是過程中那些只有真的部署到線上才會暴露的問題。這篇記錄其中九個。

先說架構

一開始有兩條路可選:

  1. Git-based:文章是 repo 裡的 Markdown,後台透過 GitHub API 提交,push 觸發重新建置。
  2. 動態資料庫:文章存在資料庫裡,前台即時查詢。

我選了第一條。理由不是「靜態比較快」這種空話,而是免費額度的風險剛好是反過來的

動態方案的每個頁面請求都會計入 Workers 的每日配額。靜態資產走 CDN,完全不計額度。也就是說,萬一哪天某篇文章被大量轉載,靜態方案反而是那個不會爆的。

加上 Markdown 在自己的 repo 裡,未來想換框架、換平台,成本接近零。

坑 1:子網域是全球共用的

我原本想要的名字已經被一家瑞士的投資公司用掉了。

*.pages.dev 不是每個帳號一份,而是全球共用的命名空間。動工前先查一下:

dig +short your-name.pages.dev

沒有輸出就是還沒被用。

坑 2:淺色模式的語法高亮整個消失

這個最陰險,因為深色模式看起來完全正常。

Astro 的雙主題程式碼高亮輸出長這樣:

<pre style="background-color:#fff;color:#24292e;--shiki-dark-bg:#24292e;--shiki-dark:#e1e4e8">

看出問題了嗎?淺色的值是直接寫在 inline style 裡的,只有深色被存成 CSS 變數

根本不存在 --shiki-light 這個變數。

而我照著對稱的直覺寫了這樣的 CSS:

.astro-code, .astro-code span {
  color: var(--shiki-light) !important;   /* 這個變數不存在 */
}

CSS 變數不存在時,整個宣告會變成 invalid at computed-value time,color 退回 inherit——淺色模式下所有程式碼都變成跟內文一樣的顏色

正確做法是只覆寫深色,淺色讓 inline style 自己處理:

[data-theme="dark"] .astro-code,
[data-theme="dark"] .astro-code span {
  background-color: var(--shiki-dark-bg) !important;
  color: var(--shiki-dark) !important;
}

坑 3:目錄掉到文章最後面

我這樣寫側邊目錄:

<div class="gap-10 lg:grid lg:grid-cols-[1fr_15rem]">
  <article>…</article>
  <aside class="order-first lg:order-none">目錄</aside>
</div>

桌機正常,手機上目錄卻跑到整篇文章的最下面

原因:order-first 只在 flex 或 grid 容器裡有效。而上面那個容器只有 lg: 以上才是 grid,在手機上它就是個普通的 block,order 完全沒作用。

實測目錄在 y=1824,文章在 y=101——讀者要捲過整篇文章才會看到目錄。

改成容器永遠是 grid,只用 lg: 控制欄數就好:

<div class="grid gap-10 lg:grid-cols-[1fr_15rem]">

坑 4:行內程式碼多了反引號

畫面上出現 `npm run build` 這種東西,反引號是看得見的字元。

Tailwind Typography 的預設行為會注入真正的反引號:

.prose code::before { content: "`"; }

移掉:

.prose :where(code):not(:where(pre *))::before,
.prose :where(code):not(:where(pre *))::after {
  content: none;
}

坑 5:每個內部連結都多一次轉址

部署上線後實測,才發現站內每一個連結都回 308。

Astro 預設輸出 blog/index.html 這種目錄結構,Cloudflare Pages 於是把 /blog/(帶斜線)當成正規網址。但我在設定裡寫了 trailingSlash: 'never',站內連結全部是 /blog(不帶斜線)。

結果:每個連結都被 308 轉到帶斜線的版本,而 canonical 標記指向的還是那個會被轉走的網址。

解法是讓輸出格式跟設定一致:

build: { format: 'file' }    // 輸出 blog.html 而非 blog/index.html

坑 6:修好坑 5 之後,canonical 全變成 .html

改成檔案式輸出後,建置期的 Astro.url.pathname 變成 /index.html/blog.html

於是 canonical 產生 https://…/blog.html,但 sitemap 和 RSS 產生的是 https://…/blog兩份訊號互相矛盾,搜尋引擎會困惑。

寫個小工具統一正規化:

export function cleanPath(pathname: string): string {
  const path = pathname
    .replace(/\/index\.html$/, '/')
    .replace(/\.html$/, '')
    .replace(/\/+$/, '');
  return path || '/';
}

這個坑的教訓是:修 bug 時要檢查有沒有引進新的不一致。我如果沒有再跑一次驗證,這個問題會一直躺在那裡。

坑 7:免費子網域沒辦法用 Access 保護

原本的計畫是用 Cloudflare Zero Trust Access 把後台入口擋起來,讓陌生人連登入畫面都看不到。

做不到。官方文件寫得很清楚:

Domains must belong to an active zone in your Cloudflare account.

pages.dev 是 Cloudflare 自己的網域,不在我的帳號底下,所以不會出現在選單裡。

這讓我重新想了一次安全模型,結論是這層本來就只是錦上添花

真正的安全邊界是 GitHub 的 OAuth 授權。沒有授權就沒有任何寫入能力。少了 Access,攻擊面只是「陌生人看得到一個他登不進去的表單」——而這正是絕大多數 Git-based CMS 的常態。

等哪天買了自訂網域,這層可以補回來。

坑 8:後台一片空白,而且沒有任何錯誤

這是最花時間的一個。症狀:

  • 頁面回 HTTP 200
  • 腳本確實載入了(2.1 MB,網路面板顯示 200)
  • console 完全乾淨,沒有任何錯誤
  • 但畫面全白,DOM 裡只有一個 <script>

沒有錯誤訊息是最麻煩的——你無從下手。

最後是把套件抓下來直接翻原始碼找到的:

npm pack @sveltia/cms
grep -oE '.{90}nc-root.{90}' package/dist/*.mjs

原來需要一個特定 id 的掛載點,缺了它程式就靜靜地什麼都不做。加上那個 div 就好了。

心得:當瀏覽器什麼都不告訴你的時候,去讀套件的實際產出檔案。壓縮過的程式碼還是可以 grep 的。

坑 9:設定檔的變數不是到處都能用

CMS 的設定裡我寫了:

summary: '{{title}} — {{year}}/{{month}}/{{day}}'

整份設定被拒絕載入,錯誤訊息是「refers to year, but no such field is defined」。

{{year}} 這類日期變數只有在檔名樣板裡才有意義,在其他欄位得引用實際存在的欄位。

回頭看

九個問題裡,只有兩個是在本機發現的。其他七個都得等到真的部署、真的用瀏覽器打開、真的去檢查產出的 HTML 才會現形。

這大概是最實際的一課:「建置成功」跟「東西是對的」是兩件不同的事。建置成功只代表沒有語法錯誤。

幾個現在會固定做的檢查:

# 轉址與狀態碼
curl -s -o /dev/null -w "%{http_code} %{num_redirects}\n" https://your-site/some-page

# canonical 跟 sitemap 說的是不是同一件事
curl -s https://your-site/some-page | grep canonical
curl -s https://your-site/sitemap-0.xml | grep loc

兩行指令,可以省下好幾週之後才會發現的 SEO 問題。

分享這篇

小紅書、微信公眾號、抖音沒有開放網頁分享端點,請用「複製連結」後手動貼上。