chore: add /webmentions 506c7beb
Steve Simkins · 2026-07-21 00:52 1 file(s) · +104 −0
src/pages/webmentions.astro (added) +104 −0
1 +
---
2 +
export const prerender = false;
3 +
4 +
import { env } from "cloudflare:workers";
5 +
import PageLayout from "@/layouts/Base.astro";
6 +
7 +
const meta = {
8 +
	title: "/webmentions",
9 +
	description: "Webmentions received across my site",
10 +
};
11 +
12 +
interface Mention {
13 +
	source: string;
14 +
	target: string;
15 +
	verifiedAt: string;
16 +
}
17 +
18 +
const kv = env.WEBMENTIONS;
19 +
const list = await kv.list({ prefix: "mention:" });
20 +
21 +
const mentions: Mention[] = [];
22 +
for (const entry of list.keys) {
23 +
	const value = await kv.get(entry.name);
24 +
	if (value) mentions.push(JSON.parse(value) as Mention);
25 +
}
26 +
27 +
// Group by the page that was mentioned, newest mention first within each group.
28 +
const byTarget = new Map<string, Mention[]>();
29 +
for (const m of mentions) {
30 +
	const group = byTarget.get(m.target) ?? [];
31 +
	group.push(m);
32 +
	byTarget.set(m.target, group);
33 +
}
34 +
const groups = [...byTarget.entries()]
35 +
	.map(([target, items]) => ({
36 +
		target,
37 +
		items: items.sort(
38 +
			(a, b) =>
39 +
				new Date(b.verifiedAt).getTime() - new Date(a.verifiedAt).getTime(),
40 +
		),
41 +
	}))
42 +
	.sort((a, b) => b.items.length - a.items.length);
43 +
44 +
const hostOf = (url: string) => {
45 +
	try {
46 +
		return new URL(url).host;
47 +
	} catch {
48 +
		return url;
49 +
	}
50 +
};
51 +
const pathOf = (url: string) => {
52 +
	try {
53 +
		return new URL(url).pathname;
54 +
	} catch {
55 +
		return url;
56 +
	}
57 +
};
58 +
---
59 +
60 +
<PageLayout meta={meta}>
61 +
  <div class="space-y-6">
62 +
    <h1 class="title mb-6">/webmentions</h1>
63 +
    <p class="text-zinc-300">
64 +
      Pages and posts around the web that have linked here.
65 +
    </p>
66 +
    {
67 +
      groups.length === 0 ? (
68 +
        <p class="text-zinc-400">No webmentions yet.</p>
69 +
      ) : (
70 +
        <div class="space-y-8">
71 +
          {groups.map((group) => (
72 +
            <section>
73 +
              <h2 class="font-semibold">
74 +
                <a class="style-link" href={group.target}>
75 +
                  {pathOf(group.target)}
76 +
                </a>
77 +
                <span class="ml-2 text-sm text-zinc-500">
78 +
                  {group.items.length}
79 +
                </span>
80 +
              </h2>
81 +
              <ul class="mt-3 space-y-2 text-sm">
82 +
                {group.items.map((m) => (
83 +
                  <li class="flex items-center gap-2">
84 +
                    <a
85 +
                      class="underline hover:text-link"
86 +
                      href={m.source}
87 +
                      target="_blank"
88 +
                      rel="nofollow ugc noopener noreferrer"
89 +
                    >
90 +
                      {hostOf(m.source)}
91 +
                    </a>
92 +
                    <span class="text-zinc-500">
93 +
                      {new Date(m.verifiedAt).toLocaleDateString()}
94 +
                    </span>
95 +
                  </li>
96 +
                ))}
97 +
              </ul>
98 +
            </section>
99 +
          ))}
100 +
        </div>
101 +
      )
102 +
    }
103 +
  </div>
104 +
</PageLayout>