North star, target readers, versioned horizons (v1.1 present the eleven-year archive → v1.2 transparency page + publishing loop → v2 a site worth subscribing to) and a wishlist. Guard rails make the existing rules explicit for unattended runs: the AI-disclosure callout is mandatory, numbers are measured not estimated, and no scheduled run writes a post. CLAUDE.md now points at it.
166 lines
8.1 KiB
Markdown
166 lines
8.1 KiB
Markdown
# CLAUDE.md — blog
|
|
|
|
Guidance for Claude Code working in this repo. See [README.md](README.md) for the
|
|
human-facing version, and [GOAL.md](GOAL.md) for where the site is going — read
|
|
it before proposing features.
|
|
|
|
## What this is
|
|
|
|
My personal website / blog, live at **https://blog.dev.gabvdl.xyz**. Astro 2
|
|
(based on the *astro-ink* theme): markdown/MDX posts, client-side Lunr search,
|
|
tags, RSS, sitemap. Static build → `dist/`, served by zipgo on raspy2.
|
|
|
|
`origin` is the self-hosted Gitea (`git.gabvdl.xyz/gabrielvidal/blog`) — that is
|
|
canonical. `github` is a secondary mirror of the old
|
|
`GabrielVidal1/gabrielvidal1.github.io` repo; nothing depends on it. There is no
|
|
CI: **nothing publishes on push**, only `npm run deploy`.
|
|
|
|
## The look: "blueprint"
|
|
|
|
One design, no switcher. The site is a cyanotype engineering drawing — white
|
|
line-work on print blue, graph-paper ground, registration ticks on cards, `FIG.`
|
|
numbering, a drawing title block for a footer. It lives in two files, and the
|
|
split is load-bearing:
|
|
|
|
- `src/styles/base.css` — **structure only**. Every rule reads design tokens
|
|
(`--bg`, `--accent`, `--font-display`, …); nothing hard-codes a colour. This
|
|
is also where all the responsive work is.
|
|
- `src/styles/blueprint.css` — **identity only**: it fills those tokens in and
|
|
adds the drawing flourishes.
|
|
|
|
Two gotchas if you touch them:
|
|
|
|
- The token block is `html:root`, not `html`. `base.css` declares its fallbacks
|
|
on `:root`, and a pseudo-class outranks a type selector *regardless of order*
|
|
— a bare `html {}` silently loses and the whole site renders in the fallback
|
|
white/pink palette.
|
|
- Design rules keep the `html ` prefix so a `blueprint.css` rule always
|
|
outweighs the single-class structural rule it refines.
|
|
|
|
## Animation islands (`src/components/islands/`)
|
|
|
|
Text and lists animate with `@gabvdl/ui`'s progressive family. They are React
|
|
islands, and the pattern in `islands/hydrated.ts` is what keeps that safe:
|
|
|
|
- `useHydrated()` flips in a **layout** effect, so the server (and the hydration
|
|
render) emit the *finished* content — real headings, a full card list — and
|
|
the animation only takes over on the client, before first paint. The HTML a
|
|
crawler or a JS-less reader receives is complete, and nothing flashes.
|
|
- `TypedText` additionally keeps the finished string in the flow as a hidden
|
|
ghost and paints the typewriter over it (`.typed`/`.typed__ghost`/`.typed__live`
|
|
in base.css). Without that, a tagline growing from one line to four shoves the
|
|
page down while it types.
|
|
|
|
**Import islands by the same specifier everywhere** — `src/components/islands/X.tsx`,
|
|
with the extension. Mixing a relative (`./islands/X`) and an aliased import of the
|
|
same island makes Astro 2's client build fail at *generate* time with
|
|
`Cannot find the built path for …`.
|
|
|
|
`ArticleExtras` (loaded on every post) works off the rendered DOM, not the
|
|
frontmatter: it wires every article image into the full-screen viewer, adds a
|
|
"play the figures as a story" control to any post with more than one image, and
|
|
portals a copy button into every code block. So **posts get all of this for
|
|
free** — there is nothing to add to a markdown file.
|
|
|
|
`@gabvdl/ui` is pinned to a version that only exists on the **local Verdaccio**
|
|
(`registry.lab` / `127.0.0.1:4873`, see the homelab `CLAUDE.md`); public npm
|
|
stops at 0.19.0. `npm install` therefore needs the registry up — `npm run build`
|
|
does not, it just uses `node_modules`.
|
|
|
|
## External links
|
|
|
|
Anything pointing off-site opens in a new tab and carries the `↗` glyph. The
|
|
rule lives in one place per surface:
|
|
|
|
- components (nav, hero, footer, link buttons) — `class="external-link"` plus
|
|
`target`/`rel`, decided by `isExternal()` in `src/utils/links.ts`;
|
|
- posts and pages — the `rehypeExternalLinks` pass in `astro.config.mjs`, at
|
|
build time so it survives with JS off.
|
|
|
|
That pass handles **three** shapes, and all three occur in this repo: markdown
|
|
link syntax (a hast `element`), raw `<a>` HTML inside a `.mdx` file (already an
|
|
MDX JSX node by then), and raw `<a>` HTML inside a `.md` file (still an
|
|
unparsed `raw` string). Adding a case only to the first is the easy mistake —
|
|
most of the older posts write raw HTML.
|
|
|
|
## Writing a post
|
|
|
|
One file: `src/content/blog/<YYYY-MM-DD>-<Title>.md` (or `.mdx`). Frontmatter is
|
|
validated by the zod schema in `src/content/config.ts` — `title` (≤100 chars),
|
|
`tags` (required array), and optional `description` / `techs` / `date` / `cover` /
|
|
`project` / `links`. Drafts are plain markdown in `src/drafts/`.
|
|
|
|
Images go in `public/assets/posts/<PostName>/` and are referenced with raw HTML:
|
|
|
|
```html
|
|
<img src="/assets/posts/<PostName>/shot.jpeg" alt="…" class="w-full rounded-lg shadow-lg">
|
|
```
|
|
|
|
## ⚠️ AI-generated posts MUST carry the disclaimer
|
|
|
|
**Any post you generate gets an AI-disclosure callout as the first thing in the
|
|
body, above the opening paragraph.** This is not optional and not something to
|
|
ask about — if you wrote the post, the callout ships with it. Only a post I wrote
|
|
myself may omit it.
|
|
|
|
Paste this block verbatim at the top of the post body (adjust only the model name
|
|
and, if the sourcing genuinely differs, the second sentence):
|
|
|
|
```html
|
|
<aside class="not-prose my-8 flex items-start gap-4 rounded-lg border border-theme-primary/30 bg-theme-primary/5 p-5 dark:border-theme-dark-primary/40 dark:bg-theme-dark-primary/10">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="mt-0.5 h-7 w-7 shrink-0 text-theme-primary dark:text-theme-dark-primary">
|
|
<path d="M12 2v4"/>
|
|
<rect x="3" y="6" width="18" height="14" rx="3"/>
|
|
<circle cx="8.5" cy="12" r="1.4" fill="currentColor" stroke="none"/>
|
|
<circle cx="15.5" cy="12" r="1.4" fill="currentColor" stroke="none"/>
|
|
<path d="M9 16.5h6"/>
|
|
<path d="M1.5 11v4M22.5 11v4"/>
|
|
</svg>
|
|
<div class="text-sm leading-relaxed">
|
|
<p class="font-bold text-theme-primary dark:text-theme-dark-primary">AI-generated article</p>
|
|
<p class="mt-1 text-gray-700 dark:text-gray-300">
|
|
This post was written by <strong>Claude Opus 5</strong>, working from my own
|
|
projects, tests, benchmarks, memories and past conversations. The
|
|
measurements, code and screenshots come from my hardware and my repositories —
|
|
the writing is the model's.
|
|
</p>
|
|
</div>
|
|
</aside>
|
|
```
|
|
|
|
`2026-08-06-Trellis2-Strix-Halo.md` is the reference example.
|
|
|
|
Alongside the callout: **don't invent numbers.** Benchmarks, timings and code
|
|
snippets in a generated post must come from a real run or a real file — measure
|
|
or read it, don't estimate and round.
|
|
|
|
## Gotchas
|
|
|
|
- **Tailwind scans markdown.** `tailwind.config.cjs` includes `.md`/`.mdx` in
|
|
`content` on purpose. Before that, a class used only in a post was emitted only
|
|
by luck (if some `.astro` file happened to use it too), so callouts and image
|
|
classes silently rendered unstyled. Don't narrow that glob.
|
|
- **Dark mode is `class`-based.** Every custom colour needs a `dark:` counterpart
|
|
— use the theme tokens (`theme-primary` / `theme-dark-primary`), not raw
|
|
palette colours, so the site's theme switcher keeps working.
|
|
- **The search index is a `postbuild` step** (`scripts/search/prepare-index.js`),
|
|
which runs *after* Astro copies `public/` → `dist/`. It writes `public/` **and**
|
|
mirrors into `dist/`; that mirror is load-bearing. Without it every deploy ships
|
|
a search index one post behind. `scripts/deploy.sh` refuses to deploy if
|
|
`dist/search-index.json` is missing.
|
|
- **`npm run deploy` skips og-screenshot** unlike the scaffolded homelab projects
|
|
— this site has a curated `public/og-image.png` and a screenshot of the index
|
|
would be strictly worse.
|
|
|
|
## Verify before deploying
|
|
|
|
```sh
|
|
npm run build # must pass; check the post renders
|
|
npx astro preview # or serve dist/ and look at the page
|
|
npm run deploy # build + zipgo deploy to raspy2
|
|
```
|
|
|
|
For a generated post, actually look at the rendered page (screenshot it) — check
|
|
the callout, the images, tables and code blocks in **both** light and dark mode
|
|
before deploying.
|