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
+26
View File
@@ -0,0 +1,26 @@
---
import { getCollection, render } from "astro:content";
import Base from "../../layouts/Base.astro";
const blogs = await getCollection("blogs");
const rendered = await Promise.all(
blogs.map(async (entry) => ({
entry,
Content: (await render(entry)).Content,
})),
);
---
<Base title="Blogs I follow">
<h1>Blogs I follow</h1>
{
rendered.map(({ entry, Content }) => (
<div class="card">
<h3>
<a href={entry.data.url}>{entry.data.title}</a>
</h3>
<Content />
</div>
))
}
</Base>
+38
View File
@@ -0,0 +1,38 @@
---
import { getCollection } from "astro:content";
import Base from "../../layouts/Base.astro";
const items = (await getCollection("bookshelf")).sort(
(a, b) =>
(b.data.date_finished?.valueOf() ?? 0) -
(a.data.date_finished?.valueOf() ?? 0),
);
---
<Base title="Bookshelf">
<h1>Bookshelf</h1>
{
items.map((entry) => (
<div class="card">
<h3>
{entry.data.title} <span class="tag">{entry.data.kind}</span>
</h3>
<p class="meta">{entry.data.authors.join(", ")}</p>
{entry.data.rating && <p class="meta">{"★".repeat(entry.data.rating)}</p>}
{entry.data.status && <p class="meta">{entry.data.status}</p>}
{entry.data.kind === "book" && entry.data.isbn && <p class="meta">ISBN {entry.data.isbn}</p>}
{entry.data.kind === "paper" && entry.data.arxiv_id && (
<p class="meta">
arXiv:{entry.data.arxiv_id}
{entry.data.url && (
<>
{" "}
· <a href={entry.data.url}>source</a>
</>
)}
</p>
)}
</div>
))
}
</Base>
+25 -14
View File
@@ -1,17 +1,28 @@
---
import { getCollection } from "astro:content";
import Base from "../layouts/Base.astro";
const writings = (await getCollection("writings", ({ data }) => !data.draft))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
.slice(0, 3);
---
---
<Base title="Home">
<h1>Ian Andwati</h1>
<p>Computer science, mathematics, life, and beliefs.</p>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<h1>Astro</h1>
</body>
</html>
<h2>Latest writings</h2>
{
writings.length === 0 ? (
<p class="meta">No writings published yet.</p>
) : (
writings.map((entry) => (
<div class="card">
<h3><a href={`/writings/${entry.id}/`}>{entry.data.title}</a></h3>
<p class="meta">{entry.data.date.toDateString()}</p>
{entry.data.description && <p>{entry.data.description}</p>}
</div>
))
)
}
</Base>
+28
View File
@@ -0,0 +1,28 @@
---
import { getCollection } from "astro:content";
import Base from "../../layouts/Base.astro";
const projects = (
await getCollection("portfolio", ({ data }) => !data.draft)
).sort(
(a, b) =>
(b.data.date_start?.valueOf() ?? 0) - (a.data.date_start?.valueOf() ?? 0),
);
---
<Base title="Portfolio">
<h1>Portfolio</h1>
{
projects.map((entry) => (
<div class="card">
<h3>{entry.data.title}</h3>
<p class="meta">{entry.data.role}</p>
{entry.data.description && <p>{entry.data.description}</p>}
{entry.data.outcome && <p>{entry.data.outcome}</p>}
{entry.data.links.map((link) => (
<a href={link.url}>{link.label}</a>
))}
</div>
))
}
</Base>
@@ -0,0 +1,27 @@
---
import { getCollection, render } from "astro:content";
import Base from "../../layouts/Base.astro";
export async function getStaticPaths() {
const writings = await getCollection("writings", ({ data }) => !data.draft);
return writings.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
---
<Base title={entry.data.title}>
<h1>{entry.data.title}</h1>
<p class="meta">
{entry.data.date.toDateString()}
{entry.data.updated && <> · updated {entry.data.updated.toDateString()}</>}
</p>
{entry.data.taxonomies?.tags.map((tag) => <span class="tag">#{tag}</span>)}
<article>
<Content />
</article>
</Base>
+22
View File
@@ -0,0 +1,22 @@
---
import { getCollection } from "astro:content";
import Base from "../../layouts/Base.astro";
const writings = (
await getCollection("writings", ({ data }) => !data.draft)
).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---
<Base title="Writings">
<h1>Writings</h1>
{
writings.map((entry) => (
<div class="card">
<h3><a href={`/writings/${entry.id}/`}>{entry.data.title}</a></h3>
<p class="meta">{entry.data.date.toDateString()}</p>
{entry.data.description && <p>{entry.data.description}</p>}
{entry.data.taxonomies?.tags.map((tag) => <span class="tag">#{tag}</span>)}
</div>
))
}
</Base>