<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>CMS on recca0120 技術筆記</title><link>https://recca0120.github.io/tags/cms/</link><description>Recent content in CMS on recca0120 技術筆記</description><generator>Hugo -- gohugo.io</generator><language>zh-hant-tw</language><lastBuildDate>Tue, 07 Apr 2026 05:36:00 +0800</lastBuildDate><atom:link href="https://recca0120.github.io/tags/cms/index.xml" rel="self" type="application/rss+xml"/><item><title>EmDash：用 Astro + Cloudflare 打造的全棧 TypeScript CMS，能取代 WordPress 嗎？</title><link>https://recca0120.github.io/2026/04/07/emdash-cms-astro-cloudflare/</link><pubDate>Tue, 07 Apr 2026 05:36:00 +0800</pubDate><guid>https://recca0120.github.io/2026/04/07/emdash-cms-astro-cloudflare/</guid><description>&lt;img src="https://recca0120.github.io/" alt="Featured image of post EmDash：用 Astro + Cloudflare 打造的全棧 TypeScript CMS，能取代 WordPress 嗎？" /&gt;&lt;p&gt;WordPress 佔了全球網站的 43%，但它是 2003 年的產物。PHP + MySQL、外掛可以直接碰資料庫、內容存成 HTML 跟 DOM 綁死。用了二十年，該有人重新想這件事了。&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://github.com/emdash-cms/emdash" target="_blank" rel="noopener"
 &gt;EmDash&lt;/a&gt; 就是這個嘗試。全棧 TypeScript，跑在 Astro 上面，用 Cloudflare 的基礎設施。目前還在 beta，但架構設計值得看一看。&lt;/p&gt;
&lt;h2 id="跟-wordpress-差在哪"&gt;&lt;a href="#%e8%b7%9f-wordpress-%e5%b7%ae%e5%9c%a8%e5%93%aa" class="header-anchor"&gt;&lt;/a&gt;跟 WordPress 差在哪
&lt;/h2&gt;&lt;p&gt;先講最大的幾個差異。&lt;/p&gt;
&lt;h3 id="外掛沙箱隔離"&gt;&lt;a href="#%e5%a4%96%e6%8e%9b%e6%b2%99%e7%ae%b1%e9%9a%94%e9%9b%a2" class="header-anchor"&gt;&lt;/a&gt;外掛沙箱隔離
&lt;/h3&gt;&lt;p&gt;WordPress 96% 的安全漏洞來自外掛。原因很直接：外掛跟主程式跑在同一個 PHP process，對資料庫和檔案系統有完整的存取權限。裝了一個有問題的外掛，整個站就暴露了。&lt;/p&gt;
&lt;p&gt;EmDash 用 Cloudflare Workers 的 Dynamic Worker Loaders 做隔離。每個外掛要宣告 capability manifest，明確列出它需要什麼權限：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;definePlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;my-plugin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;capabilities&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;read:content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;email:send&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;content:afterSave&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 只能在宣告的權限範圍內操作
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;沒有宣告 &lt;code&gt;write:content&lt;/code&gt;，就不能寫入內容。這個設計從根本上限制了外掛的攻擊面。&lt;/p&gt;
&lt;h3 id="內容格式portable-text-取代-html"&gt;&lt;a href="#%e5%85%a7%e5%ae%b9%e6%a0%bc%e5%bc%8fportable-text-%e5%8f%96%e4%bb%a3-html" class="header-anchor"&gt;&lt;/a&gt;內容格式：Portable Text 取代 HTML
&lt;/h3&gt;&lt;p&gt;WordPress 把內容存成 HTML。看起來很直覺，但問題在於 HTML 跟呈現方式綁死了。同一段內容要給 app、email、API 用，就得重新解析 DOM。&lt;/p&gt;
&lt;p&gt;EmDash 用 &lt;a class="link" href="https://www.portabletext.org/" target="_blank" rel="noopener"
 &gt;Portable Text&lt;/a&gt;，內容存成結構化的 JSON。一份內容可以給不同的 renderer 處理，不需要從 HTML 反推語意。&lt;/p&gt;
&lt;h3 id="全棧-typescript"&gt;&lt;a href="#%e5%85%a8%e6%a3%a7-typescript" class="header-anchor"&gt;&lt;/a&gt;全棧 TypeScript
&lt;/h3&gt;&lt;p&gt;WordPress 是 PHP，前端再加一層 JavaScript。EmDash 從 schema 定義到前端渲染全部都是 TypeScript，而且 schema 改了之後可以直接生成型別：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npx emdash types
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;這條指令會從資料庫 schema 產出 TypeScript 型別定義，IDE 裡改 schema 就會立刻看到型別錯誤。&lt;/p&gt;
&lt;h2 id="技術架構"&gt;&lt;a href="#%e6%8a%80%e8%a1%93%e6%9e%b6%e6%a7%8b" class="header-anchor"&gt;&lt;/a&gt;技術架構
&lt;/h2&gt;&lt;h3 id="資料庫不綁死"&gt;&lt;a href="#%e8%b3%87%e6%96%99%e5%ba%ab%e4%b8%8d%e7%b6%81%e6%ad%bb" class="header-anchor"&gt;&lt;/a&gt;資料庫不綁死
&lt;/h3&gt;&lt;p&gt;EmDash 用 &lt;a class="link" href="https://kysely.dev/" target="_blank" rel="noopener"
 &gt;Kysely&lt;/a&gt; 做資料庫抽象層，支援多種 SQL 方言：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;環境&lt;/th&gt;
 &lt;th&gt;資料庫&lt;/th&gt;
 &lt;th&gt;儲存&lt;/th&gt;
 &lt;th&gt;Session&lt;/th&gt;
 &lt;th&gt;外掛隔離&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Cloudflare&lt;/td&gt;
 &lt;td&gt;D1&lt;/td&gt;
 &lt;td&gt;R2&lt;/td&gt;
 &lt;td&gt;KV&lt;/td&gt;
 &lt;td&gt;Worker isolates&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;自架&lt;/td&gt;
 &lt;td&gt;SQLite / PostgreSQL&lt;/td&gt;
 &lt;td&gt;S3 相容 / 本機&lt;/td&gt;
 &lt;td&gt;Redis / 檔案&lt;/td&gt;
 &lt;td&gt;同程序模式&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;想跑在 Cloudflare 上就用 D1 + R2，想自架就用 SQLite + 本機檔案系統。不會被特定雲端綁住。&lt;/p&gt;
&lt;h3 id="astro-整合"&gt;&lt;a href="#astro-%e6%95%b4%e5%90%88" class="header-anchor"&gt;&lt;/a&gt;Astro 整合
&lt;/h3&gt;&lt;p&gt;EmDash 是 Astro 的 integration，設定方式跟其他 Astro 外掛一樣：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// astro.config.mjs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;emdash&lt;/span&gt; &lt;span class="kr"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;emdash/astro&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;d1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kr"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;emdash/db&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;integrations&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;emdash&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt;: &lt;span class="kt"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;查詢內容用 &lt;code&gt;getEmDashCollection&lt;/code&gt;，語法跟 Astro 的 Content Collections 很像：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;getEmDashCollection&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;emdash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;getEmDashCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;posts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;))}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;重點是這些資料是即時從資料庫撈的，不需要重新 build 整個站。&lt;/p&gt;
&lt;h2 id="功能一覽"&gt;&lt;a href="#%e5%8a%9f%e8%83%bd%e4%b8%80%e8%a6%bd" class="header-anchor"&gt;&lt;/a&gt;功能一覽
&lt;/h2&gt;&lt;h3 id="內容管理"&gt;&lt;a href="#%e5%85%a7%e5%ae%b9%e7%ae%a1%e7%90%86" class="header-anchor"&gt;&lt;/a&gt;內容管理
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;可自訂的 content types（collections），在管理介面用 UI 拉就好&lt;/li&gt;
&lt;li&gt;TipTap 富文本編輯器&lt;/li&gt;
&lt;li&gt;版本控制、草稿、排程發佈&lt;/li&gt;
&lt;li&gt;FTS5 全文搜尋&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="認證系統"&gt;&lt;a href="#%e8%aa%8d%e8%ad%89%e7%b3%bb%e7%b5%b1" class="header-anchor"&gt;&lt;/a&gt;認證系統
&lt;/h3&gt;&lt;p&gt;預設用 Passkey（WebAuthn），也支援 OAuth 和 Magic link。權限分四層：Administrator、Editor、Author、Contributor。&lt;/p&gt;
&lt;h3 id="外掛能做什麼"&gt;&lt;a href="#%e5%a4%96%e6%8e%9b%e8%83%bd%e5%81%9a%e4%bb%80%e9%ba%bc" class="header-anchor"&gt;&lt;/a&gt;外掛能做什麼
&lt;/h3&gt;&lt;p&gt;外掛能力不只是 hook 而已：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KV 儲存&lt;/li&gt;
&lt;li&gt;設定管理&lt;/li&gt;
&lt;li&gt;管理員頁面&lt;/li&gt;
&lt;li&gt;Dashboard widgets&lt;/li&gt;
&lt;li&gt;自訂 block 類型&lt;/li&gt;
&lt;li&gt;API 路由&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="ai-整合"&gt;&lt;a href="#ai-%e6%95%b4%e5%90%88" class="header-anchor"&gt;&lt;/a&gt;AI 整合
&lt;/h3&gt;&lt;p&gt;EmDash 原生支援 MCP（Model Context Protocol），可以直接用 Claude 或 ChatGPT 操作內容和 schema。也有 agent skills 幫忙做外掛和主題開發。&lt;/p&gt;
&lt;h3 id="wordpress-搬家"&gt;&lt;a href="#wordpress-%e6%90%ac%e5%ae%b6" class="header-anchor"&gt;&lt;/a&gt;WordPress 搬家
&lt;/h3&gt;&lt;p&gt;支援 WXR 匯出檔匯入、REST API 對接、WordPress.com 導入。還有一個 &lt;code&gt;gutenberg-to-portable-text&lt;/code&gt; 套件把 Gutenberg block 轉成 Portable Text。&lt;/p&gt;
&lt;h2 id="快速開始"&gt;&lt;a href="#%e5%bf%ab%e9%80%9f%e9%96%8b%e5%a7%8b" class="header-anchor"&gt;&lt;/a&gt;快速開始
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm create emdash@latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;這會跑一個 scaffold，選模板就能開始。有三種模板：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;blog&lt;/strong&gt;：分類、標籤、全文搜尋、RSS、深色模式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;marketing&lt;/strong&gt;：Hero section、定價卡片、FAQ、聯絡表單&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;portfolio&lt;/strong&gt;：專案網格、標籤篩選、案例研究頁面&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本地開發：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone https://github.com/emdash-cms/emdash.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; emdash
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pnpm install
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pnpm build
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pnpm --filter emdash-demo seed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pnpm --filter emdash-demo dev
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;管理介面在 &lt;code&gt;http://localhost:4321/_emdash/admin&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="目前的限制"&gt;&lt;a href="#%e7%9b%ae%e5%89%8d%e7%9a%84%e9%99%90%e5%88%b6" class="header-anchor"&gt;&lt;/a&gt;目前的限制
&lt;/h2&gt;&lt;p&gt;EmDash 還在 beta，幾件事要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;外掛的 Worker isolate 是 Cloudflare 付費帳號功能，免費帳號只能跑同程序模式（沒有沙箱）&lt;/li&gt;
&lt;li&gt;生態系剛起步，第三方外掛和主題數量跟 WordPress 沒得比&lt;/li&gt;
&lt;li&gt;文件還在建置中，有些功能要翻原始碼才知道怎麼用&lt;/li&gt;
&lt;li&gt;Portable Text 的學習曲線比 HTML 高，特別是要自訂 block 的時候&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="適合誰"&gt;&lt;a href="#%e9%81%a9%e5%90%88%e8%aa%b0" class="header-anchor"&gt;&lt;/a&gt;適合誰
&lt;/h2&gt;&lt;p&gt;如果你是 WordPress 重度使用者、需要上千個外掛、客戶要自己裝佈景主題，EmDash 現階段還替代不了。&lt;/p&gt;
&lt;p&gt;但如果你本來就在用 Astro，想要一個能讓非技術人員編輯內容的後台，又不想串 headless CMS 的 API，EmDash 的整合方式很直接。TypeScript 全棧、schema 改了型別跟著動、部署到 Cloudflare 幾乎零設定。&lt;/p&gt;
&lt;p&gt;它不是要殺死 WordPress，但它示範了一個 2026 年的 CMS 可以長什麼樣子。&lt;/p&gt;
&lt;h2 id="參考資源"&gt;&lt;a href="#%e5%8f%83%e8%80%83%e8%b3%87%e6%ba%90" class="header-anchor"&gt;&lt;/a&gt;參考資源
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/emdash-cms/emdash" target="_blank" rel="noopener"
 &gt;EmDash GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.portabletext.org/" target="_blank" rel="noopener"
 &gt;Portable Text 官方網站&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kysely.dev/" target="_blank" rel="noopener"
 &gt;Kysely - Type-safe SQL Query Builder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.astro.build/" target="_blank" rel="noopener"
 &gt;Astro 官方文件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://developers.cloudflare.com/workers/" target="_blank" rel="noopener"
 &gt;Cloudflare Workers 文件&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>