<?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 Tech Notes</title><link>https://recca0120.github.io/en/tags/cms/</link><description>Recent content in CMS on recca0120 Tech Notes</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Tue, 07 Apr 2026 05:36:00 +0800</lastBuildDate><atom:link href="https://recca0120.github.io/en/tags/cms/index.xml" rel="self" type="application/rss+xml"/><item><title>EmDash: A Full-Stack TypeScript CMS Built on Astro + Cloudflare — Can It Replace WordPress?</title><link>https://recca0120.github.io/en/2026/04/07/emdash-cms-astro-cloudflare/</link><pubDate>Tue, 07 Apr 2026 05:36:00 +0800</pubDate><guid>https://recca0120.github.io/en/2026/04/07/emdash-cms-astro-cloudflare/</guid><description>&lt;img src="https://recca0120.github.io/" alt="Featured image of post EmDash: A Full-Stack TypeScript CMS Built on Astro + Cloudflare — Can It Replace WordPress?" /&gt;&lt;p&gt;WordPress powers 43% of the web, but it was born in 2003. PHP + MySQL, plugins with full database access, content stored as HTML coupled to the DOM. After twenty years, it&amp;rsquo;s fair to rethink the whole thing.&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; is that attempt. Full-stack TypeScript, running on Astro, backed by Cloudflare infrastructure. Still in beta, but the architecture is worth examining.&lt;/p&gt;
&lt;h2 id="how-it-differs-from-wordpress"&gt;&lt;a href="#how-it-differs-from-wordpress" class="header-anchor"&gt;&lt;/a&gt;How It Differs from WordPress
&lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s start with the biggest differences.&lt;/p&gt;
&lt;h3 id="sandboxed-plugin-isolation"&gt;&lt;a href="#sandboxed-plugin-isolation" class="header-anchor"&gt;&lt;/a&gt;Sandboxed Plugin Isolation
&lt;/h3&gt;&lt;p&gt;96% of WordPress security vulnerabilities come from plugins. The reason is straightforward: plugins run in the same PHP process as the core, with full access to the database and filesystem. One bad plugin exposes the entire site.&lt;/p&gt;
&lt;p&gt;EmDash uses Cloudflare Workers&amp;rsquo; Dynamic Worker Loaders for isolation. Each plugin must declare a capability manifest listing exactly what permissions it needs:&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;// Can only operate within declared permissions
&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;No &lt;code&gt;write:content&lt;/code&gt; declaration, no write access. This fundamentally limits a plugin&amp;rsquo;s attack surface.&lt;/p&gt;
&lt;h3 id="content-format-portable-text-instead-of-html"&gt;&lt;a href="#content-format-portable-text-instead-of-html" class="header-anchor"&gt;&lt;/a&gt;Content Format: Portable Text Instead of HTML
&lt;/h3&gt;&lt;p&gt;WordPress stores content as HTML. Seems intuitive, but the problem is that HTML is tightly coupled to presentation. If you want the same content for an app, email, or API, you have to re-parse the DOM.&lt;/p&gt;
&lt;p&gt;EmDash uses &lt;a class="link" href="https://www.portabletext.org/" target="_blank" rel="noopener"
 &gt;Portable Text&lt;/a&gt;, storing content as structured JSON. One piece of content can be processed by different renderers without reverse-engineering semantics from HTML.&lt;/p&gt;
&lt;h3 id="full-stack-typescript"&gt;&lt;a href="#full-stack-typescript" class="header-anchor"&gt;&lt;/a&gt;Full-Stack TypeScript
&lt;/h3&gt;&lt;p&gt;WordPress is PHP with JavaScript layered on top for the frontend. EmDash is TypeScript from schema definition to frontend rendering, and schema changes generate types automatically:&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;This command generates TypeScript type definitions from the database schema. Change the schema, and your IDE immediately shows type errors.&lt;/p&gt;
&lt;h2 id="architecture"&gt;&lt;a href="#architecture" class="header-anchor"&gt;&lt;/a&gt;Architecture
&lt;/h2&gt;&lt;h3 id="no-database-lock-in"&gt;&lt;a href="#no-database-lock-in" class="header-anchor"&gt;&lt;/a&gt;No Database Lock-in
&lt;/h3&gt;&lt;p&gt;EmDash uses &lt;a class="link" href="https://kysely.dev/" target="_blank" rel="noopener"
 &gt;Kysely&lt;/a&gt; as its database abstraction layer, supporting multiple SQL dialects:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Environment&lt;/th&gt;
 &lt;th&gt;Database&lt;/th&gt;
 &lt;th&gt;Storage&lt;/th&gt;
 &lt;th&gt;Session&lt;/th&gt;
 &lt;th&gt;Plugin Isolation&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;Self-hosted&lt;/td&gt;
 &lt;td&gt;SQLite / PostgreSQL&lt;/td&gt;
 &lt;td&gt;S3-compatible / local&lt;/td&gt;
 &lt;td&gt;Redis / file&lt;/td&gt;
 &lt;td&gt;In-process mode&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Want to run on Cloudflare? Use D1 + R2. Want to self-host? Use SQLite + local filesystem. No vendor lock-in.&lt;/p&gt;
&lt;h3 id="astro-integration"&gt;&lt;a href="#astro-integration" class="header-anchor"&gt;&lt;/a&gt;Astro Integration
&lt;/h3&gt;&lt;p&gt;EmDash is an Astro integration, configured like any other Astro plugin:&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;Querying content uses &lt;code&gt;getEmDashCollection&lt;/code&gt;, with syntax similar to Astro&amp;rsquo;s 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;The key point: this data is fetched live from the database — no need to rebuild the entire site.&lt;/p&gt;
&lt;h2 id="feature-overview"&gt;&lt;a href="#feature-overview" class="header-anchor"&gt;&lt;/a&gt;Feature Overview
&lt;/h2&gt;&lt;h3 id="content-management"&gt;&lt;a href="#content-management" class="header-anchor"&gt;&lt;/a&gt;Content Management
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Customizable content types (collections) built through the admin UI&lt;/li&gt;
&lt;li&gt;TipTap rich text editor&lt;/li&gt;
&lt;li&gt;Version control, drafts, scheduled publishing&lt;/li&gt;
&lt;li&gt;FTS5 full-text search&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="authentication"&gt;&lt;a href="#authentication" class="header-anchor"&gt;&lt;/a&gt;Authentication
&lt;/h3&gt;&lt;p&gt;Passkey-first (WebAuthn) by default, with OAuth and Magic link as alternatives. Four permission levels: Administrator, Editor, Author, Contributor.&lt;/p&gt;
&lt;h3 id="plugin-capabilities"&gt;&lt;a href="#plugin-capabilities" class="header-anchor"&gt;&lt;/a&gt;Plugin Capabilities
&lt;/h3&gt;&lt;p&gt;Plugins go beyond simple hooks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KV storage&lt;/li&gt;
&lt;li&gt;Settings management&lt;/li&gt;
&lt;li&gt;Admin pages&lt;/li&gt;
&lt;li&gt;Dashboard widgets&lt;/li&gt;
&lt;li&gt;Custom block types&lt;/li&gt;
&lt;li&gt;API routes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="ai-integration"&gt;&lt;a href="#ai-integration" class="header-anchor"&gt;&lt;/a&gt;AI Integration
&lt;/h3&gt;&lt;p&gt;EmDash natively supports MCP (Model Context Protocol), allowing direct content and schema manipulation through Claude or ChatGPT. Agent skills help with plugin and theme development.&lt;/p&gt;
&lt;h3 id="wordpress-migration"&gt;&lt;a href="#wordpress-migration" class="header-anchor"&gt;&lt;/a&gt;WordPress Migration
&lt;/h3&gt;&lt;p&gt;Supports WXR export import, REST API integration, and WordPress.com import. A &lt;code&gt;gutenberg-to-portable-text&lt;/code&gt; package converts Gutenberg blocks to Portable Text.&lt;/p&gt;
&lt;h2 id="quick-start"&gt;&lt;a href="#quick-start" class="header-anchor"&gt;&lt;/a&gt;Quick Start
&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;This runs a scaffold where you pick a template. Three options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;blog&lt;/strong&gt;: categories, tags, full-text search, RSS, dark mode&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;marketing&lt;/strong&gt;: hero section, pricing cards, FAQ, contact form&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;portfolio&lt;/strong&gt;: project grid, tag filtering, case study pages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Local development:&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;Admin panel at &lt;code&gt;http://localhost:4321/_emdash/admin&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="current-limitations"&gt;&lt;a href="#current-limitations" class="header-anchor"&gt;&lt;/a&gt;Current Limitations
&lt;/h2&gt;&lt;p&gt;EmDash is still in beta. A few things to keep in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Worker isolate sandboxing requires a paid Cloudflare account; free accounts run in-process mode (no sandbox)&lt;/li&gt;
&lt;li&gt;The ecosystem is just starting — third-party plugins and themes can&amp;rsquo;t compete with WordPress&amp;rsquo;s library&lt;/li&gt;
&lt;li&gt;Documentation is still being built; some features require reading source code&lt;/li&gt;
&lt;li&gt;Portable Text has a steeper learning curve than HTML, especially for custom blocks&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="who-is-this-for"&gt;&lt;a href="#who-is-this-for" class="header-anchor"&gt;&lt;/a&gt;Who Is This For
&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;re a heavy WordPress user who needs thousands of plugins and clients installing their own themes, EmDash can&amp;rsquo;t replace that yet.&lt;/p&gt;
&lt;p&gt;But if you&amp;rsquo;re already using Astro and want a backend that lets non-technical people edit content — without wiring up a headless CMS API — EmDash&amp;rsquo;s integration is seamless. Full-stack TypeScript, types that update with schema changes, and near-zero-config Cloudflare deployment.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not trying to kill WordPress, but it demonstrates what a CMS designed in 2026 can look like.&lt;/p&gt;
&lt;h2 id="references"&gt;&lt;a href="#references" class="header-anchor"&gt;&lt;/a&gt;References
&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 Official Website&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 Documentation&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 Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>