Guidelines for Building a Static Site Generator (2026 Edition)
The Next Generation
I've built several static site generators for personal use. Early ones were to support neopolitan. Later ones were to make it easier to build sites in general.
Now, I'm looking to move my site from alanwsmith.com to al9000.com. Part of the reason I want to make the move is to pull all my sub-domains back under the main site. It'll make things a lot easier to link back and forth and to update with global templates.
I also want to expand the site by adding in my Rust Grimoire. I've got tons of rust notes on the site already, but the grimoire isn't included. Examples in it span multiple files that my current site generator isn't set up to accommodate.
TODOish
The new generator is just for me. No need to worry about it making sense for other folks. So, I'll mostly be building things as need arises. That said, there are some basics that I want to make sure I've thought through before kicking off. Here's the list (which I'll add to whenever I think of something).
- Split out the neopolitan parser so that it's its own thing that can be included in other projects.
- Use MiniJinja as the template engine (it's Jinja, but in rust).
- Every page on the site gets loaded as a template that can be included by any other page.
-
Template names are the full path to the file (as opposed to one of my current site builders where the leading
/isn't there in the template names) -
Syntax highlighting of blocks. (Return values won't include and pre tags, just the spans to put into a pre tag).
Ideally this can be
[! highlight_block("html") !][! endhighlight_block !]But I think i may have to be
[! filter highlight_block("html") !][! endfilter !]TODO: look to see if there's a way to use the built in
[! block KEY !][! endblock !]tokens and pass them extra options for the highlighting. -
Syntax highlighting of spans.
[! filter highlight_span("html") !][! endfilter !]As well as this, maybe:
[@ hlspan("html", "the string to highlight") @] -
I was originally thinking about adding a way to highlight a file directly (e.g.
[! highlight_file("key") !]), but that causes issues if the files being highlighted have minijinja tokens in them (i.e. they won't be parsed and the tokens will show up instead of the output they should produce). - Funciton to pull specific lines from a file with syntax highlighting applied (i.e. the syntax highlighting happens on the entire file and then the lines are pulled)
- Cache syntax highlighting since it can take some time.
- Use tracing library to be able to meausre performance when necessary.
-
Shorthand for
[! filter some_filter !][! endfilter !]that is[! f some_filter !][! endf !]. -
[! md !][! endmd !]filter for markdown. -
[! neo !][! endneo !]for neopolitan. - Good error messages for where things went wrong in template processing or neopolitan parsing.
- Update the most recently changed file first and send refresh to browser when its updated. (this will get tricky if the file being updated is an include. so this will only happen later if the base performance isn't good enough)
- Auto populate `/last-update/index.html` with whatever the most recent .html file to have changed is.
-
Any valid JSON files is available from a
[@ j @]variable (e.g./some/data.jsonis available at[@ j.some.data.KEYS @] -
The top level
/config.jsonfile is available at[@ c.KEYS @]. - Was originally thinking about having hooks that can run scripts before and after processing. But, the first approach will be to either run stuff manually or build the feature into the engine itself (e.g. A feature that grabs JSONs from external URls and drops then in place prior to running the build. That should cover most of what's necessary.)
- Provide JS minifier
- Provide CSS minifier
- Auto genreate RSS feeds. One for all posts. One each for different tags.
- Auto generate pages for each tag (with pagination)
- Auto generate index link pages for posts (with pagination)
- Provide details on the time it takes to render each page.
-
files and directories that have `.inc` in their names are avaialbe as includes but don't get output to the site themselves (e.g. the
wrappers.incdirectory will hold all the wrapper templates that everything uses but doesn't go out to the site). - If you put a JSON file next to a file with the same filename stem it becomes avialble via a `data` variable in during processing.
-
Arbitrary blocks of code should be able to be added to the head of the document from inside a content page. (This can be done with basic
blocktags in minijinja) - Put in template hooks with defaults to override parts of the main template.
- Create a block for including bitty templates so they can be added to the end of the page or included via another file (This is really just an implementation detail for the wrapper templates).
Neopolitan
- Need to do more thinking about how to integration Neopllitan and MiniJinja.
-
I think the biggest thing will be setting up the
-- templatein page metadata to define the minijinja template to include from. - There will have to be some things that build out a little differently. I think it's basically going to require a preprocessor that turns neopolitan files into base level HTML files that are ready to be processed by minijinja.
- More thinking (and, more importantly, prototyping) to figure that out.
Images
- A single image folder that can have any folder/file structure inside it.
- Image calls in content are done with just the image file name without the extension. The engine finds the right file and links it up based on the name.
- Auto generate responsive image sites.
- Auto generate responsive image tags with the available sizes.
-
Output files are generated in the same places as their inputs. That is,
/path/filename.htmldoes not become/path/filename/index.html. - File extension can change (e.g. "file-name.neo" becomes "file-name.html").
File Metadata
During processing every file has access to a metadata variable that holds metadata including:
- File name with extension.
- File name without extension.
- The source file extension.
- The destination file extension.
- The full string path to the source file.
- The string path to the parent folder.
- An array containing strings for the path to the parent folder split on each directory level.
-
A variable that points to the files directory for calling JSON in it. (e.g.
/path/to/file.htmland/path/to/data.jsonwould have a variable called[@ local @]that is an alias for `[@ json.path.to @]so you can do[@ local.data.KEY @] to get whatever is in the data.json file. -
TBD on how this works with includes. I think all paths are relative to the top level output page and not parsed at the include file level (e.g. if
/page/index.htmlincludes/extra.inc/details.htmlthe file path value from ametadatacall in/extra.inc/details.htmlwould show the path to/page/index.html.
Folder Metadata
- Similar to file metadata
- details tbd
Site Metadata and Functions
-
A
folders()function that returns an array of all the folders on the site where each entry is itself an array of the folder path split by directories. -
An
path_exists(PATH)that returns true if a given path exists.
Two Pass Processing Probably Not Needed
These are my original notes on setting up two pass processing. I had to do that in my existing static site builder in order for code highlighting to work when the code being highlighted pulled in other content via includes. I was using functions for that setup. I think I can wrap stuff in a different way and not have to deal with that.
- Make a two pass system so that each page can be rendered and then the rendered version be included in the next pass.
-
Standard tokes that are processed on the first pass are:
-
[! !]functions -
[@ @]output -
[# #]comments
-
-
Tokens for the second pass are:
-
[2! !2]functions -
[2@ @2]output -
[2# #2]comments
During the first pass those tokens get changed to their standard counterparts so they get used normally in the second pass.
NOTE: it may more more sense to do
[1@ @1]etc to assist with caching. The idea being that you can watch files for changes and only update the ones with[1@ @1]on the first run through and everything else could be cached and ready. (Not sure if that makes sense or not. Something to look into) -
-
Escaping tokens is done with:
-
[/! !/]functions -
[/@ @/]output -
[/# #/]comments
After the second pass those tokens are converted to strings of their non-escaped versions via find/replace.
-
Miscellaneous
- Build in preview server with hot realoading on file changes (with appropriate throttle/debounce).
- files_in_folder(FOLDER_PATH) function that gets the file list prior to processing (i.e. it would see include files that won't otherwise be sent out to the site)
-
files_in_folder_ext(FOLDER_PATH, EXTENSION) function (i.e.
files_in_folder_ext("/some/folder", "jpg")finds all the JPG files. EXTENSION can also be an array to find files with any number of extensions. - folders_in_folder(FOLDER_PATH) function
- path(STRING, STRING...) function that assembles strings into safe paths.
Version 9000
That's everything I can think of at the moment. That thinking included taking a look at my current generators and this prior post with earlier ideas. Some of that holds up. A lot I'm thinking about differently these days.
I've got bitty in pretty good shape. Moving into this njavascript.on.jsew generator isn't too far away.
Pretty excited about the update, tbh,
-a