From b46bbdcf846afce08ed246788196556c31cdab50 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Sat, 11 Jul 2026 23:37:48 -0400 Subject: [PATCH] feat(data): add builders for llms.txt and llms-full.txt files --- src/data/llms.ts | 186 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 src/data/llms.ts diff --git a/src/data/llms.ts b/src/data/llms.ts new file mode 100644 index 0000000..448be2b --- /dev/null +++ b/src/data/llms.ts @@ -0,0 +1,186 @@ +import type { CollectionEntry } from "astro:content"; +import { getCollection } from "astro:content"; +import { getAllExperience } from "@/data/experience"; +import { getAllPosts } from "@/data/post"; +import { getAllSpeaking } from "@/data/speaking"; +import { siteConfig } from "@/site.config"; + +/** Builders for /llms.txt (index) and /llms-full.txt (full content), per https://llmstxt.org */ + +const absUrl = (path: string) => new URL(path, import.meta.env.SITE).href; + +const isoDate = (date: Date) => date.toISOString().slice(0, 10); + +const yearRange = (start: Date, end?: Date) => + `${start.getFullYear()}–${end ? end.getFullYear() : "Present"}`; + +async function getAllContent() { + const [experience, projects, writing, speaking] = await Promise.all([ + getAllExperience(), + getCollection("projects").then((entries) => + entries.sort((a, b) => b.data.startDate.valueOf() - a.data.startDate.valueOf()), + ), + getAllPosts().then((entries) => + entries.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()), + ), + getAllSpeaking().then((entries) => + entries.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()), + ), + ]); + return { experience, projects, writing, speaking }; +} + +const writingUrl = (post: CollectionEntry<"writing">) => + post.data.externalUrl ?? absUrl(`/writing/${post.id}/`); + +/** Curated markdown index of the whole site. */ +export async function buildLlmsIndex(): Promise { + const { experience, projects, writing, speaking } = await getAllContent(); + + const lines: string[] = [ + `# ${siteConfig.title}`, + "", + `> ${siteConfig.description}`, + "", + `Personal site of ${siteConfig.author} — engineer and founder building decentralized identity infrastructure; co-founder and CEO of Sonr; W3C and DIF working-group member.`, + "", + "## Experience", + "", + ...experience.map((exp) => { + const { organization, role, startDate, endDate } = exp.data; + const detail = [role, organization].filter(Boolean).join(", "); + return `- [${exp.data.title}](${absUrl(`/experience/${exp.id}/`)}): ${detail} (${yearRange(startDate, endDate)})`; + }), + "", + "## Projects", + "", + ...projects.map((project) => { + const { title, description, startDate, endDate } = project.data; + const summary = description ? ` ${description}` : ""; + return `- [${title}](${absUrl(`/projects/${project.id}/`)}):${summary} (${yearRange(startDate, endDate)})`; + }), + "", + "## Writing", + "", + ...writing.map((post) => { + const { title, description, publisher, publishDate } = post.data; + const via = publisher ? ` — via ${publisher}` : ""; + return `- [${title}](${writingUrl(post)}): ${description}${via} (${isoDate(publishDate)})`; + }), + "", + "## Speaking", + "", + ...speaking.map((talk) => { + const { title, description, event, publishDate } = talk.data; + const at = event ? ` — ${event}` : ""; + return `- [${title}](${absUrl(`/speaking/${talk.id}/`)}): ${description}${at} (${isoDate(publishDate)})`; + }), + "", + "## Optional", + "", + `- [About](${absUrl("/about/")}): Bio, education, standards work`, + `- [Full content](${absUrl("/llms-full.txt")}): Every page's full text in one file`, + `- [RSS feed](${absUrl("/rss.xml")})`, + "", + ]; + + return lines.join("\n"); +} + +function section(heading: string, blocks: string[]): string[] { + return blocks.length ? [`## ${heading}`, "", ...blocks] : []; +} + +function entryBlock(header: string, meta: string[], body: string | undefined): string { + const content = body?.trim(); + return [`### ${header}`, "", ...meta, ...(content ? ["", content] : []), ""].join("\n"); +} + +/** Full markdown body of every entry, for direct LLM ingestion. */ +export async function buildLlmsFull(): Promise { + const { experience, projects, writing, speaking } = await getAllContent(); + + const lines: string[] = [ + `# ${siteConfig.title} — full content`, + "", + `> ${siteConfig.description}`, + "", + ...section( + "Experience", + experience.map((exp) => { + const { organization, role, location, url, description, tags, startDate, endDate } = + exp.data; + return entryBlock( + exp.data.title, + [ + `- URL: ${absUrl(`/experience/${exp.id}/`)}`, + `- Organization: ${organization}`, + ...(role ? [`- Role: ${role}`] : []), + ...(location ? [`- Location: ${location}`] : []), + ...(url ? [`- Website: ${url}`] : []), + `- Period: ${yearRange(startDate, endDate)}`, + ...(tags.length ? [`- Skills: ${tags.join(", ")}`] : []), + ...(description ? [`- Summary: ${description}`] : []), + ], + exp.body, + ); + }), + ), + ...section( + "Projects", + projects.map((project) => { + const { description, link, repo, tags, startDate, endDate } = project.data; + return entryBlock( + project.data.title, + [ + `- URL: ${absUrl(`/projects/${project.id}/`)}`, + ...(link ? [`- Link: ${link}`] : []), + ...(repo ? [`- Repository: ${repo}`] : []), + `- Period: ${yearRange(startDate, endDate)}`, + ...(tags.length ? [`- Skills: ${tags.join(", ")}`] : []), + ...(description ? [`- Summary: ${description}`] : []), + ], + project.body, + ); + }), + ), + ...section( + "Writing", + writing.map((post) => { + const { description, publisher, tags, publishDate, updatedDate } = post.data; + return entryBlock( + post.data.title, + [ + `- URL: ${writingUrl(post)}`, + ...(publisher ? [`- Publisher: ${publisher}`] : []), + `- Published: ${isoDate(publishDate)}`, + ...(updatedDate ? [`- Updated: ${isoDate(updatedDate)}`] : []), + ...(tags.length ? [`- Tags: ${tags.join(", ")}`] : []), + `- Summary: ${description}`, + ], + post.body, + ); + }), + ), + ...section( + "Speaking", + speaking.map((talk) => { + const { description, event, youtubeId, tags, publishDate } = talk.data; + return entryBlock( + talk.data.title, + [ + `- URL: ${absUrl(`/speaking/${talk.id}/`)}`, + ...(event ? [`- Event: ${event}`] : []), + `- Video: https://www.youtube.com/watch?v=${youtubeId}`, + `- Date: ${isoDate(publishDate)}`, + ...(tags.length ? [`- Tags: ${tags.join(", ")}`] : []), + `- Summary: ${description}`, + ], + talk.body, + ); + }), + ), + ]; + + return lines.join("\n"); +}