@riebeckite/plugin-webmention 0.0.21 → 0.0.23

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,5 +1,7 @@
1
1
  # @riebeckite/plugin-webmention
2
2
 
3
+ <!-- Generated from docs/docs/plugins/webmention.md. Edit the canonical documentation in docs/docs/plugins and run `pnpm docs:sync`. -->
4
+
3
5
  Receive Webmentions, verify that the source document really links to the
4
6
  target, store them through a pluggable provider, and render verified mentions
5
7
  near articles. The core plugin contains **no Cloudflare, Worker, database, or
@@ -40,7 +42,7 @@ export default {
40
42
 
41
43
  Without a provider the plugin uses an in-memory provider, which is fine for
42
44
  local previews but loses mentions between processes. Supply a durable adapter
43
- (see [`@riebeckite/webmention-cloudflare`](../../integrations/webmention-cloudflare/README.md))
45
+ (see [`@riebeckite/webmention-cloudflare`](https://github.com/Rerurate514/riebeckite/blob/main/packages/integrations/webmention-cloudflare/README.md))
44
46
  for production.
45
47
 
46
48
  ## Endpoints
@@ -56,24 +58,26 @@ Rejection codes: `invalid_request`, `missing_source_or_target`,
56
58
 
57
59
  ## Rendering
58
60
 
59
- At `onManifestCreated` the plugin queries the provider once, groups verified
60
- mentions by target, and appends a section to each matching entry's HTML. Core
61
- synchronizes that HTML with the content the route renders, using stable hooks:
62
-
63
- ```html
64
- <section class="rr-webmention" data-webmention data-webmention-count="2">
65
- <h2 class="rr-webmention__heading">Mentions</h2>
66
- <ul class="rr-webmention__list">
67
- <li class="rr-webmention__item rr-webmention__item--like"
68
- data-webmention-type="like">…</li>
69
- </ul>
70
- </section>
61
+ The Site owns JSX placement. Keep the Provider instance in Site composition,
62
+ pass that same instance to `webmention()`, then obtain typed component props
63
+ with `getWebmentionSectionData`. The helper accepts a `PublicContentView`, so
64
+ draft and scheduled entries cannot be rendered.
65
+
66
+ ```tsx
67
+ const mentions = await getWebmentionSectionData({
68
+ view,
69
+ slug,
70
+ config,
71
+ provider: webmentionProvider,
72
+ });
73
+
74
+ <WebmentionSection {...mentions} />
71
75
  ```
72
76
 
73
- Set `render: false` to skip build-time injection. Applications that render at
74
- request time can call `getWebmentionsForEntry({ manifest, config, provider,
75
- slug })` and `renderWebmentionSection(mentions, options)` directly, or consume
76
- the JSON feed.
77
+ An empty provider returns an empty model and `WebmentionSection` renders
78
+ nothing. The plugin does not inject build-time manifest HTML. Applications can
79
+ also call `getWebmentionsForEntry({ manifest, config, provider, slug })` or
80
+ consume the JSON feed where a lower-level query is appropriate.
77
81
 
78
82
  ## Options
79
83
 
@@ -81,7 +85,6 @@ the JSON feed.
81
85
  | ------ | ------- | ----------- |
82
86
  | `provider` | in-memory | Storage adapter implementing `WebmentionProvider`. |
83
87
  | `endpoint` | `"/webmentions"` | Receive (POST) and feed (GET) path. |
84
- | `render` | `true` | Append stored mentions to matching entries at manifest time. |
85
88
  | `headingText` | `"Mentions"` | Heading for the rendered section. |
86
89
  | `limit` | `20` | Maximum mentions rendered per article. |
87
90
  | `className` | `"rr-webmention"` | Root CSS class of the rendered section. |
@@ -109,12 +112,11 @@ manifest time, mentions whose target is not a published entry are reported as
109
112
  - `MemoryWebmentionProvider`, provider/capability types and errors
110
113
  - `parseWebmentionSource`, `findTargetLink`, `verifyWebmention`,
111
114
  `createWebmentionSourceFetcher`
112
- - `renderWebmentionSection`, `getWebmentionsForEntry`,
115
+ - `WebmentionSection`, `getWebmentionSectionData`, `getWebmentionsForEntry`,
113
116
  `groupMentionsBySlug`, `buildFeed`
114
117
  - `resolveWebmentionOptions`, `validateWebmentionOptions`
115
118
 
116
119
  ## See also
117
120
 
118
- - [Plugin guide](../../../docs/docs/reference/plugin-api.md)
119
- - [@riebeckite/webmention-cloudflare](../../integrations/webmention-cloudflare/README.md)
120
-
121
+ - [Plugin guide](https://github.com/Rerurate514/riebeckite/blob/main/docs/docs/reference/plugin-api.md)
122
+ - [@riebeckite/webmention-cloudflare](https://github.com/Rerurate514/riebeckite/blob/main/packages/integrations/webmention-cloudflare/README.md)
package/README_ja.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @riebeckite/plugin-webmention
2
2
 
3
+ <!-- Generated from docs/docs/plugins/webmention.ja.md. Edit the canonical documentation in docs/docs/plugins and run `pnpm docs:sync`. -->
4
+
3
5
  Webmention を受信し、送信元ドキュメントが本当にターゲットへリンクしているかを検証し、
4
6
  プラガブルな provider 経由で保存して、検証済みのメンションを記事の近くに表示します。
5
7
  コアプラグインには **Cloudflare・Worker・データベース・ベンダー固有のコードは一切なく**、
@@ -38,8 +40,8 @@ export default {
38
40
  ```
39
41
 
40
42
  provider を省略するとインメモリ provider が使われます。ローカルプレビューには
41
- 十分ですが、プロセスをまたぐとメンションは失われます。本番では永続 adapter
42
- ([`@riebeckite/webmention-cloudflare`](../../integrations/webmention-cloudflare/README_ja.md) を参照)
43
+ 十分ですが、プロセスをまたぐとメンションは失われます。本産では永続 adapter
44
+ ([`@riebeckite/webmention-cloudflare`](https://github.com/Rerurate514/riebeckite/blob/main/packages/integrations/webmention-cloudflare/README_ja.md) を参照)
43
45
  を指定してください。
44
46
 
45
47
  ## エンドポイント
@@ -54,23 +56,26 @@ provider を省略するとインメモリ provider が使われます。ロー
54
56
 
55
57
  ## 表示
56
58
 
57
- `onManifestCreated` で provider を一度 query し、検証済みメンションをターゲット別に
58
- グループ化して、対応する entry の HTML へセクションを追記します。Core がその HTML を
59
- コンテンツルートの描画 HTML と同期します。フックは安定した `rr-webmention` 名です。
60
-
61
- ```html
62
- <section class="rr-webmention" data-webmention data-webmention-count="2">
63
- <h2 class="rr-webmention__heading">Mentions</h2>
64
- <ul class="rr-webmention__list">
65
- <li class="rr-webmention__item rr-webmention__item--like"
66
- data-webmention-type="like">…</li>
67
- </ul>
68
- </section>
59
+ Site が JSX の配置を所有します。Provider のインスタンスは Site の composition で保持し、
60
+ 同じインスタンスを `webmention()` へ渡してから、`getWebmentionSectionData` で
61
+ コンポーネントの型付き props を取得します。helper は `PublicContentView` を受け取るため、
62
+ draft と scheduled の entry は描画できません。
63
+
64
+ ```tsx
65
+ const mentions = await getWebmentionSectionData({
66
+ view,
67
+ slug,
68
+ config,
69
+ provider: webmentionProvider,
70
+ });
71
+
72
+ <WebmentionSection {...mentions} />
69
73
  ```
70
74
 
71
- `render: false` でビルド時の追記を無効化できます。リクエスト時に描画する
72
- アプリケーションは `getWebmentionsForEntry({ manifest, config, provider, slug })` と
73
- `renderWebmentionSection(mentions, options)` を直接呼ぶか、JSON フィードを利用します。
75
+ 空の Provider は空の model を返し、`WebmentionSection` は何も描画しません。Plugin は
76
+ ビルド時に manifest HTML を注入しません。より低レベルな query が必要な場合は、
77
+ `getWebmentionsForEntry({ manifest, config, provider, slug })` を直接呼ぶか、JSON フィードを
78
+ 利用できます。
74
79
 
75
80
  ## オプション
76
81
 
@@ -78,7 +83,6 @@ provider を省略するとインメモリ provider が使われます。ロー
78
83
  | ---------- | ------ | ---- |
79
84
  | `provider` | インメモリ | `WebmentionProvider` を実装するストレージ adapter。 |
80
85
  | `endpoint` | `"/webmentions"` | 受信(POST)とフィード(GET)のパス。 |
81
- | `render` | `true` | manifest 生成時に対応 entry へメンションを追記します。 |
82
86
  | `headingText` | `"Mentions"` | 表示セクションの見出し。 |
83
87
  | `limit` | `20` | 記事ごとに表示する最大メンション数。 |
84
88
  | `className` | `"rr-webmention"` | 表示セクションのルート CSS クラス。 |
@@ -106,12 +110,12 @@ manifest 生成時、公開 entry に一致しないターゲットのメンシ
106
110
  - `MemoryWebmentionProvider`、provider / capability の型とエラー
107
111
  - `parseWebmentionSource`、`findTargetLink`、`verifyWebmention`、
108
112
  `createWebmentionSourceFetcher`
109
- - `renderWebmentionSection`、`getWebmentionsForEntry`、`groupMentionsBySlug`、
113
+ - `WebmentionSection`、`getWebmentionSectionData`、`getWebmentionsForEntry`、
114
+ `groupMentionsBySlug`、
110
115
  `buildFeed`
111
116
  - `resolveWebmentionOptions`、`validateWebmentionOptions`
112
117
 
113
118
  ## 関連
114
119
 
115
- - [プラグインガイド](../../../docs/docs/reference/plugin-api.ja.md)
116
- - [@riebeckite/webmention-cloudflare](../../integrations/webmention-cloudflare/README_ja.md)
117
-
120
+ - [プラグインガイド](https://github.com/Rerurate514/riebeckite/blob/main/docs/docs/reference/plugin-api.ja.md)
121
+ - [@riebeckite/webmention-cloudflare](https://github.com/Rerurate514/riebeckite/blob/main/packages/integrations/webmention-cloudflare/README_ja.md)
@@ -0,0 +1,8 @@
1
+ import type { WebmentionMention } from "../src/mention.js";
2
+ export type WebmentionSectionProps = {
3
+ readonly mentions: readonly WebmentionMention[];
4
+ readonly heading?: string;
5
+ readonly className?: string;
6
+ };
7
+ declare const WebmentionSection: ({ mentions, heading, className, }: Readonly<WebmentionSectionProps>) => import("hono/jsx/jsx-dev-runtime").JSX.Element;
8
+ export default WebmentionSection;
@@ -0,0 +1,45 @@
1
+ // components/webmention-section.tsx
2
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
3
+ var WebmentionSection = (({
4
+ mentions,
5
+ heading = "Mentions",
6
+ className = "rr-webmention"
7
+ }) => {
8
+ if (mentions.length === 0) return null;
9
+ return /* @__PURE__ */ jsxs(
10
+ "section",
11
+ {
12
+ class: className,
13
+ "data-webmention": true,
14
+ "data-webmention-count": mentions.length,
15
+ children: [
16
+ /* @__PURE__ */ jsx("h2", { class: `${className}__heading`, children: heading }),
17
+ /* @__PURE__ */ jsx("ul", { class: `${className}__list`, children: mentions.map((mention) => /* @__PURE__ */ jsxs(
18
+ "li",
19
+ {
20
+ class: `${className}__item ${className}__item--${mention.type}`,
21
+ children: [
22
+ /* @__PURE__ */ jsx(
23
+ "a",
24
+ {
25
+ class: `${className}__source`,
26
+ href: mention.source,
27
+ rel: "nofollow ugc",
28
+ children: mention.title ?? mention.author?.name ?? mention.source
29
+ }
30
+ ),
31
+ mention.author?.name && mention.title ? /* @__PURE__ */ jsx("span", { class: `${className}__author`, children: mention.author.name }) : null,
32
+ mention.publishedAt ? /* @__PURE__ */ jsx("time", { datetime: mention.publishedAt, children: mention.publishedAt }) : null,
33
+ mention.excerpt ? /* @__PURE__ */ jsx("p", { class: `${className}__excerpt`, children: mention.excerpt }) : null
34
+ ]
35
+ },
36
+ mention.source
37
+ )) })
38
+ ]
39
+ }
40
+ );
41
+ });
42
+ var webmention_section_default = WebmentionSection;
43
+ export {
44
+ webmention_section_default as default
45
+ };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { buildWebmentionDiagnostics } from "./src/diagnostics.js";
2
2
  import type { WebmentionOptions } from "./src/types.js";
3
+ export type { WebmentionSectionProps } from "./components/webmention-section.js";
4
+ export { default as WebmentionSection } from "./components/webmention-section.js";
3
5
  export { createWebmentionEndpoints } from "./src/endpoints.js";
4
6
  export type { WebmentionFeed, WebmentionFeedEntry, WebmentionFeedMention, } from "./src/feed.js";
5
7
  export { buildFeed, createWebmentionFeedHandler, summarizeEntry, } from "./src/feed.js";
@@ -14,7 +16,8 @@ export { assertWebmentionQuerySupported, requiredCapabilityForQuery, supportsWeb
14
16
  export type { AllMentionsQuery, AllMentionsResult, MentionsForTargetQuery, MentionsForTargetResult, WebmentionQuery, WebmentionQueryResult, } from "./src/query.js";
15
17
  export { createWebmentionReceiveHandler, parseWebmentionRequestBody, } from "./src/receive.js";
16
18
  export { renderWebmentionItem, renderWebmentionSection, WEBMENTION_ATTRIBUTE, } from "./src/render.js";
17
- export { compareMentions, countUnmatchedMentions, getWebmentionsForEntry, groupMentionsBySlug, } from "./src/server.js";
19
+ export type { WebmentionSectionData, WebmentionSectionDataInput, } from "./src/server.js";
20
+ export { compareMentions, countUnmatchedMentions, getWebmentionSectionData, getWebmentionsForEntry, groupMentionsBySlug, queryWebmentionsForEntry, } from "./src/server.js";
18
21
  export { entryPublicUrl, findTargetEntry, isAllowedTarget, } from "./src/targets.js";
19
22
  export type { ResolvedWebmentionOptions, WebmentionOptions, } from "./src/types.js";
20
23
  export { normalizeWebmentionUrl, resolvePublicUrl, urlComparisonKey, } from "./src/url.js";
@@ -38,7 +41,6 @@ export declare const webmentionPlugin: typeof webmention;
38
41
  export declare function webmention(options?: WebmentionOptions): import("@riebeckite/core").RiebeckitePlugin<Readonly<{
39
42
  provider?: import("./index.js").WebmentionProvider;
40
43
  endpoint?: string;
41
- render?: boolean;
42
44
  headingText?: string;
43
45
  limit?: number;
44
46
  className?: string;
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // index.ts
2
2
  import {
3
- appendContentBodySlot,
4
3
  createStyleAsset,
5
- definePlugin
4
+ definePlugin,
5
+ fingerprintExternalValue
6
6
  } from "@riebeckite/core";
7
7
 
8
8
  // src/provider.ts
@@ -34,15 +34,15 @@ function assertWebmentionQuerySupported(provider, query) {
34
34
 
35
35
  // src/diagnostics.ts
36
36
  var WEBMENTION_PLUGIN_NAME = "webmention";
37
- function buildWebmentionDiagnostics(provider, options) {
37
+ function buildWebmentionDiagnostics(provider) {
38
38
  const diagnostics = [];
39
- if (options.render && !supportsWebmentionCapability(provider, "query")) {
39
+ if (!supportsWebmentionCapability(provider, "query")) {
40
40
  diagnostics.push({
41
- code: "webmention-render-requires-query",
41
+ code: "webmention-query-unavailable",
42
42
  severity: "warning",
43
43
  pluginName: WEBMENTION_PLUGIN_NAME,
44
- message: "The webmention provider cannot query, so stored mentions will not render near articles.",
45
- suggestion: 'Use a provider that advertises the "query" capability or set render: false.'
44
+ message: "The webmention provider cannot query, so stored mentions are unavailable to the Site.",
45
+ suggestion: 'Use a provider that advertises the "query" capability.'
46
46
  });
47
47
  }
48
48
  if (!supportsWebmentionCapability(provider, "store")) {
@@ -170,7 +170,7 @@ function createWebmentionFeedHandler(options, provider) {
170
170
  function buildFeed(manifest, config, mentions) {
171
171
  return {
172
172
  version: "1",
173
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
173
+ generatedAt: latestVerificationTime(mentions),
174
174
  count: mentions.length,
175
175
  mentions: mentions.map((mention) => ({
176
176
  ...mention,
@@ -178,6 +178,12 @@ function buildFeed(manifest, config, mentions) {
178
178
  }))
179
179
  };
180
180
  }
181
+ function latestVerificationTime(mentions) {
182
+ return mentions.reduce(
183
+ (latest, mention) => mention.verifiedAt > latest ? mention.verifiedAt : latest,
184
+ "1970-01-01T00:00:00.000Z"
185
+ );
186
+ }
181
187
  function summarizeEntry(manifest, config, target) {
182
188
  const entry = findTargetEntry(manifest, config, target);
183
189
  if (!entry) return null;
@@ -830,7 +836,6 @@ function resolveWebmentionOptions(options = {}) {
830
836
  return {
831
837
  provider: options.provider ?? new MemoryWebmentionProvider(),
832
838
  endpoint: normalizeEndpoint(options.endpoint),
833
- render: options.render ?? true,
834
839
  headingText: normalizeText(
835
840
  options.headingText,
836
841
  DEFAULT_WEBMENTION_HEADING_TEXT
@@ -886,7 +891,7 @@ function validateWebmentionOptions(options) {
886
891
  message: "Expected a positive integer."
887
892
  });
888
893
  }
889
- for (const key of ["render", "nofollow", "allowPrivateHosts"]) {
894
+ for (const key of ["nofollow", "allowPrivateHosts"]) {
890
895
  const value = options[key];
891
896
  if (value !== void 0 && typeof value !== "boolean") {
892
897
  issues.push({ path: key, message: "Expected a boolean." });
@@ -960,42 +965,49 @@ function isAbsoluteHttpUrl(value) {
960
965
  }
961
966
  }
962
967
 
963
- // src/render.ts
964
- import { escapeHtml, escapeHtmlAttribute } from "@riebeckite/core";
965
- var WEBMENTION_ATTRIBUTE = "data-webmention";
966
- function renderWebmentionSection(mentions, options) {
967
- if (mentions.length === 0) return "";
968
- const className = escapeHtmlAttribute(options.className);
969
- const items = mentions.map((mention) => renderWebmentionItem(mention, options)).join("");
970
- return `<section class="${className}" ${WEBMENTION_ATTRIBUTE} data-webmention-count="${mentions.length}"><h2 class="${className}__heading">${escapeHtml(options.headingText)}</h2><ul class="${className}__list">${items}</ul></section>`;
971
- }
972
- function renderWebmentionItem(mention, options) {
973
- const className = escapeHtmlAttribute(options.className);
974
- const rel = options.nofollow ? ' rel="nofollow ugc"' : "";
975
- const label = firstNonEmpty(mention.title, mention.author?.name) ?? mention.source;
976
- const author = mention.author?.name !== void 0 && mention.title !== void 0 ? `<span class="${className}__author">${escapeHtml(mention.author.name)}</span>` : "";
977
- const date = mention.publishedAt === void 0 ? "" : `<time class="${className}__date" datetime="${escapeHtmlAttribute(
978
- mention.publishedAt
979
- )}">${escapeHtml(formatDate(mention.publishedAt))}</time>`;
980
- const excerpt = mention.excerpt === void 0 ? "" : `<p class="${className}__excerpt">${escapeHtml(mention.excerpt)}</p>`;
981
- return `<li class="${className}__item ${className}__item--${mention.type}" data-webmention-type="${mention.type}"><a class="${className}__source" href="${escapeHtmlAttribute(
982
- mention.source
983
- )}"${rel}>${escapeHtml(label)}</a>${author}${date}${excerpt}</li>`;
984
- }
985
- function firstNonEmpty(...values) {
986
- for (const value of values) {
987
- if (value !== void 0 && value.trim() !== "") return value;
968
+ // src/server.ts
969
+ var getWebmentionSectionData = async ({
970
+ view,
971
+ slug,
972
+ config,
973
+ provider,
974
+ heading = "Mentions",
975
+ className = "rr-webmention",
976
+ limit
977
+ }) => {
978
+ const entry = view.findRoutableBySlug(slug);
979
+ if (!entry || !supportsWebmentionCapability(provider, "query")) {
980
+ return { mentions: [], heading, className };
988
981
  }
989
- return void 0;
982
+ const target = entryPublicUrl(entry, config);
983
+ if (target === null) return { mentions: [], heading, className };
984
+ const result = await provider.query({
985
+ type: "mentions_for_target",
986
+ target,
987
+ limit
988
+ });
989
+ return {
990
+ mentions: [...result.mentions].sort(compareMentions2),
991
+ heading,
992
+ className
993
+ };
994
+ };
995
+ function getWebmentionsForEntry(input) {
996
+ if ("mentions" in input) {
997
+ return input.mentions.filter(
998
+ (mention) => hasEntryTarget(mention.target, input.entry.permalink)
999
+ ).toSorted(compareMentions2).slice(0, input.limit);
1000
+ }
1001
+ return queryWebmentionsForEntry(input);
990
1002
  }
991
- function formatDate(value) {
992
- const date = new Date(value);
993
- if (Number.isNaN(date.getTime())) return value;
994
- return date.toISOString().slice(0, 10);
1003
+ function hasEntryTarget(target, permalink) {
1004
+ try {
1005
+ return new URL(target).pathname === permalink;
1006
+ } catch {
1007
+ return false;
1008
+ }
995
1009
  }
996
-
997
- // src/server.ts
998
- async function getWebmentionsForEntry(input) {
1010
+ async function queryWebmentionsForEntry(input) {
999
1011
  const entry = input.manifest.bySlug.get(input.slug);
1000
1012
  if (!entry) return [];
1001
1013
  const target = entryPublicUrl(entry, input.config);
@@ -1042,6 +1054,83 @@ function compareMentions2(left, right) {
1042
1054
  ) || left.source.localeCompare(right.source);
1043
1055
  }
1044
1056
 
1057
+ // components/webmention-section.tsx
1058
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
1059
+ var WebmentionSection = (({
1060
+ mentions,
1061
+ heading = "Mentions",
1062
+ className = "rr-webmention"
1063
+ }) => {
1064
+ if (mentions.length === 0) return null;
1065
+ return /* @__PURE__ */ jsxs(
1066
+ "section",
1067
+ {
1068
+ class: className,
1069
+ "data-webmention": true,
1070
+ "data-webmention-count": mentions.length,
1071
+ children: [
1072
+ /* @__PURE__ */ jsx("h2", { class: `${className}__heading`, children: heading }),
1073
+ /* @__PURE__ */ jsx("ul", { class: `${className}__list`, children: mentions.map((mention) => /* @__PURE__ */ jsxs(
1074
+ "li",
1075
+ {
1076
+ class: `${className}__item ${className}__item--${mention.type}`,
1077
+ children: [
1078
+ /* @__PURE__ */ jsx(
1079
+ "a",
1080
+ {
1081
+ class: `${className}__source`,
1082
+ href: mention.source,
1083
+ rel: "nofollow ugc",
1084
+ children: mention.title ?? mention.author?.name ?? mention.source
1085
+ }
1086
+ ),
1087
+ mention.author?.name && mention.title ? /* @__PURE__ */ jsx("span", { class: `${className}__author`, children: mention.author.name }) : null,
1088
+ mention.publishedAt ? /* @__PURE__ */ jsx("time", { datetime: mention.publishedAt, children: mention.publishedAt }) : null,
1089
+ mention.excerpt ? /* @__PURE__ */ jsx("p", { class: `${className}__excerpt`, children: mention.excerpt }) : null
1090
+ ]
1091
+ },
1092
+ mention.source
1093
+ )) })
1094
+ ]
1095
+ }
1096
+ );
1097
+ });
1098
+ var webmention_section_default = WebmentionSection;
1099
+
1100
+ // src/render.ts
1101
+ import { escapeHtml, escapeHtmlAttribute } from "@riebeckite/core";
1102
+ var WEBMENTION_ATTRIBUTE = "data-webmention";
1103
+ function renderWebmentionSection(mentions, options) {
1104
+ if (mentions.length === 0) return "";
1105
+ const className = escapeHtmlAttribute(options.className);
1106
+ const items = mentions.map((mention) => renderWebmentionItem(mention, options)).join("");
1107
+ return `<section class="${className}" ${WEBMENTION_ATTRIBUTE} data-webmention-count="${mentions.length}"><h2 class="${className}__heading">${escapeHtml(options.headingText)}</h2><ul class="${className}__list">${items}</ul></section>`;
1108
+ }
1109
+ function renderWebmentionItem(mention, options) {
1110
+ const className = escapeHtmlAttribute(options.className);
1111
+ const rel = options.nofollow ? ' rel="nofollow ugc"' : "";
1112
+ const label = firstNonEmpty(mention.title, mention.author?.name) ?? mention.source;
1113
+ const author = mention.author?.name !== void 0 && mention.title !== void 0 ? `<span class="${className}__author">${escapeHtml(mention.author.name)}</span>` : "";
1114
+ const date = mention.publishedAt === void 0 ? "" : `<time class="${className}__date" datetime="${escapeHtmlAttribute(
1115
+ mention.publishedAt
1116
+ )}">${escapeHtml(formatDate(mention.publishedAt))}</time>`;
1117
+ const excerpt = mention.excerpt === void 0 ? "" : `<p class="${className}__excerpt">${escapeHtml(mention.excerpt)}</p>`;
1118
+ return `<li class="${className}__item ${className}__item--${mention.type}" data-webmention-type="${mention.type}"><a class="${className}__source" href="${escapeHtmlAttribute(
1119
+ mention.source
1120
+ )}"${rel}>${escapeHtml(label)}</a>${author}${date}${excerpt}</li>`;
1121
+ }
1122
+ function firstNonEmpty(...values) {
1123
+ for (const value of values) {
1124
+ if (value !== void 0 && value.trim() !== "") return value;
1125
+ }
1126
+ return void 0;
1127
+ }
1128
+ function formatDate(value) {
1129
+ const date = new Date(value);
1130
+ if (Number.isNaN(date.getTime())) return value;
1131
+ return date.toISOString().slice(0, 10);
1132
+ }
1133
+
1045
1134
  // index.ts
1046
1135
  var webmentionPlugin = webmention;
1047
1136
  function webmention(options = {}) {
@@ -1053,46 +1142,60 @@ function webmention(options = {}) {
1053
1142
  version: "webmention-v1",
1054
1143
  dependencyMode: "none"
1055
1144
  },
1056
- outputDependencies: [{ type: "unknown" }],
1145
+ contentOutputDependencies: [
1146
+ { type: "external", id: "webmention:all-mentions" }
1147
+ ],
1057
1148
  options,
1058
1149
  validateOptions: validateWebmentionOptions,
1059
- onManifestCreated: async (context) => {
1060
- if (!resolved.render || !supportsWebmentionCapability(provider, "query")) {
1061
- return;
1150
+ resolveExternalDependencies: async () => {
1151
+ if (!supportsWebmentionCapability(provider, "query")) {
1152
+ return [];
1062
1153
  }
1063
- let mentions;
1064
1154
  try {
1065
1155
  const result = await provider.query({ type: "all_mentions" });
1066
- mentions = result.mentions;
1156
+ return [
1157
+ {
1158
+ kind: "resolved",
1159
+ snapshot: {
1160
+ id: "webmention:all-mentions",
1161
+ fingerprint: fingerprintExternalValue(result.mentions),
1162
+ scope: { type: "content" },
1163
+ value: result.mentions
1164
+ }
1165
+ }
1166
+ ];
1067
1167
  } catch (error) {
1168
+ return [
1169
+ {
1170
+ kind: "unavailable",
1171
+ failure: {
1172
+ id: "webmention:all-mentions",
1173
+ reason: error instanceof Error ? error.message : String(error)
1174
+ }
1175
+ }
1176
+ ];
1177
+ }
1178
+ },
1179
+ onManifestCreated: async (context) => {
1180
+ if (!supportsWebmentionCapability(provider, "query")) {
1181
+ return;
1182
+ }
1183
+ const snapshot = context.externalDependencies?.get(
1184
+ "webmention:all-mentions"
1185
+ );
1186
+ if (!snapshot) {
1068
1187
  context.diagnostics.push({
1069
1188
  code: "webmention-query-failed",
1070
1189
  severity: "warning",
1071
1190
  pluginName: WEBMENTION_PLUGIN_NAME,
1072
1191
  message: "The webmention provider could not be queried, so mentions were not rendered.",
1073
1192
  meta: {
1074
- cause: error instanceof Error ? error.message : String(error)
1193
+ cause: "The external dependency snapshot was unavailable."
1075
1194
  }
1076
1195
  });
1077
1196
  return;
1078
1197
  }
1079
- const grouped = groupMentionsBySlug(
1080
- context.manifest,
1081
- context.config,
1082
- mentions
1083
- );
1084
- for (const [slug, list] of grouped) {
1085
- const entry = context.manifest.bySlug.get(slug);
1086
- if (!entry) continue;
1087
- const section = renderWebmentionSection(
1088
- list.slice(0, resolved.limit),
1089
- resolved
1090
- );
1091
- if (section === "") continue;
1092
- if (!hasSlotFragment(entry.bodySlots?.["article.footer"], section)) {
1093
- appendContentBodySlot(entry, "article.footer", section);
1094
- }
1095
- }
1198
+ const mentions = snapshot.value;
1096
1199
  const unmatched = countUnmatchedMentions(
1097
1200
  context.manifest,
1098
1201
  context.config,
@@ -1108,17 +1211,15 @@ function webmention(options = {}) {
1108
1211
  }
1109
1212
  },
1110
1213
  endpoints: createWebmentionEndpoints(resolved),
1111
- assets: [createStyleAsset(WEBMENTION_PLUGIN_NAME)],
1112
- addDiagnostics: () => buildWebmentionDiagnostics(provider, resolved)
1214
+ assets: [
1215
+ createStyleAsset(
1216
+ WEBMENTION_PLUGIN_NAME,
1217
+ "@riebeckite/plugin-webmention/style.css"
1218
+ )
1219
+ ],
1220
+ addDiagnostics: () => buildWebmentionDiagnostics(provider)
1113
1221
  });
1114
1222
  }
1115
- function hasSlotFragment(slot, fragment) {
1116
- return slot === fragment || slot?.startsWith(`${fragment}
1117
- `) || slot?.endsWith(`
1118
- ${fragment}`) || slot?.includes(`
1119
- ${fragment}
1120
- `) || false;
1121
- }
1122
1223
  export {
1123
1224
  DEFAULT_WEBMENTION_CLASS_NAME,
1124
1225
  DEFAULT_WEBMENTION_ENDPOINT,
@@ -1132,6 +1233,7 @@ export {
1132
1233
  WEBMENTION_ATTRIBUTE,
1133
1234
  WEBMENTION_CAPABILITIES,
1134
1235
  WEBMENTION_TYPES,
1236
+ webmention_section_default as WebmentionSection,
1135
1237
  assertWebmentionQuerySupported,
1136
1238
  buildFeed,
1137
1239
  buildWebmentionDiagnostics,
@@ -1144,12 +1246,14 @@ export {
1144
1246
  entryPublicUrl,
1145
1247
  findTargetEntry,
1146
1248
  findTargetLink,
1249
+ getWebmentionSectionData,
1147
1250
  getWebmentionsForEntry,
1148
1251
  groupMentionsBySlug,
1149
1252
  isAllowedTarget,
1150
1253
  normalizeWebmentionUrl,
1151
1254
  parseWebmentionRequestBody,
1152
1255
  parseWebmentionSource,
1256
+ queryWebmentionsForEntry,
1153
1257
  renderWebmentionItem,
1154
1258
  renderWebmentionSection,
1155
1259
  requiredCapabilityForQuery,
@@ -1,6 +1,5 @@
1
1
  import type { Diagnostic } from "@riebeckite/core";
2
2
  import { type WebmentionProvider } from "./provider.js";
3
- import type { ResolvedWebmentionOptions } from "./types.js";
4
3
  export declare const WEBMENTION_PLUGIN_NAME = "webmention";
5
- /** Structured health checks for the configured provider and rendering mode. */
6
- export declare function buildWebmentionDiagnostics(provider: WebmentionProvider, options: ResolvedWebmentionOptions): Diagnostic[];
4
+ /** Structured health checks for the configured provider. */
5
+ export declare function buildWebmentionDiagnostics(provider: WebmentionProvider): Diagnostic[];
@@ -1,18 +1,41 @@
1
- import type { ContentManifest, ResolvedRiebeckiteConfig } from "@riebeckite/core";
1
+ import type { ContentManifest, PluginDataHelper, PublicContentEntry, PublicContentView, ResolvedRiebeckiteConfig } from "@riebeckite/core";
2
2
  import type { WebmentionMention } from "./mention.js";
3
- import type { WebmentionProvider } from "./provider.js";
3
+ import { type WebmentionProvider } from "./provider.js";
4
4
  /**
5
5
  * Reads verified mentions for one entry. Application routes call this at
6
6
  * request/SSR time when they render mentions themselves instead of relying on
7
7
  * build-time injection.
8
8
  */
9
- export declare function getWebmentionsForEntry(input: {
9
+ type WebmentionSnapshotInput = {
10
+ readonly entry: PublicContentEntry;
11
+ readonly mentions: readonly WebmentionMention[];
12
+ readonly limit?: number;
13
+ };
14
+ type WebmentionQueryInput = {
10
15
  manifest: ContentManifest;
11
16
  config: ResolvedRiebeckiteConfig | undefined;
12
17
  provider: WebmentionProvider;
13
18
  slug: string;
14
19
  limit?: number;
15
- }): Promise<WebmentionMention[]>;
20
+ };
21
+ export type WebmentionSectionData = {
22
+ readonly mentions: readonly WebmentionMention[];
23
+ readonly heading: string;
24
+ readonly className: string;
25
+ };
26
+ export type WebmentionSectionDataInput = {
27
+ readonly view: PublicContentView;
28
+ readonly slug: string;
29
+ readonly config: ResolvedRiebeckiteConfig | undefined;
30
+ readonly provider: WebmentionProvider;
31
+ readonly heading?: string;
32
+ readonly className?: string;
33
+ readonly limit?: number;
34
+ };
35
+ export declare const getWebmentionSectionData: PluginDataHelper<WebmentionSectionDataInput, WebmentionSectionData>;
36
+ export declare function getWebmentionsForEntry(input: Readonly<WebmentionSnapshotInput>): readonly WebmentionMention[];
37
+ export declare function getWebmentionsForEntry(input: Readonly<WebmentionQueryInput>): Promise<WebmentionMention[]>;
38
+ export declare function queryWebmentionsForEntry(input: WebmentionQueryInput): Promise<WebmentionMention[]>;
16
39
  /**
17
40
  * Groups mentions by the manifest entry they target. Mentions whose target is
18
41
  * not a published entry are omitted; callers that need them can query the
@@ -22,3 +45,4 @@ export declare function groupMentionsBySlug(manifest: ContentManifest, config: R
22
45
  /** Counts mentions whose target does not match a published entry. */
23
46
  export declare function countUnmatchedMentions(manifest: ContentManifest, config: ResolvedRiebeckiteConfig | undefined, mentions: readonly WebmentionMention[]): number;
24
47
  export declare function compareMentions(left: WebmentionMention, right: WebmentionMention): number;
48
+ export {};
@@ -3,7 +3,7 @@ import type { ContentManifest, ContentManifestEntry, ResolvedRiebeckiteConfig }
3
3
  * Resolves an entry's public URL. Relative permalinks need the configured
4
4
  * base URL; without one, only an already-absolute permalink can be matched.
5
5
  */
6
- export declare function entryPublicUrl(entry: ContentManifestEntry, config: ResolvedRiebeckiteConfig | undefined): string | null;
6
+ export declare function entryPublicUrl(entry: Pick<ContentManifestEntry, "permalink">, config: ResolvedRiebeckiteConfig | undefined): string | null;
7
7
  /** Finds the content entry whose public URL matches `target`, if any. */
8
8
  export declare function findTargetEntry(manifest: ContentManifest, config: ResolvedRiebeckiteConfig | undefined, target: string): ContentManifestEntry | undefined;
9
9
  /** Whether `target` is explicitly allowed by `allowedTargets`. */
@@ -13,8 +13,6 @@ export type WebmentionOptions = Readonly<{
13
13
  provider?: WebmentionProvider;
14
14
  /** Receive (POST) and feed (GET) path. Defaults to `/webmentions`. */
15
15
  endpoint?: string;
16
- /** Render stored mentions near articles. Defaults to `true`. */
17
- render?: boolean;
18
16
  /** Heading text for the rendered section. Defaults to `"Mentions"`. */
19
17
  headingText?: string;
20
18
  /** Maximum mentions rendered per article. Defaults to `20`. */
@@ -40,7 +38,6 @@ export type WebmentionOptions = Readonly<{
40
38
  export type ResolvedWebmentionOptions = Readonly<{
41
39
  provider: WebmentionProvider;
42
40
  endpoint: string;
43
- render: boolean;
44
41
  headingText: string;
45
42
  limit: number;
46
43
  className: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-webmention",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,10 +12,17 @@
12
12
  "source": "./index.ts",
13
13
  "default": "./dist/index.js"
14
14
  },
15
+ "./components": {
16
+ "types": "./dist/components/webmention-section.d.ts",
17
+ "import": "./dist/components/webmention-section.js",
18
+ "source": "./components/webmention-section.tsx",
19
+ "default": "./dist/components/webmention-section.js"
20
+ },
15
21
  "./style.css": "./style.css"
16
22
  },
17
23
  "dependencies": {
18
- "@riebeckite/core": "0.0.21"
24
+ "hono": "^4.12.25",
25
+ "@riebeckite/core": "0.0.23"
19
26
  },
20
27
  "devDependencies": {
21
28
  "tsx": "^4.22.4"