/** * Server-side builders for the llms.txt file (llmstxt.org) and the plain-markdown * page mirrors (`.md`). Everything here is generated at request time from * live data (settings, meetups, FAQs, posts) so it never goes stale. * * Mirrors deliberately omit nav, footer, and the legal disclaimer block — an LLM * reading these needs the actual content, not repeated chrome. */ import { apiUrl } from "./api-base"; import { formatMeetupCivilDateLong, getMeetupStartUtc } from "./meetupEventTime"; export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://belgianbitcoinembassy.org"; async function fetchJson(path: string, fallback: T): Promise { try { const res = await fetch(apiUrl(path), { next: { revalidate: 300 } }); if (!res.ok) return fallback; return (await res.json()) as T; } catch { return fallback; } } function formatPostDate(value?: string): string | null { if (!value) return null; const d = new Date(value); if (Number.isNaN(d.getTime())) return null; return d.toLocaleDateString("en-GB", { year: "numeric", month: "long", day: "numeric", timeZone: "UTC", }); } /** Social channels with their human description, in display order. */ const SOCIAL_CHANNELS: { key: string; name: string; description: string }[] = [ { key: "telegram_link", name: "Telegram", description: "Main Belgian chat group for daily discussion and local coordination.", }, { key: "nostr_link", name: "Nostr", description: "Follow the BBE on the censorship-resistant social protocol.", }, { key: "x_link", name: "X", description: "Latest local announcements and event drops.", }, { key: "youtube_link", name: "YouTube", description: "Past talks, educational content, and meetup recordings.", }, { key: "discord_link", name: "Discord", description: "Technical discussions, node running, and project collaboration.", }, { key: "linkedin_link", name: "LinkedIn", description: "The Belgian Bitcoin professional network.", }, ]; function configuredChannels(settings: Record) { return SOCIAL_CHANNELS.map((c) => ({ ...c, url: settings[c.key]?.trim() })) .filter((c): c is typeof c & { url: string } => !!c.url && /^https?:\/\//i.test(c.url), ); } // --------------------------------------------------------------------------- // llms.txt // --------------------------------------------------------------------------- export async function buildLlmsTxt(): Promise { const [settings, meetups] = await Promise.all([ fetchJson>("/settings/public", {}), fetchJson("/meetups", []), ]); 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 channels = configuredChannels(settings); const channelNames = channels.map((c) => c.name).join(", "); const communityDescription = channelNames ? `How to connect on ${channelNames}` : "How to connect with the community"; 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."; return `# Belgian Bitcoin Embassy > A sovereign, non-commercial community organizing monthly Bitcoin meetups in Antwerp. Education, technical discussion, and adoption. Not a company. Belgian Bitcoin Embassy is a volunteer-run network, not a business. Content here covers meetups, FAQs about the community, and curated Bitcoin/Nostr commentary. ${upcomingLine} ## Pages - [About](${SITE_URL}/index.html.md): Who we are and what we do - [Events](${SITE_URL}/events.md): Upcoming and past Bitcoin meetups in Belgium - [FAQ](${SITE_URL}/faq.md): Common questions about the community and how to get involved - [Blog](${SITE_URL}/blog.md): Curated Bitcoin and Nostr content - [Community](${SITE_URL}/community.md): ${communityDescription} ## Optional - [Privacy](${SITE_URL}/privacy.md) - [Terms](${SITE_URL}/terms.md) - [Contact](${SITE_URL}/contact.md) `; } // --------------------------------------------------------------------------- // Page mirrors // --------------------------------------------------------------------------- export async function buildHomeMarkdown(): Promise { const meetups = await fetchJson("/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]; let nextSection = ""; if (next) { const when = formatMeetupCivilDateLong(next.date); const bits = [when, next.time, next.location].filter(Boolean).join(" · "); nextSection = ` ## Next Meetup **${next.title}** — ${bits} See all events: ${SITE_URL}/events.md`; } return `# Belgian Bitcoin Embassy > A sovereign, non-commercial community organizing monthly Bitcoin meetups in Antwerp. Education, technical discussion, and adoption. Not a company. ## The Mission "Fix the money, fix the world." We help people in Belgium understand and adopt Bitcoin through education, meetups, and community. We are not a company, but a sovereign network of individuals building a sounder future.${nextSection} ## More - Events: ${SITE_URL}/events.md - FAQ: ${SITE_URL}/faq.md - Blog: ${SITE_URL}/blog.md - Community: ${SITE_URL}/community.md `; } export async function buildEventsMarkdown(): Promise { const meetups = await fetchJson("/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 renderMeetup = (m: any): string => { const when = formatMeetupCivilDateLong(m.date); const meta = [when, m.time, m.location].filter(Boolean).join(" · "); const organizer = m.organizer?.name || "Belgian Bitcoin Embassy"; const lines = [`### ${m.title}`, "", `${meta}`, "", `Organized by ${organizer}.`]; if (m.description) lines.push("", m.description.trim()); lines.push("", `Details: ${SITE_URL}/events/${m.id}`); return lines.join("\n"); }; const sections: string[] = [ "# Events", "", "Past and upcoming Bitcoin meetups in Belgium, organized by the Belgian Bitcoin Embassy.", ]; sections.push("", "## Upcoming"); sections.push( "", upcoming.length ? upcoming.map(renderMeetup).join("\n\n") : "No upcoming events are currently scheduled. Check back soon.", ); if (past.length) { sections.push("", "## Past Events", "", past.map(renderMeetup).join("\n\n")); } return sections.join("\n") + "\n"; } export async function buildFaqMarkdown(): Promise { const faqs = await fetchJson("/faqs?all=true", []); const list = Array.isArray(faqs) ? faqs : []; const header = `# Frequently Asked Questions Everything you need to know about the Belgian Bitcoin Embassy.`; if (!list.length) { return `${header}\n\nNo FAQs are available yet.\n`; } const body = list .map((f) => `## ${f.question}\n\n${(f.answer || "").trim()}`) .join("\n\n"); return `${header}\n\n${body}\n`; } export async function buildBlogMarkdown(): Promise { const data = await fetchJson<{ posts: any[]; total: number }>( "/posts?limit=100", { posts: [], total: 0 }, ); const posts = Array.isArray(data?.posts) ? data.posts : []; const header = `# Blog Curated Bitcoin and Nostr content from the Belgian Bitcoin Embassy.`; if (!posts.length) { return `${header}\n\nNo posts have been published yet.\n`; } const body = posts .map((p) => { const date = formatPostDate(p.publishedAt || p.createdAt); const meta = [p.author, date].filter(Boolean).join(" · "); const lines = [`## ${p.title}`]; if (meta) lines.push("", `_${meta}_`); if (p.excerpt) lines.push("", p.excerpt.trim()); lines.push("", `Read: ${SITE_URL}/blog/${p.slug}`); return lines.join("\n"); }) .join("\n\n"); return `${header}\n\n${body}\n`; } export async function buildCommunityMarkdown(): Promise { const settings = await fetchJson>( "/settings/public", {}, ); const channels = configuredChannels(settings); const header = `# Community Connect with local Belgian Bitcoiners, builders, and educators across every platform.`; if (!channels.length) { return `${header}\n\nCommunity channel links are being set up. Check back soon.\n`; } const body = channels .map((c) => `- [${c.name}](${c.url}): ${c.description}`) .join("\n"); return `${header}\n\n## Channels\n\n${body}\n`; } export async function buildContactMarkdown(): Promise { const settings = await fetchJson>( "/settings/public", {}, ); const channels = configuredChannels(settings); const header = `# Contact The best way to reach us is through our community channels. We are a decentralized community — there is no central office or email inbox.`; const lines: string[] = [header, "", "## Channels"]; if (channels.length) { lines.push( "", ...channels.map((c) => `- [${c.name}](${c.url}): ${c.description}`), ); } else { lines.push("", "Community channel links are being set up. Check back soon."); } lines.push( "", "## Meetups", "", `The best way to connect is in person. Come to our monthly meetup — see upcoming events at ${SITE_URL}/events.md`, ); return lines.join("\n") + "\n"; }