EmDash 博客模板实战指南:基于 Astro 的 CMS 站点结构、内容模型与主题定制
2026/9/24 3:29:08 网站建设 项目流程
  • CMS
  • 后端
  • 前端
  • 插件系统

【免费下载链接】emdash

EmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress

项目地址:https://gitcode.com/gh_mirrors/emdas/emdash
点击查看免费下载

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.mjsAstro 配置,包含emdash()集成、数据库与存储
src/live.config.tsEmDash loader 注册(样板代码,勿修改)
seed/seed.jsonSchema 定义 + 演示内容(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 单文件数据库;
    • storagelocal({ 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 指出:站点设置的titletagline都会渲染在 header / footer 中。模板的 site-identity.ts 会读取它们并解析出siteTitlesiteTaglinesiteLogo

collections(集合)

posts集合字段:

  • title(string,必填,可搜索)——文章标题;
  • featured_image(image)——头图;
  • content(portableText,可搜索)——正文(Portable Text 富文本);
  • excerpt(text)——摘要。

postssupports声明为["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: 5showDate: true)、core:archives(归档,月度、limit: 6);
  • footer 挂载了一个content类型的小组件,渲染一段 Portable Text 简介。

sections(区块)与 content(内容)

newsletter-signupabout-author两个 theme 区块(带keywords便于检索复用),以及 7 篇演示文章 + 1 篇草稿(status: "draft",草稿不会出现在公开列表)和 1 个 About 页面。演示文章使用$media语法引用外部图片作为featured_image,每篇均声明了bylinestaxonomies归属。

页面与路由:每个 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()从图片对象(srcmeta.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.cssBase.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

项目地址:https://gitcode.com/gh_mirrors/emdas/emdash
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询