
CMS后端前端插件系统【免费下载链接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress项目地址https://gitcode.com/gh_mirrors/emdas/emdash点击查看免费下载导读本文基于 EmDash 官方技能文档 site-features.md位于templates/marketing-cloudflare与templates/marketing模板的构建技能库中系统讲解在基于 Astro 的全栈 TypeScript CMS——EmDash 中开发前台站点的全部核心功能站点设置、导航菜单、分类法Taxonomy、Widget 区域、全文搜索、SEO Meta、评论、插件页面注入Page Contributions、署名Bylines、深色模式与基础布局。读完本文你将掌握如何用emdash包提供的公开 API 与 UI 组件把模板中由 seed 数据驱动的静态页面升级为功能完整、可被搜索引擎索引、支持插件扩展的动态站点。前置认知本指南的适用场景与数据来源EmDash 站点的一切内容均由seed 数据如 templates/marketing-cloudflare/seed/seed.json、templates/blog/seed/seed.json初始化并驱动。seed 中声明了settings站点设置、collections内容集合及其字段、menus菜单、taxonomies分类法、widgetAreasWidget 区域等。本文介绍的每个功能函数本质上都是对这些 seed 声明内容的查询与渲染因此理解 seed 结构是使用下文 API 的前提。一、Site Settings站点设置站点设置是全局的键值配置用于替代在模板中硬编码站点名称、Logo 等。读取全部设置import { getSiteSettings, getSiteSetting } from emdash; // All settings const settings await getSiteSettings(); settings.title; // My Site settings.tagline; // A description settings.logo?.url; // Resolved media URL settings.favicon?.url; // Single setting const title await getSiteSetting(title);可用键Available keystitle、tagline、logo、favicon、social、timezone、dateFormat。最佳实践应使用这些函数代替硬编码的站点名称、Logo 等这样站点运营者可以在管理后台修改设置而无需改动代码。源码级的实现细节从源码 packages/core/src/settings/index.ts 可以看到getSiteSetting(key)内部会先检查同一请求内是否已经调用过getSiteSettings()若已调用则直接复用该请求级缓存的结果requestCached批处理避免对 options 表发起第二次查询——典型场景是 Base 模板先拉取全部设置子组件再按需读取单个设置此时不会产生额外数据库往返。getSiteSettings()采用多层缓存requestCached请求内去重→singleFlightCached跨请求合并在全局作用域生命周期内缓存解析值→ 分布式对象缓存cachedQuery冷启动的 isolate 无数据库往返即可命中。若需要在 Cloudflare Workers 边缘环境中做缓存失效可改用getSiteSettingsWithCacheHint()其返回的cacheHint可与Astro.cache.set()配合站点设置变化时自动清除渲染了设置的页面缓存。二、Navigation Menus导航菜单获取菜单数据import { getMenu, getMenus } from emdash; // Fetch a named menu const menu await getMenu(primary); // List all menus const menus await getMenus();getMenu(name)按名称获取单个菜单getMenus()返回全部菜单不含菜单项用于后台列表或站点导航摘要。菜单名称与 seed 中menus[].name完全对应例如 marketing 模板定义了primary、footer_product、footer_company、footer_support四个菜单。渲染一个菜单--- import { getMenu } from emdash; const primaryMenu await getMenu(primary); --- nav {primaryMenu?.items.map(item ( a href{item.url} target{item.target}{item.label}/a ))} /nav嵌套菜单下拉菜单{primaryMenu?.items.map(item ( li a href{item.url}{item.label}/a {item.children.length 0 ( ul classsubmenu {item.children.map(child ( lia href{child.url}{child.label}/a/li ))} /ul )} /li ))}MenuItem 数据结构interface MenuItem { id: string; label: string; url: string; // Resolved URL target?: string; // _blank etc. children: MenuItem[]; }源码级的实现细节从源码 packages/core/src/menus/index.ts 可以确认getMenu(name, options)支持options.locale多语言站点按区域解析菜单与options.trailingSlash是否保留尾部斜杠默认跟随宿主站点配置因此菜单url是已解析的最终 URL直接可用于a href。查询结果经requestCached与cachedQuery双层缓存命名空间为CacheNamespace.MENUS缓存键形如menu:primary:locale:trailingSlash。如需边缘缓存提示可使用getMenuWithCacheHint(name)其cacheHint标签为menu:name菜单被编辑时渲染该菜单的页面可被自动清除缓存。三、Taxonomies分类法分类与标签核心查询函数import { getTaxonomyTerms, getTerm, getEntryTerms, getEntriesByTerm } from emdash; // All terms in a taxonomy (name must match your seeds name field exactly) const categories await getTaxonomyTerms(category); const tags await getTaxonomyTerms(tag); // Single term by slug const term await getTerm(category, news); // { id, name, slug, label, children, count } // Terms for a specific entry (use data.id, not entry.id!) const postCategories await getEntryTerms(posts, post.data.id, category); const postTags await getEntryTerms(posts, post.data.id, tag); // Entries with a specific term const newsPosts await getEntriesByTerm(posts, category, news);两个关键注意事项Important:分类法名称参数必须与 seed 中定义的name字段完全一致。blog seed 使用的是单数形式的category和tag。如果写成categories会返回空结果且不报任何错误——这是最容易踩的坑。Important:getEntryTerms接收的是数据库 ULID即post.data.id而不是 slugpost.id。两者是不同的字段传错会得到空结果。展示文章的分类/标签--- const tags await getEntryTerms(posts, post.data.id, tag); --- {tags.map(t ( a href{/tag/${t.slug}}{t.label}/a ))}按分类法过滤内容--- const { entries: posts } await getEmDashCollection(posts, { where: { category: term.slug }, orderBy: { published_at: desc }, }); ---源码级的实现细节从源码 packages/core/src/taxonomies/index.ts 可以确认getTaxonomyTerms(taxonomyName, options)返回TaxonomyTerm[]支持options.locale活动区域优先再回退默认区域可通过includeCounts: false跳过计数聚合以节省查询成本。getTerm(taxonomyName, slug, options)按 slug 精确查询单个术语getEntryTerms将条目术语解析到活动区域后回退默认区域。getEntriesByTerm(collection, taxonomyName, termSlug, options)的术语查找与内容查询都尊重活动区域设置。分类法在 seed 中声明例如 templates/blog/seed/seed.json 中的taxonomies数组定义了name: categoryhierarchical: true可含父子层级与name: tag并指定各自作用于哪些集合collections: [posts]。四、Widget AreasWidget 区域Widget 区域允许在侧边栏、页脚等位置动态渲染可配置的组件搜索框、分类列表、标签云、最近文章、富文本等。声明式渲染--- import { WidgetArea } from emdash/ui; --- aside WidgetArea namesidebar / /asideWidgetArea组件会自动渲染该区域内的所有 Widget搜索、分类、标签、最近文章、富文本等并输出合适的 HTML 与 CSS 类。手动渲染精细化控制--- import { getWidgetArea } from emdash; import { PortableText } from emdash/ui; const sidebar await getWidgetArea(sidebar); --- {sidebar?.widgets.map(widget ( div classwidget {widget.title h3{widget.title}/h3} {widget.type content widget.content ( PortableText value{widget.content} / )} /div ))}源码级的实现细节从源码 packages/core/src/widgets/index.ts 可以确认getWidgetArea(name)使用单次查询 LEFT JOIN而非先取区域再取 widget的两次查询一次数据库往返即可拿到区域及其全部 widget无 widget 的区域会得到一行空 widget 列并在映射时跳过。结果经requestCached缓存键为widget-area:name同一请求内还会优先从预取的widget-areas批缓存中读取。边缘缓存变体getWidgetAreaWithCacheHint(name)可在区域内容变更时清除渲染页面的缓存。Widget 区域在 seed 中声明例如 templates/blog/seed/seed.json 的widgetAreas定义了sidebar包含core:search搜索组件等与footer包含content类型富文本 About 介绍每个 widget 有typecomponent/content等、componentId、title、content等字段。五、Search搜索LiveSearch 组件即时搜索--- import LiveSearch from emdash/ui/search; --- LiveSearch placeholderSearch... collections{[posts, pages]} /自定义 CSS 类LiveSearch placeholderSearch... classsite-search inputClasssite-search-input resultsClasssite-search-results resultClasssite-search-result collections{[posts, pages]} expandOnFocus{{ collapsed: 180px, expanded: 280px }} /expandOnFocus可让输入框在聚焦时平滑展开收起时180px展开时280px。通过 CSS 变量定制主题:root { --emdash-search-bg: var(--color-bg); --emdash-search-text: var(--color-text); --emdash-search-muted: var(--color-muted); --emdash-search-border: var(--color-border); --emdash-search-hover: var(--color-surface); --emdash-search-highlight: var(--color-text); }编程式搜索import { search } from emdash; const results await search(hello world, { collections: [posts, pages], status: published, limit: 20, }); // { results: SearchResult[], total, nextCursor? }每个结果SearchResult包含collection、id、title、slug、snippet带mark高亮的 HTML、score。返回体中nextCursor用于分页加载下一页。搜索页--- import LiveSearch from emdash/ui/search; import Base from ../layouts/Base.astro; const query Astro.url.searchParams.get(q) || ; --- Base titleSearch h1Search/h1 LiveSearch placeholderSearch posts... collections{[posts, pages]} / /Base键盘快捷键CmdK / CtrlKscript document.addEventListener(keydown, (e) { if ((e.metaKey || e.ctrlKey) e.key k) { e.preventDefault(); document.querySelector(.site-search-input)?.focus(); } }); /script搜索的前置条件搜索按集合独立启用需要三步在后台管理中编辑内容类型Edit Content Type→ 在 Features 中勾选 Search在 seed 文件中将需要参与搜索的字段标记为searchable: true只有searchable 集合的searchable 字段才会被建立索引。参考 templates/blog/seed/seed.jsonposts 集合的title与contentportableText字段均标记了searchable: truepages 集合的title、content同样如此。源码级的实现细节从源码 packages/core/src/search/query.ts 可以确认搜索底层基于 SQLiteFTS5全文索引search(query, options)内部将查询分发到各集合的 FTS 查询再按score合并排序。查询串支持 FTS5 语法AND、OR、NOT、NEAR等操作符以及双引号短语。搜索分页使用不透明的游标base64 编码的 JSON因为合并结果按分数重排后没有稳定的 keyset 列可编码游标携带页码偏移量并设有MAX_SEARCH_OFFSET 10_000上限防止伪造游标触发过深的分页扫描。针对用户输入的 FTS5 语法错误如不配对引号、孤立操作符、^*等裸特殊指令代码会按 SQLite FTS5 的错误特征fts5: syntax error、unknown special query进行识别处理。即时搜索组件实现见 packages/core/src/components/LiveSearch.astro 及配套的路由处理 packages/core/src/components/live-search-routing.ts。六、SEO Meta搜索引擎优化EmDashHead 自动注入[!IMPORTANT] 在服务端渲染的内容页上如果页面通过getEmDashEntry()获取条目并渲染了EmDashHeadEmDash 会自动应用 SEO 面板中的 description、image、canonical URL 与 noindex 设置。面板标题panel title提供社交分享与 JSON-LD 的标题贡献。面板数据复用页面已有的条目查询不会产生额外的数据库查询。EmDashHead无法设置文档title因此标题需要使用getSeoMeta()。预渲染prerendered页面、没有EmDashHead的页面、手写查询路径hand-rolled query paths以及多条目集合结果页不会获得此自动叠加。这些路径上请使用getSeoMeta()或直接读取原始 SEO 数据。使用 getSeoMeta() 手动生成当模板需要设置title或页面无法获得自动叠加时import { getSeoMeta } from emdash; const seo getSeoMeta(post, { siteTitle: My Blog, siteUrl: Astro.url.origin, path: /posts/${slug}, defaultOgImage: featuredImageUrl, // Optional fallback });在布局的head中使用title{seo.title}/title {seo.description meta namedescription content{seo.description} /} {seo.canonical link relcanonical href{seo.canonical} /} {seo.ogImage meta propertyog:image content{seo.ogImage} /} {seo.robots meta namerobots content{seo.robots} /}自定义标题与描述默认值通过defaultTitle与defaultDescription传入计算后的回退值。SEO 面板中设置的值优先于这些默认值import { getSeoMeta } from emdash; const seo getSeoMeta(post, { defaultTitle: ${post.data.title}: A practical guide, defaultDescription: Read ${post.data.title} on My Blog., });源码级的实现细节从源码 packages/core/src/seo/index.ts 可以确认getSeoMeta()的完整优先级规则标题SEO 面板标题seo.title 调用方defaultTitle 内容data.title最终格式为页面标题 titleSeparator默认 | siteTitle。描述SEO 面板描述seo.descriptiondefaultDescription 内容data.excerpt否则为null。OG 图片seo.image经buildSeoImageUrl解析为绝对 URLdefaultOgImage。Canonical显式seo.canonical经resolveSeoCanonicalUrl解析与EmDashHead叠加路径渲染一致 由siteUrlpath拼出的规范地址 null。Robotsseo.noIndex为真时输出noindex, nofollow否则为null。返回结构SeoMeta还包含ogTitle、ogDescription等完整 OG 字段可直接渲染。同一模块还导出getHreflangAlternates多语言站点的 hreflang 备用地址见 packages/core/src/seo/hreflang.ts与getContentSeo()无模板解析地直接读取原始 SEO 字段。七、Comments内置评论系统--- import { Comments, CommentForm } from emdash/ui/comments; --- Comments collectionposts contentId{post.data.id} threaded / CommentForm collectionposts contentId{post.data.id} /评论按集合在 seed 中启用commentsEnabled: true。注意contentId传入的是post.data.id数据库 ULID。threaded属性开启嵌套楼中楼评论。八、Page Contributions插件页面注入Head/Body插件可以在页面head与body中注入内容分析脚本、追踪像素、结构化数据等。为了支持这一点页面需要使用页面贡献组件--- import { EmDashHead, EmDashBodyStart, EmDashBodyEnd } from emdash/ui; import { createPublicPageContext } from emdash/page; const pageCtx createPublicPageContext({ Astro, kind: content ? content : custom, pageType: article, title: fullTitle, pageTitle: post.data.title, description, canonical, image, content: { collection: posts, id: post.data.id, slug }, }); --- html head !-- your meta tags -- EmDashHead page{pageCtx} / /head body EmDashBodyStart page{pageCtx} / !-- your content -- EmDashBodyEnd page{pageCtx} / /body /htmlkind内容页传content自定义页传custompageType如article、website等页面类型content内容页提供{ collection, id, slug }让插件能感知当前内容上下文。这样便能让插件分析、追踪像素、结构化数据等向任何页面贡献内容。九、Bylines署名作者档案Bylines 是独立于用户账户的作者档案支持客座作者guest authors与带角色标签的多作者署名。条目上的自动加载署名由查询层自动附加到每个条目上eagerly loaded模板中直接读取{/* Primary author */} {post.data.byline ( span{post.data.byline.displayName}/span )} {/* All credits (includes roleLabel for co-authors, guest essays, etc.) */} {post.data.bylines?.map(credit ( span {credit.byline.displayName} {credit.roleLabel em ({credit.roleLabel})/em} /span ))}entry.data.byline—— 主作者BylineSummary无主作者时为nullentry.data.bylines——ContentBylineCredit数组每项含.byline、.roleLabel、.source。独立查询函数import { getByline, getBylineBySlug } from emdash; // Look up a specific byline const byline await getBylineBySlug(jane-doe);BylineSummary 数据结构interface BylineSummary { id: string; slug: string; displayName: string; bio: string | null; avatarMediaId: string | null; websiteUrl: string | null; isGuest: boolean; }ContentBylineCredit 数据结构interface ContentBylineCredit { byline: BylineSummary; sortOrder: number; roleLabel: string | null; // e.g., Guest essay, Photographer source?: explicit | inferred; // inferred fallback from author_id }roleLabel可标注Guest essay、Photographer等合作角色source为inferred表示该署名是从author_id回退推断出来的explicit表示显式指定。源码级的实现细节从源码 packages/core/src/bylines/index.ts 可以确认getByline(id)按 ID、getBylineBySlug(slug, options)按 slug支持options.locale经区域链解析查询作者档案底层均由BylineRepository实现批量场景可使用getBylinesForEntries(collection, entries)一次查询多篇条目的署名映射避免 N1 查询。十、Dark Mode Pattern无闪烁的深色模式基于 Cookie 的主题切换方案在样式加载前同步应用主题类避免页面闪烁FOUC!-- In head, before styles load -- script is:inline (function () { var c document.cookie; var i c.indexOf(theme); var theme i 0 ? c.slice(i 6).split(;)[0] : null; if (theme dark || theme light) { document.documentElement.classList.add(theme); } else if (window.matchMedia((prefers-color-scheme: dark)).matches) { document.documentElement.classList.add(dark); } })(); /script然后使用随.dark类变化的 CSS 变量:root { --color-bg: #ffffff; --color-text: #1a1a1a; } :root.dark { --color-bg: #0d0d0d; --color-text: #ededed; }关键点脚本必须用is:inline且放在head中、样式加载之前保证在首次绘制前应用主题优先读取themeCookie用户显式选择否则回退到系统偏好prefers-color-scheme: dark其余样式全部基于--color-bg、--color-text等变量派生实现全站换肤。十一、Layout Pattern典型基础布局综合以上全部能力一个典型的 EmDash 基础布局如下--- import { getMenu, getEmDashCollection } from emdash; import { WidgetArea, EmDashHead, EmDashBodyStart, EmDashBodyEnd } from emdash/ui; import { createPublicPageContext } from emdash/page; import LiveSearch from emdash/ui/search; interface Props { title: string; description?: string | null; image?: string | null; content?: { collection: string; id: string; slug?: string | null }; } const { title, pageTitle, description, image, content } Astro.props; const menu await getMenu(primary); const pageCtx createPublicPageContext({ Astro, kind: content ? content : custom, pageType: website, title, pageTitle: pageTitle ?? title, description, image, content, }); --- !doctype html html langen head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / title{title}/title {description meta namedescription content{description} /} EmDashHead page{pageCtx} / /head body EmDashBodyStart page{pageCtx} / header nav a href/My Site/a LiveSearch placeholderSearch... collections{[posts, pages]} / {menu?.items.map(item ( a href{item.url}{item.label}/a ))} /nav /header main slot / /main footer WidgetArea namefooter / /footer EmDashBodyEnd page{pageCtx} / /body /html这个布局把本文的多数主题串成一条线getMenu(primary)渲染主导航、LiveSearch提供即时搜索、createPublicPageContextEmDashHead/EmDashBodyStart/EmDashBodyEnd支撑 SEO 自动叠加与插件注入、WidgetArea渲染页脚 Widget。作为Props传入的content让内容页复用同一布局时自动获得完整 SEO 与插件能力。总结与最佳实践清单功能推荐用法说明站点设置getSiteSettings()/getSiteSetting(key)替代硬编码站点名、Logo键title、tagline、logo、favicon、social、timezone、dateFormat导航菜单getMenu(primary)名称与 seed 的menus[].name一致url已解析可直接使用分类法getTaxonomyTerms/getEntryTerms/getEntriesByTerm名称须与 seedname精确一致category、tag单数getEntryTerms传data.idULID而非 slugWidget 区域WidgetArea namesidebar /或getWidgetArea()自动渲染区域内全部 widget手动渲染可精细化控制搜索LiveSearch/search()需后台勾选集合 Search 特性 seed 字段searchable: trueFTS5 支持AND/OR/NOT/NEAR与短语SEOEmDashHead自动getSeoMeta()标题自动叠加仅覆盖 SSR 内容页预渲染页、无EmDashHead页需手动getSeoMeta()评论Comments/CommentFormseed 中commentsEnabled: true按集合启用插件注入EmDashHead/EmDashBodyStart/EmDashBodyEnd结合createPublicPageContext()为任意页面开放插件贡献能力署名entry.data.byline/entry.data.bylines主作者 多作者角色标签支持客座作者深色模式Cookie is:inline前置脚本无闪烁优先用户选择回退系统偏好基础布局上述能力组合成Base.astro一份布局统一处理导航、搜索、SEO、插件、页脚实践要点回顾seed 是站点数据的唯一事实来源功能函数名称与其声明严格对应搜索与 SEO 都存在自动能力与手动兜底两条路径理解触发条件才能避免踩坑data.idULID与idslug的区分贯穿分类法、评论、署名与 SEO 的各个 API务必按文档约定传参。赞分享CMS后端前端插件系统【免费下载链接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress项目地址https://gitcode.com/gh_mirrors/emdas/emdash点击查看免费下载相关推荐EmDash 站点功能实战站点设置、导航菜单、分类法、搜索、SEO 与评论的完整主题开发指南EmDash 站点功能实战站点设置、导航菜单、分类法、搜索、SEO 与评论的完整主题开发指南 导读 EmDash 是一个基于 Astro 的全栈 TypeScCMS后端前端插件系统EmDash 站点功能全解析从站点设置、导航菜单到搜索与 SEO 的完整实践指南EmDash 站点功能全解析从站点设置、导航菜单到搜索与 SEO 的完整实践指南 导读 EmDash 是一套基于 Astro 的全栈 TypeScript CCMS后端前端插件系统EmDash 站点功能实战从站点设置、导航菜单到搜索与 SEO 的完整接入指南EmDash 站点功能实战从站点设置、导航菜单到搜索与 SEO 的完整接入指南 EmDash 是一个基于 Astro 构建的全栈 TypeScript CMSCMS后端前端插件系统创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。