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 会使用请求的语言区域,然后是配置的默认语言区域。有关回退规则
和翻译路由,请参见国际化 (i18n)。
status 接受 "published"、"draft" 或 "archived"。不要在公开
路由中请求草稿。当访问者需要临时访问某一条未发布条目时,请使用预览流程。
在数据库中排序结果
orderBy 将字段名映射到 "asc" 或 "desc"。请使用存储的字段名,而不是 entry.data 中
返回的驼峰命名:
const { entries } = await getEmDashCollection("posts", {
orderBy: {
published_at: "desc",
title: "asc",
},
});
系统列使用其数据库名称,如 created_at、updated_at 和 published_at。
自定义字段使用其集合 slug,如 title 或 priority。返回的数据将系统
日期映射为 createdAt、updatedAt 和 publishedAt,但这些驼峰命名的属性名不是
有效的 orderBy 字段。
EmDash 使用第一个有效的 orderBy 字段作为分页键,并以内容 ID 作为稳定的
决胜项。没有 orderBy 时,集合默认按 created_at 降序排列。当站点经常按自定义标量
字段排序或过滤时,请将这些字段标记为已索引;随着集合增长,索引可避免全表
扫描。
对集合分页
连续滚动的信息流用游标,带页码的分页用偏移。它们是相互独立的分页 模型,不能在同一个带类型的查询中组合使用。
游标分页
游标分页从上一次查询返回的最后一个条目之后继续。请在多次请求之间保持排序
不变,并原样传回 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.rewrite("/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 页使用偏移量零,表示 “从第一个条目开始”。偏移分页便于按页码定位,但在两次请求之间新增的条目 可能使后面的页发生位移。当这种位移会造成困惑时,请使用游标。
查询并渲染单条条目
下面的运行时路由按 slug 读取一篇文章,渲染其特色图片和 Portable Text 正文, 并将查询失败与条目不存在区分开:
---
import { decodeSlug, getEmDashEntry } from "emdash";
import { Image, PortableText } from "emdash/ui";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.rewrite("/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.rewrite("/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 块和 iframe 块。当站点添加自己的 Portable Text 块时,请传入自定义组件。
带有 isolated: true 的 HTML 块会在沙盒 iframe 中渲染其 html、css 和 js,该 iframe 会根据内容自动调整大小。
其他 HTML 块会将经过清理的 html 直接渲染在页面中。如果替换 htmlBlock 渲染器,请将隔离块传给
emdash/ui 中的 HtmlBlock,或在 sandbox 不含 allow-same-origin 的 iframe 中渲染它们。请自行清理内联 HTML,
并且只允许站点打算信任的 iframe 主机。
iframe 块通过 emdash/ui 中的 Iframe 渲染为响应式、延迟加载的 iframe,并带有 sandbox 和
strict-origin-when-cross-origin 引用来源策略 (referrer policy)。位于你站点自身主机上的页面会失去同源访问权限,
并且只会渲染 https 来源。
视频块通过 emdash/ui 中的 Video 渲染。媒体库中的视频使用浏览器自带的播放器播放,带有控件和说明文字;如果配置了存储的公开 URL,则从该 URL 播放。没有公开 URL 时,视频通过 /_emdash/api/media/file/ 提供。来自媒体提供商的视频由该提供商渲染。没有 asset 的空视频块不会渲染任何内容。
启用 Astro 的内容安全策略后,
浏览器会阻止隔离 HTML 块中的样式和脚本,因此这些块渲染时不带 CSS、JavaScript,也没有自动高度。iframe 块同样会失去
自定义尺寸,并且策略中的 frame-src 必须允许它们所嵌入的主机。媒体位于其他主机上的视频块(例如存储的公开 URL 或媒体提供商)需要在 media-src 中允许该主机。
PortableText 在编辑模式下将表格显示为只读占位符。要本地化其初始标签,请传入 tablePlaceholder={translatedLabel}。默认值为 "Table (edit in admin)",不影响已发布的表格内容。
读取引用字段
reference 字段将条目链接到另一个集合中的条目,关联介绍如何定义。其值不属于 data。向 getEmDashEntry 传入 references 选项,以字段 slug 为键,指明页面要渲染的字段,每个字段都会返回为一页条目:
---
import { getEmDashEntry } from "emdash";
const { entry: post } = await getEmDashEntry("posts", Astro.params.slug, {
references: { author: true, related_posts: { limit: 6 } },
});
if (!post) return Astro.rewrite("/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,一个 data 对象(日期为 Date 对象,媒体值已解析),以及一个限定在该被引用条目上的 edit 代理,因此在可视化编辑中点击某张卡片时,会打开该卡片所指向的条目。Byline 和分类法术语是例外——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 和 no-index 选项,并以条目中的值作为回退。当前的博客模板
把这个结果传给其基础布局:
---
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 的手写 meta 标签
不会应用编辑者设置的规范 URL 或 no-index 设置。
预览 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 参考。若要围绕这些 查询构建一个可运行的示例,请继续阅读创建博客。