- CMS
- 后端
- 前端
- 插件系统
【免费下载链接】emdash
EmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress
EmDash 是一个基于 Astro 构建的全栈 TypeScript CMS(被项目描述为 WordPress 的精神续作),而templates/blog正是官方为"以写作为核心产品"的博客场景准备的完整模板。本文以 templates/blog/CLAUDE.md 为骨架,结合模板内的源码、配置与种子数据,系统讲解该模板的启动命令、关键文件职责、内容 Schema 设计、页面路由实现、视觉系统与定制方法,帮助你快速上手并二次开发一个属于自己的 EmDash 博客站点。
模板定位:一个"写作即产品"的博客
根据 CLAUDE.md 中的模板说明,这是一个包含posts(文章)、pages(页面)、categories(分类)、tags(标签)、全文搜索与 RSS的博客模板,定位是"个人写作、技术写作、独立通讯(indie newsletters),以及任何以写作为核心产品的场景"。它的视觉基调是"编辑部技术美学"(editorial-tech aesthetic):自信的无衬线字体、克制的强调色、带 bylines(署名)与阅读时间(reading time)的真实文章结构。
整套模板由以下文件构成(位于 templates/blog):
| 文件/目录 | 用途 |
|---|---|
| astro.config.mjs | Astro 配置,包含emdash()集成、数据库与存储 |
| src/live.config.ts | EmDash loader 注册(样板代码,勿修改) |
| seed/seed.json | Schema 定义 + 演示内容(collections、fields、taxonomies、menus、widgets) |
| emdash-env.d.ts | 集合的生成类型(dev server 启动时自动重新生成) |
| src/layouts/Base.astro | 基础布局,承载 EmDash 接线(菜单、搜索、页面贡献) |
| src/pages/ | Astro 页面——全部服务端渲染 |
快速启动:两条命令跑起来
CLAUDE.md 给出了最核心的两条命令:
pnpm dev # 启动 Astro 开发服务器 npx emdash types # 根据运行中的站点重新生成 TypeScript 类型配套脚本在 package.json 中定义完整:dev(astro dev)、build(astro build)、preview(astro preview)、start(node ./dist/server/entry.mjs)、typecheck(astro check)。其中start表明该模板构建后以Node 独立模式(standalone)运行。
管理后台(Admin UI)地址为http://localhost:4321/_emdash/admin。开发模式下启动后即可在后台登录、写文章、管理分类标签,所有改动实时反映到前台页面。
类型生成机制
npx emdash types会基于运行中的站点(即当前 seed 的集合与字段定义)重新生成 emdash-env.d.ts。这个文件的类型会在 dev server 启动时自动重新生成,因此当你修改了 seed 中的字段后,重启开发服务器即可获得带完整类型提示的查询 API 体验。
关键文件地图
CLAUDE.md 用一张表概括了核心文件职责,理解它们就等于理解了模板的运行骨架:
- astro.config.mjs— Astro 配置,含
emdash()集成、数据库和存储配置; - src/live.config.ts— EmDash loader 注册,属于"样板代码,不要修改";
- seed/seed.json— Schema 定义与演示内容(集合、字段、分类法、菜单、组件区);
- emdash-env.d.ts— 集合的生成类型;
- src/layouts/Base.astro— 基础布局,含 EmDash 接线(菜单、搜索、页面贡献);
- src/pages/— 全部服务端渲染的 Astro 页面。
架构核心:emdash()集成、数据库与存储
astro.config.mjs 是整个模板的装配中心:
import node from "@astrojs/node"; import react from "@astrojs/react"; import auditLog from "@emdash-cms/plugin-audit-log"; import { defineConfig, fontProviders } from "astro/config"; import emdash, { local } from "emdash/astro"; import { sqlite } from "emdash/db"; export default defineConfig({ output: "server", adapter: node({ mode: "standalone", }), image: { layout: "constrained", responsiveStyles: true, }, integrations: [ react(), emdash({ database: sqlite({ url: "file:./data.db" }), storage: local({ directory: "./uploads", baseUrl: "/_emdash/api/media/file", }), plugins: [auditLog], }), ], fonts: [ /* 见下文"字体管线" */ ], devToolbar: { enabled: false }, });要点解读:
output: "server"+ Node adapter:这呼应了 CLAUDE.md 的硬性规则——所有内容页面必须服务端渲染,禁止为 CMS 内容使用getStaticPaths();emdash()集成接收三类配置:database:这里用sqlite({ url: "file:./data.db" }),本地开发默认 SQLite 单文件数据库;storage:local({ directory: "./uploads", baseUrl: "/_emdash/api/media/file" }),媒体文件存储在./uploads,通过/_emdash/api/media/file对外提供;plugins:默认挂载了@emdash-cms/plugin-audit-log(审计日志插件),这是工作区内的官方插件;
react()集成用于后台管理 UI 与评论组件等 React 渲染部分;devToolbar: { enabled: false }关闭了 Astro 默认的 dev toolbar。
内容加载器:live.config.ts
src/live.config.ts 定义了 EmDash 的 Live Content Collections:
import { defineLiveCollection } from "astro:content"; import { emdashLoader } from "emdash/runtime"; export const collections = { _emdash: defineLiveCollection({ loader: emdashLoader() }), };它建立了_emdash集合,覆盖数据库中所有内容类型。页面中通过getEmDashCollection()与getEmDashEntry()按类型查询具体内容——这正是 CLAUDE.md 要求"不要修改"的原因:它是模板与 EmDash 运行时之间的固定接线。
内容 Schema:seed.json 决定一切
CLAUDE.md 中的 Schema 一节可以用 seed/seed.json 完整印证。整个文件以$schema声明、version: 1开头,meta提供元信息,然后依次定义:
settings(站点设置)
"settings": { "title": "My Blog", "tagline": "Thoughts on building for the web" }CLAUDE.md 指出:站点设置的title和tagline都会渲染在 header / footer 中。模板的 site-identity.ts 会读取它们并解析出siteTitle、siteTagline、siteLogo。
collections(集合)
posts集合字段:
title(string,必填,可搜索)——文章标题;featured_image(image)——头图;content(portableText,可搜索)——正文(Portable Text 富文本);excerpt(text)——摘要。
posts的supports声明为["drafts", "revisions", "search", "seo"],即支持草稿、修订历史、全文搜索与 SEO,且commentsEnabled: true开启了评论。
pages集合字段:
title(string,必填,可搜索);content(portableText,可搜索)。
用于/about等静态页面。
taxonomies(分类法)
{ "name": "category", "label": "Categories", "hierarchical": true, "collections": ["posts"], "terms": [...] } { "name": "tag", "label": "Tags", "hierarchical": false, "collections": ["posts"], "terms": [...] }注意name是查询时使用的精确标识(如"category"而非"categories")——这是 CLAUDE.md 规则列表中的明确要求。分类是层级化的(hierarchical: true),标签则扁平。种子数据中分类含 development / design / notes,标签含 webdev / opinion / tools / creativity。
bylines(署名)
"bylines": [ { "id": "byline-editorial", "slug": "emdash-editorial", "displayName": "EmDash Editorial" }, { "id": "byline-guest", "slug": "guest-contributor", "displayName": "Guest Contributor", "isGuest": true } ]文章通过bylines关联作者署名,isGuest标记客座作者。
menus(菜单)
单一的primary菜单默认包含 Home、About、Posts 三个自定义链接项("type": "custom")。此外 Base.astro 还会尝试读取可选的social菜单用于页脚"Connect"栏——如果 seed 未定义,该栏只显示 RSS。
widgetAreas(组件区)
seed 定义了sidebar(单篇文章页右侧栏)与footer(页脚)两个组件区:
- sidebar 挂载了
core:search(搜索)、core:categories(分类)、core:tags(标签)、core:recent-posts(最新文章,count: 5、showDate: true)、core:archives(归档,月度、limit: 6); - footer 挂载了一个
content类型的小组件,渲染一段 Portable Text 简介。
sections(区块)与 content(内容)
newsletter-signup与about-author两个 theme 区块(带keywords便于检索复用),以及 7 篇演示文章 + 1 篇草稿(status: "draft",草稿不会出现在公开列表)和 1 个 About 页面。演示文章使用$media语法引用外部图片作为featured_image,每篇均声明了bylines与taxonomies归属。
页面与路由:每个 URL 都对应一个 Astro 文件
CLAUDE.md 的 Pages 表列出了模板的完整路由,在 src/pages 中一一对应:
| 页面 | 路径 | 内容 |
|---|---|---|
| 首页 | / | 精选文章 Hero(大图 + 摘要)、最新文章网格 |
| 全部文章 | /posts | 文章计数、带摘要与标签徽章的文章列表 |
| 文章详情 | /posts/[slug] | 头图、标题、正文、左侧元信息列(作者 + 日期)、右侧 TOC + 搜索 + 分类栏 |
| 搜索 | /search | 全文搜索 UI |
| 页面 | /pages/[slug] | 静态页面内容(Portable Text) |
| 分类 | /category/[slug] | 按分类过滤的文章 |
| 标签 | /tag/[slug] | 按标签过滤的文章 |
| RSS | /rss.xml | 生成的 feed |
首页的数据查询模式
src/pages/index.astro 是理解模板查询约定的最佳范例:
- 用
getEmDashCollection("posts", { orderBy: { published_at: "desc" }, limit: POSTS_PER_PAGE + 1 })让数据库做切片(POSTS_PER_PAGE = 7,多取 1 条用于判断是否显示"View all"),而不是拉全量再在 JS 里裁剪; - 自动挑选第一篇有
featured_image的文章作为 Hero,其余进网格; - 用
getTermsForEntries("posts", ids, "tag")一次性批量查询多篇文章的标签,避免逐篇调用getEntryTerms()造成 N+1 查询; - 查完内容后立即
Astro.cache.set(cacheHint)。
文章详情页的三栏阅读布局
src/pages/posts/[slug].astro 实现了 CLAUDE.md 强调的"招牌三栏阅读视图":
- 左侧元信息列(
--meta-col-width,180px):sticky 定位,展示 bylines 头像与署名、发布时间、阅读时间、标签; - 中间正文列(
--content-width,680px):<PortableText value={post.data.content} />渲染富文本,并用Image组件渲染头图; - 右侧栏(
--gutter-width,200px):客户端脚本从正文的 h2/h3 构建 TOC(带 IntersectionObserver 滚动高亮),下方是<WidgetArea name="sidebar" />渲染的侧栏组件区; - 底部还有
Continue reading相关文章区块与<Comments>/<CommentForm>评论系统(seed 中commentsEnabled: true)。
页面还展示了 SEO 的完整链路:getSeoMeta(post, {...})生成标题、描述、OG 图、canonical、robots,再通过getImageUrl()从图片对象(src或meta.storageKey)推导 OG 回退图,最后以createPublicPageContext()注入 Base 布局。
搜索页:走 FTS 而不是 JS 过滤
src/pages/search.astro 的注释点明了设计意图:调用 EmDash 的search(query, { collections: ["posts"], limit: 30 })全文搜索 API,由数据库完成分词、词干化与排序,而不是把全部文章拉到 JS 里过滤——后者在文章数超过几百篇后会迅速不可用。返回结果中的snippet已包含<mark>高亮标签,页面用set:html渲染。
六条硬性规则(来自 CLAUDE.md)
- 所有内容页面必须服务端渲染(
output: "server"),CMS 内容禁用getStaticPaths(); - 图片字段是对象
{ src, alt }而非字符串,必须用"emdash/ui"的<Image image={...} />渲染; entry.id是 slug(用于 URL),entry.data.id是数据库 ULID(用于getEntryTerms等 API 调用);- 查询内容的页面必须调用
Astro.cache.set(cacheHint)设置缓存提示; - 查询中的分类法名称必须与 seed 的
"name"字段完全一致(例如"category"而不是"categories")。
这些规则在源码中都能找到对应实现:所有页面均带cacheHint调用,getTermsForEntries使用entry.data.id,页面跳转链接使用p.id(slug)。
视觉系统:单字体、单强调色、三栏文章版式
CLAUDE.md 的 "Visual character" 一节精确描述了模板的视觉 DNA,源码可在 src/styles/tokens.css 验证:
- 单一字体族:全站使用Inter(绑定
--font-body),标题默认复用正文字体,靠字重与字号建立层级(--font-weight-heading600、--font-weight-display700 用于 h1/页标题),h1/h2 收紧字距(--tracking-tight/--tracking-snug);JetBrains Mono绑定--font-mono,用于行内代码与代码块; - 品牌色
#0066cc(--color-brand),用于链接、文章卡片标题 hover 与搜索框 focus 光环;另有--color-text-secondary与--color-muted用于次级文本与元信息。不要添加第二个强调色; - 文章三栏版式是标志性功能:左栏元信息、中间 680px 正文、右栏搜索 + TOC + 分类。桌面端不要把它压成一栏——"这个布局在向读者传递:这是值得阅读的内容"。
深色模式与主题切换
tokens.css 中所有颜色都用light-dark(<light>, <dark>)定义,同一个 token 同时携带明暗两套值,无需维护单独的暗色面板。Base.astro 的内联脚本会在页面渲染前立即根据themecookie 应用主题(防止闪烁),页脚提供 light / dark / system 三个切换按钮,通过写themecookie 与给<html>加 class 实现。此外还有针对不支持light-dark()的旧浏览器(Safari < 17.5、Chrome < 123)的纯亮色回退块。
定制化:token 覆盖、字体管线与 CSS 变量清单
设计 token 的分层覆盖机制
CLAUDE.md 明确指出:设计 token 的默认值全部在src/styles/tokens.css,而重新换肤应该改src/styles/theme.css——因为 theme.css 中的声明是**无分层(unlayered)**的,永远压过@layer base的默认值,无需技巧性的选择器优先级对抗。不要为了视觉改动而去编辑tokens.css或Base.astro。
theme.css 的注释给出了示例:
:root { --color-brand: light-dark(#0f766e, #2dd4bf); --font-heading: "Iowan Old Style", Georgia, serif; --radius: 8px; }覆盖颜色的两条规则:
- 用纯色值覆盖会同时改变明暗两套模式;
- 想保持明暗区分,就用
light-dark(<light>, <dark>)覆盖。
字体管线(astro.config.mjs 的 fonts 配置)
Webfont 在 astro.config.mjs 的fonts:数组中配置:
fonts: [ { provider: fontProviders.google(), name: "Inter", cssVariable: "--font-body", weights: [400, 500, 600, 700], fallbacks: ["sans-serif"], }, { provider: fontProviders.google(), name: "JetBrains Mono", cssVariable: "--font-mono", weights: [400, 500], fallbacks: ["monospace"], }, ],换正文衬线字体的操作是:修改绑定cssVariable: "--font-body"的条目的name。CLAUDE.md 推荐的无衬线替代:Geist、IBM Plex Sans、Söhne(需授权)、Public Sans;如果想要衬线正文博客,可换 Source Serif、Crimson Pro 或 Lora——但换衬线后要把--font-size-base提到1.0625rem以保可读性。若只想让标题用别的字体(或改用系统字体)而不碰字体管线,直接在 theme.css 覆盖--font-heading或--font-body即可。Base.astro 通过<Font cssVariable="--font-body" preload />加载并预取字体。
值得掌握的 CSS 变量(完整列表见 tokens.css)
- 品牌与配色:
--color-brand、--color-brand-hover、--color-on-brand、--color-brand-ring; - 页面配色:
--color-bg、--color-bg-subtle、--color-surface、--color-text、--color-text-secondary、--color-muted、--color-border、--color-border-subtle; - 字体:
--font-body、--font-heading、--font-mono; - 字重:
--font-weight-heading(600)/--font-weight-display(700)——换衬线时可调低; - 字距:
--tracking-tight/--tracking-snug/--tracking-wide/--tracking-wider,用于标题与元信息标签; - 布局:
--content-width(680px,正文列)、--wide-width(1200px,最大容器)、--gutter-width(200px,文章页右侧 TOC 栏)、--meta-col-width(180px,文章页左侧元信息列); - 头像:
--avatar-size-{xs,sm,md,lg},对应卡片、列表、精选、单篇文章等不同尺度的 byline 头像。
值得留意的是 tokens.css 中的全套字号阶梯(--font-size-xs到--font-size-5xl)、行高(--leading-tight到--leading-relaxed)、间距(--spacing-1到--spacing-24)与阴影 token(--shadow-sm/--shadow-lg),它们共同支撑了整套版式的统一性。
反向清单:"不要做什么"
CLAUDE.md 的 "What not to do" 是一份极具实操价值的边界清单:
- 不要添加第二个强调色或彩色区块背景——页面应该是黑、白、一种蓝;
- 不要用展示型无衬线体(Bebas、Anton 等)替换 Inter——标题层级靠字重而非新奇字体;
- 不要在桌面端折叠文章侧栏——它是阅读体验的一部分;
- 不要用模板式博客文案("Welcome to my blog"、"Stay tuned for more")——写一句真正说明这个博客是干什么的 tagline;
- 不要用三篇一模一样的占位文章填充首页——只有一篇真实文章就展示一篇;
- 不要在没有审核计划时开启评论——模板默认不附带评论系统是有原因的。
Agent 技能与文档接入(面向 AI 编码工具)
CLAUDE.md 的 Skills 与 Documentation 两节面向 AI Agent 场景,但同样值得人类开发者了解:
- 技能存放于
.agents/skills/,按任务加载:building-emdash-site(内容查询、Portable Text 渲染、Schema 设计、seed 文件、菜单/组件区/搜索/SEO/评论/byline 等站点特性,建议从这里开始)、creating-plugins(用 hooks、storage、admin UI、API routes 与 Portable Text block 类型构建插件)、emdash-cli(内容管理、seed、类型生成与可视化编辑流程的 CLI 命令); - EmDash 文档以 MCP server 形式提供,模板自带了
.mcp.json、.cursor/mcp.json、.vscode/mcp.json,使 Claude Code、Cursor、VS Code 能自动发现文档服务器;其他工具(OpenCode、Windsurf 等)需要一次性手动配置。
延伸阅读
- 模板根说明:templates/blog/README.md(若存在)
- Agent 模板约定:templates/blog/AGENTS.md 与 templates/blog/AGENTS-template.md
- 版本变更记录:templates/blog/CHANGELOG.md
- 同系列云托管模板:templates/blog-cloudflare(Cloudflare Workers 版本),以及 marketing、portfolio、starter 等姊妹模板(见 templates)
- 全仓库技能库:skills/building-emdash-site/SKILL.md、skills/creating-plugins/SKILL.md、skills/emdash-cli/SKILL.md
- CMS
- 后端
- 前端
- 插件系统
【免费下载链接】emdash
EmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress
相关推荐
EmDash 博客模板深度指南:基于 Astro 的全栈 CMS 站点搭建与定制
EmDash 博客模板深度指南:基于 Astro 的全栈 CMS 站点搭建与定制 EmDash 是一个基于 Astro 构建的全栈 TypeScript CMS
CMS后端前端插件系统EmDash 博客模板(Cloudflare 版)实战指南:基于 Astro + D1 + R2 的编辑器优先 CMS 站点搭建
EmDash 博客模板(Cloudflare 版)实战指南:基于 Astro + D1 + R2 的编辑器优先 CMS 站点搭建 本指南围绕 EmDash 官方
CMS后端前端插件系统EmDash Blank 模板实战:基于 Astro 从零搭建无预设 CMS 站点的最小基底
EmDash Blank 模板实战:基于 Astro 从零搭建无预设 CMS 站点的最小基底 导读 templates/blank 是 EmDash(一个构建在
CMS后端前端插件系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考