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. */} - -