Skip to content
HTML Is Fun Again
HTML, but with my favorite shortcuts. *

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?

* Image generated with Pixlr AI Image Generator.