EmDash 页面在请求时使用 getEmDashCollection() 和 getEmDashEntry() 读取内容。前者返回列表,后者按 slug 或内容 ID 返回一条条目。两者都将错误作为数据返回,以便页面决定如何响应。
捆绑模板使用 Astro 的服务器输出。因此,编辑者发布后,访问者会在下一次渲染时收到最新已发布修订。已保存的草稿在发布或通过有效预览 URL 请求之前保持私密。
查询集合
以下页面加载最近发布的七篇文章。公开集合查询默认 status: "published",因此过滤器无需重复。
---
import { getEmDashCollection } from "emdash";
const { entries: posts, error, cacheHint } = await getEmDashCollection("posts", {
orderBy: { published_at: "desc" },
limit: 7,
});
if (error) {
console.error("Failed to load posts:", error);
return new Response("Unable to load posts", { status: 500 });
}
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li>
<a href={`/posts/${post.id}`}>{post.data.title}</a>
</li>
))}
</ul>
排序和限制在 EmDash 水合条目之前于数据库中完成。这避免了加载整个集合并在页面中排序或切片。
结果包含:
entries,在无匹配或查询失败时为空数组;error,对失败的查询设置,但不对空结果设置;cacheHint,携带 Astro 缓存的标签和最后修改时间;nextCursor,当有限的游标页还有更多条目时设置;以及hasMore,报告有限的游标或偏移页是否还有下一页。
条目标识符
每个结果有两个职责不同的标识符:
entry.id是内容加载器生成的面向 URL 的标识符。通常是 slug。当国际化为区域设置加前缀时,会包含前缀。从集合结果构建链接时使用此值。entry.data.id是数据库中存储的稳定内容 ID。编辑者更改 slug 时它不会改变。当 API、分类法助手、页面上下文或关系期望内容 ID 时使用它。
getEmDashEntry() 接受 slug 或稳定内容 ID。启用国际化时,slug 查找限定在某个区域设置;内容 ID 直接标识该行。
过滤集合
在第二个参数中传递过滤器。以下查询返回 news 分类中 series 字段为 engineering 的已发布文章:
const { entries } = await getEmDashCollection("posts", {
where: {
category: "news",
series: "engineering",
},
});
命名分类法的键匹配已分配的术语 slug。其他键匹配集合字段。多个键以 AND 逻辑组合。一个键上的数组匹配任一列出的值,因此 { category: ["news", "updates"] } 匹配任一分类。
使用 locale 显式请求一种语言:
const { entries: frenchPosts } = await getEmDashCollection("posts", {
locale: "fr",
orderBy: { published_at: "desc" },
});
若省略 locale,EmDash 使用请求区域设置,然后使用配置的默认区域设置。回退规则与翻译路由见国际化。
status 接受 "published"、"draft" 或 "archived"。不要从公开路由请求草稿。当访问者需要临时访问未发布条目时,使用预览流程。
在数据库中排序结果
orderBy 将字段名映射到 "asc" 或 "desc"。使用存储的字段名,而非 entry.data 中返回的 camelCase 名称:
const { entries } = await getEmDashCollection("posts", {
orderBy: {
published_at: "desc",
title: "asc",
},
});
系统列使用其数据库名称,如 created_at、updated_at 和 published_at。自定义字段使用其集合 slug,如 title 或 priority。返回的数据将系统日期映射到 createdAt、updatedAt 和 publishedAt,但这些 camelCase 属性名不是有效的 orderBy 字段。
EmDash 使用第一个有效的 orderBy 字段作为分页键,并以内容 ID 作为稳定的决胜局。没有 orderBy 时,集合默认按 created_at 降序。当站点经常按自定义标量字段排序或过滤时,请将其标记为已索引;索引可避免集合增长时的全表扫描。
对集合分页
对连续 feed 使用游标,对编号页使用偏移。它们是独立的分页模型,不能在一个类型化查询中组合。
游标分页
游标分页从上一次查询返回的最后一条条目之后继续。请求之间保持排序不变,并原样传回 nextCursor,不要检查或更改它。
当存在另一页时,以下路由渲染 Older posts 链接:
---
import { getEmDashCollection } from "emdash";
const cursor = Astro.url.searchParams.get("cursor") ?? undefined;
const { entries: posts, nextCursor, error } = await getEmDashCollection("posts", {
limit: 10,
cursor,
orderBy: { published_at: "desc" },
});
if (error) return new Response("Unable to load posts", { status: 500 });
---
<ul>
{posts.map((post) => <li><a href={`/posts/${post.id}`}>{post.data.title}</a></li>)}
</ul>
{nextCursor && (
<a href={`/posts?cursor=${encodeURIComponent(nextCursor)}`}>Older posts</a>
)}
最后一页没有 nextCursor。游标分页不计算总页数,也不提供上一页游标;若界面需要返回导航,请在浏览器历史中保留较早的 URL。
偏移分页
偏移分页适合 /posts/page/3 这类路由。将页码转换为偏移,并用 hasMore 做下一页链接:
---
import { getEmDashCollection } from "emdash";
const parsedPage = Number(Astro.params.page ?? "1");
if (!Number.isInteger(parsedPage) || parsedPage < 1) {
return Astro.redirect("/404");
}
const perPage = 10;
const { entries: posts, hasMore, error } = await getEmDashCollection("posts", {
limit: perPage,
offset: (parsedPage - 1) * perPage,
orderBy: { published_at: "desc" },
});
if (error) return new Response("Unable to load posts", { status: 500 });
---
<ul>
{posts.map((post) => <li><a href={`/posts/${post.id}`}>{post.data.title}</a></li>)}
</ul>
<nav aria-label="Post pages">
{parsedPage > 1 && <a href={`/posts/page/${parsedPage - 1}`}>Newer posts</a>}
{hasMore && <a href={`/posts/page/${parsedPage + 1}`}>Older posts</a>}
</nav>
偏移必须是非负整数。第 1 页使用偏移 0,表示「从第一条条目开始」。偏移分页易于按页码寻址,但请求之间新增的条目可能使后面的页发生移动。当这种移动会造成困惑时,请使用游标。
查询并渲染单条条目
以下运行时路由按 slug 读取文章,渲染其特色图片和 Portable Text 正文,并区分查询失败与缺失条目:
---
import { decodeSlug, getEmDashEntry } from "emdash";
import { Image, PortableText } from "emdash/ui";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/404");
const { entry: post, error, isPreview, cacheHint } = await getEmDashEntry("posts", slug);
if (error) {
console.error("Failed to load post:", error);
return new Response("Unable to load post", { status: 500 });
}
if (!post) return Astro.redirect("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
{isPreview && <p>This is an unpublished preview.</p>}
<article>
{post.data.featured_image && <Image image={post.data.featured_image} priority />}
<h1>{post.data.title}</h1>
<PortableText value={post.data.content} />
</article>
PortableText 为 EmDash 的标准块提供渲染器,包括图片、图库、代码、表格和已净化的 HTML 块。当站点添加自己的 Portable Text 块时,传入自定义组件。若替换 htmlBlock 渲染器,请净化 HTML,并仅允许站点打算信任的 iframe 主机。
PortableText 在编辑模式下将表格显示为只读占位符。要本地化其初始标签,请传入 tablePlaceholder={translatedLabel}。默认值为 "Table (edit in admin)",不影响已发布的表格内容。
读取引用字段
reference 字段 将条目链接到另一集合中的条目,关系 介绍如何定义。其值不是 data 的一部分。向 getEmDashEntry 传入按字段 slug 键控的 references 选项,命名页面要渲染的字段,每个字段会作为一页条目返回:
---
import { getEmDashEntry } from "emdash";
const { entry: post } = await getEmDashEntry("posts", Astro.params.slug, {
references: { author: true, related_posts: { limit: 6 } },
});
if (!post) return Astro.redirect("/404");
const author = post.references?.author.entries[0];
---
<article>
<h1>{post.data.title}</h1>
{author && <p>By {author.data.name}</p>}
<ul>
{post.references?.related_posts.entries.map((related) => (
<li><a href={`/posts/${related.data.slug}`}>{related.data.title}</a></li>
))}
</ul>
</article>
true 请求默认上限 50 条的第一页。对包含更多内容的字段使用 { limit, cursor },每页最多 100 条。
每个被引用的条目都是与直接加载相同形状的 ContentEntry:一个 id、一个带 Date 对象日期和已解析媒体值的 data 对象,以及作用域限定在被引用条目上的 edit 代理,因此在可视化编辑中点击卡片会打开该卡片所关于的条目。署名和分类术语是例外——EmDash 不会将它们水合到被引用条目上,因此请从条目本身读取 data.bylines 和 data.terms。
当字段位于关系的父端时,条目按编辑者排列的顺序到达。子端的字段列出指向它的条目,这些条目没有自己的顺序。
该选项在两个方向都是可选启用。不传 references 的调用不会运行额外查询,未纳入选择的字段不会被读取。选择字段的调用每个字段一次链接查询,加上每个不同目标集合一次条目查询,无论每个字段持有多少条目。
对引用字段分页
getEmDashReferences 使用上一页返回的游标获取单个字段的下一页:
import { getEmDashReferences } from "emdash";
const { entries, nextCursor } = await getEmDashReferences("posts", post.id, "related_posts", {
cursor,
limit: 20,
});
它从与 getEmDashEntry 相同的请求上下文读取草稿可见性,因此在预览中开始的遍历会继续看到待定选择。页面 id 在 i18n 加前缀时携带其区域设置(fr/about),传回的就是该 id。
在缓存输出的路由上,将页面返回的 cacheHint 与路由自身的合并,以便对所命名条目的写入会使页面过期。
预览中的引用字段
公开渲染看到已发布条目和已发布选择。条目的预览,或处于可视化编辑中的编辑者,会看到未发布条目和条目草稿中暂存的选择,因此预览链接会显示页面发布后将拥有的引用。
应用 SEO 与编辑功能
对于支持 SEO 的集合,getSeoMeta() 会解析编辑者的 SEO 标题、描述、图片、规范 URL 和无索引选择,并带来自条目的回退。当前博客模板将该结果传给其基础布局:
---
import { getSeoMeta } from "emdash";
const seo = getSeoMeta(post, {
siteTitle: "My Blog",
siteUrl: Astro.url.origin,
path: Astro.url.pathname,
});
---
<Base
title={seo.title}
pageTitle={seo.ogTitle}
description={seo.description}
image={seo.ogImage}
canonical={seo.canonical}
robots={seo.robots}
>
<!-- Post content -->
</Base>
包含 <EmDashHead> 的布局可将相同的 SEO 字段和插件贡献应用到服务器渲染的内容页。仅读取 data.title 或 data.excerpt 的手写元标签不会应用编辑者的规范 URL 或无索引设置。
预览 URL 无需单独查询。中间件验证 _preview 令牌,且 getEmDashEntry() 返回匹配的草稿并带有 isPreview: true。预览与可视化编辑指南 说明了 URL 生成以及用于内联编辑的 entry.edit 注解。
生成 TypeScript 类型
开发服务器根据活动架构生成 emdash-env.d.ts。在项目的 TypeScript 配置中包含该文件,以便像 "posts" 这样的集合名自动选择生成的 Post 数据类型。
对于远程 EmDash 实例,CLI 可以获取架构并将类型写入 .emdash/types.ts:
npx emdash types --url https://cms.example.com
该命令接受 API 令牌或自定义认证头。这些选项见生成类型。
每个绑定到关系的引用字段的集合都会获得第二个接口 {Collection}References,注册在同一 slug 下。getEmDashEntry 将其结果收窄到 references 选项命名的字段,每页的条目携带目标集合的接口:
const { entry: post } = await getEmDashEntry("posts", "my-post", {
references: { author: true },
});
// post?.references?.author.entries[0] carries the Author interface
// post?.references?.related_posts is a type error: it was not selected
运行时渲染与缓存
Node.js 和 Cloudflare 博客模板在 astro.config.mjs 中设置 output: "server"。它们的内容查询在每次服务器渲染时运行,因此新发布的修订有资格进入下一次请求。若您有意预渲染某条路由,其 HTML 包含构建时可用的内容,只有再次构建后才会改变。
当 Astro 的缓存启用时,将查询的 cacheHint 传给 Astro.cache.set()。EmDash 将响应对应到集合和条目标签,以便发布可以失效受影响的缓存页面。除非延迟更新是明确的产品决策,否则不要用很长的固定 Cache-Control 寿命替换该集成。
有关精确签名和较少见的过滤器,请参见 JavaScript API 参考。要围绕这些查询构建可运行的示例,请继续参阅创建博客。