A case study on how I made a Docker-ready image that serves a website with zero javascript packaged, but still has a live configuration.
architecture
Published on Feb 24, 2026
Updated on Feb 24, 2026
Whoo, it’s been a hot minute since I took on a commission. My friend, who is a
tech administrator and also the community ambassador of the trans-identities community,
reached out to me for a simple configurable landing page for her March launch.
Requirements Analysis (it had a term for it apparently)
I did not know this process that I have been doing all my life has a specific term
for it, called Requirements Analysis. Ironically, this is the process that is severely
lacking in my capstone project, leading to a
development team that can’t agree with each other on anything.
For this project, the client seems to want the following:
Configuration: She breathes Docker and configurations, but she doesn’t code
herself. It needs a way for her to tweak the content, without having to touch
the source code or the container even after deployment.
Fits her budget: Usually, the client would need to commission a designer or
a design agency, gets their designed framework. Then commission a separate
developer, and have the design be built up. She took advantage of me knowing
certain parts of design and development, and got a budget-fitting quote for a
lightweight landing page.
It IS a landing page: Let’s not complicate everything with React or MERN
stack everyone yaps about. It’s a content page first and foremost, and she would
love for it to show up on search engines.
I settled on AstroJS, but using the standalone SSR module, for the following reasons:
The configuration needs hot reload. SSR allows Astro to build a landing page at
the time of request. She doesn’t need to rebuild the image or reload the container
to have the landing page’s content change. (Wowie)
Astro uses a zero JS delivery by default. The user should not have to download
a bunch of React runtimes just to view some content! I like React, but the prevalence
of it for simple things like content websites are way too bad.
Static vs Dynamic Websites
Something I noticed is that a lot of people don’t understand the blurry lines between
what you would consider a static website and a dynamic website, and to be honest,
I don’t either because I don’t think everyone agreed on a solid definition. A good
way to think about it is about how the page is built and delivered.
Technology
The browser receives
Like a?
Pros & Cons
SSG
Full HTML
Frozen Pizza
Fastest, can be put on a S3 or CDN. But you need to recook (rebuild) every time you change a piece.
Partial Hydration
Skeleton HTML + JS to get content
DIY Pizza Kit
Slowest, but can be on a S3 or CDN. The browser has to assemble the pizza before you can eat.
SSR
Full HTML
Restaurant Meal
Fast, can’t be on a S3 or CDN*. A cook cooks a pizza when you need it, hot.
Implementing the Live Config
To give her that nice configuration, I used js-yaml and zod. By using schema.safeParse,
I ensured that if she makes a typo in the YAML, the site doesn’t just crash silently
but it catches the error (and also calls her a dumbass but who cares).
import yaml from "js-yaml";import fs from "fs";import z from "zod";const filePath = process.env.CONFIG_FILE || `${process.cwd()}/config.yml`;const schema = z.object({ ...});type ConfigSchema = z.infer<typeof schema>;export { type ConfigSchema };export function loadConfig() { const data = yaml.load(fs.readFileSync(filePath, "utf8")); return schema.safeParse(data);}
The z.infer is to allow TypeScript and vstls (I think I spelled that right) can
auto-complete typenames.
The Markdown Compromise
Usually, you’d expect users to configure just raw text, because that’s the easiest
thing to monitor, and you wouldn’t have to care about edge cases, since raw text
easily wraps around in modern browsers. The problem here is that it’s not as customizable
with raw text, as the client wants some level of bolding, and linking in the content.
Thankfully, she is very happy with Markdown.
Retrospective
What we couldn’t do
This is a short retrospective section of the commission as a whole. There were a
few things that were dropped from the final product, due to some issues with several
requirements, as well as being rather out of scope for the March launch.
The February 11th deadline was tight, especially with Lunar New Year falling
right in the middle (Feb 17). Between visiting my father’s hometown and a trip
with my mother, the “dev window” was tiny. (There are a lot of responsibilities
that comes with being the eldest sister in the family). We had to drop a few:
A cycling version of the logo. I’d love some decorative scripting that doesn’t
affect the entire page too much, so noscript users still can enjoy the content.
A CSS-based infinite carousel. It seems like it doesn’t matter what you do, the
infinite carousel WILL look wrong in a certain resolution, a certain zoom size,
a certain DPI, or a certain laggy machine.
A live player count indicator and some self-made drawing. I’d love to draw some
silly indicators for a player count, but it seems like her game server kept going
down, and didn’t really have a way to grab a player’s count from outside, so we
put that as “Out of Scope”.
What we could
But overall, we were able to complete some rather cool features that she was happy
with:
Full HTML content delivered on request, with very low JS footprint. Even a person
using NoJS extension can still access the page mostly, just no animations.
A live config that when edited, affects all subsequent HTML generations.
All deployable with Docker, with no installation needed on the host.
I think that works rather well, or maybe I’m not as good to make it better yet. I
didn’t want to rely on React or crazy amount of animations or interactivity, maybe
this philosophy fit the client’s needs for accessibility for her diverse community.