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?