Wire Astro content collections to the shared content/ directory

Adds a custom Astro loader that reads the Zola-style TOML frontmatter
(+++ ... +++) directly, so content/ stays the single source of truth
with no YAML conversion step. Defines writings/portfolio/bookshelf/
blogs collections and basic list/detail pages for each, rendering the
seed entries end-to-end (verified in-browser via `pnpm --filter site
dev`). This is the first runnable MVP: home, writings list + post
detail, portfolio, bookshelf (books and papers), and curated blogs
all render from real content/ files.
This commit is contained in:
2026-08-30 18:14:34 +03:00
parent cae62d4a66
commit 16c8dff5d5
13 changed files with 383 additions and 18 deletions
+59
View File
@@ -0,0 +1,59 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { extname, join, relative } from "node:path";
import type { Loader } from "astro/loaders";
import { marked } from "marked";
import { parse as parseToml } from "smol-toml";
const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/;
function walk(dir: string): string[] {
const out: string[] = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...walk(full));
} else if (entry.isFile() && extname(entry.name) === ".md") {
out.push(full);
}
}
return out;
}
/**
* Loads content from Zola-style TOML frontmatter (`+++ ... +++`) Markdown
* files, so the site can read the same content/ directory Strapi's custom
* provider will sync to — no YAML/JSON conversion step.
*/
export function tomlContentLoader(contentDir: string): Loader {
return {
name: "toml-content-loader",
load: async ({ store, config, logger }) => {
store.clear();
const dirUrl = new URL(contentDir, config.root);
let files: string[] = [];
try {
statSync(dirUrl);
files = walk(dirUrl.pathname);
} catch {
logger.warn(`toml-content-loader: no directory at ${dirUrl.pathname}`);
return;
}
for (const file of files) {
const raw = readFileSync(file, "utf8");
const match = raw.match(FRONTMATTER_RE);
if (!match) {
logger.warn(
`toml-content-loader: skipping ${file}, no +++ frontmatter found`,
);
continue;
}
const [, frontmatter, body] = match;
const data = parseToml(frontmatter) as Record<string, unknown>;
const relPath = relative(dirUrl.pathname, file);
const id = relPath.replace(/\/index\.md$/, "").replace(/\.md$/, "");
const html = await marked.parse(body.trim());
store.set({ id, data, rendered: { html } });
}
},
};
}