mirror of
https://github.com/andwati/andwati.com.git
synced 2026-10-22 02:54:26 +00:00
Extend llms.txt to all content types; fix a gitignore bug
scripts/generate-llms.astro-site.mjs covers writings, portfolio, bookshelf, and blogs (unblocked now that site.config.ts exists from the feeds/meta work), wired into apps/site's build script. Also fixes .gitignore: the bare `public` pattern (meant to ignore the Zola build's root-level output directory) was matching at any depth, so apps/site/public/ and apps/cms/public/ were being silently excluded from every commit despite `git add` reporting them as staged for directory adds (the ignore-file warning only surfaces for an explicitly named path, not a recursive directory add — easy to miss). Scoped to `/public` so only the root output dir is ignored. Picks up robots.txt, the Astro scaffold's favicon, llms.txt, and Strapi's own public/robots.txt, none of which were actually tracked until now.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
// Generates apps/site/public/llms.txt for the new Astro site, covering all
|
||||
// four content types. Run before `astro build` (wired into
|
||||
// apps/site/package.json's build script) so it's included as a static file.
|
||||
//
|
||||
// Site metadata is duplicated from apps/site/src/site.config.ts rather than
|
||||
// imported — this script runs as plain Node outside Vite/Astro's pipeline,
|
||||
// and the config is small enough that duplicating it beats adding a
|
||||
// cross-module-system import.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
|
||||
const baseUrl = "https://andwati.com";
|
||||
const siteTitle = "Thoughts and Musings";
|
||||
const summary =
|
||||
"Ian Andwati's personal digital-legacy site covering computer science, mathematics, life, and beliefs — writings, portfolio, and a curated bookshelf.";
|
||||
|
||||
// Resolved relative to this file, not process.cwd() — this script runs as
|
||||
// part of apps/site's build script, whose cwd is apps/site, not the repo
|
||||
// root where content/ lives.
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const contentDir = path.join(root, "content");
|
||||
const outFile = path.join(root, "apps", "site", "public", "llms.txt");
|
||||
|
||||
const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/;
|
||||
|
||||
function walk(dir) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) return walk(full);
|
||||
return entry.isFile() && entry.name.endsWith(".md") ? [full] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function slugFor(dir, file) {
|
||||
const rel = path.relative(dir, file).split(path.sep).join("/");
|
||||
return rel.endsWith("/index.md")
|
||||
? rel.slice(0, -"/index.md".length)
|
||||
: rel.slice(0, -".md".length);
|
||||
}
|
||||
|
||||
function readEntries(typeDir) {
|
||||
return walk(typeDir)
|
||||
.map((file) => {
|
||||
const raw = fs.readFileSync(file, "utf8");
|
||||
const match = raw.match(FRONTMATTER_RE);
|
||||
if (!match) return null;
|
||||
const frontmatter = parseToml(match[1]);
|
||||
return { slug: slugFor(typeDir, file), frontmatter };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const writings = readEntries(path.join(contentDir, "writings"))
|
||||
.filter((e) => !e.frontmatter.draft)
|
||||
.sort((a, b) =>
|
||||
String(b.frontmatter.date).localeCompare(String(a.frontmatter.date)),
|
||||
);
|
||||
const portfolio = readEntries(path.join(contentDir, "portfolio")).filter(
|
||||
(e) => !e.frontmatter.draft,
|
||||
);
|
||||
const bookshelf = readEntries(path.join(contentDir, "bookshelf"));
|
||||
const blogs = readEntries(path.join(contentDir, "blogs"));
|
||||
|
||||
function link(title, url, description) {
|
||||
return `- [${title}](${url})${description ? `: ${description.replace(/\s+/g, " ").trim()}` : ""}`;
|
||||
}
|
||||
|
||||
const lines = [
|
||||
`# ${siteTitle}`,
|
||||
"",
|
||||
`> ${summary}`,
|
||||
"",
|
||||
"## Main pages",
|
||||
"",
|
||||
`- [Home](${baseUrl}/): Introduction and latest writings.`,
|
||||
`- [Writings](${baseUrl}/writings/): Long-form essays and technical posts.`,
|
||||
`- [Portfolio](${baseUrl}/portfolio/): Projects and professional work.`,
|
||||
`- [Bookshelf](${baseUrl}/bookshelf/): Books and papers read, with notes.`,
|
||||
`- [Blogs](${baseUrl}/blogs/): Curated external blogs.`,
|
||||
`- [Tags](${baseUrl}/tags/): Writings grouped by topic.`,
|
||||
"",
|
||||
"## Writings",
|
||||
"",
|
||||
...writings.map((e) =>
|
||||
link(
|
||||
e.frontmatter.title,
|
||||
`${baseUrl}/writings/${e.slug}/`,
|
||||
e.frontmatter.description,
|
||||
),
|
||||
),
|
||||
"",
|
||||
"## Portfolio",
|
||||
"",
|
||||
...portfolio.map((e) =>
|
||||
link(e.frontmatter.title, `${baseUrl}/portfolio/`, e.frontmatter.outcome),
|
||||
),
|
||||
"",
|
||||
"## Bookshelf",
|
||||
"",
|
||||
...bookshelf.map((e) =>
|
||||
link(
|
||||
e.frontmatter.title,
|
||||
`${baseUrl}/bookshelf/`,
|
||||
(e.frontmatter.authors ?? []).join(", "),
|
||||
),
|
||||
),
|
||||
"",
|
||||
"## Blogs followed",
|
||||
"",
|
||||
...blogs.map((e) => link(e.frontmatter.title, e.frontmatter.url)),
|
||||
"",
|
||||
"## Feeds and discovery",
|
||||
"",
|
||||
`- [RSS feed](${baseUrl}/rss.xml): Full writings feed in RSS format.`,
|
||||
`- [JSON feed](${baseUrl}/feed.json): Full writings feed in JSON Feed format.`,
|
||||
`- [Sitemap](${baseUrl}/sitemap-index.xml): Machine-readable index of public pages.`,
|
||||
"",
|
||||
];
|
||||
|
||||
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
||||
fs.writeFileSync(outFile, lines.join("\n"));
|
||||
console.log(
|
||||
`Generated ${path.relative(root, outFile)} (${writings.length} writings, ${portfolio.length} portfolio, ${bookshelf.length} bookshelf, ${blogs.length} blogs).`,
|
||||
);
|
||||
Reference in New Issue
Block a user