Personal site. Astro static site, markdown-authored.
| Route | What it is |
|---|---|
/ |
Bio, combined GitHub + GitLab contribution graph, recent posts and projects |
/blog/ |
All posts |
/blog/<slug>/ |
A single post |
/projects/ |
Curated project list from src/data/projects.ts |
/cv/ |
Career timeline from src/data/cv.ts, plus the portrait |
/rss.xml |
Feed |
/sitemap-index.xml |
Sitemap, generated by @astrojs/sitemap |
Create a new file in src/content/posts/<slug>.md:
---
title: Your post title
pubDate: 2026-05-14
description: One sentence for search results, social cards and the feed.
---
Body content in markdown.The filename becomes the URL slug. pubDate is required; description is
optional but worth writing, since it fills the meta description, the Open Graph
and Twitter cards, the JSON-LD, and the RSS item summary.
Links in a post that point off-site get target="_blank" and
rel="noopener noreferrer" automatically.
To draft without publishing, add draft: true to the frontmatter. Drafts are
hidden in production builds and visible in pnpm dev.
Edit src/data/projects.ts. The list renders in the order it is written; the
first three also appear on the home page.
The home page merges a trailing year of GitHub and self-hosted GitLab activity into one calendar. Both are fetched at build time, so the graph is as fresh as the last deploy.
Copy .env.example to .env and fill in what you have:
GITHUB_TOKEN: any token withread:user. Locally you can skip it; the build falls back togh auth tokenfrom the GitHub CLI.GITLAB_TOKEN: a token fromgit.cloo-gmbh.dewith theread_apiscope. GitLab has no calendar API, so the numbers are derived from the event stream: a push counts for its commits, everything else counts once.
Neither token is required for the build to succeed. A source without a token is
skipped, a source that fails is logged and skipped, and if nothing is
reachable the last successful fetch in .cache/contributions.json is used. With
no data at all the section is simply left off the page.
To keep the graph current without pushing, redeploy on a schedule.
The favicon is the lowercase y from Inter at weight 600, pulled straight out
of the font file so the mark matches the type on the page. favicon.svg carries
its own prefers-color-scheme rule and flips to light in a dark browser theme;
favicon.ico is the legacy fallback at 16, 32 and 48. The Apple touch icon and
the two manifest icons sit on the site background, because iOS fills alpha with
black and a maskable icon gets cropped to a circle.
Regenerating them means re-running the extraction against the Inter file Astro
downloads into node_modules/.astro/fonts/, so the script is not wired into the
build. src/pages/site.webmanifest.js is generated, so the name and description
follow src/consts.ts.
src/components/Portrait.astro owns the photo on the cv page. Clicking it opens
a native <dialog>, which brings the focus trap and Escape handling with it,
and the picture is animated from the thumbnail to full size with a FLIP pass
over the Web Animations API. Escape, the close button, the backdrop and the
picture itself all close it, and under prefers-reduced-motion the dialog just
appears.
The social card is a separate crop, see below.
Handled in src/layouts/BaseLayout.astro: canonical URL, meta description,
Open Graph and Twitter card tags, and JSON-LD (Person on regular pages,
BlogPosting on posts). @astrojs/sitemap writes the sitemap, which
public/robots.txt points at.
The social card is public/og.jpg, a 1200x630 crop of the portrait. It is a
plain file, not generated at build time, so replacing the photo means
re-cropping it by hand.
Posts used to live under /posts/<slug>/. redirects in astro.config.mjs
keeps those URLs working.
pnpm install
pnpm dev # http://localhost:4321
pnpm build # output: dist/
pnpm preview # serve the production build locallyCoolify Static Site application.
- Build command:
pnpm install --frozen-lockfile && pnpm build - Publish directory:
dist - Environment: set
GITHUB_TOKENandGITLAB_TOKENas build-time variables. - Auto-deploy on push to
main.
- Astro 7 (static output)
- Content Collections (markdown + Zod schema), rendered by Sätteri with one small hast plugin for external links
@astrojs/rssfor the feed,@astrojs/sitemapfor the sitemap- Inter and Geist Mono, self-hosted by Astro's font pipeline, so there are no third-party font requests, which also keeps the site clean under GDPR
- Plain CSS. UI labels are lowercase throughout; motion uses one shared easing
curve and is disabled under
prefers-reduced-motion <ClientRouter />for crossfaded page navigation (the only client script)