From 8b977ff5f94301c0c45f3957a7a880cb4184df24 Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Mon, 18 May 2026 18:14:02 -0300 Subject: [PATCH 1/3] post: beginning again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A meta post kicking off the new notebook era. Honest about prior attempts stalling, frames writing as a thinking tool rather than an audience-facing thing, sets out what the notebook will and won't try to be. Tagged "meta" — content-about-the-notebook rather than any of the existing topical tags. --- src/pages/writing/beginning-again/index.mdx | 52 +++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/pages/writing/beginning-again/index.mdx diff --git a/src/pages/writing/beginning-again/index.mdx b/src/pages/writing/beginning-again/index.mdx new file mode 100644 index 0000000..c12b9f6 --- /dev/null +++ b/src/pages/writing/beginning-again/index.mdx @@ -0,0 +1,52 @@ +import ArticleLayout from "@/layouts/ArticleLayout.astro"; + +export const metadata = { + 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. + + From 17aa97cc9611c7fcf25d518008ec1d596c10afd1 Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Tue, 19 May 2026 16:39:40 -0300 Subject: [PATCH 2/3] refactor: posts use yaml frontmatter + layout reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Posts no longer need to wrap themselves in . The layout is referenced from yaml frontmatter and the body is pure markdown. Cleaner, less boilerplate, less JSX in things that should read like prose. Example post header — what authors write now: --- layout: '@/layouts/ArticleLayout.astro' title: Beginning again date: 2026-05-18 tag: meta description: ... --- I've started a blog several times. ... Changes: - ArticleLayout reads from `Astro.props.frontmatter` when used as a layout reference, falls back to direct props when used inline from an .astro file (backward-compat for project pages). Auto-composes `meta` as "date · tag" so authors don't have to compute it. - YAML dates are auto-parsed into Date objects, which broke string sorts and rendered as ISO timestamps. Coerced to YYYY-MM-DD via a duck-typed asDateString helper in the layout, the writing index, and the homepage. - Listing pages (homepage, /writing/) now accept both `frontmatter` (yaml-front-matter pattern) and `metadata` (export const pattern). - Templates (note.mdx, architecture.mdx, project.mdx) updated to the yaml-frontmatter shape. project.mdx uses `frontmatter.X` to read fields inside the body for the ProjectMeta block. - Beginning-again post rewritten in the new format — header is yaml, body is markdown, no JSX wrapping. - cspell: add "frontmatter". Project pages in `src/pages/projects//index.astro` keep their existing pattern (export const metadata + ArticleLayout wrapper) — ArticleLayout still supports it via direct props. --- cspell.json | 2 +- src/layouts/ArticleLayout.astro | 65 ++++++++++++++++++--- src/pages/index.astro | 23 ++++++-- src/pages/writing/beginning-again/index.mdx | 25 +++----- src/pages/writing/index.astro | 23 ++++++-- src/templates/architecture.mdx | 29 +++------ src/templates/note.mdx | 32 ++++------ src/templates/project.mdx | 52 ++++++++--------- 8 files changed, 144 insertions(+), 107 deletions(-) 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..e66cdab 100644 --- a/src/layouts/ArticleLayout.astro +++ b/src/layouts/ArticleLayout.astro @@ -3,18 +3,69 @@ 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 — date · tag is the convention. +const explicitMeta = Astro.props.meta ?? fm.meta; +const composedMeta = [dateStr, fm.tag].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 index c12b9f6..aa7faa5 100644 --- a/src/pages/writing/beginning-again/index.mdx +++ b/src/pages/writing/beginning-again/index.mdx @@ -1,19 +1,10 @@ -import ArticleLayout from "@/layouts/ArticleLayout.astro"; - -export const metadata = { - 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.", -}; - - +--- +layout: '@/layouts/ArticleLayout.astro' +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*. @@ -48,5 +39,3 @@ 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..1110a17 100644 --- a/src/templates/architecture.mdx +++ b/src/templates/architecture.mdx @@ -1,29 +1,20 @@ -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.", -}; +--- +layout: '@/layouts/ArticleLayout.astro' +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 +34,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..f57e8fb 100644 --- a/src/templates/note.mdx +++ b/src/templates/note.mdx @@ -1,30 +1,20 @@ -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.", -}; +--- +layout: '@/layouts/ArticleLayout.astro' +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 +38,3 @@ error message or log output here ```bash # commands here ``` - - diff --git a/src/templates/project.mdx b/src/templates/project.mdx index 77b4158..a2d6dc8 100644 --- a/src/templates/project.mdx +++ b/src/templates/project.mdx @@ -1,39 +1,35 @@ -import ArticleLayout from "@/layouts/ArticleLayout.astro"; -import ProjectMeta from "@/components/ui/ProjectMeta.astro"; +--- +layout: '@/layouts/ArticleLayout.astro' +title: Project name +description: 'One sentence: what it is and why it matters.' +label: system +tags: [go, kubernetes] +status: active +repo: '' +demo: '' +meta: system +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 +54,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. */} - - From 0e7be53357fdbc03c12b9c97a4bf9ff8ddceb89e Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Tue, 19 May 2026 16:46:33 -0300 Subject: [PATCH 3/3] feat: default ArticleLayout for posts under writing/ and projects/ The \`layout: '@/layouts/ArticleLayout.astro'\` line was boilerplate on every post. Made it optional via a small remark plugin in astro.config.mjs that injects the layout into the frontmatter for any mdx file under /pages/writing/ or /pages/projects/ that doesn't already have one. Authors can still override per-post by setting \`layout:\` explicitly. While here: - Layout's \`meta\` composition now falls back to \`fm.label\` so project pages auto-render their label above the title without needing an explicit \`meta:\` line. - Drop the \`layout:\` line from note / architecture / project templates and the beginning-again post. - Drop the \`meta: system\` redundancy from the project template. --- astro.config.mjs | 30 +++++++++++++++++++-- src/layouts/ArticleLayout.astro | 7 +++-- src/pages/writing/beginning-again/index.mdx | 1 - src/templates/architecture.mdx | 1 - src/templates/note.mdx | 1 - src/templates/project.mdx | 2 -- 6 files changed, 33 insertions(+), 9 deletions(-) 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/src/layouts/ArticleLayout.astro b/src/layouts/ArticleLayout.astro index e66cdab..91e50ed 100644 --- a/src/layouts/ArticleLayout.astro +++ b/src/layouts/ArticleLayout.astro @@ -62,9 +62,12 @@ function asDateString(d: unknown): string | undefined { const dateStr = asDateString(fm.date); -// Compose `meta` if not explicit — date · tag is the convention. +// Compose `meta` if not explicit: +// posts: "YYYY-MM-DD · tag" +// projects: "label" const explicitMeta = Astro.props.meta ?? fm.meta; -const composedMeta = [dateStr, fm.tag].filter(Boolean).join(" · "); +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/writing/beginning-again/index.mdx b/src/pages/writing/beginning-again/index.mdx index aa7faa5..1fffe85 100644 --- a/src/pages/writing/beginning-again/index.mdx +++ b/src/pages/writing/beginning-again/index.mdx @@ -1,5 +1,4 @@ --- -layout: '@/layouts/ArticleLayout.astro' title: Beginning again date: 2026-05-18 tag: meta diff --git a/src/templates/architecture.mdx b/src/templates/architecture.mdx index 1110a17..c6cf969 100644 --- a/src/templates/architecture.mdx +++ b/src/templates/architecture.mdx @@ -1,5 +1,4 @@ --- -layout: '@/layouts/ArticleLayout.astro' title: The decision or system being described date: YYYY-MM-DD tag: architecture diff --git a/src/templates/note.mdx b/src/templates/note.mdx index f57e8fb..46171f8 100644 --- a/src/templates/note.mdx +++ b/src/templates/note.mdx @@ -1,5 +1,4 @@ --- -layout: '@/layouts/ArticleLayout.astro' title: Short, specific title describing the exact problem date: YYYY-MM-DD tag: field note diff --git a/src/templates/project.mdx b/src/templates/project.mdx index a2d6dc8..0ab683d 100644 --- a/src/templates/project.mdx +++ b/src/templates/project.mdx @@ -1,5 +1,4 @@ --- -layout: '@/layouts/ArticleLayout.astro' title: Project name description: 'One sentence: what it is and why it matters.' label: system @@ -7,7 +6,6 @@ tags: [go, kubernetes] status: active repo: '' demo: '' -meta: system backHref: /projects/ backLabel: projects ---