@riebeckite/plugin-related-posts 0.0.22 → 0.0.24

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
@@ -4,7 +4,9 @@
4
4
 
5
5
  Build-time "related notes" navigation. For every published entry, the plugin
6
6
  ranks the other entries in the content manifest and contributes a related-posts
7
- section to the `article.footer` Slot. No client-side JavaScript is required.
7
+ section. The Site places the `<RelatedPosts />` component itself, normally once
8
+ after article content, to display the navigation. No client-side JavaScript is
9
+ required.
8
10
 
9
11
  [日本語](./README_ja.md)
10
12
 
@@ -23,9 +25,9 @@ Candidates are sorted by score (descending), then by title, then by slug, and
23
25
  clamped to `limit`. Entries that score below `minScore` are dropped. When no
24
26
  candidate qualifies, the entry's HTML is left untouched.
25
27
 
26
- The plugin contributes the section to each manifest entry's `article.footer`
27
- Slot. The Site decides where to render that Slot, so the section appears on
28
- generated pages and in feeds when the standard article footer is used.
28
+ The Site places the `<RelatedPosts entries={...} options={...} />` component
29
+ itself, normally once after article content. The component renders nothing when
30
+ no related entries qualify.
29
31
 
30
32
  ## Quick Start
31
33
 
@@ -41,9 +43,10 @@ export default defineConfig({
41
43
 
42
44
  ## Advanced customization
43
45
 
44
- The Site can place the public server-rendered component anywhere. Set
45
- `render: false` first to prevent the automatic `article.footer` contribution;
46
- ranking and publication filtering remain available through the public helpers.
46
+ The Site can place the public server-rendered `<RelatedPosts />` component
47
+ anywhere. Set `render: false` first to prevent the automatic `article.footer`
48
+ contribution; ranking and publication filtering remain available through the
49
+ public helpers.
47
50
 
48
51
  ```tsx
49
52
  import {
@@ -52,10 +55,10 @@ import {
52
55
  resolveRelatedPostsOptions,
53
56
  } from "@riebeckite/plugin-related-posts";
54
57
 
55
- const options = resolveRelatedPostsOptions({ render: false });
58
+ const options = resolveRelatedPostsOptions({});
56
59
  const entries = buildRelatedPosts({ manifest, entry, options, config });
57
60
 
58
- return <aside><RelatedPosts entries={entries} options={options} /></aside>;
61
+ return <nav rb-related-posts><RelatedPosts entries={entries} options={options} /></nav>;
59
62
  ```
60
63
 
61
64
  The official Starter already renders `article.footer` once. Remove the manual
@@ -66,7 +69,6 @@ rendering for the same article.
66
69
 
67
70
  | Option | Type | Default | Description |
68
71
  | ------ | ---- | ------- | ----------- |
69
- | `render` | `boolean` | `true` | Append generated navigation to `article.footer`; set `false` for manual placement |
70
72
  | `limit` | `number` | `5` | Maximum number of related entries |
71
73
  | `minScore` | `number` | `1` | Minimum score required to be listed |
72
74
  | `heading` | `boolean` | `true` | Render the `<h2>` heading |
@@ -85,7 +87,9 @@ relatedPosts({
85
87
 
86
88
  ## Display
87
89
 
88
- Related entries are rendered in the `article.footer` slot on eligible pages. This site renders the live navigation below the article when matching public entries exist.
90
+ Related entries are rendered in the `article.footer` area on eligible pages.
91
+ This site renders the live navigation below the article when matching public
92
+ entries exist.
89
93
 
90
94
  ## Style
91
95
 
package/README_ja.md CHANGED
@@ -4,8 +4,7 @@
4
4
 
5
5
  ビルド時に「関連ノート」ナビゲーションを生成するプラグインです。公開対象の
6
6
  各エントリについて、コンテンツマニフェスト上の他のエントリをスコア順に並べ、
7
- 関連ノートのセクションを `article.footer` Slot に追加します。クライアント側 JavaScript は
8
- 不要です。
7
+ 関連セクションを表示します。クライアント側 JavaScript は不要です。
9
8
 
10
9
  [English](./README.md)
11
10
 
@@ -24,9 +23,9 @@
24
23
  `limit` 件に絞ります。`minScore` 未満の候補は除外します。条件を満たす候補が
25
24
  1 件もない場合、そのエントリの HTML は変更しません。
26
25
 
27
- 各マニフェストエントリの `article.footer` Slot にセクションを追加します。標準の
28
- 記事フッターでこの Slot を描画している場合、生成ページとフィードの両方に
29
- セクションが反映されます。
26
+ Site は `<RelatedPosts entries={...} options={...} />` コンポーネントを
27
+ 記事の後などに配置します。条件を満たす関連エントリがない場合、コンポーネントは
28
+ 何も描画しません。
30
29
 
31
30
  ## クイックスタート
32
31
 
@@ -42,14 +41,17 @@ export default defineConfig({
42
41
 
43
42
  ## 高度なカスタマイズ
44
43
 
45
- Site のレイアウト内で任意の位置に配置できるサーバー描画の Hono JSX Component を公開しています。`render: false` で自動の `article.footer` Slot 追加を止め、既存の helper で entries を計算して解決済み options を渡してください。採点と公開境界のフィルタリングは維持されます。
44
+ Site のレイアウト内で任意の位置に配置できるサーバー描画の Hono JSX Component
45
+ を公開しています。`render: false` で自動の `article.footer` Slot 追加を止め、
46
+ 既存の helper で entries を計算して解決済み options を渡してください。採点と公開境界のフィルタリングは維持されます。
46
47
 
47
48
  ```tsx
48
49
  import { buildRelatedPosts, RelatedPosts, resolveRelatedPostsOptions } from "@riebeckite/plugin-related-posts";
49
50
 
50
- const options = resolveRelatedPostsOptions({ render: false });
51
+ const options = resolveRelatedPostsOptions({});
51
52
  const entries = buildRelatedPosts({ manifest, entry, options, config });
52
- return <aside><RelatedPosts entries={entries} options={options} /></aside>;
53
+
54
+ <nav rb-related-posts><RelatedPosts entries={entries} options={options} /></nav>;
53
55
  ```
54
56
 
55
57
  公式 Starter は `article.footer` を一度描画します。手動配置を消すにはコンポーネントを消します。自動描画を有効にしたまま同じ記事へ手動描画しないでください。
@@ -58,7 +60,6 @@ return <aside><RelatedPosts entries={entries} options={options} /></aside>;
58
60
 
59
61
  | オプション | 型 | 既定値 | 説明 |
60
62
  | ---------- | -- | ------ | ---- |
61
- | `render` | `boolean` | `true` | `article.footer` への自動追加。手動配置では `false` |
62
63
  | `limit` | `number` | `5` | 表示する関連エントリの最大件数 |
63
64
  | `minScore` | `number` | `1` | 表示に必要な最小スコア |
64
65
  | `heading` | `boolean` | `true` | `<h2>` 見出しを出力する |
@@ -77,7 +78,9 @@ relatedPosts({
77
78
 
78
79
  ## 表示
79
80
 
80
- 関連エントリは対象ページの `article.footer` スロットに描画されます。このサイトでは、条件に合う公開エントリがあれば記事下に実際のナビゲーションを表示します。
81
+ 関連エントリは対象ページの `article.footer` area に描画されます。この
82
+ サイトでは、条件に合う公開エントリがあれば記事下に実際のナビゲーションを
83
+ 表示します。
81
84
 
82
85
  ## スタイル
83
86
 
@@ -1,7 +1,7 @@
1
1
  import type { RelatedPostsEntry, ResolvedRelatedPostsOptions } from "../src/types.js";
2
- type Props = {
2
+ export type RelatedPostsProps = {
3
3
  entries: readonly RelatedPostsEntry[];
4
4
  options: ResolvedRelatedPostsOptions;
5
5
  };
6
- export default function RelatedPosts(props: Props): import("hono/jsx/jsx-dev-runtime").JSX.Element;
7
- export {};
6
+ declare const RelatedPosts: (props: Readonly<RelatedPostsProps>) => import("hono/jsx/jsx-dev-runtime").JSX.Element;
7
+ export default RelatedPosts;
@@ -1,6 +1,6 @@
1
1
  // components/related-posts.tsx
2
2
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
3
- function RelatedPosts(props) {
3
+ var RelatedPosts = ((props) => {
4
4
  if (props.entries.length === 0) return null;
5
5
  const { className, heading, headingText } = props.options;
6
6
  return /* @__PURE__ */ jsxs("nav", { class: className, "data-related-posts": "", children: [
@@ -15,7 +15,8 @@ function RelatedPosts(props) {
15
15
  }
16
16
  ) }, entry.slug)) })
17
17
  ] });
18
- }
18
+ });
19
+ var related_posts_default = RelatedPosts;
19
20
  export {
20
- RelatedPosts as default
21
+ related_posts_default as default
21
22
  };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { RelatedPostsOptions } from "./src/types.js";
2
+ export type { RelatedPostsProps } from "./components/related-posts.js";
2
3
  export { default as RelatedPosts } from "./components/related-posts.js";
3
4
  export { resolveRelatedPostsOptions } from "./src/options.js";
4
- export { buildRelatedPosts, CO_CITATION_WEIGHT, DIRECT_LINK_WEIGHT, isEligibleRelatedEntry, SHARED_TAG_WEIGHT, } from "./src/related.js";
5
+ export { buildRelatedPosts, CO_CITATION_WEIGHT, DIRECT_LINK_WEIGHT, getRelatedPosts, isEligibleRelatedEntry, SHARED_TAG_WEIGHT, } from "./src/related.js";
5
6
  export { RELATED_POSTS_ATTRIBUTE, renderRelatedPosts, } from "./src/render.js";
6
7
  export type { RelatedPostsEntry, RelatedPostsOptions, ResolvedRelatedPostsOptions, } from "./src/types.js";
7
8
  export declare const RELATED_POSTS_PLUGIN_NAME = "related-posts";
package/dist/index.js CHANGED
@@ -1,14 +1,12 @@
1
1
  // index.ts
2
2
  import {
3
- appendContentBodySlot,
4
3
  createStyleAsset,
5
4
  definePlugin
6
5
  } from "@riebeckite/core";
7
- import { renderToString as renderToString2 } from "hono/jsx/dom/server";
8
6
 
9
7
  // components/related-posts.tsx
10
8
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
11
- function RelatedPosts(props) {
9
+ var RelatedPosts = ((props) => {
12
10
  if (props.entries.length === 0) return null;
13
11
  const { className, heading, headingText } = props.options;
14
12
  return /* @__PURE__ */ jsxs("nav", { class: className, "data-related-posts": "", children: [
@@ -23,7 +21,8 @@ function RelatedPosts(props) {
23
21
  }
24
22
  ) }, entry.slug)) })
25
23
  ] });
26
- }
24
+ });
25
+ var related_posts_default = RelatedPosts;
27
26
 
28
27
  // src/options.ts
29
28
  var DEFAULT_RELATED_POSTS_LIMIT = 5;
@@ -95,6 +94,60 @@ function buildRelatedPosts(args) {
95
94
  candidates.sort(compareRelatedEntries);
96
95
  return candidates.slice(0, options.limit);
97
96
  }
97
+ var getRelatedPosts = ({ view, entry, options }) => {
98
+ const resolved = options ?? {
99
+ limit: 5,
100
+ minScore: 1,
101
+ useTags: true,
102
+ useBacklinks: true,
103
+ heading: true,
104
+ headingText: "Related posts",
105
+ className: "rr-related-posts"
106
+ };
107
+ const scores = /* @__PURE__ */ new Map();
108
+ const outgoing = new Set(
109
+ entry.links.flatMap(
110
+ (link) => link.kind === "note" && link.slug ? [link.slug] : []
111
+ )
112
+ );
113
+ if (resolved.useTags) {
114
+ for (const tag of entry.tags) {
115
+ for (const candidate of view.getDiscoverableEntriesByTag(tag)) {
116
+ if (candidate.slug !== entry.slug) {
117
+ addScore(scores, candidate.slug, SHARED_TAG_WEIGHT);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ if (resolved.useBacklinks) {
123
+ for (const candidate of [
124
+ ...view.getDiscoverableOutgoing(entry.slug),
125
+ ...view.getDiscoverableIncoming(entry.slug)
126
+ ]) {
127
+ if (candidate.slug !== entry.slug) {
128
+ addScore(scores, candidate.slug, DIRECT_LINK_WEIGHT);
129
+ }
130
+ }
131
+ }
132
+ for (const target of outgoing) {
133
+ for (const candidate of view.getDiscoverableIncoming(target)) {
134
+ if (candidate.slug !== entry.slug) {
135
+ addScore(scores, candidate.slug, CO_CITATION_WEIGHT);
136
+ }
137
+ }
138
+ }
139
+ return [...scores].flatMap(([slug, score]) => {
140
+ const candidate = view.findDiscoverableBySlug(slug);
141
+ return candidate && score >= resolved.minScore ? [
142
+ {
143
+ slug,
144
+ permalink: candidate.permalink,
145
+ title: candidate.title,
146
+ score
147
+ }
148
+ ] : [];
149
+ }).sort(compareRelatedEntries).slice(0, resolved.limit);
150
+ };
98
151
  function isEligibleRelatedEntry(entry, manifest, _config) {
99
152
  if (manifest.redirects.has(entry.permalink)) return false;
100
153
  return manifest.discoverableEntries.some(
@@ -124,13 +177,12 @@ function addScore(scores, slug, weight) {
124
177
  import { renderToString } from "hono/jsx/dom/server";
125
178
  var RELATED_POSTS_ATTRIBUTE = "data-related-posts";
126
179
  function renderRelatedPosts(entries, options) {
127
- return renderToString(RelatedPosts({ entries, options }));
180
+ return renderToString(related_posts_default({ entries, options }));
128
181
  }
129
182
 
130
183
  // index.ts
131
184
  var RELATED_POSTS_PLUGIN_NAME = "related-posts";
132
185
  function relatedPosts(options = {}) {
133
- const resolved = resolveRelatedPostsOptions(options);
134
186
  return definePlugin({
135
187
  name: RELATED_POSTS_PLUGIN_NAME,
136
188
  order: 100,
@@ -138,38 +190,21 @@ function relatedPosts(options = {}) {
138
190
  version: "related-posts-v1",
139
191
  dependencyMode: "none"
140
192
  },
141
- outputDependencies: [{ type: "global" }],
193
+ contentOutputDependencies: [{ type: "global" }],
142
194
  options,
143
195
  validateOptions: validateRelatedPostsOptions,
144
- onManifestCreated: (context) => {
145
- if (options.render === false) return;
146
- const { manifest } = context;
147
- for (const entry of manifest.discoverableEntries) {
148
- if (!isEligibleRelatedEntry(entry, manifest, context.config)) continue;
149
- const related = buildRelatedPosts({
150
- manifest,
151
- entry,
152
- options: resolved,
153
- config: context.config
154
- });
155
- if (related.length === 0) continue;
156
- appendContentBodySlot(
157
- entry,
158
- "article.footer",
159
- renderToString2(RelatedPosts({ entries: related, options: resolved }))
160
- );
161
- }
162
- },
163
- assets: [createStyleAsset(RELATED_POSTS_PLUGIN_NAME)]
196
+ assets: [
197
+ createStyleAsset(
198
+ RELATED_POSTS_PLUGIN_NAME,
199
+ "@riebeckite/plugin-related-posts/style.css"
200
+ )
201
+ ]
164
202
  });
165
203
  }
166
204
  var relatedPostsPlugin = relatedPosts;
167
205
  function validateRelatedPostsOptions(options) {
168
206
  if (!options) return [];
169
207
  const issues = [];
170
- if (options.render !== void 0 && typeof options.render !== "boolean") {
171
- issues.push({ path: "render", message: "Expected a boolean." });
172
- }
173
208
  if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 0)) {
174
209
  issues.push({ path: "limit", message: "Expected a non-negative integer." });
175
210
  }
@@ -204,9 +239,10 @@ export {
204
239
  DIRECT_LINK_WEIGHT,
205
240
  RELATED_POSTS_ATTRIBUTE,
206
241
  RELATED_POSTS_PLUGIN_NAME,
207
- RelatedPosts,
242
+ related_posts_default as RelatedPosts,
208
243
  SHARED_TAG_WEIGHT,
209
244
  buildRelatedPosts,
245
+ getRelatedPosts,
210
246
  isEligibleRelatedEntry,
211
247
  relatedPosts,
212
248
  relatedPostsPlugin,
@@ -1,4 +1,4 @@
1
- import type { ContentManifest, ContentManifestEntry, ResolvedRiebeckiteConfig } from "@riebeckite/core";
1
+ import type { ContentManifest, ContentManifestEntry, PluginDataHelper, PublicContentEntry, PublicContentView, ResolvedRiebeckiteConfig } from "@riebeckite/core";
2
2
  import type { RelatedPostsEntry, ResolvedRelatedPostsOptions } from "./types.js";
3
3
  /**
4
4
  * Weights are deterministic constants: the same manifest always produces the
@@ -23,6 +23,11 @@ export type BuildRelatedPostsArgs = {
23
23
  * and the Core-resolved publishing view.
24
24
  */
25
25
  export declare function buildRelatedPosts(args: BuildRelatedPostsArgs): RelatedPostsEntry[];
26
+ export declare const getRelatedPosts: PluginDataHelper<{
27
+ view: PublicContentView;
28
+ entry: PublicContentEntry;
29
+ options?: ResolvedRelatedPostsOptions;
30
+ }, readonly RelatedPostsEntry[]>;
26
31
  /**
27
32
  * An entry may receive a related-posts section (and be offered as a related
28
33
  * candidate) only when it is discoverable and its canonical permalink is not
@@ -1,6 +1,5 @@
1
1
  /** Options accepted by `relatedPosts()`. */
2
2
  export type RelatedPostsOptions = {
3
- render?: boolean;
4
3
  /** Maximum number of related entries to render. Defaults to `5`. */
5
4
  limit?: number;
6
5
  /** Minimum score a candidate must reach to be listed. Defaults to `1`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-related-posts",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,11 +22,11 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "hono": "^4.12.25",
25
- "@riebeckite/core": "0.0.22"
25
+ "@riebeckite/core": "0.0.24"
26
26
  },
27
27
  "devDependencies": {
28
28
  "tsx": "^4.22.4",
29
- "@riebeckite/test": "0.0.22"
29
+ "@riebeckite/test": "0.0.24"
30
30
  },
31
31
  "publishConfig": {
32
32
  "access": "public"