Walks every post's <img>/<a> (markdown + raw HTML, .md/.mdx, plus the frontmatter cover field) across both content collections, flags missing local assets and internal links that don't match any real route, and can optionally probe external refs for dead hosts (HEAD-then-GET, cached, concurrency-capped, off by default). Wired into `npm run build`'s postbuild step in warn mode — it can never fail the build, only crash-guards + logs a warning. External checks are a separate on-demand npm run audit:links:full since they touch the network and would make every build network-dependent otherwise. Report artefacts land in .ai/link-audit/ (gitignored). First real run: 0 missing local assets, 4 broken internal links (dead pre-Astro /Projects/... style paths from the old site structure), 18 dead external refs with --external (6 confirmed, 12 low-confidence itch.io-style 403s that are more likely bot-blocking than actually dead — flagged as such in the report rather than reported at face value). Ticks the "Link & image audit" wishlist item in GOAL.md; "Fix what the audit finds" is next and deliberately untouched here.
10 KiB
GOAL — where blog is going
North star
A blog worth subscribing to: honest, measured write-ups of what the lab actually did — every number taken from a real run, every AI-written word labelled as such — on a site I host, that loads in a second, and that will still resolve in ten years.
The archive spans 2015 to today: student game-design essays, internships, prototypes, and now the homelab. The bet is that the lab is the material — a Strix Halo box doing image→3D, agents running from a phone, a Backrooms map generator — and that writing it down carefully is worth more than posting often.
Modest by design: the goal is not an audience number. It is an archive that is presentable end to end, a publishing loop that doesn't stall, and a transparency record nobody else in this genre bothers to keep.
Target
- Me — writing is how the lab work gets thought through and remembered. The post is the artefact that outlives the project.
- The person who hit this from a search — someone debugging ROCm on gfx1151, or wondering whether agents-from-a-phone is real. They want measurements and code, not a listicle. They should be able to read it with JS off.
- People who follow the lab — via RSS, on their own reader, with no newsletter signup and no tracking beyond self-hosted Matomo.
What exists today (v1.0)
Astro 2 (astro-ink lineage) with MDX posts, client-side Lunr search, tags, RSS
and a sitemap; static build → zipgo on raspy2 at blog.dev.gabvdl.xyz. One
design, the blueprint — a cyanotype engineering drawing, graph-paper ground,
FIG. numbering, a drawing title block for a footer — with a load-bearing split
between base.css (structure, tokens only) and blueprint.css (identity).
Animation islands from @gabvdl/ui hydrate without flashing and leave complete
HTML for crawlers; ArticleExtras gives every post a full-screen image viewer, a
"play the figures as a story" control and copy buttons for free. 24 posts, three
of them recent AI-written lab articles carrying the mandatory AI-disclosure
callout. Analytics is self-hosted Matomo. No CI: nothing publishes on push,
only npm run deploy.
Being worked on
Horizons
Short term — v1.1: the archive is presentable (now)
Eleven years of posts moved through three themes and two hosts. Before writing anything new, make what exists hold up: no broken images or dead links, tags that mean something, every post legible in the blueprint look in both colour schemes, and correct metadata (dates, descriptions, canonical URLs, per-post social cards).
Middle term — v1.2: a publishing loop and a transparency record
Make the site say out loud what it is: a public page listing which posts were written by a model and which by me, wired to the lab's AI-transparency convention. Alongside it, the mechanics that keep publishing from stalling — a draft queue that renders locally, series/collection pages, prev-next navigation, reading time, and a full-content RSS feed that reads well in a reader.
Long term — v2 / someday
A site people subscribe to: a steady rhythm of lab write-ups, a few of them the canonical English-language reference for something niche (ROCm on Strix Halo, agent harness economics), an archive page that makes eleven years navigable, and a look that is unmistakably this site and nobody else's. Still static, still self-hosted, still free of trackers.
Wishlist
Ordered roughly by value. Items marked [human] need a taste call or writing that is mine to do — the goal-keeper must skip those.
The archive holds up
- Link & image audit: a script that walks every post's
<img>and<a>, checks local assets exist and reports external 404s, run as part ofnpm run build(warn) with a report artefact. Several 2015-2021 posts have already had broken images fixed by hand. —scripts/audit/link-image-audit.mjs, wired intopostbuild(internal checks only — fast, no network); full run with external HTTP checks vianpm run audit:links:full. Report at.ai/link-audit/report.{json,md}. First real run found 4 broken internal links (dead pre-Astro/Projects/...-style paths) and, with--external, 18 dead external refs (6 confirmed 404/unreachable, 12 low-confidence 403s — mostly itch.io, which blocks scripted requests from this host regardless of whether the page is live). 0 missing local assets. Fixing what it found is the next wishlist item, deliberately left undone here. - Fix what the audit finds — one pass over the old posts, replacing dead
external images with local copies under
public/assets/posts/<Post>/. - Tag taxonomy pass: consolidate the tag vocabulary (they accreted over
eleven years), give each tag a description, and make
/tagsa real index rather than a word cloud. - Dark-mode audit of the old posts — tables, inline code, callouts and raw-HTML blocks in pre-2023 posts, which predate the token system.
- Frontmatter completeness: every post gets a
description(used by search, RSS and og:description) and a sanecover; validate it in the zod schema so a missing one fails the build. - Per-post og:image: generate a blueprint-styled social card per post
(title + date + tags on the graph-paper ground) at build time. The site's
curated
public/og-image.pngstays as the home-page default — this is per-post, not a screenshot. - Archive page: all posts by year, with counts — eleven years is too much for one scroll.
Transparency (the disclosure record)
/transparencypage: every post listed with who wrote it — me, or a named model working from my repos and runs — derived from frontmatter, not hand-maintained. Explain the rule in one paragraph: any generated post carries the callout, and no number in one is estimated.author/generated_byfrontmatter field validated by the schema, feeding both that page and a small badge on the post header.- Build-time check: a post whose
generated_byis set but whose body lacks the AI-disclosure callout fails the build. The rule currently lives inCLAUDE.mdand depends on the agent remembering it — make it mechanical. - Adopt the lab's ai-transparency widget convention if it fits without
bloating the page (
~/projects/ai-transparency-widget).
The publishing loop
- Prev/next post navigation and related-by-tag links at the end of a post.
- Reading time + word count in the post header.
- Full-content RSS (currently likely summary-only) with correct enclosures for cover images, validated against a feed validator.
- Draft preview route —
src/drafts/renders inastro devbehind a flag and is excluded from the build, so a half-written post can be looked at without publishing it. - Series / collection support: a
seriesfrontmatter field with an index page (the three 2026 lab posts are already a series in everything but name). - Search quality: the Lunr index should cover descriptions and tags, and
the results UI should show a snippet — the index is already a
postbuildstep whosedist/mirror is load-bearing, don't break that. - Blueprint-styled 404 page and a proper
/about.
Craft & performance
- Lighthouse pass on a post page — image dimensions,
loading="lazy", font loading, no layout shift from the animation islands. - Image pipeline: convert the large post screenshots to WebP/AVIF with width variants at build time; the TRELLIS post alone is image-heavy.
- Print stylesheet for a long technical post.
- [human] Decide whether the blueprint identity extends to the RSS/og surfaces or stays on-site only.
Writing (mine)
- [human] A post on the homelab itself — Traefik, Authelia, the generated root compose, and why it is worth running.
- [human] A post on what a year of agent-run projects actually cost, using the real numbers the lab already records.
Non-goals (for now)
- A newsletter, signups, or an email list. RSS is the subscription.
- Any third-party analytics, ads, comments platform, or embedded tracker. Self-hosted Matomo, cookie-less, is the ceiling.
- CI that publishes on push. Deploying is a deliberate act; the deploy script's guard (refuse without a fresh search index) is part of that.
- A CMS or a headless backend. A post is one markdown file in the repo.
- Chasing SEO or posting frequency for its own sake. Fewer, measured posts.
- A second design. The switcher was deliberately removed; blueprint is the look.
Guard rails (for the goal-keeper)
- One wishlist item per run, finished end-to-end, then commit and push.
originis the self-hosted Gitea (canonical);githubis a dormant mirror — pushing there is optional and nothing depends on it. - Never publish a generated post without the AI-disclosure callout, verbatim
as specified in
CLAUDE.md, as the first thing in the body. This is not negotiable and not something to ask about. - Never invent a number. Benchmarks, timings, costs and code in a post come from a real run or a real file — measure or read it, never estimate.
- Don't write a new post on a scheduled run. Editorial voice is Gabriel's; the goal-keeper improves the site, fixes the archive, and prepares tooling. A post is written only when asked for by name.
- Respect the CSS split:
base.cssis structure and reads tokens only;blueprint.cssfills them. Tokens live onhtml:root(a barehtml {}loses to:rootregardless of order), and design rules keep thehtmlprefix. - Import islands by one specifier (
src/components/islands/X.tsx, with the extension) — mixing relative and aliased imports breaks the Astro client build at generate time. - Don't narrow the Tailwind content glob — it scans
.md/.mdxon purpose, or classes used only inside posts vanish. - The search index is a
postbuildstep that writespublic/and mirrors intodist/; that mirror is load-bearing.npm run deployrefuses without it. - Verify before deploying:
npm run build, then actually look at the rendered page (screenshot it) in both light and dark mode.