@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 +20 -7
- package/README_ja.md +22 -7
- package/dist/components/webmention-section.d.ts +8 -0
- package/dist/components/webmention-section.js +45 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +180 -76
- package/dist/src/diagnostics.d.ts +2 -3
- package/dist/src/server.d.ts +28 -4
- package/dist/src/targets.d.ts +1 -1
- package/dist/src/types.d.ts +0 -3
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -58,12 +58,26 @@ Rejection codes: `invalid_request`, `missing_source_or_target`,
|
|
|
58
58
|
|
|
59
59
|
## Rendering
|
|
60
60
|
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
Site が JSX の配置を所有します。Provider のインスタンスは Site の composition で保持し、
|
|
60
|
+
同じインスタンスを `webmention()` へ渡してから、`getWebmentionSectionData` で
|
|
61
|
+
コンポーネントの型付き props を取得します。helper は `PublicContentView` を受け取るため、
|
|
62
|
+
draft と scheduled の entry は描画できません。
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
- `
|
|
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 {
|
|
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
|
|
37
|
+
function buildWebmentionDiagnostics(provider) {
|
|
38
38
|
const diagnostics = [];
|
|
39
|
-
if (
|
|
39
|
+
if (!supportsWebmentionCapability(provider, "query")) {
|
|
40
40
|
diagnostics.push({
|
|
41
|
-
code: "webmention-
|
|
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
|
|
45
|
-
suggestion: 'Use a provider that advertises the "query" capability
|
|
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: (
|
|
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 ["
|
|
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/
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
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
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
1145
|
+
contentOutputDependencies: [
|
|
1146
|
+
{ type: "external", id: "webmention:all-mentions" }
|
|
1147
|
+
],
|
|
1057
1148
|
options,
|
|
1058
1149
|
validateOptions: validateWebmentionOptions,
|
|
1059
|
-
|
|
1060
|
-
if (!
|
|
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
|
-
|
|
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:
|
|
1193
|
+
cause: "The external dependency snapshot was unavailable."
|
|
1075
1194
|
}
|
|
1076
1195
|
});
|
|
1077
1196
|
return;
|
|
1078
1197
|
}
|
|
1079
|
-
const
|
|
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: [
|
|
1112
|
-
|
|
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
|
|
6
|
-
export declare function buildWebmentionDiagnostics(provider: WebmentionProvider
|
|
4
|
+
/** Structured health checks for the configured provider. */
|
|
5
|
+
export declare function buildWebmentionDiagnostics(provider: WebmentionProvider): Diagnostic[];
|
package/dist/src/server.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
}
|
|
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 {};
|
package/dist/src/targets.d.ts
CHANGED
|
@@ -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`. */
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
24
|
+
"hono": "^4.12.25",
|
|
25
|
+
"@riebeckite/core": "0.0.23"
|
|
19
26
|
},
|
|
20
27
|
"devDependencies": {
|
|
21
28
|
"tsx": "^4.22.4"
|