@riebeckite/plugin-webmention 0.0.22 → 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
@@ -58,12 +58,26 @@ Rejection codes: `invalid_request`, `missing_source_or_target`,
58
58
 
59
59
  ## Rendering
60
60
 
61
- At `onManifestCreated` the plugin queries the provider once, groups verified mentions by target, and appends a live section to each matching entry. Core synchronizes it with the content the route renders.
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} />
75
+ ```
62
76
 
63
- Set `render: false` to skip build-time injection. Applications that render at
64
- request time can call `getWebmentionsForEntry({ manifest, config, provider,
65
- slug })` and `renderWebmentionSection(mentions, options)` directly, or consume
66
- 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.
67
81
 
68
82
  ## Options
69
83
 
@@ -71,7 +85,6 @@ the JSON feed.
71
85
  | ------ | ------- | ----------- |
72
86
  | `provider` | in-memory | Storage adapter implementing `WebmentionProvider`. |
73
87
  | `endpoint` | `"/webmentions"` | Receive (POST) and feed (GET) path. |
74
- | `render` | `true` | Append stored mentions to matching entries at manifest time. |
75
88
  | `headingText` | `"Mentions"` | Heading for the rendered section. |
76
89
  | `limit` | `20` | Maximum mentions rendered per article. |
77
90
  | `className` | `"rr-webmention"` | Root CSS class of the rendered section. |
@@ -99,7 +112,7 @@ manifest time, mentions whose target is not a published entry are reported as
99
112
  - `MemoryWebmentionProvider`, provider/capability types and errors
100
113
  - `parseWebmentionSource`, `findTargetLink`, `verifyWebmention`,
101
114
  `createWebmentionSourceFetcher`
102
- - `renderWebmentionSection`, `getWebmentionsForEntry`,
115
+ - `WebmentionSection`, `getWebmentionSectionData`, `getWebmentionsForEntry`,
103
116
  `groupMentionsBySlug`, `buildFeed`
104
117
  - `resolveWebmentionOptions`, `validateWebmentionOptions`
105
118
 
package/README_ja.md CHANGED
@@ -40,7 +40,7 @@ export default {
40
40
  ```
41
41
 
42
42
  provider を省略するとインメモリ provider が使われます。ローカルプレビューには
43
- 十分ですが、プロセスをまたぐとメンションは失われます。本番では永続 adapter
43
+ 十分ですが、プロセスをまたぐとメンションは失われます。本産では永続 adapter
44
44
  ([`@riebeckite/webmention-cloudflare`](https://github.com/Rerurate514/riebeckite/blob/main/packages/integrations/webmention-cloudflare/README_ja.md) を参照)
45
45
  を指定してください。
46
46
 
@@ -56,11 +56,26 @@ provider を省略するとインメモリ provider が使われます。ロー
56
56
 
57
57
  ## 表示
58
58
 
59
- `onManifestCreated` で provider を一度 query し、検証済みメンションをターゲット別にグループ化して、対応する entry へ実際の表示セクションを追記します。Core はこれをコンテンツルートの描画内容と同期します。
59
+ Site が JSX の配置を所有します。Provider のインスタンスは Site の composition で保持し、
60
+ 同じインスタンスを `webmention()` へ渡してから、`getWebmentionSectionData` で
61
+ コンポーネントの型付き props を取得します。helper は `PublicContentView` を受け取るため、
62
+ draft と scheduled の entry は描画できません。
60
63
 
61
- `render: false` でビルド時の追記を無効化できます。リクエスト時に描画する
62
- アプリケーションは `getWebmentionsForEntry({ manifest, config, provider, slug })` と
63
- `renderWebmentionSection(mentions, options)` を直接呼ぶか、JSON フィードを利用します。
64
+ ```tsx
65
+ const mentions = await getWebmentionSectionData({
66
+ view,
67
+ slug,
68
+ config,
69
+ provider: webmentionProvider,
70
+ });
71
+
72
+ <WebmentionSection {...mentions} />
73
+ ```
74
+
75
+ 空の Provider は空の model を返し、`WebmentionSection` は何も描画しません。Plugin は
76
+ ビルド時に manifest HTML を注入しません。より低レベルな query が必要な場合は、
77
+ `getWebmentionsForEntry({ manifest, config, provider, slug })` を直接呼ぶか、JSON フィードを
78
+ 利用できます。
64
79
 
65
80
  ## オプション
66
81
 
@@ -68,7 +83,6 @@ provider を省略するとインメモリ provider が使われます。ロー
68
83
  | ---------- | ------ | ---- |
69
84
  | `provider` | インメモリ | `WebmentionProvider` を実装するストレージ adapter。 |
70
85
  | `endpoint` | `"/webmentions"` | 受信(POST)とフィード(GET)のパス。 |
71
- | `render` | `true` | manifest 生成時に対応 entry へメンションを追記します。 |
72
86
  | `headingText` | `"Mentions"` | 表示セクションの見出し。 |
73
87
  | `limit` | `20` | 記事ごとに表示する最大メンション数。 |
74
88
  | `className` | `"rr-webmention"` | 表示セクションのルート CSS クラス。 |
@@ -96,7 +110,8 @@ manifest 生成時、公開 entry に一致しないターゲットのメンシ
96
110
  - `MemoryWebmentionProvider`、provider / capability の型とエラー
97
111
  - `parseWebmentionSource`、`findTargetLink`、`verifyWebmention`、
98
112
  `createWebmentionSourceFetcher`
99
- - `renderWebmentionSection`、`getWebmentionsForEntry`、`groupMentionsBySlug`、
113
+ - `WebmentionSection`、`getWebmentionSectionData`、`getWebmentionsForEntry`、
114
+ `groupMentionsBySlug`、
100
115
  `buildFeed`
101
116
  - `resolveWebmentionOptions`、`validateWebmentionOptions`
102
117
 
@@ -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.22",
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.22"
24
+ "hono": "^4.12.25",
25
+ "@riebeckite/core": "0.0.23"
19
26
  },
20
27
  "devDependencies": {
21
28
  "tsx": "^4.22.4"