diff --git a/astro.config.mjs b/astro.config.mjs
index b314ecb..ef1ec55 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -6,9 +6,35 @@ import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import path from 'path';
+/**
+ * Inject a default `layout:` into MDX frontmatter when the file lives under
+ * a content-page directory and the author didn't set one. Lets posts skip
+ * the boilerplate header line in 99% of cases.
+ */
+function injectDefaultLayout(layoutPath, includeDirs) {
+ return () => (_tree, file) => {
+ const fm = file?.data?.astro?.frontmatter;
+ if (!fm || fm.layout) return;
+ const sourcePath = file.history?.[0] ?? file.path ?? '';
+ if (!includeDirs.some((dir) => sourcePath.includes(dir))) return;
+ fm.layout = layoutPath;
+ };
+}
+
export default defineConfig({
site: 'https://viniciusdc.github.io',
- integrations: [mdx({ remarkPlugins: [remarkMath], rehypePlugins: [rehypeKatex] })],
+ integrations: [
+ mdx({
+ remarkPlugins: [
+ remarkMath,
+ injectDefaultLayout('@/layouts/ArticleLayout.astro', [
+ '/pages/writing/',
+ '/pages/projects/',
+ ]),
+ ],
+ rehypePlugins: [rehypeKatex],
+ }),
+ ],
devToolbar: { enabled: false },
vite: {
plugins: [tailwindcss()],
@@ -18,4 +44,4 @@ export default defineConfig({
},
},
},
-});
\ No newline at end of file
+});
diff --git a/cspell.json b/cspell.json
index 4a9e145..f12d4d5 100644
--- a/cspell.json
+++ b/cspell.json
@@ -10,7 +10,7 @@
"keycloak", "oidc", "oauth", "saml", "jwt", "jwks",
"fastapi", "pydantic", "scrapy", "uvicorn",
"vinicius", "cerutti", "Ceture", "viniciusdc", "Catarina", "UFSC",
- "astro", "tailwind", "shadcn", "fontsource", "lhci", "lightbox",
+ "astro", "tailwind", "shadcn", "fontsource", "lhci", "lightbox", "frontmatter",
"woff", "woff2", "oklch", "rgb", "rgba", "hsl",
"clamp", "minmax", "prefers",
"nebari", "jmespath", "snistrict",
diff --git a/src/layouts/ArticleLayout.astro b/src/layouts/ArticleLayout.astro
index 8000e44..91e50ed 100644
--- a/src/layouts/ArticleLayout.astro
+++ b/src/layouts/ArticleLayout.astro
@@ -3,18 +3,72 @@ import Layout from "@/layouts/Layout.astro";
import PageRule from "@/components/ui/PageRule.astro";
import ImageLightbox from "@/components/ui/ImageLightbox.astro";
-interface Props {
- title: string;
+/**
+ * Two ways to use this layout:
+ *
+ * 1. As an Astro layout reference from MDX/MD frontmatter:
+ * ---
+ * layout: '@/layouts/ArticleLayout.astro'
+ * title: ...
+ * date: 2026-05-18
+ * tag: meta
+ * description: ...
+ * ---
+ * The body markdown renders into the layout's . `meta` is
+ * auto-composed as "date · tag" when both are present.
+ *
+ * 2. As a wrapping component from an `.astro` page that needs JSX/props:
+ *
+ * ...
+ *
+ */
+interface Frontmatter {
+ title?: string;
description?: string;
- meta: string;
- subtitle: string;
- backHref: string;
- backLabel: string;
+ date?: string;
+ tag?: string;
+ subtitle?: string;
+ meta?: string;
+ backHref?: string;
+ backLabel?: string;
nextHref?: string;
nextLabel?: string;
}
-const { title, description, meta, subtitle, backHref, backLabel, nextHref, nextLabel } = Astro.props;
+interface Props extends Frontmatter {
+ frontmatter?: Frontmatter;
+}
+
+const fm = Astro.props.frontmatter ?? {};
+const title = Astro.props.title ?? fm.title ?? "Untitled";
+const description = Astro.props.description ?? fm.description;
+const subtitle = Astro.props.subtitle ?? fm.subtitle ?? description ?? "";
+const backHref = Astro.props.backHref ?? fm.backHref ?? "/writing/";
+const backLabel = Astro.props.backLabel ?? fm.backLabel ?? "writing";
+const nextHref = Astro.props.nextHref ?? fm.nextHref;
+const nextLabel = Astro.props.nextLabel ?? fm.nextLabel;
+
+// YAML auto-parses ISO dates into Date objects. Normalize to YYYY-MM-DD
+// so authors can write `date: 2026-05-18` without remembering to quote it.
+// `instanceof Date` can fail across realms; duck-type instead.
+function asDateString(d: unknown): string | undefined {
+ if (!d) return undefined;
+ if (typeof (d as { toISOString?: () => string }).toISOString === "function") {
+ return (d as Date).toISOString().slice(0, 10);
+ }
+ // Already a string — strip any trailing ISO time component just in case.
+ return String(d).slice(0, 10);
+}
+
+const dateStr = asDateString(fm.date);
+
+// Compose `meta` if not explicit:
+// posts: "YYYY-MM-DD · tag"
+// projects: "label"
+const explicitMeta = Astro.props.meta ?? fm.meta;
+const labelOrTag = fm.tag ?? (fm as { label?: string }).label;
+const composedMeta = [dateStr, labelOrTag].filter(Boolean).join(" · ");
+const meta = explicitMeta ?? composedMeta;
---
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 6d64e41..fe0d41e 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -10,7 +10,10 @@ interface PostMeta {
tag: string;
description: string;
}
-interface PostModule { metadata?: PostMeta; }
+// `.mdx`/`.md` files with YAML frontmatter expose it as `frontmatter`.
+// `.astro` files (and `.mdx` files using `export const metadata`) expose `metadata`.
+// Accept either so we can mix patterns during the migration.
+interface PostModule { metadata?: PostMeta; frontmatter?: PostMeta; }
function toHref(base: string, path: string) {
return base + path.replace(/^\.\/[^/]+/, '').replace(/\/index\.(astro|mdx)$/, '/');
@@ -19,9 +22,21 @@ function toHref(base: string, path: string) {
const writingAstro = import.meta.glob('./writing/*/index.astro', { eager: true });
const writingMdx = import.meta.glob('./writing/*/index.mdx', { eager: true });
-const allRecent = [
- ...Object.entries({ ...writingAstro, ...writingMdx }).map(([p, m]) => ({ href: toHref('/writing', p), ...m.metadata! })),
-]
+// YAML auto-parses ISO dates into Date objects; normalize so sort/compare works.
+// `instanceof Date` can fail across realms; duck-type instead.
+function asDateString(d: unknown): string | undefined {
+ if (!d) return undefined;
+ if (typeof (d as { toISOString?: () => string }).toISOString === "function") {
+ return (d as Date).toISOString().slice(0, 10);
+ }
+ return String(d).slice(0, 10);
+}
+
+const allRecent = Object.entries({ ...writingAstro, ...writingMdx })
+ .map(([p, m]) => {
+ const data = m.frontmatter ?? m.metadata ?? {} as PostMeta;
+ return { href: toHref('/writing', p), ...data, date: asDateString(data.date) };
+ })
.filter(p => p.title && p.date)
.sort((a, b) => b.date!.localeCompare(a.date!));
diff --git a/src/pages/writing/beginning-again/index.mdx b/src/pages/writing/beginning-again/index.mdx
new file mode 100644
index 0000000..1fffe85
--- /dev/null
+++ b/src/pages/writing/beginning-again/index.mdx
@@ -0,0 +1,40 @@
+---
+title: Beginning again
+date: 2026-05-18
+tag: meta
+description: "Notes on starting a notebook — what's here, what isn't, what I'm trying not to do."
+---
+
+I've started a blog several times. Most attempts stalled within a few months. Partly because I picked a niche too narrow — *Kubernetes field notes* was the last one — and partly because I kept forgetting what writing is actually *for*.
+
+For me it isn't for an audience. It's for thinking.
+
+I'll have a vague intuition about something, sit down to write a paragraph, and ten minutes in realize the intuition didn't survive contact with sentences. The diagram I had in my head turns out not to compile. The reading I was working from doesn't quite say what I thought it said. That's the value: writing finds the gaps that thinking-in-your-head papers over.
+
+So this notebook is a different premise from the ones before. It's for me. Kept in public because writing for a vague hypothetical *someone* forces a level of clarity that private notes never reach — but the audience is incidental.
+
+## What lives here
+
+Mostly whatever has my attention.
+
+Software, often — Kubernetes most of my paid work: bare-metal clusters, identity, GPU runtimes, the small failures that take a week to find. But also AI papers I'm chewing on, math I keep wandering back to (my degree is in applied math; I haven't found a way to leave it alone), design ideas, games that won't leave me alone, the occasional philosophical thing without a clean answer.
+
+The connective tissue isn't a topic. It's me.
+
+## What I'm trying not to do
+
+Wait for things to be finished. A lot of what shows up here will be drafts I want to come back to. Some of it will be wrong — I'll either fix the post or leave a note saying so — and that's fine. Waiting for perfect is how I lost the last several years of writing I meant to do.
+
+Chase an audience either. If you're here, hi, and thank you. But the bar for whether a post exists is whether I learned something from writing it, not whether anyone else learned something reading it.
+
+## What I'm trying to do
+
+Write small. Short notes. Reactions. Sketches of ideas. Not every entry needs to be an essay.
+
+Write honestly. If I don't understand something, I want the post to show that. If I change my mind later, I want the next post to show that too. The pretense of always-having-known is exhausting and not even useful.
+
+Write more often than I think is sensible.
+
+## See you in a bit
+
+That's it for now. If you find something worth reading here — or worth correcting — please tell me.
diff --git a/src/pages/writing/index.astro b/src/pages/writing/index.astro
index 6b3eb60..0bdeef5 100644
--- a/src/pages/writing/index.astro
+++ b/src/pages/writing/index.astro
@@ -4,7 +4,8 @@ import PageRule from "@/components/ui/PageRule.astro";
interface PostMeta { title: string; date?: string; tag: string; description: string; }
interface DebugMeta { label: string; title: string; description: string; tags: string[]; href?: string; }
-interface PostModule { metadata?: PostMeta | DebugMeta; }
+// Accept either `frontmatter` (YAML at the top of .mdx) or `metadata` (export const).
+interface PostModule { metadata?: PostMeta | DebugMeta; frontmatter?: PostMeta | DebugMeta; }
function toHref(path: string) {
return '/writing' + path.replace(/^\.\//, '/').replace(/\/index\.(astro|mdx)$/, '/');
@@ -19,14 +20,24 @@ const allModules = {
...import.meta.glob('./*/index.mdx', { eager: true }),
};
+// YAML auto-parses ISO dates into Date objects; normalize so sort/compare works.
+// `instanceof Date` can fail across realms; duck-type instead.
+function asDateString(d: unknown): string | undefined {
+ if (!d) return undefined;
+ if (typeof (d as { toISOString?: () => string }).toISOString === "function") {
+ return (d as Date).toISOString().slice(0, 10);
+ }
+ return String(d).slice(0, 10);
+}
+
const entries = Object.entries(allModules)
- .filter(([, m]) => m.metadata)
- .map(([path, m]) => {
- const meta = m.metadata!;
+ .map(([path, m]) => [path, m.frontmatter ?? m.metadata] as const)
+ .filter((tuple): tuple is readonly [string, PostMeta | DebugMeta] => tuple[1] != null)
+ .map(([path, meta]) => {
if (isDebug(meta)) {
- return { href: meta.href ?? toHref(path), title: meta.title, date: undefined, tag: meta.tags[0] ?? 'debugging', description: meta.description };
+ return { href: meta.href ?? toHref(path), title: meta.title, date: undefined as string | undefined, tag: meta.tags[0] ?? 'debugging', description: meta.description };
}
- return { href: toHref(path), title: meta.title, date: meta.date, tag: meta.tag, description: meta.description };
+ return { href: toHref(path), title: meta.title, date: asDateString(meta.date), tag: meta.tag, description: meta.description };
})
.sort((a, b) => (b.date ?? '').localeCompare(a.date ?? ''));
diff --git a/src/templates/architecture.mdx b/src/templates/architecture.mdx
index 59c58ac..c6cf969 100644
--- a/src/templates/architecture.mdx
+++ b/src/templates/architecture.mdx
@@ -1,29 +1,19 @@
-import ArticleLayout from "@/layouts/ArticleLayout.astro";
-
-export const metadata = {
- title: "The decision or system being described",
- date: "YYYY-MM-DD",
- tag: "architecture",
- description: "What this essay argues or explains — one sentence.",
-};
+---
+title: The decision or system being described
+date: YYYY-MM-DD
+tag: architecture
+description: What this essay argues or explains — one sentence.
+---
{/*
Checklist before publishing:
[ ] Title frames the decision, not just the component ("X as Y" or "Why we shaped X this way")
- [ ] metadata.description describes what the essay argues or explains
- [ ] metadata.tag matches the area: operator design · state systems · platform · reliability
+ [ ] description describes what the essay argues or explains
+ [ ] tag matches the area: operator design · state systems · platform · reliability
[ ] Sections flow: context → constraint → decision → tradeoffs
[ ] No placeholder text remains
*/}
-
-
## Context
{/* What is the system or problem space? What forces or constraints shaped this? Keep it tight. */}
@@ -43,5 +33,3 @@ export const metadata = {
## Boundaries
{/* Where does this design stop? What's explicitly out of scope? */}
-
-
diff --git a/src/templates/note.mdx b/src/templates/note.mdx
index 7f3a0dd..46171f8 100644
--- a/src/templates/note.mdx
+++ b/src/templates/note.mdx
@@ -1,30 +1,19 @@
-import ArticleLayout from "@/layouts/ArticleLayout.astro";
-
-export const metadata = {
- title: "Short, specific title describing the exact problem",
- date: "YYYY-MM-DD",
- tag: "field note",
- description: "One sentence. What does this note explain and why does it matter.",
-};
+---
+title: Short, specific title describing the exact problem
+date: YYYY-MM-DD
+tag: field note
+description: One sentence. What does this note explain and why does it matter.
+---
{/*
Checklist before publishing:
[ ] Title is specific — describes the exact problem, not just the area
- [ ] metadata.date is accurate (YYYY-MM-DD)
- [ ] metadata.tag is one of: field note · debugging · security · architecture · platform
- [ ] metadata.description is one sentence — states what the note explains
- [ ] backHref/nextHref links are correct
+ [ ] date is accurate (YYYY-MM-DD)
+ [ ] tag is one of: field note · debugging · security · architecture · platform
+ [ ] description is one sentence — states what the note explains
[ ] No placeholder text remains
*/}
-
-
## Background
{/* What were you doing when you hit this? Give just enough context. One short paragraph. */}
@@ -48,5 +37,3 @@ error message or log output here
```bash
# commands here
```
-
-
diff --git a/src/templates/project.mdx b/src/templates/project.mdx
index 77b4158..0ab683d 100644
--- a/src/templates/project.mdx
+++ b/src/templates/project.mdx
@@ -1,39 +1,33 @@
-import ArticleLayout from "@/layouts/ArticleLayout.astro";
-import ProjectMeta from "@/components/ui/ProjectMeta.astro";
+---
+title: Project name
+description: 'One sentence: what it is and why it matters.'
+label: system
+tags: [go, kubernetes]
+status: active
+repo: ''
+demo: ''
+backHref: /projects/
+backLabel: projects
+---
-export const metadata = {
- label: "system", // system · tool · operator · library · script
- title: "Project name",
- description: "One sentence: what it is and why it matters.",
- tags: ["go", "kubernetes"], // used by /projects/ listing
- status: "active", // active · dormant · archived
- repo: "", // optional: full URL
- demo: "", // optional: full URL
-};
+import ProjectMeta from "@/components/ui/ProjectMeta.astro";
{/*
Checklist before publishing:
[ ] Title is concrete — names the thing, not the category
- [ ] metadata.label is one of: system · tool · operator · library · script
- [ ] metadata.tags are short, lowercase, useful for filtering on /projects/
- [ ] metadata.status is honest (don't say "active" if you haven't touched it in a year)
- [ ] metadata.repo / metadata.demo set if applicable; otherwise leave as ""
+ [ ] label is one of: system · tool · operator · library · script
+ [ ] tags are short, lowercase, useful for filtering on /projects/
+ [ ] status is honest (don't say "active" if you haven't touched it in a year)
+ [ ] repo / demo set if applicable; otherwise leave as ""
+ [ ] Update `meta:` if you change `label` (it's what shows above the title)
[ ] No placeholder text remains
*/}
-
-
## What it is
@@ -58,5 +52,3 @@ export const metadata = {
{/* Where it is now: deployed, maintained, abandoned, replaced by something else.
If you'd recommend or warn off others, say so. */}
-
-