Files
BelgianBitcoinEmbassy/frontend/app/blog/[slug]/remarkNostr.ts
T
bbeandCursor 2ef68222bf 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>
2026-06-29 03:57:32 +02:00

65 lines
2.0 KiB
TypeScript

import { nip19 } from "nostr-tools";
// Matches NIP-27 `nostr:` references (and bare bech32 entities) embedded in
// article text: npub/nprofile (mentions) and note/nevent/naddr (notes).
const NOSTR_RE =
/(?:nostr:)?((?:npub|nprofile|note|nevent|naddr)1[023456789acdefghjklmnpqrstuvwxyz]+)/gi;
function isValidEntity(bech32: string): boolean {
try {
const { type } = nip19.decode(bech32);
return ["npub", "nprofile", "note", "nevent", "naddr"].includes(type);
} catch {
return false;
}
}
// Splits a plain-text value into text + `link` nodes, where each link's url is
// `nostr:<bech32>`. The markdown `a` renderer detects that scheme and swaps in
// the live profile/note component.
function splitText(value: string): any[] {
const out: any[] = [];
let last = 0;
NOSTR_RE.lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = NOSTR_RE.exec(value)) !== null) {
const bech32 = m[1];
if (!isValidEntity(bech32)) continue;
if (m.index > last) {
out.push({ type: "text", value: value.slice(last, m.index) });
}
out.push({
type: "link",
url: `nostr:${bech32}`,
children: [{ type: "text", value: m[0] }],
});
last = m.index + m[0].length;
}
if (out.length === 0) return [{ type: "text", value }];
if (last < value.length) out.push({ type: "text", value: value.slice(last) });
return out;
}
// Remark plugin: walk the mdast tree and replace `nostr:` tokens inside text
// nodes. Skips code and existing links so identifiers there are left untouched.
export function remarkNostr() {
return (tree: any) => {
const walk = (node: any) => {
if (!node || !Array.isArray(node.children)) return;
const next: any[] = [];
for (const child of node.children) {
if (child.type === "text") {
next.push(...splitText(child.value));
continue;
}
if (child.type !== "link" && child.type !== "inlineCode" && child.type !== "code") {
walk(child);
}
next.push(child);
}
node.children = next;
};
walk(tree);
};
}