feat: resolve live Nostr references in blog posts and add embeds

Support naddr/nevent/note slugs for unindexed posts, cache naddr lookups,
render Nostr embeds in markdown, and add a consistency check for events mirrors.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
bbe
2026-06-29 03:57:32 +02:00
co-authored by Cursor
parent 6023991f5c
commit 2ef68222bf
23 changed files with 1066 additions and 264 deletions
+14 -36
View File
@@ -7,14 +7,17 @@
* reading these needs the actual content, not repeated chrome.
*/
import { apiUrl } from "./api-base";
import { formatMeetupCivilDateLong, getMeetupStartUtc } from "./meetupEventTime";
import { formatMeetupCivilDateLong } from "./meetupEventTime";
import { fetchMeetupsLive, partitionMeetups, countUpcoming } from "./meetupsData";
export const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL || "https://belgianbitcoinembassy.org";
// Live (uncached) so the mirrors always reflect the same backend state as the
// rendered pages — no ISR drift between /events, /events.md, and /llms.txt.
async function fetchJson<T>(path: string, fallback: T): Promise<T> {
try {
const res = await fetch(apiUrl(path), { next: { revalidate: 300 } });
const res = await fetch(apiUrl(path), { cache: "no-store" });
if (!res.ok) return fallback;
return (await res.json()) as T;
} catch {
@@ -85,14 +88,10 @@ function configuredChannels(settings: Record<string, string>) {
export async function buildLlmsTxt(): Promise<string> {
const [settings, meetups] = await Promise.all([
fetchJson<Record<string, string>>("/settings/public", {}),
fetchJson<any[]>("/meetups", []),
fetchMeetupsLive(),
]);
const now = new Date();
const upcomingCount = (Array.isArray(meetups) ? meetups : []).filter((m) => {
const start = getMeetupStartUtc(m.date, m.time || "00:00");
return !Number.isNaN(start.getTime()) && start >= now;
}).length;
const upcomingCount = countUpcoming(meetups);
const channels = configuredChannels(settings);
const channelNames = channels.map((c) => c.name).join(", ");
@@ -101,12 +100,14 @@ export async function buildLlmsTxt(): Promise<string> {
? `How to connect on ${channelNames}`
: "How to connect with the community";
// Always state the count (including zero) so it stays machine-parseable and
// verifiably consistent with /events and /events.md.
const upcomingLine =
upcomingCount > 0
? `There ${upcomingCount === 1 ? "is" : "are"} currently ${upcomingCount} upcoming meetup${
upcomingCount === 1 ? "" : "s"
} scheduled.`
: "Meetups run monthly; check the events page for the next date.";
: "There are currently 0 upcoming meetups scheduled; the community meets monthly.";
return `# Belgian Bitcoin Embassy
@@ -135,19 +136,8 @@ Belgian Bitcoin Embassy is a volunteer-run network, not a business. Content here
// ---------------------------------------------------------------------------
export async function buildHomeMarkdown(): Promise<string> {
const meetups = await fetchJson<any[]>("/meetups", []);
const now = new Date();
const next = (Array.isArray(meetups) ? meetups : [])
.filter((m) => {
if (m.status && m.status !== "PUBLISHED") return false;
const start = getMeetupStartUtc(m.date, m.time || "00:00");
return !Number.isNaN(start.getTime()) && start >= now;
})
.sort(
(a, b) =>
getMeetupStartUtc(a.date, a.time || "00:00").getTime() -
getMeetupStartUtc(b.date, b.time || "00:00").getTime(),
)[0];
const meetups = await fetchMeetupsLive();
const next = partitionMeetups(meetups).upcoming[0];
let nextSection = "";
if (next) {
@@ -182,20 +172,8 @@ We help people in Belgium understand and adopt Bitcoin through education, meetup
}
export async function buildEventsMarkdown(): Promise<string> {
const meetups = await fetchJson<any[]>("/meetups", []);
const list = Array.isArray(meetups) ? meetups : [];
const now = new Date();
const upcoming = list.filter((m) => {
const start = getMeetupStartUtc(m.date, m.time || "00:00");
return !Number.isNaN(start.getTime()) && start >= now;
});
const past = list
.filter((m) => {
const start = getMeetupStartUtc(m.date, m.time || "00:00");
return !Number.isNaN(start.getTime()) && start < now;
})
.reverse();
const meetups = await fetchMeetupsLive();
const { upcoming, past } = partitionMeetups(meetups);
const renderMeetup = (m: any): string => {
const when = formatMeetupCivilDateLong(m.date);