在 Eleventy(11ty)中優雅地使用 Svelte:一套實用的 Vite-based islands 架構
目錄
執行範例
這是用 svelte 寫的元件,整合在 markdown file.
為什麼我想在 11ty 裡用 Svelte?
Eleventy(11ty)是一個我非常喜歡的靜態網站產生器:
- 快
- 簡單
- 不綁框架
- 非常適合寫技術文章
但寫久了總會遇到一個問題:
文章裡想放一點互動的東西
例如:
- 小型計算器
- Demo widget
- 可重複使用的互動範例
- 教學用的即時操作元件
這時候我並不想:
- 把整個網站搬去 SvelteKit
- 或為了一個元件引入一整套 SPA 架構
於是我選擇了一條折衷但非常實用的路:
11ty 負責靜態內容
Svelte 只負責「局部互動」
這篇文章會介紹什麼?
這篇會完整說明我目前在用的一套做法:
- 在 11ty 文章中用 shortcode 插入 Svelte 元件
- Svelte 元件集中管理、每個元件一個檔案
- 透過 Vite 自動打包整合到 11ty build
- 支援 Barba.js 無刷新換頁
- 最後會比較這套做法和 11ty is-land 的差異
整合步驟
整個流程其實很單純:
- Markdown 文章中放一個「Svelte 插槽」
- 11ty build 時輸出普通 HTML
- 瀏覽器載入後:
- JS 掃描插槽
- 找到對應的 Svelte 元件
mount()掛載上去
Markdown
↓
HTML(含 data-svelte-component 插槽)
↓
Vite bundle(含 Svelte)
↓
Client-side mount專案目錄結構: 把 Svelte 元件集中管理:
src/
├─ svelte/ # 專門放 Svelte 元件
│ ├─ Counter.svelte
│ └─ MyWidget.svelte
└─ assets/
└─ js/
├─ site.js
└─ svelte-loader.js
好處很明確:
- 元件與文章內容完全解耦
- 同一個元件可以在不同文章重複使用
- 不會污染 11ty 的模板系統
Step 1:在 11ty 中啟用 Svelte(Vite)
我使用的是官方的 plugin:
- @11ty/eleventy-plugin-vite
- @sveltejs/vite-plugin-svelte
修改 eleventy.config.js
import EleventyVitePlugin from "@11ty/eleventy-plugin-vite"; // 11ty 官方 Vite 插件
import { svelte } from "@sveltejs/vite-plugin-svelte"; // Svelte Vite 插件
export default async function (eleventyConfig) {
eleventyConfig.addPlugin(EleventyVitePlugin, { // 啟用 11ty Vite 插件
viteOptions: {
plugins: [
svelte(), // 讓 Vite 能編譯 .svelte 檔案
],
},
});
return {}; // 返回 11ty 配置物件
}這裡的重點觀念是:
11ty 不負責編譯 Svelte
Svelte 完全交給 Vite
Step 2:建立一個 Svelte shortcode(關鍵)
這一步是整個架構的靈魂,也沒那麼誇張啦,就是使用 11ty shortcode 產生 html 放 svelte 元件位置
修改 eleventy.config.js
eleventyConfig.addShortcode("svelte", (name, props = {}) => { // 註冊 "svelte" shortcode
const propsString = JSON.stringify(props) // 將 props 物件序列化為 JSON 字串
.replace(/'/g, "'"); // 將單引號轉義為 HTML 實體,避免 HTML 屬性解析錯誤
return ` // 返回 HTML 字串,創建一個空的 div 作為 Svelte 元件掛載點
<div
data-svelte-component="${name}" // 指定要掛載的 Svelte 元件名稱
data-svelte-props='${propsString}'> // 以 JSON 格式傳遞 props 給元件
</div>
`;
});這個 shortcode 做的事情只有一個:
在 HTML 中留下「我之後要掛哪個 Svelte 元件」的資訊
Step 3:在 Markdown 文章中使用
放在文章任何位置都行。
{% svelte "Counter", { "count": 10, "label": "目前點擊數" } %}
<!-- 使用 svelte shortcode 插入 Counter 元件,並傳遞初始 props -->此時請記住一件事:
這裡不會渲染 Svelte 只會產生 shortcode 設定的 HTML,例如:
<div
data-svelte-component="Counter"
data-svelte-props='{"count":10,"label":"目前點擊數"}'>
</div>Step 4:撰寫一個 Svelte 元件
ex: Counter.svelte
<script>
let { count = 0, label = "Count" } = $props(); // 解構 props,設定預設值
</script>
<div class="p-4 border rounded my-4"> <!-- 使用 Tailwind CSS 樣式 -->
<p>{label}: {count}</p> <!-- 顯示標籤和計數值 -->
<button onclick={() => count--}>-1</button> <!-- 減少按鈕 -->
<button onclick={() => count++}>+1</button> <!-- 增加按鈕 -->
</div>這是一個非常普通的 Svelte 元件,沒有任何 11ty 耦合。
Step 5:Client-side Svelte Loader(核心)
基本版本(全部一起打包)
ex: svelte-loader.js
import { mount } from "svelte"; // Svelte 5:掛載元件
const modules = import.meta.glob("../../svelte/*.svelte", { eager: true }); // 小元件:直接全打包
const components = {}; // { ComponentName: Component }
for (const filePath in modules) { // "../../svelte/Counter.svelte"
const name = filePath.split("/").pop().replace(".svelte", ""); // "Counter"
components[name] = modules[filePath].default; // 註冊元件
}
export function initSvelteComponents(root = document) { // 允許傳入 Barba container
root.querySelectorAll("[data-svelte-component]").forEach((el) => {
if (el.dataset.svelteInitialized) return; // 已初始化就跳過
const name = el.dataset.svelteComponent; // 元件名稱
const Component = components[name]; // 取得元件
if (!Component) { // 以前你這邊是 return(太安靜)
console.warn(`[svelte] component not found: "${name}"`);
return;
}
let props = {};
const raw = el.dataset.svelteProps || "{}"; // props 字串(預設空物件)
try {
props = JSON.parse(raw); // parse props JSON
} catch (err) {
console.warn(`[svelte] invalid props JSON for "${name}": ${raw}`, err); // 以前你是吞掉
props = {};
}
mount(Component, { target: el, props }); // 掛載元件
el.dataset.svelteInitialized = "true"; // 標記完成
});
}這裡利用了 Vite 的 import.meta.glob:
- 自動掃描
src/svelte/ - 不需要手動 import 每個元件
- 新增元件只需要放檔案即可
Step 6:整合 Barba.js
如果你使用 Barba.js 或 PJAX 類工具,這一步非常必要:
import { initSvelteComponents } from "./svelte-loader.js"; // 匯入 Svelte 元件初始化函數
document.addEventListener("DOMContentLoaded", () => { // 頁面首次載入時
initSvelteComponents(); // 初始化所有 Svelte 元件
});
barba.hooks.afterEnter(() => { // Barba.js 換頁完成後
initSvelteComponents(); // 重新初始化新頁面的 Svelte 元件
});原因很簡單:
Barba 換頁不會重新載入 JS,但新頁面需要重新掛載 Svelte
那這套和 11ty is-land 有什麼不同?
簡單對照
| 項目 | 本文做法 | 11ty is-land |
|---|---|---|
| 架構 | CSR islands | SSR + hydration |
| 初始 HTML | 插槽是空的 | 可預渲染 |
| SEO | 不太好 | 較佳 |
| 載入策略 | 手動控制 | 宣告式(on:visible) |
| Barba 相容 | 非常好 | 需額外處理 |
| 複雜度 | 不好說 | 不好說 |
實務建議
- 部落格互動元件 → 本文這套
- SEO 關鍵內容元件 → is-land
- 已有 Vite / Barba 架構 → 本文這套會非常順
大多數使用 Svelte 的目的應該是為了互動操作,如果是為了 SEO 產生內容,那這樣用 Svelte 就不太適合了。
結語
這套做法本質上是一種:簡單可控的 islands 架構
它不追求「什麼都幫你包好」,而是讓你清楚知道:
- 元件什麼時候被載入
- JS 什麼時候被執行
- 換頁時發生了什麼事
如果你正在用 11ty 寫技術部落格,又偶爾需要一點互動,svelte 是一個還不錯的方式。


