Build notes
How this was built
OBSIDIA looks expensive. It is not. It is a static Astro site, a few kilobytes of hand-written shader, five generated images and a two-command deploy. Here is the whole recipe, honestly, so you can do the same.
The stack
- Astro for a zero-runtime static site, with Tailwind CSS v4 for tokens and layout.
- Hand-written WebGL for the hero, no Three.js.
- Recraft V4.1 for the still imagery.
- Cloudflare Pages for hosting, free, on the edge.
Everything is self-hosted and same-origin so it can run under a strict Content Security Policy with no exceptions.
The signature: a shader, not a framework
The obsidian in the hero is a single fullscreen triangle running a fragment shader. Reaching for a 3D framework felt wrong here: the parse cost of a large library is exactly the kind of thing that drags a Lighthouse score down. Instead the whole object is a raymarched signed-distance field, which is a few kilobytes of GLSL and almost no JavaScript.
The shape is six spheres orbiting the origin, blended into one blobby mass with a smooth minimum. That gives a stable distance field that is cheap to march, unlike noise-displaced geometry which forces tiny steps and artefacts.
// six orbiting spheres, blended into one organic mass with a smooth-min
float map(vec3 p) {
float grow = 0.82 + 0.30 * uPhase; // blooms as you scroll
float k = 0.52 - 0.16 * uPhase; // blend tightens with phase
float d = 1e5;
for (int i = 0; i < 6; i++) {
vec3 c = /* a sinusoidal orbit driven by uTime + i */;
float r = 0.34 + 0.07 * sin(uTime + float(i) * 2.1);
d = smin(d, length(p - c) - r, k);
}
return d;
}The look comes from the shading. At each hit point we perturb the normal with a little value noise for glassy micro-facets, then compute a Fresnel term and use it to drive a thin-film iridescencethrough a cosine palette. That palette is the entire brand: teal, ice, violet, magenta, gold.
// thin-film iridescence: hue from view angle + surface noise, mapped
// through an IQ cosine palette tuned to OBSIDIA's spectral range
float fres = pow(1.0 - dot(n, v), 3.4);
float hue = fres * 1.1 + bump * 0.5 + n.y * 0.25;
vec3 irid = spectral(hue); // teal -> ice -> violet -> magenta -> gold
col = vec3(0.02, 0.022, 0.035); // near-black obsidian body
col += irid * fres * 1.35; // spectral rim
col += spectral(hue + 0.3) * spec * 1.6; // sharp glintsTwo things keep it fast and calm: the internal buffer renders at 55 to 82 percent of the CSS size depending on screen width (the upscale reads as soft, not blurry), and a prefers-reduced-motion visitor gets a single static frame with no animation loop at all.
Motion under a strict CSP
The production build ships a hash-based Content Security Policy with no'unsafe-inline'. That bans inline styleattributes and inline event handlers, which is a good discipline. The rules: styling lives in scoped <style> blocks and CSS custom properties, behaviour lives in bundled scripts, and anything that needs to vary is passed through data- attributes. Astro hashes its own inline scripts into the policy for you.
security: {
csp: {
directives: [
"default-src 'self'",
"img-src 'self' data: blob:",
"worker-src 'self' blob:",
// ...Astro adds sha256 hashes for its own inline scripts automatically
],
},
}The imagery
The five specimen stills are one prompt family in Recraft V4.1: black volcanic glass, oil-slick iridescence, pure black background. Keeping the lighting and background identical across prompts is what makes them read as a set. The exact prompts are on the prompts page.
They are imported through Astro's image pipeline, which resizes them and re-encodes to WebP at build time, so a one-megabyte source becomes a right-sized, lazily-loaded asset without any manual work.
Deploy in two commands
Cloudflare Pages hosts static output for free on their edge network. The one catch worth knowing: set the production branch explicitly, or your first deploy lands as a preview and the main URL looks empty.
# one-time: create the project (production branch = main)
npx wrangler pages project create obsidia --production-branch main
# every deploy after that:
npm run build
npx wrangler pages deploy dist --project-name obsidia --branch mainThe budget it hits
- Accessibility 100, contrast AA on every line of text.
- A full static end state for reduced-motion visitors.
- No framework runtime, a handful of small bundled scripts.
- Every asset same-origin, so the strict CSP has zero exceptions.
That is the entire trick. Pick one signature moment worth doing by hand, keep everything else quiet and cheap, and let the contrast do the work.
This is one of three showcase sites. See the prompts, or go back to Specimen 01.