/** * 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 } 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(path: string, fallback: T): Promise { try { const res = await fetch(apiUrl(path), { cache: "no-store" }); 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", {}), fetchMeetupsLive(), ]); const upcomingCount = countUpcoming(meetups); 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"; // 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.` : "There are currently 0 upcoming meetups scheduled; the community meets monthly."; 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 fetchMeetupsLive(); const next = partitionMeetups(meetups).upcoming[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 fetchMeetupsLive(); const { upcoming, past } = partitionMeetups(meetups); 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"; }