All Engineering

I Accidentally Created a Jamstack Framework

I just wanted to write HTML, CSS, and JS by hand. DRY and SOLID had other plans.

I didn't want a framework for this site. No React, no Next.js, no Eleventy. Just HTML, CSS, and JS, written directly, so there was nothing between me and the page.

Writing it by hand

Every page started as its own index.html. A header, a nav, a footer, some content in between. No build step, no config, no node_modules to argue with. I could open a file, change a sentence, refresh the browser, and see exactly what I wrote.

For a handful of pages, this was great. Then I wrote a sixth post, and a seventh, and noticed I was copy-pasting the same header, the same footer, the same SEO meta tags into every single file, by hand, every time.

DRY shows up first

Copy-pasting a header is fine once. By the fourth time, it stops being fine, and don't repeat yourself stops being a slogan and starts being the thing I actually need.

So the pages that were identical became a template:

<!-- templates/post.html -->
<header class="site-header">...</header>
<main>
  {{seo}}
  {{title}}
  {{column}}
  
              
              
  
</main>
<footer>...</footer>

And the parts that changed became data:

{
  "title": "Adding Custom Liquid Filters in Rails",
  "date": "Dec 2025",
  "categories": ["engineering"],
  "tags": ["Rails", "Liquid"]
}

A small Node script reads the JSON, fills in the template, writes the HTML file. Nothing clever. Just a function that replaces {{title}} with a title.

Then I wanted Markdown

JSON is fine for a title and a list of tags. It is a miserable way to write a paragraph with a bolded word and a link in it. I wanted to write posts the way I write everything else: in Markdown.

I could have pulled in a library. Instead I asked what this site actually needed — headings, paragraphs, lists, a blockquote, a code block, an image, a handful of inline styles — and nothing else. No footnotes, no tables inside the Markdown syntax, no plugin system. A narrow, known set of rules.

That's where single responsibility earned its keep. Instead of one function that tries to parse all of Markdown, I wrote one function per block type, each one doing exactly one job:

function renderInline(copy) { /* bold, italic, mark, code, links */ }
function renderList(items, ordered) { /* ul or ol, nested */ }
function renderQuote(quote) { /* blockquote + citation */ }
function renderFigure(figure) { /* image + caption */ }

The main loop just looks at each line, decides which function owns it, and hands it off. Adding a rule later — a divider, a table via raw HTML passthrough — meant adding one more small function, not touching the others. That's the part of SOLID that actually mattered here: not the whole acronym, just the part where a function doing one thing is easier to trust than a function doing six.

What I'd actually built

Content lives in Markdown or JSON. A template defines the shape of a page. A script combines the two into static HTML, builds a sitemap, and writes category and index pages while it's at it.

Content, template, build step, static output. That's Jamstack. I didn't set out to build a static site generator — I set out to avoid one, by writing everything directly — and ended up writing a small one anyway, because DRY and SOLID don't care whether you meant to build a framework. They just push every codebase toward one, one repeated line and one overloaded function at a time.

Mine is about 1,300 lines of plain Node with no dependencies. It is, unmistakably, a framework. It just has exactly one user.