Files
blog/GOAL.md
Gabriel Vidal d40c354c4a feat(audit): add a link & image audit script, warn-only in the build
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.
2026-08-10 01:58:30 +02:00

190 lines
10 KiB
Markdown

# 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
<!-- Claims by goal-keeper agents. One bullet per in-flight item; the agent
removes its own line in the same commit that ticks the checkbox. Leave
the section empty (this comment only) when nothing is in flight. -->
## 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
- [x] **Link & image audit**: a script that walks every post's `<img>` and `<a>`,
checks local assets exist and reports external 404s, run as part of
`npm 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 into `postbuild` (internal checks only — fast, no network); full run
with external HTTP checks via `npm 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 `/tags` a 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 sane `cover`; 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.png` stays 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)
- [ ] **`/transparency` page**: 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_by` frontmatter field** validated by the schema,
feeding both that page and a small badge on the post header.
- [ ] **Build-time check**: a post whose `generated_by` is set but whose body
lacks the AI-disclosure callout fails the build. The rule currently lives
in `CLAUDE.md` and 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 in `astro dev` behind a
flag and is excluded from the build, so a half-written post can be looked
at without publishing it.
- [ ] **Series / collection support**: a `series` frontmatter 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 `postbuild`
step whose `dist/` 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. `origin`
is the self-hosted **Gitea** (canonical); `github` is 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.css` is structure and reads tokens only;
`blueprint.css` fills them. Tokens live on `html:root` (a bare `html {}` loses
to `:root` regardless of order), and design rules keep the `html ` prefix.
- **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`/`.mdx` on purpose,
or classes used only inside posts vanish.
- The **search index is a `postbuild` step** that writes `public/` *and* mirrors
into `dist/`; that mirror is load-bearing. `npm run deploy` refuses without it.
- Verify before deploying: `npm run build`, then actually look at the rendered
page (screenshot it) in **both** light and dark mode.