Making the engine for my new website

26th 11 2025 meta programming

I just set up the first, second or third personal website of mine, depending on how you count. It has a quite interesting build system, which I'll describe here. The bottom of this page acts as a test of my markdown rendering system.

Design

After using my last build system (from TheMasterArchiver) I wanted something more extensible. While writing pages in html gives me a lot of freedom, I ended up copying large chunks between pages, making it impossible to update. In addition to that, all logic was put in generate.py, which made it really large.

So, the 2 goals of my new build system are:

I split the whole system into two base classes BaseSource and BaseOutput. Sources represent source files, and produce Outputs, which represent output files. A page can produce multiple outputs, because it can have embedded images and stuff.

Most pages are written using markdown, which is then rendered into html. I also use Jinja to add the same shell to all pages, and to template stuff in "complex" pages.

Build-time logic

The way I've implemeneted logic is with "Python pages", which are straight up python files, which get run at build-time and produce outputs. I wanted to avoid touching cursed python concepts, like modifying sys.path, so I used subprocess instead of importing. But this came with two problems:

  1. I now need to send output object across a process boundary

  2. I need to read files from the python script's dir, but also import files from the website root

The first one was quite easy to fix: I just make a function that converts an output into json and back, and send that using stdout. The second one is trickier and required setting PYTHONPATH for the child process. I also use another environment variable: _ROOT to pass through the root of the website to make some important functions work.

Negatives

Despite my best efforts, the code spagettified itself. For example many thigs are shared between blog.html (The template for a blog entry, located in the markdown source) and blog.html (The template for the blog index, located in pages). It's also weird that these are handled in two completely different plases. Luckily, since all page logic is contained withing the page itself, I don't actually have to touch that part of the code often.

Assets are more limited too. Here an asset is either website-wide (like the global stylesheet), or page-specific (like an embedded image). You can share assets between pages, but they have to be generated in the same place, for example all blog entries are generated in the markdown source, so they can all share the same stylesheet.

Positives

While using markdown pages is quite limited, the main advantage of this system is Python pages. The fact that each source can produce multiple outputs also creates interesting behaviour. For example the blog index page pages/blog/index/default.py produces not only the index, but also subpages for each tag.

Before I had to manually shove all images into one assets folder, and manually write the url's for them in the page I'm making. Now the markdown source automatically figures out the urls for images, which the slight caveat that any page that uses images must be in it's own folder, but you can't really avoid this.

Uploading

The uploading flow stayed the same, but I never really explained it, so here it is:

When I push my changes to codeberg, they get picked up by codeberg CI. They install the required packages (which is a step that unfortunately can't be cached, because at this point the repository has already been checked out)

Uploading to codeberg pages is trivial, but uploading to neocities is a little bit more difficult, because of their API. There is also a bug in neocities that forbids uploading more than 128 files at a time, so I had to work around that.

The markdown test

Heading 1.1

Heading 1.1.1

Paragraph of text with emphasis and bold text.

Another paragraph.

Heading 1.1.1.1

A second paragraph that contains inline code

Heading 1.1.1.2

And here's some outer code.

It     preserved
   has
                 spacing

And special characters like "quotes" and <angle brackets>.

Heading 2

Heading 2.1.1.1

Here's a link