分类法是应用于一个或多个集合的命名分类。EmDash 从用于文章的层级 category 分类法和平坦的 tag 分类法开始。站点也可以定义诸如 genre、topic 或 difficulty 的分类法。
术语属于某个分类法。像「Guides」这样的分类可以有子分类,而标签和其他平坦分类法只有一层。
管理术语
在 EmDash 管理中从 Taxonomies 打开一个分类法。
-
点击 Add Category、Add Tag,或当前分类法的等效操作。
-
输入标签和 slug。对于层级分类法,当术语属于另一个术语之下时,选择父级。
-
添加可选描述,然后创建术语。
-
使用术语列表中的移动控件,在当前父组内设置顺序。
编辑者从内容条目中的分类法面板分配术语。分类法定义控制哪些集合显示每个面板。
删除术语会移除其在内容上的分配。不会删除内容条目本身。
向多篇文章添加一个术语
编辑者可以在集合列表中选择文章,然后点击 Add 加上该分类法的单数名称(例如 Add tag),或打开某个分类法的页面并点击 Add to posts 粘贴公开文章 URL(每行一个,最多 50)。当集合适用多个分类法时,列表会显示 Add term,对话框会询问使用哪个分类法。选择现有术语或在对话框中创建一个,然后点击 Review posts。检查每个匹配的标题和语言,然后点击显示将更新多少篇文章的按钮。与本站已发布文章不完全匹配的链接会被标记,而不是猜测。只有使用所选分类法的集合中的条目才能被匹配。
添加术语会立即生效,即使文章还有其他未发布的草稿编辑;它不会发布那些编辑。现有术语保持不变,已有该术语的文章会被跳过。结果列表显示哪些文章已更新、被跳过或无法更新;对写入失败使用 Retry failures。URL 匹配使用站点配置的公开源和集合 URL 模式,包括日期和语言路径。
添加自定义分类法
当现有集合需要单独分类时,创建分类法。
-
打开 Taxonomies 并点击 New Taxonomy。
-
输入标签和稳定名称。名称以小写字母开头,仅包含小写字母、数字和下划线。
-
若术语需要父子关系,启用 Hierarchical。
-
选择可以使用该分类法的每个集合,然后点击 Create Taxonomy。
-
添加初始术语并将它们分配给内容。
模板查询稳定名称。更改显示标签不需要更改模板。
自定义分类法使用与分类和标签相同的查询与过滤辅助函数。以下示例读取 genre 术语,并按其中一个 slug 过滤书籍:
import { getEmDashCollection, getTaxonomyTerms } from "emdash";
const genres = await getTaxonomyTerms("genre", { includeCounts: false });
const { entries: scienceFictionBooks } = await getEmDashCollection("books", {
where: { genre: "science-fiction" },
});
删除分类法
在管理中打开分类法,然后从页眉的操作菜单中选择 Delete taxonomy 并确认。该操作需要编辑者和管理员持有的 taxonomies:manage 权限。
删除分类法会删除其在每种语言中的术语,并从归入其下的内容中移除这些术语。内容条目本身会保留。
查询术语列表
使用 getTaxonomyTerms() 渲染分类法索引、导航列表或一组过滤器。层级分类法通过每个术语的 children 数组返回一棵树。
术语计数默认包含,并且需要对分类法已分配的集合做聚合。当组件不显示计数时,可以跳过这部分工作。
下面的组件渲染不带计数的分类链接:
---
import { getTaxonomyTerms } from "emdash";
import { getRelativeLocaleUrl } from "astro:i18n";
const locale = Astro.currentLocale;
const categories = await getTaxonomyTerms("category", {
locale,
includeCounts: false,
});
function categoryHref(slug: string) {
const path = `/category/${slug}`;
return locale ? getRelativeLocaleUrl(locale, path) : path;
}
---
<nav aria-label="Categories">
<ul>
{categories.map((category) => (
<li>
<a href={categoryHref(category.slug)}>{category.label}</a>
{category.children.length > 0 && (
<ul>
{category.children.map((child) => (
<li><a href={categoryHref(child.slug)}>{child.label}</a></li>
))}
</ul>
)}
</li>
))}
</ul>
</nav>
当组件显示使用量时,省略 includeCounts: false 并渲染 term.count。该计数包含在查询所用语言环境中公开可见的条目。
构建分类法归档
在查找术语之前,先解码动态路由参数。使用相同的语言环境查询术语和内容,并让生成的路径经过 Astro 的语言环境 URL 辅助函数。
下面的路由列出某个分类下已发布的文章:
---
import { decodeSlug, getEmDashCollection, getTerm } from "emdash";
import { getRelativeLocaleUrl } from "astro:i18n";
import Base from "../../layouts/Base.astro";
const locale = Astro.currentLocale;
const slug = decodeSlug(Astro.params.slug);
const category = slug
? await getTerm("category", slug, { locale, includeCounts: false })
: null;
if (!category) {
return Astro.rewrite("/404");
}
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
locale,
where: { category: category.slug },
orderBy: { published_at: "desc" },
});
function postHref(postSlug: string) {
const path = `/posts/${postSlug}`;
return locale ? getRelativeLocaleUrl(locale, path) : path;
}
---
<Base title={`${category.label} posts`}>
<h1>{category.label}</h1>
{category.description && <p>{category.description}</p>}
{posts.length > 0 ? (
<ul>
{posts.map((post) => (
post.data.slug && (
<li>
<a href={postHref(post.data.slug)}>{post.data.title}</a>
</li>
)
))}
</ul>
) : (
<p>No posts in this category.</p>
)}
</Base>
where 以分类法名称为键、术语 slug 为值。查询的排序标识符使用 published_at 之类的数据库字段名;条目数据则将对应的值暴露为 publishedAt。
请在 postHref() 中使用集合实际的公开路由。如果集合使用自定义 urlPattern,请根据该模式构建链接,而不是假定为 /posts/{slug}。
显示条目的术语
getEmDashEntry() 和 getEmDashCollection() 会把已分配的术语注入到 entry.data.terms 上。请读取该值,而不是为列表中的每个条目各运行一次 getEntryTerms() 查询。
下面的组件渲染随文章一起加载的分类和标签:
---
import type { ContentEntry, InferCollectionData } from "emdash";
import { getRelativeLocaleUrl } from "astro:i18n";
interface Props {
post: ContentEntry<InferCollectionData<"posts">>;
}
const { post } = Astro.props;
const locale = Astro.currentLocale;
const categories = post.data.terms?.category ?? [];
const tags = post.data.terms?.tag ?? [];
function termHref(taxonomy: string, slug: string) {
const path = `/${taxonomy}/${slug}`;
return locale ? getRelativeLocaleUrl(locale, path) : path;
}
---
{categories.length > 0 && (
<ul aria-label="Categories">
{categories.map((category) => (
<li>
<a href={termHref("category", category.slug)}>{category.label}</a>
</li>
))}
</ul>
)}
{tags.length > 0 && (
<ul aria-label="Tags">
{tags.map((tag) => (
<li>
<a href={termHref("tag", tag.slug)}>{tag.label}</a>
</li>
))}
</ul>
)}
当你只有集合名称和条目 ID 时,使用 getEntryTerms()。当若干条目未被内容查询注入术语时,使用 getTermsForEntries() 批量获取这些条目的术语。
翻译分类法与术语
分类法定义和术语在每种语言环境下各有一行。EmDash 会记录哪些行是同一分类法或同一术语的翻译。内容分配使用这一共享身份,因此在一种语言环境中做出的分配,在另一种语言环境存在对应翻译术语时,会解析到该翻译术语。
分类法定义把字段分别划归分类法和各语言环境:
| 字段 | 属于 | 效果 |
|---|---|---|
name | 分类法 | 创建后固定。每种语言环境的定义使用相同名称。 |
hierarchical、collections | 分类法 | 每种语言环境相同。通过任一语言环境更改其中任一项,会对所有语言环境生效。 |
label、labelSingular | 语言环境 | 每种语言环境的定义各有自己的值。 |
为另一语言环境中已存在的名称创建的定义会加入该分类法(无论是否带有 translationOf),并沿用其 hierarchical 和 collections。用不同的值创建会失败;请通过更新来更改它们。没有自己定义的语言环境仍会列出该分类法的术语,并显示其回退链上第一个有定义的语言环境的标签,否则显示默认语言环境的标签,再否则显示语言环境代码最小的语言环境的标签。
使用分类法页面上的语言环境切换器,在每个已配置的语言环境中管理术语。打开术语的编辑对话框,并使用其 Translations 面板添加或打开另一种语言环境。翻译后的术语可以使用不同的 slug 和标签。
术语的父级和位置由所有语言环境共享。未指定父级而创建的翻译,会采用其术语的父级和位置。在不同的父级下创建翻译,或通过任一语言环境更改父级,会在所有语言环境中移动该术语。
查询辅助函数在提供显式语言环境时使用该语言环境。否则它们使用当前请求的语言环境,再退回到配置的默认值。请求的翻译不存在时,单个术语的查找会遵循配置的回退链。
有关语言环境路由与回退配置,请参见国际化 (i18n);有关编辑条目,请参见处理内容。JavaScript API 参考记录了分类法查询辅助函数。对于程序化更改,请使用 Bearer 令牌进行身份验证,并向每个会更改状态的请求添加 X-EmDash-Request: 1。请求体与响应请参见分类法端点。