分类法

本页内容

分类法是应用于一个或多个集合的命名分类。EmDash 从用于文章的层级 category 分类法和平坦的 tag 分类法开始。站点也可以定义诸如 genre、topic 或 difficulty 的分类法。

术语属于某个分类法。像「Guides」这样的分类可以有子分类,而标签和其他平坦分类法只有一层。

管理术语

在 EmDash 管理中从 Taxonomies 打开一个分类法。

  1. 点击 Add Category、Add Tag,或当前分类法的等效操作。

  2. 输入标签和 slug。对于层级分类法,当术语属于另一个术语之下时,选择父级。

  3. 添加可选描述,然后创建术语。

  4. 使用术语列表中的移动控件,在当前父组内设置顺序。

编辑者从内容条目中的分类法面板分配术语。分类法定义控制哪些集合显示每个面板。

删除术语会移除其在内容上的分配。不会删除内容条目本身。

向多篇文章添加一个术语

编辑者可以在集合列表中选择文章,然后点击 Add 加上该分类法的单数名称(例如 Add tag),或打开某个分类法的页面并点击 Add to posts 粘贴公开文章 URL(每行一个,最多 50)。当集合适用多个分类法时,列表会显示 Add term,对话框会询问使用哪个分类法。选择现有术语或在对话框中创建一个,然后点击 Review posts。检查每个匹配的标题和语言,然后点击显示将更新多少篇文章的按钮。与本站已发布文章不完全匹配的链接会被标记,而不是猜测。只有使用所选分类法的集合中的条目才能被匹配。

添加术语会立即生效,即使文章还有其他未发布的草稿编辑;它不会发布那些编辑。现有术语保持不变,已有该术语的文章会被跳过。结果列表显示哪些文章已更新、被跳过或无法更新;对写入失败使用 Retry failures。URL 匹配使用站点配置的公开源和集合 URL 模式,包括日期和语言路径。

添加自定义分类法

当现有集合需要单独分类时,创建分类法。

  1. 打开 Taxonomies 并点击 New Taxonomy。

  2. 输入标签和稳定名称。名称以小写字母开头,仅包含小写字母、数字和下划线。

  3. 若术语需要父子关系,启用 Hierarchical。

  4. 选择可以使用该分类法的每个集合,然后点击 Create Taxonomy。

  5. 添加初始术语并将它们分配给内容。

模板查询稳定名称。更改显示标签不需要更改模板。

自定义分类法使用与分类和标签相同的查询与过滤辅助函数。以下示例读取 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。请求体与响应请参见分类法端点。