Source for formingworlds.space, the group website of the Forming Worlds Lab at the Kapteyn Astronomical Institute, University of Groningen.
The site is a dark-themed Hugo static site (the "Field Station" design language) hosted on GitHub Pages, with a small Cloudflare Worker for URL shortening on go.formingworlds.space and timlichtenberg.net.
- Hugo 0.147+ (extended), static site generator
- Tailwind CSS v4 via the native
css.TailwindCSSHugo pipe (no PostCSS, notailwind.config.js) - GitHub Actions for CI/CD to GitHub Pages on push to
main - Formspree as the contact form backend
- Cloudflare Workers + KV for the
go.formingworlds.spaceshortlinks (separate deploy path, seeworkers/shortlinks/)
npm install # Tailwind v4 CLI + tailwindcss
hugo server # dev server, usually http://localhost:1313
hugo --minify # production build into ./publicHugo picks an alternative port if 1313 is busy. Check with lsof -i :1313.
After adding a new section-specific layout directory, restart the dev server with --disableFastRender once so Hugo notices it.
Team data lives in data/team.json (a flat array), not in markdown front matter.
- Add a profile photo to
static/img/faces/<slug>.avif. Convert from JPG/PNG with:Target under 100 KB. Make sure both pixel dimensions are even (Safari's AVIF decoder rejects odd dimensions silently).sips -s format avif -s formatOptions 45 input.jpg --out static/img/faces/<slug>.avif
- Append a new entry to
data/team.jsonat the end of the current-members section (above the alumni block). New members go last in chronological join order; do not sort by role or group. - If the member needs a profile page, set
page: trueand addcontent/team/<slug>.md.
Each entry looks like:
{
"name": "Firstname Lastname",
"slug": "firstname-lastname",
"role": "PhD student",
"group": "phd",
"image": "/img/faces/firstname-lastname.avif",
"topics": ["topic 1", "topic 2"],
"page": true,
"links": { "email": "...", "github": "...", "orcid": "..." }
}Groups: lead, postdoc, phd, student, staff, alumni. Alumni render in a compact 5-column grid without social icons.
content/ Markdown pages (each section has _index.md)
contact/ two-column contact page + getting-to-kapteyn sub-page
join/ position cards (PhD, postdoc, student, code of conduct)
publications/ year-divided publication list
research/ goals and prose
team/ team intro page (members come from data/team.json)
kapteyn-groningen/ Groningen city + institute page
layouts/
_default/ baseof, list, single (list supports banner_image hero)
index.html homepage slideshow + about section
partials/ header, footer, head, team-card-data
<section>/ section-specific list and single templates
assets/css/main.css Tailwind v4 CSS-first config (@theme + component classes)
data/ team.json, slides.json, group_photos.json, research_topics.json, groningen_gallery.json
static/img/ faces/, brand/, research/, illustrations/, logos/, groningen/
static/CNAME custom-domain marker for GitHub Pages
workers/shortlinks/ Cloudflare Worker for go.formingworlds.space + timlichtenberg.net
.github/workflows/ deploy.yml builds Hugo and publishes to GitHub Pages
hugo.toml site config, menus, params (analytics, formspree, social links)
The authoritative reference is the comment block at the top of assets/css/main.css. The short version:
- Three color tokens: ink (cool neutral ramp), signal (brand blue, links and primary CTAs), magma (warm accent for callouts and the
.btn-warmvariant). All defined in@themeso the palette has a single source of truth. - Heading font is Geist. Section headings are sentence-case and end with a period ("Our team.", "Publications.", "Research.").
- Long-form text uses
.prose-content; small section labels usetext-xs uppercase tracking-wider text-ink-400. - Component primitives in
main.css:.container-site,.btn-primary/.btn-ghost/.btn-warm,.team-card,.alumni-grid,.prose-content,.year-divider,.gallery-masonry.
Anti-patterns to avoid:
- New tokens outside the ink / signal / magma vocabulary. Extend if you must, otherwise reuse.
- Raw hex codes in component CSS. Always reference
var(--color-*). - Arbitrary-value Tailwind utilities in layout templates (
gap-3,py-32,grid-cols-[24px_1fr], etc.). Hugo's TW4 pipe does not reliably emit them. Use inlinestyle=""for one-offs or define a class inmain.css. - Em-dashes and en-dashes in user-facing copy. Use commas, semicolons, colons, or parentheses.
The navy-*, sky-*, and accent-* tokens from the original Wix port still exist as aliases in @theme but should not be used for new work.
content/publications/_index.md is the canonical list, edited by hand. Lab members are bolded in author lists. Entries follow:
AuthorA X, **T Lichtenberg**, AuthorB Y. [Title.](scix_arxiv-stable_URL) *Journal* vol, page (year). [pdf](arxiv_pdf_url)
The list is split by year via .year-divider headings (most recent first, with everything up to and including 2022 collapsed under "≤ 2022"). The sidebar links (ADS, SciX, arXiv, Google Scholar) are defined in layouts/publications/list.html and duplicated for the mobile pill nav.
Push to main triggers .github/workflows/deploy.yml, which:
- Installs Hugo extended 0.147 and Node 22, runs
npm ci. - Calls
actions/configure-pages(this auto-detects the custom domain and passes the correct--baseURL). - Runs
hugo --minify. - Publishes
./publicto GitHub Pages.
Custom domain formingworlds.space (plus www, ips, aes subdomains) is served via Cloudflare DNS pointing at GitHub Pages IPs. The static/CNAME file is what tells GitHub Pages to expect that domain.
If you change the custom domain in GitHub Pages settings, re-trigger a deploy: the configure-pages action caches the previous base URL otherwise and asset paths break.
https://go.formingworlds.space/<slug> 302-redirects to a target URL stored in Cloudflare KV. The same Worker also serves timlichtenberg.net/<slug> (slugs prefixed tl: in KV, fall through to /team/tim-lichtenberg/).
Implementation lives in workers/shortlinks/; see workers/shortlinks/README.md for the add/update/delete workflow. The Worker deploys via wrangler and is independent of the Hugo build.
- Branch naming for non-trivial work:
tl/<short-description>. - Direct pushes to
mainare allowed for small content updates (publications, team data, copy fixes). Larger structural changes should go through a feature branch. - Generated images and screenshots from local sessions stay out of the repo. The root-level globs in
.gitignore(/*.png,/*.jpg,/*.webp, etc.) catch the common cases. CLAUDE.mdcarries project-specific instructions for AI-assisted edits; treat it as part of the codebase.