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:
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Single source of truth for meetup data used by the public /events page, the
|
||||
* /events.md mirror, and the /llms.txt summary line. Centralizing the fetch +
|
||||
* upcoming/past partition guarantees those three can never disagree on the count.
|
||||
*
|
||||
* Fetched with `no-store` so every render reflects the live backend — this is
|
||||
* what keeps crawlers (which don't run JS) and llms.txt consumers from seeing a
|
||||
* stale or build-time-empty list.
|
||||
*/
|
||||
import { apiUrl } from "./api-base";
|
||||
import { getMeetupStartUtc } from "./meetupEventTime";
|
||||
|
||||
export interface Meetup {
|
||||
id: string;
|
||||
title: string;
|
||||
date: string;
|
||||
time?: string;
|
||||
location?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
organizer?: { name?: string; slug?: string } | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** Fetch all publicly-visible meetups from the backend, live (uncached). */
|
||||
export async function fetchMeetupsLive(): Promise<Meetup[]> {
|
||||
try {
|
||||
const res = await fetch(apiUrl("/meetups"), { cache: "no-store" });
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? (data as Meetup[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export interface PartitionedMeetups {
|
||||
upcoming: Meetup[];
|
||||
past: Meetup[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Split meetups into upcoming (start >= now, soonest first) and past (start <
|
||||
* now, most recent first). Meetups with an unparseable date are dropped.
|
||||
*/
|
||||
export function partitionMeetups(
|
||||
meetups: Meetup[],
|
||||
now: Date = new Date(),
|
||||
): PartitionedMeetups {
|
||||
const upcoming: Meetup[] = [];
|
||||
const past: Meetup[] = [];
|
||||
|
||||
for (const m of meetups) {
|
||||
const start = getMeetupStartUtc(m.date, m.time || "00:00");
|
||||
if (Number.isNaN(start.getTime())) continue;
|
||||
if (start >= now) upcoming.push(m);
|
||||
else past.push(m);
|
||||
}
|
||||
|
||||
const startMs = (m: Meetup) =>
|
||||
getMeetupStartUtc(m.date, m.time || "00:00").getTime();
|
||||
upcoming.sort((a, b) => startMs(a) - startMs(b));
|
||||
past.sort((a, b) => startMs(b) - startMs(a));
|
||||
|
||||
return { upcoming, past };
|
||||
}
|
||||
|
||||
/** Number of upcoming meetups — the single value llms.txt and events.md share. */
|
||||
export function countUpcoming(meetups: Meetup[], now: Date = new Date()): number {
|
||||
return partitionMeetups(meetups, now).upcoming.length;
|
||||
}
|
||||
Reference in New Issue
Block a user