The site for 3rd Party Influence — live minimal techno on Eurorack modular synths.
Static SvelteKit. Migrated off Notion + Super.so in September 2026.
Super rendered the Notion pages, but gave headings ids like
id="block-293a7c7350e6802aab17da71dfcbba24" — the raw Notion block UUID. Unguessable,
meaningless, and it changes if the block is recreated, so a section could never be
linked to. Here the same heading is #aaa11-martin-gordon, generated by rehype-slug,
and every heading carries a visible-on-hover anchor link.
Other things that came with the move: custom <head> and per-page OG tags, a real 404
page, gigs held as data instead of retyped in two places, and builds that fail loudly on
a broken link or a mistyped image id.
adapter-static prerenders every route to a plain HTML file. Node is used at build time
only. The build/ directory is the whole site — deploy it to any static host.
npm install
npm run dev # regenerates images, then starts the dev server
npm run build # -> build/ (runs `npm run images` first via prebuild)
npm run preview # serve build/ locally
npm run images # regenerate static/images/ from media/src/
content/ the pages, as markdown - this is what you edit
index.md -> /
module-co2e.md -> /module-co2e
artists-activists-and-audiences/
aaa-upcoming.md -> /artists-activists-and-audiences/aaa-upcoming
lib/
components/ Photo, Embed, Columns, NextGig, GigList, Nav, Footer, Seo
data/
gigs.json every gig, past and upcoming - single source of truth
nav.json the main nav, in order
links.json social URLs
photos.json per-image alt, caption and photographer credit
photo-sizes.json GENERATED by npm run images
routes/
+page.svelte renders content/index.md
[...path]/ renders everything else, including nested pages
media/
web/ 1920px sources scraped from the old site (committed)
originals/ drop high-res here later; wins automatically. See its README.
scripts/
build-images.mjs generates responsive avif/webp/jpg variants
migration/ one-shot scripts that pulled the content out of Super
static/images/ GENERATED - gitignored
Edit the markdown in src/content/. Headings become anchors automatically, so
## Some Section is linkable at #some-section. Renaming a heading changes its
anchor — if a link to it exists somewhere, update that too.
One entry in src/lib/data/gigs.json:
{
"date": "2026-11-14",
"parts": [
{ "text": "BMS61", "url": "https://berlinmodularsociety.com/" },
{ "text": "Klunkerkranich", "url": "https://klunkerkranich.org/" },
{ "text": "Berlin" }
],
"note": "Closing set",
"sets": [
{ "time": "22:00", "what": "3rd Party Influence", "where": "Rooftop" }
]
}parts is the title, joined with commas. Any part can carry a url, so links live in
the title itself - "BMS61" points at Berlin Modular Society, the venue at the venue.
There is no separate row of links.
note is one short line under the date. sets renders as set times: each takes time
and what, plus optional who (who is playing, if it is not the band), where (which
room) and url (a link on the act, e.g. ambienttaichi.com).
Only date and parts are required. Anything dated today or later shows as upcoming;
the rest fall into the archive. The homepage "Next gig" section and /gig-archive both
read this file, so they cannot disagree.
Dates are evaluated at build time, then re-checked in the browser after hydration - so a gig will not sit there stale between deploys. If you want the served HTML itself always current, schedule a periodic rebuild on your host.
Filenames carry the rights holder as a prefix: <rights-holder>_<description>.<ext>.
Use 3pi_ for the band's own photos, a slugified name for anyone else's, or unknown_
if you genuinely do not know. See COPYRIGHT.md.
- Put the file in
media/web/(ormedia/originals/if it's high-res), named with the id you want, e.g.3pi_bms61-closing-set.jpg. - Add an entry to
src/lib/data/photos.json, keyed by the id:Set"3pi_bms61-closing-set": { "id": "3pi_bms61-closing-set", "file": "3pi_bms61-closing-set.jpg", "alt": "3PI playing the closing set at BMS61", "caption": "Closing set at BMS61, Klunkerkranich", "captionHtml": "Closing set at BMS61, Klunkerkranich", "credit": null, "creditUrl": null }
creditwhenever someone else took it — it renders under the photo. - Reference it:
<Photo id="3pi_bms61-closing-set" /> npm run images
A <Photo> with an unknown id fails the build rather than shipping a broken image.
<Embed src="https://www.youtube.com/embed/..." /> — YouTube, SoundCloud, Bandcamp,
Mixcloud and Google Calendar/Forms all work. Audio players get a fixed height, video gets
16:9.
Build command npm run build, publish directory build.
Keep the existing URLs. Every slug from the Super site is preserved, including the nested
/artists-activists-and-audiences/aaa-upcoming.
.github/workflows/deploy.yml builds on every push to main and publishes build/ as a
Pages artifact. Do not use a docs/ folder — that is the older deploy-from-branch
method and would mean committing 57 MB of generated images on every content change.
build/ and static/images/ stay gitignored.
One-time setup: Settings → Pages → Source → GitHub Actions. Then push to main.
Three files exist purely for Pages, and the site breaks without them:
| File | Why |
|---|---|
static/.nojekyll |
Without it Jekyll strips _app/ — the leading underscore — and every stylesheet and script 404s |
static/CNAME |
The custom domain. Delete this file if you are not using one |
build/404.html |
Generated by adapter: adapter({ fallback: '404.html' }). Pages serves it for unknown paths, and it boots the app so +error.svelte renders |
The workflow caches static/images/, keyed on the contents of media/. Without that,
every deploy re-encodes 531 image variants with sharp, which dominates the build time.
Change a photo and only that photo is re-encoded.
workflow_dispatch is enabled so you can rebuild from the Actions tab without pushing —
useful because gig dates are evaluated at build time.
The CNAME file only tells GitHub which domain to answer for. You still have to move DNS
off Vercel. For an apex domain, replace the existing records with GitHub's Pages A/AAAA
records (confirm the current values in GitHub's "Managing a custom domain" docs before
changing anything), then tick Enforce HTTPS in Settings → Pages once the certificate
is issued. That is the actual cutover from the Super site — do it last, once you have
checked the Pages URL.
Serving from username.github.io/3rdPartyInfluence rather than a custom domain needs
kit.paths.base set, and every absolute path in the project prefixed with it — the nav,
footer, breadcrumb, favicon, the /images/... srcsets in Photo.svelte, and the two
internal links inside the markdown. It is not just a config line. A custom domain, or a
<username>.github.io repo, avoids the whole problem.
Cloudflare Pages and Netlify take the same npm run build / build pair. They give you
control over cache headers and, on Cloudflare, on-the-fly image resizing. Neither is
necessary — the responsive variants are pre-generated — but for a site that is mostly
photographs they are a better long-term home than Pages.
npm run images reads each source once and writes AVIF, WebP and a JPEG/PNG fallback at
640 / 1280 / 1920px, plus the intrinsic dimensions into photo-sizes.json so every
<img> ships width/height and never causes layout shift.
Generated files are gitignored. Only the sources in media/ are committed.
Split, because this repo holds both software and a band's content:
- Code — AGPLv3. See LICENSE.
- Content — text, gig data, and all photographs — all rights reserved.
17 of the photographs were taken by four other photographers and are not ours to relicense. Full detail and per-photographer credits in COPYRIGHT.md.