Colophon

Framework & Language

The site is built with Astro, a modern static-site generator. Content is authored in TypeScript and Astro components. The output is a fully static site — no client-side JavaScript framework, no runtime server. You can find the full source code and CI/CD setup at https://github.com/shaftoe/personal-website.

Styling

Theme forked from zaggonaut. All styling uses Tailwind CSS v4, processed through the @tailwindcss/vite plugin. Typography is handled by @tailwindcss/typography. Icons provided by @lucide/astro. Dark mode respects the user’s system preference and can be toggled manually, the choice is persisted in localStorage.

Fonts

Content

Blog posts live as Markdown files with YAML frontmatter, managed through Astro’s content collections. The changelog is parsed directly from the project’s CHANGELOG.md file at build time using marked. All Markdown pages and articles are rendered with Astro’s native Sätteri processor, extended with a small custom hast plugin that appends a shareable anchor link to every heading — so any section of a long page can be deep-linked directly (try hovering a heading on this very page). Each article’s reading time is estimated at build time from its prose: code blocks are stripped and the remaining word count is divided at 200 words per minute, so the figure stays in sync with the content automatically.

Tooling

Infrastructure

Versioning and deploys are fully automated. Every push to master triggers a release workflow powered by semantic-release, which analyzes conventional commit messages, bumps package.json version, updates CHANGELOG.md, and publishes a GitHub Release.

When a new version tag is created, a deploy workflow triggers a Netlify build that runs bun run build and publishes the resulting dist/ directory to their CDN. The same workflow also runs on a schedule every four hours to keep the homepage’s Bluesky posts and blog content up to date without manual intervention.

Analytics

Web analytics are powered by Umami, a simple, fast, privacy-focused, open-source alternative to Google Analytics. It gives total control over the data and does not violate the privacy of visitors. The tracking script is built from source and served self-hosted — see the Privacy Policy for details. It is loaded conditionally and skipped on the expenses page when the user is authenticated.

HTML Validation

Generated HTML is validated against the W3C Markup Validator (vnu-jar) to ensure standards compliance. Every page produced by the build is checked automatically as part of the test suite. Known framework-level exceptions (Astro module script placement after </html>, astro-island inline styles, heading hierarchy in blog snippet cards) are suppressed with documented justifications.

Profile Picture

The homepage avatar is not a static asset — it is fetched at build time from the author’s self-hosted PDS (social.l3x.in) and converted to WebP with Sharp. Three PDS-native XRPC calls are made (no dependency on Bluesky’s central infrastructure): handle → DID resolution, profile record fetch, and blob download. If the PDS is unreachable or the profile has no avatar, the build fails rather than serving a stale image. A pixelated variant used in the Hero reveal animation is generated from the same source via nearest-neighbour downscale/upscale so the pair always stays in sync.

Social Images

Open Graph and Twitter Card images are generated at build time as PNGs using Sharp (which leverages librsvg for SVG rendering). Each image is a 1200×630 terminal-style banner using the Press Start 2P pixel font, with a dark background, traffic-light window chrome, and green accent colors matching the site’s theme. Three variants are generated: a default banner for the homepage and general pages, a blog-specific banner, and a 404 page banner. The images are referenced via og:image and twitter:image meta tags in the <head> of every page.

Performance & SEO

Structured Data (JSON-LD)

The site emits JSON-LD structured data to help search engines understand its content and enable rich results:

The schemas are built by src/lib/jsonld.ts from siteConfig (the same single source of truth used everywhere else) and rendered via a small JsonLd.astro component using Astro’s set:html directive — no third-party dependency required.

Standard.site Publishing (AT Protocol Lexicons)

This blog is published to the ATmosphere using the community-maintained Standard.site lexicons for long-form publishing. This extends the existing ATproto integration (which already powers the microblog via the self-hosted PDS at social.l3x.in) to the long-form articles, giving them the same cross-platform discoverability that the microblog posts already enjoy — without changing where the canonical content lives (Markdown in Git).

Records are written to the PDS by two CLI scripts (standard:publication and standard:documents), which derive the record values from the existing siteConfig and post frontmatter and persist the resulting AT URIs into a committed sidecar file (src/data/standard.json). The static build reads that sidecar to emit the .well-known endpoint and the per-article <link> tags — so the public build needs no PDS write credentials. The core logic lives in src/lib/standard.ts.

Both syncs are idempotent (records are keyed by a stable record key reused across runs) and skip unchanged records: each record’s source is hashed and compared to the hash stored at the last sync, so re-running with no new or edited posts performs no PDS writes (a no-op publication sync needs no credentials at all). To keep the AT Protocol records in step with the canonical Markdown, the maintainer re-runs standard:documents after publishing or editing a post (or --slug <slug> for a single post) and standard:publication after changing the site title, description or hero image, then commits the updated src/data/standard.json. A --force flag on either script bypasses the change detection and re-publishes every record.

Keeping the documents in sync is enforced automatically: the lefthook pre-commit hook runs standard:documents --check, a credential-free, network-free drift check that compares the committed sidecar against the current posts. When everything is in sync it reports “all good” and lets the commit through; when a new or edited post hasn’t been re-synced into the sidecar it fails the commit, telling the committer to run the sync and stage src/data/standard.json alongside the post. (The check passes before adoption and is a no-op for commits that don’t touch the blog.)

IndieWeb Compatibility

This site tries to follows IndieWeb principles and be a good citizen of the independent web:

Other Bits