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,100 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Consistency check: confirms the number of upcoming meetups reported by the
|
||||
* live /events page, the /events.md mirror, and the /llms.txt summary all match.
|
||||
*
|
||||
* Catches the exact "confidently wrong" drift this check exists to prevent:
|
||||
* a mirror or summary going stale relative to the rendered page.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/check-consistency.mjs [baseUrl]
|
||||
* BASE_URL=https://belgianbitcoinembassy.org node scripts/check-consistency.mjs
|
||||
*
|
||||
* Exits 0 if all three agree, 1 on any mismatch or fetch failure.
|
||||
*/
|
||||
|
||||
const baseUrl = (
|
||||
process.argv[2] ||
|
||||
process.env.BASE_URL ||
|
||||
"http://localhost:3000"
|
||||
).replace(/\/$/, "");
|
||||
|
||||
async function getText(path) {
|
||||
const url = `${baseUrl}${path}`;
|
||||
const res = await fetch(url, {
|
||||
headers: { "User-Agent": "bbe-consistency-check" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`GET ${url} -> ${res.status}`);
|
||||
}
|
||||
return res.text();
|
||||
}
|
||||
|
||||
/** /llms.txt: "There are currently N upcoming meetup(s) scheduled." */
|
||||
function parseLlmsTxt(text) {
|
||||
const m = text.match(/currently\s+(\d+)\s+upcoming meetup/i);
|
||||
if (!m) throw new Error("llms.txt: could not find upcoming-meetup count line");
|
||||
return Number(m[1]);
|
||||
}
|
||||
|
||||
/** /events.md: count "### " headings within the "## Upcoming" section. */
|
||||
function parseEventsMd(text) {
|
||||
const lines = text.split("\n");
|
||||
let inUpcoming = false;
|
||||
let count = 0;
|
||||
let sawUpcoming = false;
|
||||
for (const line of lines) {
|
||||
if (/^##\s+Upcoming\b/.test(line)) {
|
||||
inUpcoming = true;
|
||||
sawUpcoming = true;
|
||||
continue;
|
||||
}
|
||||
if (/^##\s+/.test(line) && inUpcoming) inUpcoming = false; // next section
|
||||
if (inUpcoming && /^###\s+/.test(line)) count += 1;
|
||||
}
|
||||
if (!sawUpcoming) throw new Error("events.md: no '## Upcoming' section found");
|
||||
return count;
|
||||
}
|
||||
|
||||
/** /events: data-upcoming-count attribute rendered into the HTML. */
|
||||
function parseEventsHtml(text) {
|
||||
const m = text.match(/data-upcoming-count="(\d+)"/);
|
||||
if (!m) throw new Error("events: data-upcoming-count attribute not found in HTML");
|
||||
return Number(m[1]);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [llmsTxt, eventsMd, eventsHtml] = await Promise.all([
|
||||
getText("/llms.txt"),
|
||||
getText("/events.md"),
|
||||
getText("/events"),
|
||||
]);
|
||||
|
||||
const counts = {
|
||||
"/llms.txt": parseLlmsTxt(llmsTxt),
|
||||
"/events.md": parseEventsMd(eventsMd),
|
||||
"/events": parseEventsHtml(eventsHtml),
|
||||
};
|
||||
|
||||
const values = Object.values(counts);
|
||||
const allMatch = values.every((v) => v === values[0]);
|
||||
|
||||
console.log(`Consistency check against ${baseUrl}`);
|
||||
for (const [name, value] of Object.entries(counts)) {
|
||||
console.log(` ${name.padEnd(12)} upcoming = ${value}`);
|
||||
}
|
||||
|
||||
if (!allMatch) {
|
||||
console.error(
|
||||
`\n✗ MISMATCH: upcoming-meetup counts disagree (${values.join(", ")}).`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n✓ All sources agree: ${values[0]} upcoming meetup(s).`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`✗ Consistency check failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user