HTML Is Fun Again
Phillip Harrington Apr 2026
TL;DR: I made a tiny build package called
graspr-build that turns custom HTML tags into plain HTML
at build time. The site is just HTML again. And I love it.
In the colophon I said, "Maybe I'll do a write-up on it one of these days." Today is one of those days.
The previous version was fine
Before this, the site was built with EJS templates and a little Node script that stitched the partials together. It worked! It had a header partial and a footer partial, I'd drop a new file in, run the build, and away we went. But "fine" isn't fun.
Every time I went to add a new page, I'd find myself copying the same boilerplate from the last one I made, then forgetting to update the title, then noticing six months later. The whole experience was a notch fancier than writing raw HTML and a notch clunkier than I wanted. Not the end of the world. But also... not the dream.
So I wrote graspr-build
graspr-build is a tiny package I made that does one
thing: it turns custom HTML tags into plain HTML at build time.
That's it. That's the whole trick.
Instead of writing the same <a> tag with the same
five Tailwind classes every time I want a link, I write
<lnk to="/colophon">colophon</lnk> and let
graspr-build expand it. Same for headings, figures,
lists, code spans, bylines, the whole works. If I find myself
writing the same chunk of markup twice, I make a new tag for it
and never type the long form again.
My pages don't look like a wall of div soup anymore. They look like... HTML. Tiny, declarative HTML that says what the thing is and gets out of the way.
Writing a page is just writing a page
To publish an article, I make one HTML file in
content/pages. The filename becomes the URL. The first
line picks the layout. After that I write the content using
whichever custom tags I feel like using. There's no front matter
to wrangle, no markdown plugin chain to debug, no "now go register
this in the index file" step. The build script handles the rest.
When it's that simple, you'd think I would publish more often. 😂
The dev server matches prod
The other thing graspr-build ships is a small Vite
plugin that serves the abstract URLs the same way prod does. So
/colophon in the dev server is the same
/colophon you get on the real site — no extension, no
trailing slash, no redirect. The plugin renders each page on the
fly from content/pages and Vite handles the live
reload.
This sounds like a small thing but it's the part I'm proudest of. Local dev matches prod. There is no "well, on the deployed version it's different." It's just the same.
At build time
npm run build runs Vite and then
graspr-build-pages, which walks
content/pages, expands all the custom tags, and writes
out the final HTML files into dist/. From there a CI
step on AWS CodeBuild syncs dist/ to an S3 bucket and
invalidates the CloudFront cache. That's the whole pipeline.
No SPA. No router. No database. No node-sass. No anything-else. Just HTML files in a bucket and a CDN in front of them.
Why I'm hyped
I love this setup. I love that I can publish a new article by making a single HTML file. I love that the file mostly looks like the thing I wrote, not like a template-engine puzzle. I love that I built the build tool myself, so when I want a new tag, I just add it. No ticket. No upstream. No "well, the framework doesn't quite support that." It's my framework. It supports whatever I want.
graspr-build
is small on purpose.
It's not trying to be Next or Astro or Eleventy. It's not trying to
be anything but the thinnest possible layer between me and the HTML
I want to ship. And after years of fighting with bigger tools, that
thinness is the whole point.
HTML is fun again. Who knew?