@riebeckite/plugin-related-posts 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @riebeckite/plugin-related-posts
2
2
 
3
3
  Build-time "related notes" navigation. For every published entry, the plugin
4
- ranks the other entries in the content manifest and appends a related-posts
5
- section to the rendered HTML. No client-side JavaScript is required.
4
+ ranks the other entries in the content manifest and contributes a related-posts
5
+ section to the `article.footer` Slot. No client-side JavaScript is required.
6
6
 
7
7
  [日本語](./README_ja.md)
8
8
 
@@ -21,9 +21,9 @@ Candidates are sorted by score (descending), then by title, then by slug, and
21
21
  clamped to `limit`. Entries that score below `minScore` are dropped. When no
22
22
  candidate qualifies, the entry's HTML is left untouched.
23
23
 
24
- The plugin appends the section to the manifest entry's HTML. Core synchronizes
25
- that HTML with the content the route renders, so the section appears on generated
26
- pages and in feeds.
24
+ The plugin contributes the section to each manifest entry's `article.footer`
25
+ Slot. The Site decides where to render that Slot, so the section appears on
26
+ generated pages and in feeds when the standard article footer is used.
27
27
 
28
28
  ## Usage
29
29
 
@@ -37,6 +37,22 @@ export default defineConfig({
37
37
  });
38
38
  ```
39
39
 
40
+ ## Component
41
+
42
+ In addition to the automatic `article.footer` Slot contribution, the navigation
43
+ is available as a server-rendered Hono JSX Component for placement in a Site
44
+ layout. Build the entries with the existing helper and pass resolved options:
45
+
46
+ ```tsx
47
+ import RelatedPosts from "@riebeckite/plugin-related-posts/components";
48
+
49
+ <RelatedPosts entries={related} options={resolvedOptions} />;
50
+ ```
51
+
52
+ `related` is the result of `buildRelatedPosts()` and `resolvedOptions` is the
53
+ result of `resolveRelatedPostsOptions()`. Import `style.css` when the Plugin is
54
+ not registered.
55
+
40
56
  ## Options
41
57
 
42
58
  | Option | Type | Default | Description |
@@ -85,6 +101,7 @@ import "@riebeckite/plugin-related-posts/style.css";
85
101
  - `resolveRelatedPostsOptions(options?)` — apply option defaults
86
102
  - `buildRelatedPosts({ manifest, entry, options, config? })` — rank related entries
87
103
  - `renderRelatedPosts(entries, options)` — render the navigation HTML
104
+ - `RelatedPosts` and `@riebeckite/plugin-related-posts/components` — Hono JSX Component
88
105
  - Types: `RelatedPostsOptions`, `ResolvedRelatedPostsOptions`, `RelatedPostsEntry`
89
106
 
90
107
  ## Limitations
@@ -96,4 +113,3 @@ import "@riebeckite/plugin-related-posts/style.css";
96
113
  ## See also
97
114
 
98
115
  - [Plugin guide](../../../docs/en/docs/reference/plugin-api.md)
99
-
package/README_ja.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ビルド時に「関連ノート」ナビゲーションを生成するプラグインです。公開対象の
4
4
  各エントリについて、コンテンツマニフェスト上の他のエントリをスコア順に並べ、
5
- 関連ノートのセクションを HTML に追記します。クライアント側 JavaScript は
5
+ 関連ノートのセクションを `article.footer` Slot に追加します。クライアント側 JavaScript は
6
6
  不要です。
7
7
 
8
8
  [English](./README.md)
@@ -22,8 +22,8 @@
22
22
  `limit` 件に絞ります。`minScore` 未満の候補は除外します。条件を満たす候補が
23
23
  1 件もない場合、そのエントリの HTML は変更しません。
24
24
 
25
- マニフェストエントリの HTML にセクションを追記します。Core がその HTML を
26
- コンテンツルートの描画 HTML と同期するため、生成ページとフィードの両方に
25
+ 各マニフェストエントリの `article.footer` Slot にセクションを追加します。標準の
26
+ 記事フッターでこの Slot を描画している場合、生成ページとフィードの両方に
27
27
  セクションが反映されます。
28
28
 
29
29
  ## 使い方
@@ -38,6 +38,22 @@ export default defineConfig({
38
38
  });
39
39
  ```
40
40
 
41
+ ## Component として使う
42
+
43
+ 自動の `article.footer` Slot への追加に加え、Site のレイアウト内で任意の位置に
44
+ 配置できる、サーバー描画の Hono JSX Component も公開しています。既存の helper
45
+ で entries を計算し、解決済みの options を渡してください。
46
+
47
+ ```tsx
48
+ import RelatedPosts from "@riebeckite/plugin-related-posts/components";
49
+
50
+ <RelatedPosts entries={related} options={resolvedOptions} />;
51
+ ```
52
+
53
+ `related` には `buildRelatedPosts()` の戻り値を、`resolvedOptions` には
54
+ `resolveRelatedPostsOptions()` の戻り値を渡します。Plugin を登録しない場合は
55
+ `style.css` も読み込んでください。
56
+
41
57
  ## オプション
42
58
 
43
59
  | オプション | 型 | 既定値 | 説明 |
@@ -86,6 +102,7 @@ import "@riebeckite/plugin-related-posts/style.css";
86
102
  - `resolveRelatedPostsOptions(options?)` — オプションの既定値を適用する
87
103
  - `buildRelatedPosts({ manifest, entry, options, config? })` — 関連エントリを採点・整列する
88
104
  - `renderRelatedPosts(entries, options)` — ナビゲーション HTML を生成する
105
+ - `RelatedPosts` と `@riebeckite/plugin-related-posts/components` — Hono JSX Component
89
106
  - 型: `RelatedPostsOptions`, `ResolvedRelatedPostsOptions`, `RelatedPostsEntry`
90
107
 
91
108
  ## 制約
@@ -94,7 +111,6 @@ import "@riebeckite/plugin-related-posts/style.css";
94
111
  - 判定材料はタグ・直接リンク・共引用のみです。読了時間や新しさ、フォルダは
95
112
  順位を決定的に保つため意図的に使いません。
96
113
 
97
- ## ????
98
-
99
- - [?????????](../../../docs/ja/docs/reference/plugin-api.md)
114
+ ## 関連リンク
100
115
 
116
+ - [プラグイン API](../../../docs/ja/docs/reference/plugin-api.md)
@@ -0,0 +1,7 @@
1
+ import type { RelatedPostsEntry, ResolvedRelatedPostsOptions } from "../src/types.js";
2
+ type Props = {
3
+ entries: readonly RelatedPostsEntry[];
4
+ options: ResolvedRelatedPostsOptions;
5
+ };
6
+ export default function RelatedPosts(props: Props): import("hono/jsx/jsx-dev-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,21 @@
1
+ // components/related-posts.tsx
2
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
3
+ function RelatedPosts(props) {
4
+ if (props.entries.length === 0) return null;
5
+ const { className, heading, headingText } = props.options;
6
+ return /* @__PURE__ */ jsxs("nav", { class: className, "data-related-posts": "", children: [
7
+ heading ? /* @__PURE__ */ jsx("h2", { class: `${className}__heading`, children: headingText }) : null,
8
+ /* @__PURE__ */ jsx("ul", { children: props.entries.map((entry) => /* @__PURE__ */ jsx("li", { class: `${className}__item`, children: /* @__PURE__ */ jsx(
9
+ "a",
10
+ {
11
+ class: `${className}__link`,
12
+ href: entry.permalink,
13
+ "data-related-score": entry.score,
14
+ children: entry.title
15
+ }
16
+ ) }, entry.slug)) })
17
+ ] });
18
+ }
19
+ export {
20
+ RelatedPosts as default
21
+ };
package/dist/index.d.ts CHANGED
@@ -1,16 +1,10 @@
1
1
  import type { RelatedPostsOptions } from "./src/types.js";
2
+ export { default as RelatedPosts } from "./components/related-posts.js";
2
3
  export { resolveRelatedPostsOptions } from "./src/options.js";
3
4
  export { buildRelatedPosts, CO_CITATION_WEIGHT, DIRECT_LINK_WEIGHT, isEligibleRelatedEntry, SHARED_TAG_WEIGHT, } from "./src/related.js";
4
5
  export { RELATED_POSTS_ATTRIBUTE, renderRelatedPosts, } from "./src/render.js";
5
6
  export type { RelatedPostsEntry, RelatedPostsOptions, ResolvedRelatedPostsOptions, } from "./src/types.js";
6
7
  export declare const RELATED_POSTS_PLUGIN_NAME = "related-posts";
7
- /**
8
- * Build-time "related notes" navigation.
9
- *
10
- * For every published entry it ranks the other entries in the manifest, then
11
- * appends a `<nav>` section to the manifest entry HTML, which is the final
12
- * rendering source. No client runtime is required.
13
- */
14
8
  export declare function relatedPosts(options?: RelatedPostsOptions): import("@riebeckite/core").RiebeckitePlugin<RelatedPostsOptions>;
15
9
  /** Alias matching the `*Plugin` suffix used by other plugin factories. */
16
10
  export declare const relatedPostsPlugin: typeof relatedPosts;
package/dist/index.js CHANGED
@@ -1,8 +1,29 @@
1
1
  // index.ts
2
2
  import {
3
+ appendContentBodySlot,
3
4
  createStyleAsset,
4
5
  definePlugin
5
6
  } from "@riebeckite/core";
7
+ import { renderToString as renderToString2 } from "hono/jsx/dom/server";
8
+
9
+ // components/related-posts.tsx
10
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
+ function RelatedPosts(props) {
12
+ if (props.entries.length === 0) return null;
13
+ const { className, heading, headingText } = props.options;
14
+ return /* @__PURE__ */ jsxs("nav", { class: className, "data-related-posts": "", children: [
15
+ heading ? /* @__PURE__ */ jsx("h2", { class: `${className}__heading`, children: headingText }) : null,
16
+ /* @__PURE__ */ jsx("ul", { children: props.entries.map((entry) => /* @__PURE__ */ jsx("li", { class: `${className}__item`, children: /* @__PURE__ */ jsx(
17
+ "a",
18
+ {
19
+ class: `${className}__link`,
20
+ href: entry.permalink,
21
+ "data-related-score": entry.score,
22
+ children: entry.title
23
+ }
24
+ ) }, entry.slug)) })
25
+ ] });
26
+ }
6
27
 
7
28
  // src/options.ts
8
29
  var DEFAULT_RELATED_POSTS_LIMIT = 5;
@@ -100,18 +121,10 @@ function addScore(scores, slug, weight) {
100
121
  }
101
122
 
102
123
  // src/render.ts
103
- import { escapeHtml, escapeHtmlAttribute } from "@riebeckite/core";
124
+ import { renderToString } from "hono/jsx/dom/server";
104
125
  var RELATED_POSTS_ATTRIBUTE = "data-related-posts";
105
126
  function renderRelatedPosts(entries, options) {
106
- if (entries.length === 0) return "";
107
- const className = escapeHtmlAttribute(options.className);
108
- const heading = options.heading ? `<h2 class="${className}__heading">${escapeHtml(options.headingText)}</h2>` : "";
109
- const items = entries.map(
110
- (entry) => `<li class="${className}__item"><a class="${className}__link" href="${escapeHtmlAttribute(
111
- entry.permalink
112
- )}" data-related-score="${entry.score}">${escapeHtml(entry.title)}</a></li>`
113
- ).join("");
114
- return `<nav class="${className}" ${RELATED_POSTS_ATTRIBUTE}>${heading}<ul>${items}</ul></nav>`;
127
+ return renderToString(RelatedPosts({ entries, options }));
115
128
  }
116
129
 
117
130
  // index.ts
@@ -120,6 +133,7 @@ function relatedPosts(options = {}) {
120
133
  const resolved = resolveRelatedPostsOptions(options);
121
134
  return definePlugin({
122
135
  name: RELATED_POSTS_PLUGIN_NAME,
136
+ order: 100,
123
137
  processedContentCache: {
124
138
  version: "related-posts-v1",
125
139
  dependencyMode: "none"
@@ -130,7 +144,6 @@ function relatedPosts(options = {}) {
130
144
  onManifestCreated: (context) => {
131
145
  const { manifest } = context;
132
146
  for (const entry of manifest.discoverableEntries) {
133
- if (entry.html.includes(RELATED_POSTS_ATTRIBUTE)) continue;
134
147
  if (!isEligibleRelatedEntry(entry, manifest, context.config)) continue;
135
148
  const related = buildRelatedPosts({
136
149
  manifest,
@@ -139,7 +152,11 @@ function relatedPosts(options = {}) {
139
152
  config: context.config
140
153
  });
141
154
  if (related.length === 0) continue;
142
- entry.html = `${entry.html}${renderRelatedPosts(related, resolved)}`;
155
+ appendContentBodySlot(
156
+ entry,
157
+ "article.footer",
158
+ renderToString2(RelatedPosts({ entries: related, options: resolved }))
159
+ );
143
160
  }
144
161
  },
145
162
  assets: [createStyleAsset(RELATED_POSTS_PLUGIN_NAME)]
@@ -183,6 +200,7 @@ export {
183
200
  DIRECT_LINK_WEIGHT,
184
201
  RELATED_POSTS_ATTRIBUTE,
185
202
  RELATED_POSTS_PLUGIN_NAME,
203
+ RelatedPosts,
186
204
  SHARED_TAG_WEIGHT,
187
205
  buildRelatedPosts,
188
206
  isEligibleRelatedEntry,
@@ -1,8 +1,4 @@
1
1
  import type { RelatedPostsEntry, ResolvedRelatedPostsOptions } from "./types.js";
2
2
  /** Boolean attribute that marks the generated navigation for tests and styling. */
3
3
  export declare const RELATED_POSTS_ATTRIBUTE = "data-related-posts";
4
- /**
5
- * Renders the related-posts navigation. Returns an empty string when there is
6
- * nothing to show, so callers can leave an entry's HTML untouched.
7
- */
8
4
  export declare function renderRelatedPosts(entries: readonly RelatedPostsEntry[], options: ResolvedRelatedPostsOptions): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-related-posts",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,14 +12,21 @@
12
12
  "source": "./index.ts",
13
13
  "default": "./dist/index.js"
14
14
  },
15
+ "./components": {
16
+ "types": "./dist/components/related-posts.d.ts",
17
+ "import": "./dist/components/related-posts.js",
18
+ "source": "./components/related-posts.tsx",
19
+ "default": "./dist/components/related-posts.js"
20
+ },
15
21
  "./style.css": "./style.css"
16
22
  },
17
23
  "dependencies": {
18
- "@riebeckite/core": "0.0.15"
24
+ "hono": "^4.12.25",
25
+ "@riebeckite/core": "0.0.17"
19
26
  },
20
27
  "devDependencies": {
21
28
  "tsx": "^4.22.4",
22
- "@riebeckite/test": "0.0.15"
29
+ "@riebeckite/test": "0.0.17"
23
30
  },
24
31
  "publishConfig": {
25
32
  "access": "public"
package/style.css CHANGED
@@ -7,7 +7,7 @@
7
7
  transparent
8
8
  );
9
9
 
10
- margin-top: 3rem;
10
+ margin-block-start: var(--rb-space-6, 3rem);
11
11
  padding-top: 1.5rem;
12
12
  border-top: 1px solid var(--rb-related-posts-rule);
13
13
  color: var(--rb-related-posts-ink);
@@ -44,8 +44,3 @@
44
44
  .rb-related-posts__link:focus-visible {
45
45
  text-decoration-color: var(--rb-related-posts-muted);
46
46
  }
47
-
48
- html[data-theme="dark"] .rb-related-posts,
49
- html.dark .rb-related-posts {
50
- --rb-related-posts-ink: var(--rb-color-paper, #ffffff);
51
- }