Finally got finished with this, i had an older blog (Back when this was on Neocities) which was more like a diary where each post i'd just talk about what i was doing and what was going on in my life. A part of changing my blog philosophy was realizing that i don't really care about reading other peoples diaries (Unless they're like really interesting, or something) and generally all written media i read has themes and something to say, so i'm glad to say i'm heading to a more essay-like blog where i just write about my projects, or any topic that comes to mind

This blog is just a sub-site of my main site which just serves for me to have a space on the internet where i can just link and showcase all the things i do here. And unfortunately i can't say i'm becoming unreliant on third party services since this website is hosted on github, but it's as personal as it can get. One of the technical changes from the last blog to this one is that it's all in one stream rather than split across separate sections. The older one was just me adding new paragraphs on an html file, now i'd like to say it's a sort of framework, a Python script turns them into a static site with an index, permalinks, and an RSS feed (Which i tried to make for the older blog but sort of failed).

The title

I went into this expecting to spend most of the effort on the generator and a small amount of effort on the design. It came out almost exactly backwards. The Python script that turns a folder of posts into a working site with an index and an RSS feed took about a hundred and fifty lines and just copying from an example (Lovely work, thank you)

The part everyone assumes is hard

People usually think of databases for posts and comments and users and maybe authentication if you ever want to log in and write something when i say something like that. That's the Wordpress-shaped assumption, and it's not wrong exactly, that is what a general-purpose publishing platform needs, because it's trying to serve millions of different sites with different requirements. But none of that is actually load-bearing for a single person's blog with no comments, no login, and no reason to ever have more than a few hundred posts. Stripped down to what this specific site needs i just needed a folder of pages which represent these posts (Formatted very simple, will get into later), then give those posts a permalink, produce a list (Sorted by dates) and then produce the feed

A post is an html file. A site is a folder of html files turned. The entire job of the generator is to read the data, fill in a template, and write the finished result. No database, because the filesystem already is the database. The source is a folder you can look at directly, diff in version control, and read without running a server, with a text editor as the admin panel and no need to login because there's nothing to log into, publishing a post is just committing a file.

Choosing the post format

The first real decision was what a single post file should actually look like. Markdown was the obvious first instinct, it's what almost every static site generator uses, and it's genuinely nicer to write in than raw HTML for plain paragraphs. But this site already had a codebase before this, introducing markdown would have meant introducing a markdown parser, which meant a dependency, which meant a version to track, which meant one more thing that could silently change behavior between when a post was written and when it was rebuilt six months later.

So instead, a post is just html, with a small frontmatter comment on top

<!--
title: Writing A Blog Framework Is Surprisingly Easy
date: 2026-08-07
type: essay
tags: #meta, #diary, #web, #dev
excerpt: A part of changing my blog philosophy was realizing that i don't really care about reading other peoples diaries (Unless they're like really interesting, or something) and generally all written media i read has themes and something to say, so i'm glad to say i'm heading to a more essay-like blog where i just write about my projects, or any topic that comes to mind.
-->
<p>Body, as plain HTML.</p>

Parsing that is a regex to split the comment block from the body, and a loop over lines to turn key: value pairs into a dictionary. There are no external parsers or libraries in question here. And there's no real trade for me honestly, i don't mind writing paragraphs in html, i've done it before (and it really is just like writing english (which makes it a lot more boring than programming, honestly)), and in exchange the only really moving part is just python's basic library. (But even using Python is unneeded if you want to update everything manually)

The templating problem, and deliberately not solving it properly

Once posts can be parsed into a dictionary of fields, they need to be dropped into a page. This is normally where a real templating engine shows up, such as Jinja2, Mustache, just something with inheritance and includes and filters. I considered it for about five minutes and then didn't do it, for the same dependency-aversion reason as the markdown decision. What actually got built instead is

PLACEHOLDER_RE = re.compile(r"\{\{\s*(\w+)\s*\}\}")

def render(template_text, ctx):
    def repl(m):
        key = m.group(1)
        return str(ctx.get(key, ""))
    return PLACEHOLDER_RE.sub(repl, template_text)

That's the entire templating engine. {{TITLE}} in a template gets swapped for whatever ctx["TITLE"] holds. It's deliberately dumber than a real templating language, and that's kinda just what i was going for, this framework does in Python instead, in build.py, where it's a function with a name instead of a piece of syntax to look up in documentation. Building the archive sidebar, for instance, is a Python function called build_archive_nav() that returns a finished string of html, which then gets dropped into {{ARCHIVE_NAV}} like any other value.

making the tags do something

Tags existed as visual elements before they did anything, ignoring the query string entirely, because nothing was reading it obviously. Fixing that was a small script that runs on page load, reads location.search, and hides every row in the post table whose tag list doesn't include the requested tag

const params = new URLSearchParams(location.search);
const tag = params.get('tag');
document.querySelectorAll('#threadBody tr').forEach(row => {
  const tags = (row.dataset.tags || '').split(' ');
  if (!tags.includes(tag)) row.classList.add('tag-hidden');
});

Every row in the generated table carries its tags in a data-tags attribute, written once at build time by the same Python function that writes the row's title and date. It just reads the data from the post page

Automating the committing because i'm forgetful sometimes

Everything up to this point produces a working site, as long as someone remembers to run python3 build.py before pushing. That "as long as" is exactly the kind of condition that holds for the first ten posts and quietly fails on the eleventh, after a busy week, when a post gets written and pushed and the generated index.html and rss.xml just don't reflect it. A GitHub Actions workflow watches for pushes that touch the posts folder, runs the exact same build script in a clean environment, and commits the result straight into the folder GitHub Pages serves.

So, was it worth it

Yeah, and with a real asterisk on it. The website really is small enough to read in five minutes and hold entirely in your head, which is not something you can say about almost any general-purpose CMS. That part of the claim holds up.

The asterisk is that a blog is not just a generator. It's also a stylesheet that has nothing to do with blogging specifically and everything to do with "i wanna make this look cool" and that took more total time than the framework did. There's also a real ceiling on how far this specific approach scales which i'll probably have to find out the hard way. My website as a whole has been going through many many iterations the past two years and i'm relatively happy with this sort of 'minimalist' aesthetic going on with the entire thing

That's the first post for this blog, and i hope there will be more, please report typos here (And any future typos) to me :)