iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Modern Web

Vue 前端工程師視角看 Flutter Web系列 第 27

Day 27|補救方案:預渲染、meta 標籤注入、混合架構

  • 分享至 

  • xImage
  •  

模組六|SEO/SSR(Day 25–28)

先備:Day 26(爬蟲拿到的是一支 <script>)。今天講三帖藥,其中一帖是安慰劑,我會說清楚是哪一帖。

昨天確診,今天開藥。先把結論放前面,免得你抱太多希望。

結論先講:

三帖藥裡只有一帖能根治,而它的藥名叫「內容還是交給 DOM」。

另外兩帖,一帖接近安慰劑、一帖是止痛藥。三帖我都會給出「什麼時候該吃」,但不會假裝它們等價。

Vue 怎麼做

同樣的病(CSR SPA 對爬蟲空白),Vue 世界的解法譜系昨天回顧過:prerender、SSR、SSG、ISR,全部建立在同一個地基上——Vue 元件能在 Node 環境 render 成 HTML 字串。所以對 Vue 來說這些是框架原生手段,routeRules 一行設定的事。

而路由級的 meta 在 Vue 這邊更是連想都不用想。這是要解決的問題:一百個商品頁,每一頁分享出去的預覽卡片要有自己的標題、描述、縮圖。Nuxt 的寫法是把它當成元件的一部分——下面這段就放在頁面元件的 <script setup> 區塊裡:

const { data: product } = await useAsyncData(() => $fetch(`/api/products/${id}`))

// 這幾行會在 server render 時就寫進 HTML 的 <head>
useSeoMeta({
  title: () => `${product.value.name}|Waypoint Air`,
  description: () => product.value.summary,
  ogImage: () => product.value.cover,
})

重點在 useAsyncDatauseSeoMeta 跑在同一個 server 端的 render 週期裡——資料抓完、meta 寫好、HTML 吐出去,爬蟲拿到的是成品。等一下你會看到,Flutter Web 要做到同一件事,得把這個流程搬到框架外面重做一次。

記住這個地基,因為 Flutter Web 的三帖補救全都缺這塊:它的元件 render 出來是像素。所有補救方案本質上都是在畫布旁邊另外生出一份爬蟲能吃的 HTML,差別只在生多少、生在哪。

Flutter Web 怎麼做

先講我自己的起點,因為它決定了這三帖藥各自要從多遠的地方開始走。Waypoint Air 的 Flutter 端整個部署設定就是那支 vercel.json,全檔四行:

{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

一條 SPA rewrite,沒有任何預渲染設定、沒有任何 server 端中介層。也就是說,下面三帖藥我一帖都沒吃——這一篇講的是選項,不是我的實戰紀錄,這點先說清楚。

帖一:預渲染/動態轉譯(dynamic rendering)。 傳統做法是用 headless Chrome 對爬蟲 UA 出一份 render 完的 HTML snapshot。對 CSR 的 Vue/React SPA 這招有效——render 完有 DOM 文字可抓。但 Flutter render 完是 <canvas>,snapshot 進去還是沒有正文;就算開語意樹去撈 aria-label,撈出來的是朗讀順序的碎片,不是文章。所以 Flutter 圈實務上的「預渲染」變成:替每條要 SEO 的路由,用模板另外生一份 HTML 版內容——講白了就是同一份內容寫兩次,CMS 和模板層又多一份要同步的事實來源。雪上加霜的是,Google 官方已把 dynamic rendering 標為不建議的過渡性方案。這帖是安慰劑成分居多。

帖二:meta 標籤注入。 治標但真的能止痛。server 端(Nitro、Express、Cloud Functions 都行)攔下請求,依路由把 title/description/og 寫進 Flutter 的 index.html 再回傳:

// server 端:依路由把 meta 寫進 Flutter 的 index.html 再回傳
app.get('*', async (req, res) => {
  const meta = await metaByRoute(req.path)      // 查這條路由的 title/og 資料
  const html = indexTemplate.replace('<!--SEO_META-->', renderMetaTags(meta))
  res.send(html)
})

社群分享卡片復活、搜尋結果的標題和描述正確——昨天那個「LINE 預覽空白」的症狀解掉了。但爬蟲點進來,正文還是 canvas:卡片漂亮、內文零索引、排名上不去。止痛藥,不是抗生素。

帖三:混合架構。 唯一的根治:需要 SEO 的頁面用 Nuxt(或任何 DOM 框架)寫,Flutter 只負責 app 本體,用路徑切開——//blog/pricing 走 Nuxt,/app 走 Flutter。更細的版本是 element embedding:Flutter 不再接管整個頁面,只接管 HTML 裡的一個元素,周圍的內容照常是可索引的 DOM:

// element embedding:Flutter 只接管 #chart-app 這個元素,而非整個 body
_flutter.loader.load({
  onEntrypointLoaded: async (engineInitializer) => {
    const appRunner = await engineInitializer.initializeEngine({
      hostElement: document.querySelector('#chart-app'),
    });
    await appRunner.runApp();
  },
});

這是官方支援的模式,也是我認為 Flutter Web 在「有 SEO 需求的產品」裡唯一站得住的姿勢:Nuxt 頁面負責內容與索引,Flutter 負責頁面裡那塊 canvas 密集的互動區。

路徑切割版的部署其實不複雜:同一個 domain 底下用反向代理或平台的 rewrite 規則(Nginx、Vercel、Netlify 都行),把 /app/* 指向 Flutter 的靜態產物,其餘路由交給 Nuxt。真正麻煩的是體驗接縫——從 Nuxt 頁面點進 /app 是一次全頁重載,前端狀態不共享,登入態要靠 cookie domain 對齊撐起來。這些都解得掉,但要在時程估算裡誠實留位置,不要當成「反正就加個轉址」。

差異與坑

坑一:同一個詞,兩種待遇。 「預渲染」在 Vue 是一等公民,在 Flutter 是體外循環。評估補救方案時先問一句「render 出來的東西裡有沒有文字 DOM」,就能瞬間分辨真藥和安慰劑。症狀:你買了一個號稱支援 SPA 預渲染的服務,接上去之後 snapshot 抓回來一看,<body> 裡還是只有那張 canvas。
坑二:meta 注入的隱形天花板。 搜尋結果好看不等於搜得到。description 是給人看的摘要,不是排名依據;沒有可索引的正文,長尾關鍵字全軍覆沒。症狀:分享到社群的預覽卡片變漂亮了,老闆很滿意,但三個月後自然搜尋流量一動也沒動。
坑三:混合架構的帳單。 兩套技術棧同時養——routing 邊界的 rewrite 規則、auth/cookie 跨區共享、設計系統雙軌維護。諷刺的是,這正是你當初想用 Flutter Web 省掉的那種成本。症狀:使用者從 /pricing(Nuxt)點進 /app(Flutter)時要重新登入一次,因為兩邊的 cookie 網域設定沒對齊,而這種 bug 只在正式環境的網域結構下才會出現。
坑四:meta 注入記得加快取。 那層 server 攔截會落在每一個請求的關鍵路徑上,別讓每個爬蟲請求都打一次資料庫;查出來的 meta 用路由當 key 快取住,失效策略跟著內容更新走。症狀:某天爬蟲密集來抓,你的資料庫連線數爆掉,而 APM 上看到的慢查詢全部來自那個「只是塞個 title」的中介層。
坑五:開藥之後要驗收。 不管上哪帖,用 Search Console 的成效報表和網址檢查工具建立基準線再迭代,別用「感覺有變好」交差。SEO 的回饋週期以週計,越早開始量測越早知道藥有沒有效。症狀:三個月後有人問「所以那個 SEO 改善做完有效嗎」,而你手上沒有改之前的數字,只能說「應該有吧」。
坑六:這件事在 agent 時代要重新算一次。 我全程用 AI agent 開發,而「同時養 Nuxt + Flutter 兩棧」對 agent 工作流的邊際成本,遠低於對一個人類團隊。混合架構在 2021 年是奢侈品,在 2026 年的 agent 工作流裡是可負擔選項——這是我認為值得重新評估它的原因。症狀:這一條的症狀在人身上——同一個提案,2021 年報上去會被打回「人力不夠」,2026 年再報一次,卡住的地方通常換成了設計系統要維護兩套,而不是程式碼寫不完。

小結與下一篇

一句話:meta 注入救卡片、預渲染救門面、只有混合架構救排名——而混合架構的本質就是承認「內容型頁面還是 DOM 的地盤」。明天 Day 28 收官:把這三天的證據壓成一個能帶進選型會議的決策框架,正面回答「SEO 導向專案該不該選 Flutter Web」。

參考資料

如果你卡在語法

深入原理


上一篇
Day 26|Flutter Web 的 SEO 困境:爬蟲看到的是什麼
下一篇
Day 28|SEO 導向專案該不該選 Flutter Web?決策框架
系列文
Vue 前端工程師視角看 Flutter Web30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言