ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

EmDash 内容查询与渲染指南:用 getEmDashCollection / getEmDashEntry 构建 Astro 驱动的内容站点

EmDash 内容查询与渲染指南:用 getEmDashCollection / getEmDashEntry 构建 Astro 驱动的内容站点 CMS后端前端插件系统【免费下载链接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress项目地址https://gitcode.com/gh_mirrors/emdas/emdash点击查看免费下载EmDash 是一个基于 Astro 构建的全栈 TypeScript CMSWordPress 的现代后继者其核心能力之一是把 CMS 内容无缝暴露给前端页面在.astro页面中通过emdash包提供的查询函数按集合collection取数、渲染 Portable Text 富文本、输出 CMS 图片并开启可视化编辑。本篇指南以 querying-and-rendering.md 为骨架逐项讲解内容查询 API、缓存集成、富文本与图片渲染、分页、SEO 元数据与常见页面模式并结合 query.ts 等源码说明底层实现帮助你在自己的 EmDash 站点上写出类型安全、可缓存、可点选编辑的页面。内容查询 API 总览EmDash 的查询函数统一从emdash包导入它们在内部包装 Astro 的getLiveCollection/getLiveEntry并附加类型过滤与内容水合bylines、taxonomy terms源码入口见 index.ts 的 re-export 与 query.ts。核心函数有两个getEmDashCollection(type, filter?)—— 获取一个集合的多条条目返回{ entries, error, cacheHint, nextCursor }getEmDashEntry(type, id, options?)—— 按 slug或数据库 ID获取单条条目返回{ entry, error, isPreview, cacheHint }。两个函数都遵循“错误不抛出、随结果返回”的 Astro 风格集合查询在出错时返回空entries数组并附带error字段单条查询在未找到时entry为null这不属于错误只有真正的数据库异常才会设置error。因此页面里可以直接用if (!post) return Astro.redirect(/404)处理不存在的情况。集合查询getEmDashCollection最基本的用法是不带任何过滤条件取回整个集合import { getEmDashCollection } from emdash; // Basic const { entries: posts } await getEmDashCollection(posts);带选项的用法如下// With options const { entries: posts, cacheHint } await getEmDashCollection(posts, { status: published, limit: 10, orderBy: { published_at: desc }, where: { category: news }, });选项说明与源码中 CollectionFilterBase 的定义一致选项类型说明statusdraft \| published \| archived按内容状态过滤limitnumber返回的最大条目数cursorstring不透明游标用于 keyset 分页把上一次结果的nextCursor传进来即可翻页offsetnumber偏移分页的跳过条数与cursor互斥同时传入在编译期报错orderBy{ field: asc \| desc }排序字段与方向默认{ created_at: desc }也支持多字段如{ published_at: desc, title: asc }whereRecordstring, WhereValue按字段值、taxonomy 词条或 byline 过滤数组表示 OR 语义如{ category: [news, featured] }还支持日期范围{ published_at: { gte: 2024-01-01, lt: 2025-01-01 } }localestring配置了 i18n 时按语言过滤如en/frwhere的细节值得展开taxonomy 名称会被自动识别并走 JOIN 过滤如{ category: news }过滤归类到该词条的条目保留键byline会通过_emdash_content_bylines中间表按署名过滤含合著条目{ byline: [01HXYZ..., 01HABC...] }表示任一署名匹配其余键则作为内容表的列过滤。见 query.ts 的注释与示例。单条查询getEmDashEntryimport { getEmDashEntry } from emdash; const { entry: post, cacheHint } await getEmDashEntry(posts, slug); if (!post) { return Astro.redirect(/404); }getEmDashEntry接受可选的{ locale }选项。在配置了 i18n 时它会沿着“请求语言 → fallback 语言 → 默认语言”的 fallback 链解析命中的条目会在结果中带出fallbackLocale字段预览_previewtoken与编辑模式由中间件通过 AsyncLocalStorage 注入请求上下文查询函数自动读取无需额外传参见 query.ts 的 locale 链与 draft 分支逻辑。Entry 结构与“两个 id”的陷阱查询返回的每条 entry 都是统一的ContentEntryT形状interface ContentEntryT { id: string; // The slug (used in URLs) data: T; // All fields, including system fields edit: EditProxy; // Visual editing attributes (spread onto elements) }data中既包含系统字段也包含你在 CMS 里定义的自定义字段。以一篇 post 为例interface PostData { id: string; // Database ULID (use for taxonomy lookups, etc.) slug: string; status: string; title: string; featured_image?: { id: string; src?: string; alt?: string; width?: number; height?: number; }; content?: PortableTextBlock[]; createdAt: Date; updatedAt: Date; publishedAt: Date | null; // Bylines (eagerly loaded) byline: BylineSummary | null; // Primary author bylines: ContentBylineCredit[]; // All credits (with roleLabel, source) // ... your custom fields }最重要的一个约定entry.id是 slug用于拼 URLentry.data.id才是数据库 ULID用于调用getEntryTerms等 API。千万不要混用二者URL 用entry.id如/posts/${post.id}而需要按数据库主键做查询例如取 taxonomy terms时必须传entry.data.id。从源码看entryDatabaseId正是读取data.idquery.ts。另外注意bylines/byline是**急切水合eagerly hydrated**的字段——查询返回时已附带署名数据不需要额外请求taxonomy terms 则被水合到entry.data.terms按 taxonomy 名称分组的TaxonomyTerm[]对象这意味着列表页循环条目时无需再逐条调用getEntryTerms造成 N1 查询见 query.ts 的批量 JOIN 实现。缓存永远调用 Astro.cache.set(cacheHint)查询结果都带有cacheHint用于 Astro 的 Route Caching内容变更时自动失效缓存--- const { entries: posts, cacheHint } await getEmDashCollection(posts); Astro.cache.set(cacheHint); ---请务必调用Astro.cache.set(cacheHint)——它让页面在 CMS 内容更新时自动失效并重建是生产站点缓存一致性的前提。从实现看集合与单条查询都经过请求级缓存同一渲染周期内相同(type, filter)的重复查询只执行一次与分布式对象缓存L2按collection filter 有效 locale缓存 JSON 快照见 query.tscacheHint携带的tags/lastModified就是给路由缓存做失效判断的依据。此外对于limit小于 10 的小型“最近 N 篇”组件查询层会把 limit 归并到共享桶bucket以合并重复取数所以多个侧边栏小组件同时渲染也不会各自查库query.ts。渲染 Portable TextCMS 里的富文本字段如content是 Portable Text 块数组用emdash/ui的PortableText组件渲染--- import { PortableText } from emdash/ui; --- PortableText value{post.data.content} /它内置支持标准块段落、标题、列表、引用、代码块、图片与行内标记加粗、斜体、代码、删除线、链接。组件源码见 PortableText.astro并从 components/index.ts 统一导出。自定义块类型营销页通常需要自定义块如 hero、features 等 CMS 区块。通过componentsprop 传入类型到组件的映射--- import { PortableText } from emdash/ui; import Hero from ./blocks/Hero.astro; import Features from ./blocks/Features.astro; const customTypes { marketing.hero: Hero, marketing.features: Features, }; --- PortableText value{page.data.content} components{{ type: customTypes }} /每个自定义组件会收到该 block 的数据作为 props你可以在组件内部读取字段并自行排版。Image 组件CMS 图片字段是对象不是字符串CMS 中的图片字段一律是对象含id、src、alt、width、height等必须使用 EmDash 的Image组件渲染。正确与错误的写法对比--- import { Image } from emdash/ui; --- {/* Correct -- passes the image object */} Image image{post.data.featured_image} / {/* Also works with explicit props */} {post.data.featured_image?.src ( img src{post.data.featured_image.src} alt{post.data.featured_image.alt || } / )}{/* WRONG -- image is an object, not a string */} img src{post.data.featured_image} /把对象直接塞给原生img src是新手最常见的错误渲染结果是[object Object]。Image组件源码见 Image.astro 与 EmDashImage.astro处理对象并输出正确的img属性如果确实要用原生标签务必像上面的“显式 props”写法那样取.src与.alt。可视化编辑展开 entry.edit 属性每条 entry 都携带edit代理对象把它展开到展示对应字段的元素上即可启用“点击即编辑”h1 {...post.edit.title}{post.data.title}/h1 p {...post.edit.excerpt}{post.data.excerpt}/p div {...post.edit.featured_image} Image image{post.data.featured_image} / /div当管理员登录并浏览站点时这些属性会附加编辑标注实现行内点选编辑普通访客拿到的是 no-op 版本展开后不产生任何副作用。实现上createEditable/createNoopvisual-editing/editable.ts 的引入处会根据请求上下文是否编辑模式决定附加真实代理还是空操作Portable Text 数组还会被贴上非枚举的编辑元数据tagEditableFields见 query.ts让富文本字段同样可被编辑定位。常见页面模式列表页如/posts/index.astro--- import { getEmDashCollection, getEntryTerms } from emdash; import { Image } from emdash/ui; import Base from ../../layouts/Base.astro; const { entries: posts, cacheHint } await getEmDashCollection(posts, { orderBy: { published_at: desc }, }); Astro.cache.set(cacheHint); const sortedPosts posts.toSorted((a, b) { const dateA a.data.publishedAt?.getTime() ?? 0; const dateB b.data.publishedAt?.getTime() ?? 0; return dateB - dateA; }); --- Base titlePosts {sortedPosts.map(post ( article {post.data.featured_image Image image{post.data.featured_image} /} a href{/posts/${post.id}}{post.data.title}/a {post.data.excerpt p{post.data.excerpt}/p} /article ))} /Base注意publishedAt是Date对象所以手动排序要用getTime()比较链接一律使用post.idslug。详情页如/posts/[slug].astro--- import { getEmDashEntry, getEntryTerms, getSeoMeta } from emdash; import { Image, PortableText } from emdash/ui; import Base from ../../layouts/Base.astro; const { slug } Astro.params; if (!slug) return Astro.redirect(/404); const { entry: post, cacheHint } await getEmDashEntry(posts, slug); if (!post) return Astro.redirect(/404); Astro.cache.set(cacheHint); const seo getSeoMeta(post, { siteTitle: My Blog, siteUrl: Astro.url.origin, path: /posts/${slug}, }); const tags await getEntryTerms(posts, post.data.id, tag); --- Base title{seo.title} description{seo.description} article {post.data.featured_image ( div {...post.edit.featured_image} Image image{post.data.featured_image} / /div )} h1 {...post.edit.title}{post.data.title}/h1 PortableText value{post.data.content} / {tags.length 0 ( div {tags.map(t a href{/tag/${t.slug}}{t.label}/a)} /div )} /article /Base这里示范了三个要点getSeoMeta导出自 seo/index.ts根据 entry 生成 SEO 标题与描述getEntryTerms(posts, post.data.id, tag)必须传数据库 ULIDpost.data.id而非 slug可视化编辑属性与Image/PortableText组合使用。分类归档页如/category/[slug].astro--- import { getTerm, getEmDashCollection } from emdash; import Base from ../../layouts/Base.astro; const { slug } Astro.params; const term slug ? await getTerm(category, slug) : null; if (!term) return Astro.redirect(/404); const { entries: posts } await getEmDashCollection(posts, { where: { category: term.slug }, orderBy: { published_at: desc }, }); --- Base title{${term.label} posts} h1{term.label}/h1 {posts.map(post ( a href{/posts/${post.id}}{post.data.title}/a ))} /BasegetTerm按(taxonomy, slug)取单个词条含 label、slug、children 等并支持 locale fallback 链与可选的可见条目计数includeCounts: false可跳过计数查询实现见 taxonomies/index.ts。归档页随后用where: { category: term.slug }过滤出该分类下的条目。RSS 源如/rss.xml.tsimport type { APIRoute } from astro; import { getEmDashCollection } from emdash; const siteTitle My Site; export const GET: APIRoute async ({ url }) { const siteUrl url.origin; const { entries: posts } await getEmDashCollection(posts, { orderBy: { published_at: desc }, limit: 20, }); const items posts .filter((p) p.data.publishedAt) .map((post) { const postUrl ${siteUrl}/posts/${post.id}; return item title${escapeXml(post.data.title)}/title link${postUrl}/link guid isPermaLinktrue${postUrl}/guid pubDate${post.data.publishedAt!.toUTCString()}/pubDate description${escapeXml(post.data.excerpt || )}/description /item; }) .join(\n); return new Response( ?xml version1.0 encodingUTF-8? rss version2.0 xmlns:atomhttp://www.w3.org/2005/Atom channel title${escapeXml(siteTitle)}/title link${siteUrl}/link atom:link href${siteUrl}/rss.xml relself typeapplication/rssxml/ languageen-us/language lastBuildDate${new Date().toUTCString()}/lastBuildDate ${items} /channel /rss, { headers: { Content-Type: application/rssxml; charsetutf-8, Cache-Control: public, max-age3600, }, }, ); }; function escapeXml(s: string): string { return s .replace(//g, amp;) .replace(//g, lt;) .replace(//g, gt;) .replace(//g, quot;) .replace(//g, apos;); }要点过滤掉未发布的条目p.data.publishedAt为 null 的过滤掉、用entry.id拼 URL、用toUTCString()输出 RFC 822 时间并对所有动态文本做 XML 转义。404 页面/404.astro--- import Base from ../layouts/Base.astro; --- Base titleNot Found h1Page not found/h1 pThe page youre looking for doesnt exist./p a href/Go home/a /Base空状态当集合还没有内容时展示一个友好的空状态引导去后台创建第一篇内容{posts.length 0 ? ( section h2No posts yet/h2 pCreate your first post in the admin panel./p a href/_emdash/admin/content/posts/newCreate a post/a /section ) : ( /* ... render posts ... */ )}分页cursor 与 offset 两种方式getEmDashCollection内置两种互斥的分页方式keyset 游标分页cursor与偏移分页offset。源码中的类型定义保证了二者不能同时传入同时提供在编译期报错见 query.ts。游标分页推荐用于“下一页”翻页把上一次结果的nextCursor传入下一次请求的cursor即可nextCursor为undefined表示没有更多结果--- const cursor Astro.url.searchParams.get(cursor) ?? undefined; const { entries, nextCursor, cacheHint } await getEmDashCollection(posts, { limit: 10, cursor, orderBy: { published_at: desc }, }); Astro.cache.set(cacheHint); --- {entries.map(post ( a href{/posts/${post.id}}{post.data.title}/a ))} {nextCursor a href{?cursor${nextCursor}}Next page/a}游标是不透明字符串由查询层按“排序值 数据库 ID”编码encodeEntryCursor见 query.ts编码时对日期列优先使用原始存储字符串以避免时区/精度损耗因此翻页稳定、不受新插入数据影响。偏移分页用于/page/2这类编号归档结果中的hasMore表示是否还有后续条目适合渲染“下一页”链接而无需计算总数const perPage 20; const { entries, hasMore } await getEmDashCollection(posts, { limit: perPage, offset: (page - 1) * perPage, orderBy: { published_at: desc }, });两种方式下只要传了limit结果都会附带hasMore字段默认limit 1探测法判断见 query.ts。日期格式化查询返回的日期字段createdAt、updatedAt、publishedAt都是Date对象直接用toLocaleDateString或Intl.DateTimeFormat格式化const formatted post.data.publishedAt?.toLocaleDateString(en-US, { year: numeric, month: long, day: numeric, });注意publishedAt可能为null未发布取值时要先判空如可选链?.。实战要点小结所有查询函数从emdash导入类型由生成到站点里的emdash-env.d.ts提供EmDashCollections接口扩展后集合名与字段自动获得类型推断见 query.ts拼错集合名或字段名会得到编译期错误每次查询后都调用Astro.cache.set(cacheHint)保证内容变更自动失效缓存URL 用entry.idslug数据库 API如getEntryTerms用entry.data.idULIDCMS 图片必须用Image组件传对象不要直接src{对象}富文本用PortableText自定义 CMS 区块通过components{{ type: {...} }}注入管理端登录后把post.edit.*展开到元素上即获得行内点选编辑能力列表、详情、归档、RSS、404、空状态、分页、日期格式化可直接复制上面的代码模式改造到自己的站点。赞分享CMS后端前端插件系统【免费下载链接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress项目地址https://gitcode.com/gh_mirrors/emdas/emdash点击查看免费下载相关推荐EmDash 内容查询与渲染实战从 getEmDashCollection 到 Portable Text 的完整指南EmDash 内容查询与渲染实战从 getEmDashCollection 到 Portable Text 的完整指南 这篇技术指南围绕 EmDash 前端模CMS后端前端插件系统EmDash 内容查询与渲染实战指南getEmDashCollection、Portable Text 与可视化编辑EmDash 内容查询与渲染实战指南getEmDashCollection、Portable Text 与可视化编辑 EmDash 是一个基于 Astro 的CMS后端前端插件系统EmDash 内容查询与渲染实战指南getEmDashCollection、PortableText 与常见页面模式EmDash 内容查询与渲染实战指南getEmDashCollection、PortableText 与常见页面模式 导读 本文基于 EmDash 官方站点构CMS后端前端插件系统创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表