分类法

本页内容

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

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

管理术语

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

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

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

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

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

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

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

向多篇文章添加一个标签

编辑者可以在集合列表中选择文章并点击 Add tag,或打开 Tags 并点击 Add to posts 粘贴公开文章 URL(每行一个,最多 50)。选择现有标签或在对话框中创建,然后点击 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.redirect("/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 和标签。

术语的父级和位置由每种语言环境共享。在没有父级的情况下创建的翻译会采用其术语的父级和位置。在不同父级下创建翻译,或通过任一语言环境更改父级,会在每种语言环境中移动该术语。

查询辅助函数在提供时使用显式语言环境。否则使用当前请求语言环境,然后是配置的默认值。当请求的翻译不存在时,单术语查找遵循配置的回退链。

有关语言环境路由与回退配置,请参见 Internationalization;有关编辑条目,请参见 Working with Content。运行时 API 参考 记录了分类法查询辅助函数。对于程序化更改,使用 Bearer 令牌进行身份验证,并向每个更改状态的请求添加 X-EmDash-Request: 1。请求体与响应请参见 分类法端点。