Color mode

Properties

Render each note's frontmatter as an Obsidian-style property panel at build time. No client-side JavaScript is required.

日本語

Overview

When the content manifest is created, properties() renders a section.rb-properties[data-properties] from every entry's frontmatter. The note HTML is left untouched and the panel is published on ContentManifestEntry.bodySlots["article.metadata"], so the Site decides where to render it. The frontmatter stays the single source of truth — there is nothing to write in the Markdown body.

Values are rendered by type:

Value Output
Array ul.rb-properties__list with one item per element
Tag key (tags / tag) or a value starting with # a.rb-properties__tag linking to the site tag route
Boolean true / false with data-boolean
Number The number with data-number
ISO date string or Date <time datetime>
[[wikilink]] or URL inside a string Resolved link (wikilinks use the content index)
Nested object A nested <dl>

Values that cannot be rendered as structured HTML are emitted as escaped text and reported as a properties-unrenderable-value warning.

Usage

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

Options

Option Type Default Description
title string | null "Properties" Panel heading. null omits it
include string[] unset Render only these keys
exclude string[] ["publish", "permalink", "aliases", "redirect_from"] Keys to hide
order string[] unset Display order for selected keys: listed keys first, then the rest in frontmatter order
hideEmpty boolean true Skip null, "", [], and {} values
className string "rb-properties" Root CSS class
collapsed boolean false Render inside a <details> element
ts
properties({
  title: "メタデータ",
  exclude: ["publish", "permalink", "aliases", "redirect_from", "draft"],
  collapsed: true,
});

Rendering the metadata slot

The plugin writes the panel to ContentManifestEntry.bodySlots["article.metadata"]. The Site route passes bodySlots to its article component and chooses the metadata position in its layout. Combine include and order to decide which keys are shown and in what order:

tsx
// app/components/article.tsx (site side)
import type { ContentBodySlots } from "@riebeckite/core";
import { ContentSlot } from "@riebeckite/honox/ui";
 
function SiteArticle({ bodySlots }: { bodySlots?: ContentBodySlots }) {
  return (
    <ContentSlot
      slots={bodySlots}
      name="article.metadata"
      class="site-article__metadata"
    />
  );
}
ts
properties({
  include: ["title", "created", "updated", "tags"],
  order: ["title", "created", "updated", "tags"],
});

The handoff follows the ContentManifestEntry.bodySlots contract. A plugin never owns routes or the shell.

Exports

  • properties(options?) / propertiesPlugin(options?) — plugin factory
  • resolvePropertiesOptions(options?) — default resolution
  • renderPropertiesPanel(frontmatter, options?, context?) — pure renderer
  • buildTagHref(tag) — tag route builder
  • Types: PropertiesOptions, ResolvedPropertiesOptions, PropertiesRenderContext, PropertiesLinkResolver, PropertiesMessage

See also

History

1 changesCollapseExpand
1 + <!-- Generated from packages/plugins/properties/README.md. Do not edit this page directly; edit the package README and run `pnpm docs:sync`. -->
2 +
3 + # Properties
4 +
5 + Render each note's frontmatter as an Obsidian-style property panel at build
6 + time. No client-side JavaScript is required.
7 +
8 + [日本語](./properties.md)
9 +
10 + ## Overview
11 +
12 + When the content manifest is created, `properties()` renders a
13 + `section.rb-properties[data-properties]` from every entry's frontmatter. The
14 + note HTML is left untouched and the panel is published on
15 + `ContentManifestEntry.bodySlots["article.metadata"]`, so the Site decides where
16 + to render it. The frontmatter stays the single source of truth — there is
17 + nothing to write in the Markdown body.
18 +
19 + Values are rendered by type:
20 +
21 + | Value | Output |
22 + | ----- | ------ |
23 + | Array | `ul.rb-properties__list` with one item per element |
24 + | Tag key (`tags` / `tag`) or a value starting with `#` | `a.rb-properties__tag` linking to the site tag route |
25 + | Boolean | `true` / `false` with `data-boolean` |
26 + | Number | The number with `data-number` |
27 + | ISO date string or `Date` | `<time datetime>` |
28 + | `[[wikilink]]` or URL inside a string | Resolved link (wikilinks use the content index) |
29 + | Nested object | A nested `<dl>` |
30 +
31 + Values that cannot be rendered as structured HTML are emitted as escaped text
32 + and reported as a `properties-unrenderable-value` warning.
33 +
34 + ## Usage
35 +
36 + ```ts
37 + import { defineConfig } from "@riebeckite/core";
38 + import { properties } from "@riebeckite/plugin-properties";
39 +
40 + export default defineConfig({
41 + // ...
42 + plugins: [properties()],
43 + });
44 + ```
45 +
46 + ## Options
47 +
48 + | Option | Type | Default | Description |
49 + | ------ | ---- | ------- | ----------- |
50 + | `title` | `string \| null` | `"Properties"` | Panel heading. `null` omits it |
51 + | `include` | `string[]` | unset | Render only these keys |
52 + | `exclude` | `string[]` | `["publish", "permalink", "aliases", "redirect_from"]` | Keys to hide |
53 + | `order` | `string[]` | unset | Display order for selected keys: listed keys first, then the rest in frontmatter order |
54 + | `hideEmpty` | `boolean` | `true` | Skip `null`, `""`, `[]`, and `{}` values |
55 + | `className` | `string` | `"rb-properties"` | Root CSS class |
56 + | `collapsed` | `boolean` | `false` | Render inside a `<details>` element |
57 +
58 + ```ts
59 + properties({
60 + title: "メタデータ",
61 + exclude: ["publish", "permalink", "aliases", "redirect_from", "draft"],
62 + collapsed: true,
63 + });
64 + ```
65 +
66 + ### Rendering the metadata slot
67 +
68 + The plugin writes the panel to `ContentManifestEntry.bodySlots["article.metadata"]`.
69 + The Site route passes `bodySlots` to its article component and chooses the
70 + metadata position in its layout. Combine `include` and `order` to decide which
71 + keys are shown and in what order:
72 +
73 + ```tsx
74 + // app/components/article.tsx (site side)
75 + import type { ContentBodySlots } from "@riebeckite/core";
76 + import { ContentSlot } from "@riebeckite/honox/ui";
77 +
78 + function SiteArticle({ bodySlots }: { bodySlots?: ContentBodySlots }) {
79 + return (
80 + <ContentSlot
81 + slots={bodySlots}
82 + name="article.metadata"
83 + class="site-article__metadata"
84 + />
85 + );
86 + }
87 + ```
88 +
89 + ```ts
90 + properties({
91 + include: ["title", "created", "updated", "tags"],
92 + order: ["title", "created", "updated", "tags"],
93 + });
94 + ```
95 +
96 + The handoff follows the
97 + [`ContentManifestEntry.bodySlots`](../framework/honox-integration.en.md)
98 + contract. A plugin never owns routes or the shell.
99 +
100 + ## Exports
101 +
102 + - `properties(options?)` / `propertiesPlugin(options?)` — plugin factory
103 + - `resolvePropertiesOptions(options?)` — default resolution
104 + - `renderPropertiesPanel(frontmatter, options?, context?)` — pure renderer
105 + - `buildTagHref(tag)` — tag route builder
106 + - Types: `PropertiesOptions`, `ResolvedPropertiesOptions`, `PropertiesRenderContext`, `PropertiesLinkResolver`, `PropertiesMessage`
107 +
108 + ## See also
109 +
110 + - [Plugin guide](../reference/plugin-api.en.md)
111 +