Color mode

Taxonomy

Build-time tag and folder taxonomy for Riebeckite: listing data, per-term RSS / Atom / JSON feeds, related-tag navigation, and SEO metadata. No client-side JavaScript is required.

日本語

Overview

taxonomy() reads the manifest's public entries and produces two sets of listing terms with the Core collection contract (buildContentCollections):

  • Tag terms group by tags under /tags/<slug>.
  • Folder terms group by folder under /folders/<path>.

Entries always link through their resolved permalink; the plugin never reconstructs a URL from a slug.

The plugin owns data, feeds, SEO, and the /tags/<tag> and /folders/<path> page types. Per-term feeds are written as static files through the build's generated-output sink. A site renders the page types through its generic Riebeckite catch-all route; no taxonomy-specific application route is needed.

Usage

ts
import { defineConfig } from "@riebeckite/core";
import { taxonomy } from "@riebeckite/plugin-taxonomy";
 
export default defineConfig({
  // ...
  plugins: [
    taxonomy({
      tags: true,
      folders: true,
      related: true,
    }),
  ],
});

Options

Option Type Default Description
tags boolean true Generate tag terms
folders boolean true Generate folder terms
tagsBasePath string "/tags" Tag listing prefix
foldersBasePath string "/folders" Folder listing prefix
folderDepth number 0 Folder grouping depth; 0 keeps the full path
minEntries number 1 Drop terms with fewer entries
related boolean true Compute related-tag navigation
relatedLimit number 8 Maximum related tags per tag
feeds { rss?, atom?, json? } all true Per-term feed formats
feedLimit number 50 Maximum entries per feed
resolveTitle (context) => string #value / path Custom term title
className string "rr-taxonomy" Root CSS class of page fragments
dataEndpoint string "/taxonomy/index.json" JSON data endpoint path

Data endpoint

The plugin registers one JSON endpoint (the endpoint contract is mounted by the HonoX integration, so it never contains framework routing itself):

text
GET /taxonomy/index.json
json
{
  "tags": [
    {
      "kind": "tag",
      "value": "featured",
      "title": "#featured",
      "path": "/tags/featured",
      "permalink": "/tags/featured",
      "count": 1,
      "entries": [{ "slug": "example", "permalink": "/notes/example", "title": "Example Note", "updated": null, "summary": "..." }],
      "related": [],
      "feeds": { "rss": "/tags/featured/feed.xml", "atom": "/tags/featured/atom.xml", "json": "/tags/featured/feed.json" }
    }
  ],
  "folders": []
}

The payload is a deterministic, JSON-safe projection: it never includes rendered HTML, so it is safe to ship to an app route or a browser.

Generated feeds

At build time the plugin emits one feed per term and format through context.output.emit:

Format Path
RSS 2.0 /tags/<slug>/feed.xml
Atom /tags/<slug>/atom.xml
JSON Feed 1.1 /tags/<slug>/feed.json

Folder terms get the same files under /folders/<path>/…. Feed channels carry their own term title and self link, so a tag subscription is distinguishable from the site-wide feeds owned by @riebeckite/plugin-seo. Only published, non-noindex entries reach the manifest's public view and therefore the feeds. RSS and Atom term feeds expose entry summaries. JSON term feeds use the same summary as content_text and do not duplicate rendered article HTML.

When related is enabled, each tag term carries tags that co-occur on the same entries, ranked by shared-entry count then alphabetically, clamped to relatedLimit. Related navigation is rendered by renderTaxonomyPage:

html
<nav class="rr-taxonomy__related" aria-label="Related tags" data-rr-taxonomy-related>
  <ul>
    <li class="rr-taxonomy__related-item">
      <a class="rr-taxonomy__related-link" href="/tags/featured" data-rr-taxonomy-related-count="2">#featured</a>
    </li>
  </ul>
</nav>

SEO

buildTaxonomySeo(config, term) returns SeoMetadata for a listing page. It delegates to the configured Core seo extension point (for example @riebeckite/plugin-seo) so titles, canonical URLs, and JSON-LD stay consistent with the rest of the site, and falls back to a minimal object when no SEO provider is registered.

Folder index notes

Folder entry resolution is owned by @riebeckite/plugin-folder-pages, so taxonomy only generates tag and folder terms from the manifest's public view. Enable that plugin when a note at <folder>/README.md or <folder>/index.md should become the folder's landing page.

Page types

taxonomy() registers two page types. taxonomy-term renders one tag or folder page and includes feed discovery <link rel="alternate"> metadata. taxonomy-index renders the all-tags list at tagsBasePath and the all-folders list at foldersBasePath, linking to every term. Both derive their SSG paths and resolver from the public manifest, so unpublished entries never appear on a tag, folder, or index page.

Use pluginPageSsgParams(content) and resolveRiebeckiteRoute(content, path) from @riebeckite/honox/server in the site's generic catch-all route. This is the same wiring used for every plugin page type.

Style

The package ships style.css with the stable rr-taxonomy root hook and --rr-taxonomy-* tokens (falling back to --rb-*). Register it like any other plugin stylesheet:

ts
import "@riebeckite/plugin-taxonomy/style.css";

Exports

  • taxonomy(options?) — plugin factory
  • taxonomyPlugin — alias of taxonomy
  • resolveTaxonomyOptions(options?) — apply option defaults
  • resolveTaxonomyOptionsFromConfig(config) — read resolved options back from a config
  • buildTaxonomyIndex(entries, options) — build tag and folder terms
  • serializeTaxonomyIndex(index) / serializeTaxonomyTerm(term) — JSON-safe projections
  • renderTaxonomyPage(term, options) / renderRelatedTerms(term, options) — page fragments
  • renderTaxonomyIndexPage(kind, terms, options) — all-tags/all-folders page fragment
  • renderTermFeed(config, term, format, limit?) / buildFeedHeadTags(term) — per-term feeds
  • buildTaxonomySeo(config, term) — listing-page SEO metadata
  • slugifyTaxonomyValue(value) — URL/file slug
  • buildTaxonomyAbsoluteUrl(config, pathOrUrl) — absolute URLs
  • Types: TaxonomyOptions, ResolvedTaxonomyOptions, TaxonomyTerm, TaxonomyIndex, TaxonomyPage, TaxonomyTermData, TaxonomyIndexData

Limitations

  • Taxonomy is fixed at build time. A full rebuild always recomputes correctly.
  • Per-term feeds are static build output; the fixed JSON data endpoint is the only runtime surface. A dev server does not enumerate per-term feed files.
  • Terms only reflect the manifest's public view; unpublished or noindex entries are excluded.

See also

History

1 changesCollapseExpand
1 + <!-- Generated from packages/plugins/taxonomy/README.md. Do not edit this page directly; edit the package README and run `pnpm docs:sync`. -->
2 +
3 + # Taxonomy
4 +
5 + Build-time tag and folder taxonomy for Riebeckite: listing data, per-term
6 + RSS / Atom / JSON feeds, related-tag navigation, and SEO metadata. No
7 + client-side JavaScript is required.
8 +
9 + [日本語](./taxonomy.md)
10 +
11 + ## Overview
12 +
13 + `taxonomy()` reads the manifest's public entries and produces two sets of
14 + listing terms with the Core collection contract (`buildContentCollections`):
15 +
16 + - **Tag terms** group by `tags` under `/tags/<slug>`.
17 + - **Folder terms** group by folder under `/folders/<path>`.
18 +
19 + Entries always link through their resolved `permalink`; the plugin never
20 + reconstructs a URL from a slug.
21 +
22 + The plugin owns data, feeds, SEO, and the `/tags/<tag>` and `/folders/<path>`
23 + page types. Per-term feeds are written as static files through the build's
24 + generated-output sink. A site renders the page types through its generic
25 + Riebeckite catch-all route; no taxonomy-specific application route is needed.
26 +
27 + ## Usage
28 +
29 + ```ts
30 + import { defineConfig } from "@riebeckite/core";
31 + import { taxonomy } from "@riebeckite/plugin-taxonomy";
32 +
33 + export default defineConfig({
34 + // ...
35 + plugins: [
36 + taxonomy({
37 + tags: true,
38 + folders: true,
39 + related: true,
40 + }),
41 + ],
42 + });
43 + ```
44 +
45 + ## Options
46 +
47 + | Option | Type | Default | Description |
48 + | ------ | ---- | ------- | ----------- |
49 + | `tags` | `boolean` | `true` | Generate tag terms |
50 + | `folders` | `boolean` | `true` | Generate folder terms |
51 + | `tagsBasePath` | `string` | `"/tags"` | Tag listing prefix |
52 + | `foldersBasePath` | `string` | `"/folders"` | Folder listing prefix |
53 + | `folderDepth` | `number` | `0` | Folder grouping depth; `0` keeps the full path |
54 + | `minEntries` | `number` | `1` | Drop terms with fewer entries |
55 + | `related` | `boolean` | `true` | Compute related-tag navigation |
56 + | `relatedLimit` | `number` | `8` | Maximum related tags per tag |
57 + | `feeds` | `{ rss?, atom?, json? }` | all `true` | Per-term feed formats |
58 + | `feedLimit` | `number` | `50` | Maximum entries per feed |
59 + | `resolveTitle` | `(context) => string` | `#value` / path | Custom term title |
60 + | `className` | `string` | `"rr-taxonomy"` | Root CSS class of page fragments |
61 + | `dataEndpoint` | `string` | `"/taxonomy/index.json"` | JSON data endpoint path |
62 +
63 + ## Data endpoint
64 +
65 + The plugin registers one JSON endpoint (the endpoint contract is mounted by the
66 + HonoX integration, so it never contains framework routing itself):
67 +
68 + ```
69 + GET /taxonomy/index.json
70 + ```
71 +
72 + ```json
73 + {
74 + "tags": [
75 + {
76 + "kind": "tag",
77 + "value": "featured",
78 + "title": "#featured",
79 + "path": "/tags/featured",
80 + "permalink": "/tags/featured",
81 + "count": 1,
82 + "entries": [{ "slug": "example", "permalink": "/notes/example", "title": "Example Note", "updated": null, "summary": "..." }],
83 + "related": [],
84 + "feeds": { "rss": "/tags/featured/feed.xml", "atom": "/tags/featured/atom.xml", "json": "/tags/featured/feed.json" }
85 + }
86 + ],
87 + "folders": []
88 + }
89 + ```
90 +
91 + The payload is a deterministic, JSON-safe projection: it never includes rendered
92 + HTML, so it is safe to ship to an app route or a browser.
93 +
94 + ## Generated feeds
95 +
96 + At build time the plugin emits one feed per term and format through
97 + `context.output.emit`:
98 +
99 + | Format | Path |
100 + | ------ | ---- |
101 + | RSS 2.0 | `/tags/<slug>/feed.xml` |
102 + | Atom | `/tags/<slug>/atom.xml` |
103 + | JSON Feed 1.1 | `/tags/<slug>/feed.json` |
104 +
105 + Folder terms get the same files under `/folders/<path>/…`. Feed channels carry
106 + their own term title and self link, so a tag subscription is distinguishable
107 + from the site-wide feeds owned by `@riebeckite/plugin-seo`. Only published,
108 + non-`noindex` entries reach the manifest's public view and therefore the feeds.
109 + RSS and Atom term feeds expose entry summaries. JSON term feeds use the same
110 + summary as `content_text` and do not duplicate rendered article HTML.
111 +
112 + ## Related tags
113 +
114 + When `related` is enabled, each tag term carries tags that co-occur on the same
115 + entries, ranked by shared-entry count then alphabetically, clamped to
116 + `relatedLimit`. Related navigation is rendered by `renderTaxonomyPage`:
117 +
118 + ```html
119 + <nav class="rr-taxonomy__related" aria-label="Related tags" data-rr-taxonomy-related>
120 + <ul>
121 + <li class="rr-taxonomy__related-item">
122 + <a class="rr-taxonomy__related-link" href="/tags/featured" data-rr-taxonomy-related-count="2">#featured</a>
123 + </li>
124 + </ul>
125 + </nav>
126 + ```
127 +
128 + ## SEO
129 +
130 + `buildTaxonomySeo(config, term)` returns `SeoMetadata` for a listing page. It
131 + delegates to the configured Core `seo` extension point (for example
132 + `@riebeckite/plugin-seo`) so titles, canonical URLs, and JSON-LD stay consistent
133 + with the rest of the site, and falls back to a minimal object when no SEO
134 + provider is registered.
135 +
136 + ## Folder index notes
137 +
138 + Folder entry resolution is owned by
139 + [`@riebeckite/plugin-folder-pages`](./folder-pages.en.md), so taxonomy only
140 + generates tag and folder terms from the manifest's public view. Enable that
141 + plugin when a note at `<folder>/README.md` or `<folder>/index.md` should become
142 + the folder's landing page.
143 +
144 + ## Page types
145 +
146 + `taxonomy()` registers two page types. `taxonomy-term` renders one tag or
147 + folder page and includes feed discovery `<link rel="alternate">` metadata.
148 + `taxonomy-index` renders the all-tags list at `tagsBasePath` and the
149 + all-folders list at `foldersBasePath`, linking to every term. Both derive
150 + their SSG paths and resolver from the public manifest, so unpublished entries
151 + never appear on a tag, folder, or index page.
152 +
153 + Use `pluginPageSsgParams(content)` and `resolveRiebeckiteRoute(content, path)`
154 + from `@riebeckite/honox/server` in the site's generic catch-all route. This is
155 + the same wiring used for every plugin page type.
156 +
157 + ## Style
158 +
159 + The package ships `style.css` with the stable `rr-taxonomy` root hook and
160 + `--rr-taxonomy-*` tokens (falling back to `--rb-*`). Register it like any other
161 + plugin stylesheet:
162 +
163 + ```ts
164 + import "@riebeckite/plugin-taxonomy/style.css";
165 + ```
166 +
167 + ## Exports
168 +
169 + - `taxonomy(options?)` — plugin factory
170 + - `taxonomyPlugin` — alias of `taxonomy`
171 + - `resolveTaxonomyOptions(options?)` — apply option defaults
172 + - `resolveTaxonomyOptionsFromConfig(config)` — read resolved options back from a config
173 + - `buildTaxonomyIndex(entries, options)` — build tag and folder terms
174 + - `serializeTaxonomyIndex(index)` / `serializeTaxonomyTerm(term)` — JSON-safe projections
175 + - `renderTaxonomyPage(term, options)` / `renderRelatedTerms(term, options)` — page fragments
176 + - `renderTaxonomyIndexPage(kind, terms, options)` — all-tags/all-folders page fragment
177 + - `renderTermFeed(config, term, format, limit?)` / `buildFeedHeadTags(term)` — per-term feeds
178 + - `buildTaxonomySeo(config, term)` — listing-page SEO metadata
179 + - `slugifyTaxonomyValue(value)` — URL/file slug
180 + - `buildTaxonomyAbsoluteUrl(config, pathOrUrl)` — absolute URLs
181 + - Types: `TaxonomyOptions`, `ResolvedTaxonomyOptions`, `TaxonomyTerm`, `TaxonomyIndex`, `TaxonomyPage`, `TaxonomyTermData`, `TaxonomyIndexData`
182 +
183 + ## Limitations
184 +
185 + - Taxonomy is fixed at build time. A full rebuild always recomputes correctly.
186 + - Per-term feeds are static build output; the fixed JSON data endpoint is the
187 + only runtime surface. A dev server does not enumerate per-term feed files.
188 + - Terms only reflect the manifest's public view; unpublished or `noindex`
189 + entries are excluded.
190 +
191 + ## See also
192 +
193 + - [Plugin guide](../reference/plugin-api.en.md)
194 + - [Content system](../framework/content-system.en.md)
195 +