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
- IBM Plex Mono — the primary body font, loaded from Fontsource CDN.
- Press Start 2P — the display/heading font (pixel art style), loaded from Fontsource CDN.
- Literata Variable — a variable serif font for reading, also from Fontsource.
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
- Bun — JavaScript runtime and package manager.
- Biome — fast linter and formatter, replacing ESLint + Prettier.
- Astro · Check — static type analysis for
.astrofiles. - Vitest — unit tests and integration tests.
- Nu HTML Validator (vnu-jar) — validates all generated HTML pages against the W3C spec.
- Netlify — hosting and continuous deployment.
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
- Preconnect to font CDN — A
<link rel="preconnect">hint forcdn.jsdelivr.neteliminates DNS and TCP latency on first font load. - Theme color & color-scheme —
<meta name="theme-color">and<meta name="color-scheme">meta tags eliminate the white flash for dark-mode users and tint the browser chrome to match. - Sitemap link in head — A
<link rel="sitemap">tag in every page’s<head>helps crawlers discover the sitemap without parsingrobots.txt.
Structured Data (JSON-LD)
The site emits JSON-LD structured data to help search engines understand its content and enable rich results:
Person+WebSiteon the homepage — a@graphblock gives search engines a machine-readable identity (name, avatar, job title, social profiles viasameAs) and identifies the site root. The Person node is referenced by@idthroughout, so authorship data is defined once and linked, not duplicated.BlogPostingon every article page — includes headline, description, publish date, keywords, and anauthor/publisherback-reference to the homepage Person node. This makes posts eligible for Google 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).
site.standard.publication— a single record on the PDS describing this site as a publication (base URL, name, description, icon). The domain↔record link is verified via a static/.well-known/site.standard.publicationendpoint that returns the publication’s AT URI as plain text.site.standard.document— one record per blog article (title, description, path, tags,publishedAt, full plain-texttextContent), each linking back to the publication record via itssitefield. Every article page emits a<link rel="site.standard.document">tag pointing at its record’s AT URI.
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:
- Microformats2 — Blog posts and article snippets are marked up with microformats2 classes (
h-entry,h-card,p-name,e-content,p-summary,dt-published,u-url,p-uid,p-category,p-author). This makes the content machine-readable and consumable by IndieWeb tools, readers, and search engines. rel="me"links — Profile links to Bluesky, my Forge, GitHub, my GPG key, and my SSH keys includerel="me", enabling IndieAuth identity verification and cross-site identity proof.- Identity verification — The /keys page links to my public GPG and SSH keys, allowing visitors to verify signatures, encrypt messages, and confirm identity out-of-band. The GPG key is also advertised via a
<link rel="pgpkey">tag in every page’s<head>, following the IndieWeb OpenPGP linking convention. - RSS feeds — A full blog feed at /rss.xml and a microblog feed at /microblog.xml ensure content is syndication-friendly and subscribable from any RSS reader. Both feeds are built with
@astrojs/rssand the microblog feed is sourced from the author’s self-hosted ATproto PDS (social.l3x.in). Each feed is discovered automatically via a<link rel="alternate" type="application/rss+xml">tag in every page’s<head>and is listed on the /follow page. - Blogroll — The /blogroll page follows the tradition of linking to other personal websites and independent blogs.
- Postroll — The /postroll page curates link recommendations, a pattern aligned with the IndieWeb ethos of sharing discovery.
- TIL — The /til page collects short “Today I Learned” entries from Bluesky posts tagged
#til, acting as a microblog-style knowledge log — a form of personal wiki native to the IndieWeb. - Canonical URLs — Every page includes a
<link rel="canonical">tag for unambiguous permalink identity. - Semantic HTML — Proper use of
<article>,<nav>,<main>,<time>(withdatetimeattributes), and other semantic elements ensures structural clarity for parsers and assistive technology. - Open Graph & Twitter Cards — Rich
og:*andtwitter:*meta tags provide accurate social previews when content is shared.
Other Bits
- Sitemap generated by @astrojs/sitemap. A
301redirect from /sitemap points to the sitemap index XML. - Legacy blog redirects — The blog previously lived on Tumblr and then Jekyll/Firebase where posts used two legacy URL patterns. Old links shared on social media still use those paths. A build-time Astro integration generates a Netlify
_redirectsfile with301redirects for every published post, covering both patterns: root-slug (Tumblr era, e.g./my-article) and Jekyll date permalinks (e.g./2020/06/17/my-article.html). The integration scans the generated output and reads frontmatter timestamps, so new posts are covered automatically. - /expenses — a private, password-protected expense tracker backed by a serverless API. Built as a Svelte 5 SPA island embedded in the Astro page via
client:load. - /usage — a live dashboard showing current quota usage and remaining balance for the author’s LLM accounts (e.g. Z.ai, DeepSeek), fetched on load from a public serverless API. Built as a Svelte 5 island via
client:load.