Color mode

Series

Ordered multi-part posts ("series") for Riebeckite. At build time, notes that share a series name get the same generated navigation listing every part in order, with the current part marked and previous/next links.

日本語

Overview

series() reads series metadata from frontmatter, groups every matching manifest entry, sorts the group, and appends a <nav class="rb-series"> block to each note in the series. Links use the permalinks resolved by Core, so plugins such as permalink are respected. A single-note series renders no navigation block.

The plugin also declares page types, so it exposes a list page at /series and one landing page per series at /series/<name>. Both are built from the discoverable manifest, so unlisted, draft, and scheduled notes never appear.

The plugin is build-time only: it ships a stylesheet asset and no client runtime.

Usage

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

Frontmatter contract

Key Type Required Description
series string yes Series name used for grouping.
series_order number recommended Position within the series (ascending).
series_title string no Display title for the series heading.
yaml
---
title: Installing the thing
series: Build a thing
series_order: 2
---

Notes missing a valid series_order are still included; they are ordered after the numbered parts, using date/created/published, then title, then slug. Ties always resolve deterministically.

Options

Option Type Default Description
key string "series" Frontmatter key that names the series.
orderKey string "series_order" Frontmatter key holding the numeric order.
titleKey string "series_title" Frontmatter key overriding the series heading.
heading boolean true Render the series heading above the list.
className string "rb-series" Base CSS class for generated markup.
positionLabel boolean false Add a Part N of M label for the current note.
basePath string "/series" Base path for the generated pages. An empty string disables them.

Output

Each note in a series of two or more parts gets the following block appended to its HTML:

html
<nav class="rb-series" data-series="Build a thing"
     aria-label="Series navigation">
  <p class="rb-series__title">
    <a class="rb-series__link" href="/build-a-thing">Build a thing</a>
  </p>
  <ol class="rb-series__list">
    <li class="rb-series__item">
      <a class="rb-series__link" href="/part-1" data-series-order="1">Part 1</a>
    </li>
    <li class="rb-series__item">
      <a class="rb-series__link" href="/part-2" data-series-order="2"
         aria-current="page">Part 2</a>
    </li>
  </ol>
  <div class="rb-series__nav">
    <a class="rb-series__prev" rel="prev" href="/part-1">&larr; Part 1</a>
    <a class="rb-series__next" rel="next" href="/part-3">Part 3 &rarr;</a>
  </div>
</nav>

All text and attributes are escaped. The injected HTML is written back to both the manifest entry and the processed content object so the page route, feeds, and search see the same markup.

Page types

The plugin registers two page types. Both derive their output from manifest.discoverableEntries, so non-public notes are excluded.

Page type Path Output
series-list <basePath> (/series) <section class="rb-series rb-series--list"> listing each series with a link to its landing page. Generated only when at least one series exists.
series-index <basePath>/<name> (/series/<name>) The renderSeriesIndex() section for one series, in the existing order.

The <name> segment is a lowercase, hyphenated slug; non-ASCII names fall back to percent-encoding. Set basePath to match a custom routing scheme, or pass basePath: "" to disable the pages and render them yourself.

Exports

  • series(options?) / seriesPlugin(options?) — plugin factory
  • buildSeriesIndex(manifest, name, options?) — ordered members for one series (SeriesIndex | null); useful for landing pages
  • renderSeriesIndex(manifest, name, options?) — standalone <section> block for a whole series
  • renderSeriesList(manifest, options?, label?) — standalone <section> block listing every series; returns "" when there are none
  • seriesLandingPath(name, options?) — landing path for a series, or "" when the pages are disabled
  • seriesSlug(name) — URL segment used by a series landing page
  • renderSeriesNavigation(index, currentSlug, options?) — a single navigation block
  • collectSeriesIndexes(manifest, options?) — every series in first-seen order
  • resolveSeriesOptions(options?) — options with defaults applied
  • DEFAULT_SERIES_BASE_PATH — default basePath ("/series")
  • Types: SeriesOptions, ResolvedSeriesOptions, SeriesMember, SeriesIndex

Series landing page

The plugin generates these pages automatically. To render them yourself, or to reuse the markup elsewhere, combine the exported helpers:

ts
import { buildSeriesIndex, renderSeriesIndex } from "@riebeckite/plugin-series";
 
// inside a route component, with the resolved manifest:
const index = buildSeriesIndex(manifest, "Build a thing");
const html = renderSeriesIndex(manifest, "Build a thing");

Diagnostics

Emitted with pluginName: "series" and severity: "warning":

Code Meaning
series-invalid-name The series key is present but is not a non-empty string.
series-missing-order The note has no valid numeric order key; fallback ordering is used.
series-duplicate-order Two or more notes share the same (series, order) pair.

CSS hooks

style.css styles .rb-series, .rb-series__title, .rb-series__position, .rb-series__list, .rb-series__item, .rb-series__nav, .rb-series__prev, .rb-series__next, and the .rb-series--index variant. The current part is matched with .rb-series__item a[aria-current="page"].

Limitations

  • A note belongs to exactly one series.
  • A series of one note produces no navigation block on the note, but it still gets a list entry and a landing page.
  • series_order must be a finite number; numeric strings are not coerced.
  • Series names that slugify to the same segment share the first landing page.
  • Generated pages use /series by default; change basePath to avoid clashes with existing routes.

See also

History

1 changesCollapseExpand
1 + <!-- Generated from packages/plugins/series/README.md. Do not edit this page directly; edit the package README and run `pnpm docs:sync`. -->
2 +
3 + # Series
4 +
5 + Ordered multi-part posts ("series") for Riebeckite. At build time, notes that
6 + share a series name get the same generated navigation listing every part in
7 + order, with the current part marked and previous/next links.
8 +
9 + [日本語](./series.md)
10 +
11 + ## Overview
12 +
13 + `series()` reads series metadata from frontmatter, groups every matching
14 + manifest entry, sorts the group, and appends a `<nav class="rb-series">` block
15 + to each note in the series. Links use the permalinks resolved by Core, so
16 + plugins such as `permalink` are respected. A single-note series renders no
17 + navigation block.
18 +
19 + The plugin also declares page types, so it exposes a list page at `/series`
20 + and one landing page per series at `/series/<name>`. Both are built from the
21 + discoverable manifest, so unlisted, draft, and scheduled notes never appear.
22 +
23 + The plugin is build-time only: it ships a stylesheet asset and no client
24 + runtime.
25 +
26 + ## Usage
27 +
28 + ```ts
29 + import { defineConfig } from "@riebeckite/core";
30 + import { series } from "@riebeckite/plugin-series";
31 +
32 + export default defineConfig({
33 + // ...
34 + plugins: [series()],
35 + });
36 + ```
37 +
38 + ## Frontmatter contract
39 +
40 + | Key | Type | Required | Description |
41 + | --- | ---- | -------- | ----------- |
42 + | `series` | `string` | yes | Series name used for grouping. |
43 + | `series_order` | `number` | recommended | Position within the series (ascending). |
44 + | `series_title` | `string` | no | Display title for the series heading. |
45 +
46 + ```yaml
47 + ---
48 + title: Installing the thing
49 + series: Build a thing
50 + series_order: 2
51 + ---
52 + ```
53 +
54 + Notes missing a valid `series_order` are still included; they are ordered after
55 + the numbered parts, using `date`/`created`/`published`, then `title`, then
56 + `slug`. Ties always resolve deterministically.
57 +
58 + ## Options
59 +
60 + | Option | Type | Default | Description |
61 + | ------ | ---- | ------- | ----------- |
62 + | `key` | `string` | `"series"` | Frontmatter key that names the series. |
63 + | `orderKey` | `string` | `"series_order"` | Frontmatter key holding the numeric order. |
64 + | `titleKey` | `string` | `"series_title"` | Frontmatter key overriding the series heading. |
65 + | `heading` | `boolean` | `true` | Render the series heading above the list. |
66 + | `className` | `string` | `"rb-series"` | Base CSS class for generated markup. |
67 + | `positionLabel` | `boolean` | `false` | Add a `Part N of M` label for the current note. |
68 + | `basePath` | `string` | `"/series"` | Base path for the generated pages. An empty string disables them. |
69 +
70 + ## Output
71 +
72 + Each note in a series of two or more parts gets the following block appended to
73 + its HTML:
74 +
75 + ```html
76 + <nav class="rb-series" data-series="Build a thing"
77 + aria-label="Series navigation">
78 + <p class="rb-series__title">
79 + <a class="rb-series__link" href="/build-a-thing">Build a thing</a>
80 + </p>
81 + <ol class="rb-series__list">
82 + <li class="rb-series__item">
83 + <a class="rb-series__link" href="/part-1" data-series-order="1">Part 1</a>
84 + </li>
85 + <li class="rb-series__item">
86 + <a class="rb-series__link" href="/part-2" data-series-order="2"
87 + aria-current="page">Part 2</a>
88 + </li>
89 + </ol>
90 + <div class="rb-series__nav">
91 + <a class="rb-series__prev" rel="prev" href="/part-1">&larr; Part 1</a>
92 + <a class="rb-series__next" rel="next" href="/part-3">Part 3 &rarr;</a>
93 + </div>
94 + </nav>
95 + ```
96 +
97 + All text and attributes are escaped. The injected HTML is written back to both
98 + the manifest entry and the processed content object so the page route, feeds,
99 + and search see the same markup.
100 +
101 + ## Page types
102 +
103 + The plugin registers two page types. Both derive their output from
104 + `manifest.discoverableEntries`, so non-public notes are excluded.
105 +
106 + | Page type | Path | Output |
107 + | --------- | ---- | ------ |
108 + | `series-list` | `<basePath>` (`/series`) | `<section class="rb-series rb-series--list">` listing each series with a link to its landing page. Generated only when at least one series exists. |
109 + | `series-index` | `<basePath>/<name>` (`/series/<name>`) | The `renderSeriesIndex()` section for one series, in the existing order. |
110 +
111 + The `<name>` segment is a lowercase, hyphenated slug; non-ASCII names fall back
112 + to percent-encoding. Set `basePath` to match a custom routing scheme, or pass
113 + `basePath: ""` to disable the pages and render them yourself.
114 +
115 + ## Exports
116 +
117 + - `series(options?)` / `seriesPlugin(options?)` — plugin factory
118 + - `buildSeriesIndex(manifest, name, options?)` — ordered members for one series
119 + (`SeriesIndex | null`); useful for landing pages
120 + - `renderSeriesIndex(manifest, name, options?)` — standalone `<section>` block
121 + for a whole series
122 + - `renderSeriesList(manifest, options?, label?)` — standalone `<section>` block
123 + listing every series; returns `""` when there are none
124 + - `seriesLandingPath(name, options?)` — landing path for a series, or `""` when
125 + the pages are disabled
126 + - `seriesSlug(name)` — URL segment used by a series landing page
127 + - `renderSeriesNavigation(index, currentSlug, options?)` — a single navigation
128 + block
129 + - `collectSeriesIndexes(manifest, options?)` — every series in first-seen order
130 + - `resolveSeriesOptions(options?)` — options with defaults applied
131 + - `DEFAULT_SERIES_BASE_PATH` — default `basePath` (`"/series"`)
132 + - Types: `SeriesOptions`, `ResolvedSeriesOptions`, `SeriesMember`, `SeriesIndex`
133 +
134 + ### Series landing page
135 +
136 + The plugin generates these pages automatically. To render them yourself, or to
137 + reuse the markup elsewhere, combine the exported helpers:
138 +
139 + ```ts
140 + import { buildSeriesIndex, renderSeriesIndex } from "@riebeckite/plugin-series";
141 +
142 + // inside a route component, with the resolved manifest:
143 + const index = buildSeriesIndex(manifest, "Build a thing");
144 + const html = renderSeriesIndex(manifest, "Build a thing");
145 + ```
146 +
147 + ## Diagnostics
148 +
149 + Emitted with `pluginName: "series"` and `severity: "warning"`:
150 +
151 + | Code | Meaning |
152 + | ---- | ------- |
153 + | `series-invalid-name` | The series key is present but is not a non-empty string. |
154 + | `series-missing-order` | The note has no valid numeric order key; fallback ordering is used. |
155 + | `series-duplicate-order` | Two or more notes share the same `(series, order)` pair. |
156 +
157 + ## CSS hooks
158 +
159 + `style.css` styles `.rb-series`, `.rb-series__title`, `.rb-series__position`,
160 + `.rb-series__list`, `.rb-series__item`, `.rb-series__nav`, `.rb-series__prev`,
161 + `.rb-series__next`, and the `.rb-series--index` variant. The current part is
162 + matched with `.rb-series__item a[aria-current="page"]`.
163 +
164 + ## Limitations
165 +
166 + - A note belongs to exactly one series.
167 + - A series of one note produces no navigation block on the note, but it still
168 + gets a list entry and a landing page.
169 + - `series_order` must be a finite number; numeric strings are not coerced.
170 + - Series names that slugify to the same segment share the first landing page.
171 + - Generated pages use `/series` by default; change `basePath` to avoid clashes
172 + with existing routes.
173 +
174 + ## See also
175 +
176 + - [Plugin guide](../reference/plugin-api.en.md)
177 +