Static Site Generator features

Most, probably all, static site generators have many more features than I need. I am working on making my own simple static site generator. The only thing I need is the equivalent of SSIs. Cloudflare Pages do not support SSIs but I could preview a site locally using a server that supports SSIs then when I push to GitHub an action could process the HTML files and do the substitution.

I am interested in suggestions for other features. I know that is vague but I am not sure how to be specific about what features might be worthwhile. Hopefully someone can use the suggestions even if I do not.

Have you not considered using PHP includes for your project?

This is for static websites. The definition of static websites is that the server does nothing, in other words no PHP. Static websites can be hosted for little or no cost. GitHub Pages and Cloudflare Pages are hosted for free.

From my experience, if you’re building a simple static site generator, I’d keep the feature set very small and intentional.

Besides basic includes/partials (SSI-style), the most useful additions I’ve seen are:

  • A simple layout system (header/footer wrapping pages)

  • Basic variable support (site title, base URL, env values)

  • Markdown → HTML conversion (even optional)

  • A minimal build step for asset copying/minification

Everything else (taxonomies, complex templating, plugins) tends to add more maintenance than value unless the site is large.

In real projects, the best static generators are the ones that stay boring and predictable — easy to reason about, easy to debug, and fast to build.

I had a discussion with ChatGPT and it made it clear that the idea of using the SSI feature of servers like IIS and Apache is not good. When I want to preview locally a site that I am using a Static Site Generator (SSG) with I should execute the SSG locally.

If I understand what you are saying then that is what the include feature is for.

That sounds useful. If I do that then that makes my first comment above more relevant.

I had a discussion with Google Gemini and there are very many advantages and disadvantages of Markdown. For me support of Markdown is not sufficiently advantageous for the substantial amount of additional processing it requires, primarily the processing (programming) I would do,

I will keep that in mind. Minification can be done separately, less frequently than each execution of the SSG. Automatic minification could be a useful feature of a SSG that supports many more features but for now I will keep the SSG simple. It’s worth considering. Unless I misunderstand what you mean by copying of assets, I think that is typicaly done by GitHub Actions,

There are enough libraries our there to parse markdown to html. There is no need to code that yourself.

Worst comes to worst you can always let AI write one for you.

This is one I use on my site.

Then you shouldn’t be using SSIs

My post and a subsequent reply is relevant to that. I won’t repeat what I already said. If however I still intended to use SSIs then it would help to have an explanation of why not to use them.

I was not sure whether to say that the common definition of a static website is misleading and inaccurate. Servers do stuff when they serve a static website, even if it is only copying a file from its file system to the internet.

  • They are a potential security risk (potentially vulnerable to XSS attacks)
  • They can cause issues with DOM rendering.
  • They’re obsolete and have been replaced by other approaches (React/Node/etc) where the dynamic outputs are built server side and rendered to the client browser cleanly.
  • They’re limited in functionality (which is why they’ve been replaced with other approaches)

I have heard of SSI, but never understood what it was. After asked AI, I interpret this as a method to make the static site more DRY? By adding some repeated HTML into pages by Apache etc?

If I understand this correct, it may be possible to use Javascript to inject HTML templates snippets into the static page? Mimic SSI?

Or html templates?

<!-- In a separate file -->
<template id="header-template">
  <header>
    SSI mimic?
    <nav><!-- shared nav --></nav>
  </header>
</template>

<!-- In page -->
<div id="header"></div>

<script>
  const header = document.getElementById('header');
  const template = document.getElementById('header-template').content.cloneNode(true);
  header.appendChild(template);
</script>

I must have been outside the door when they were talking about SSI’s, I just jumped straight to PHP in 2004.

I’ve just watched a 17 year old video on them

I can see the appeal from a “keep it simple” approach over Node/React etc, but if security is an issue then that is a concern.

Note that I said I wanted to use SSIs only for local viewing. And static sites have no need for security.

Yes. HTML such as headers, footers and sidebars.

That is a possibility. Doing it in a Static Site Generator would be more efficient. I was considering use of SSIs only for previewing locally.

Templates are a very common feature of SSGs. ChatGPT explained to me that what I am trying to do is the equivalent. My static site can have files of partial HTML that gets inserted into pages.

A touch of SPA?

That is not a feature of a Static Site Generator.

This method is used by SPA, but I agree it is not SPA we are talking about. Just a “tiny touch” of SPA. It is more like “sub templates” to make it more DRY. Injected by Apache, Nginx or Javascript?

The following is my specifications. I intend to get AI to write the code. If anyone wants to they can generate a version customized for them, even using a different language. I am interested in comments. I have gotten many reviews from AI but a real person might have useful suggestions. I think that Path.GetFullPath is unique to C# and probably needs to be changed in the specifications if a different language is used.


Snippets Static Sites Generator

Create a C# .Net 8 HTML Post-Processor for Static Sites GitHub Action that can be executed as a Ubuntu Runner. The input will be a configuration file, an input directory and a snippets directory and the output will be an output directory in which html files will be modified. Use GitHub Actions error/warning annotations. When run as a GitHub Action the program will execute as a Ubuntu application in a Docker container. When run locally for testing it will execute as a Windows application. The contents of output_dir (see below) will separately be deployed as the static website.

Configuration File

The configuration file is a yaml file in the following format.

input_dir: "{dir}"
output_dir: "{dir}"
snippets_dir: "{dir}"

block_substitutions:    # list of zero or more objects, each with:
  - name: "{blockname}"    # required
    file: "{filename}"  	# required, relative path

inline_substitutions:     # list of zero or more objects, each with:
  - name: "{substitutionname}"     # required
    text: "{substitutiontext}"	 # required

Where block_substitutions and inline_substitutions are arrays of objects.

If the configuration file has invalid yaml then issue error message MSG003 and exit with code 1. (MSG003 will be improved based on parser output after the code is generated.)

When a configuration list contains duplicate name values (within the same list) the first occurrence is to be used; for each subsequent duplicate emit warning MSG012 (use the parser node line number for the annotation) and ignore the duplicate entry — comparisons are case‑insensitive.

A blockname can be the same as a substitutionname and both are case-insensitive.

If the configuration file contains any top-level keys other than input_dir, output_dir, snippets_dir, block_substitutions, and inline_substitutions, issue error message MSG001 and exit with code 2. If any entry in block_substitutions contains keys other than name and file, or any entry in inline_substitutions contains keys other than name and text, issue error message MSG002 and exit with code 3.

For input_dir use src as the default if not specified. For output_dir use public as the default if not specified. For snippets_dir use snippets as the default if not specified.

Issue error message MSG004 and exit with code 4 when the specified input_dir does not exist. Issue error message MSG005 and exit with code 5 when the specified snippets_dir does not exist. Issue error message MSG015, MSG016 or MSG017 (whichever is appropriate) then exit with code 6 when either input_dir, output_dir or snippets_dir are the same. Issue error message MSG007 and exit with code 7 when either input_dir, output_dir or snippets_dir overlap (one is a subdirectory of the other).

If a file with filename does not exist then show warning message MSG010 and ignore the blockname.

Processing

Create output_dir if it doesn’t exist and clear it if it does. Issue message MSG013 and exit with code 8 if the directory cannot be created. Issue message MSG014 and exit with code 9 if the directory cannot be cleared.

Cache the contents of block_substitutions files and support processing of html files in parallel. Make the snippet cache thread-safe and immutable once built.

Use iteration instead of recursion to copy all files, directories and subdirectories from the input_dir directory to the output_dir directory. Copy file contents only, do not copy timestamps, permissions and extended attributes.

Process every file in the output directory and subdirectories that have a html extension in the following manner.

The contents of each html file will have comments containing a tag and a name (as specified below) replaced. All other contents will not be changed.

If an input HTML file contains a BOM then emit warning MSG008 and preserve the input file’s BOM and encoding when writing the output file; snippet files must have any BOM removed before their contents are inserted (inserted snippets must not reintroduce a BOM), and encoding conversions should preserve the original HTML file encoding where possible.

The HTML comments (SSG block markers) are in one of the following two forms:

<!--ssgb {name}--> — marks the beginning of a named block

<!--ssgi {name}--> — marks an inline (or include) reference

If (ssgb or ssgi) is not followed by whitespace then ignore the comment. Whitespace may optionally appear between <!-- and the tag keyword, and between the name and -->. No whitespace can be in name and name is case insensitive.

Allow ssgb and ssgi comments with the same name to exist multiple times in a html file and processed as in the following.

When a named block comment is encountered and name matches a blockname in the block_substitutions section then replace (first remove) the comment with the contents of the file in the snippets_dir directory with the filename in the corresponding filename.

If a snippet has a BOM then remove the BOM before inserting the contents in the output. If the name does not match then show warning message MSG009 and leave the comment as-is. When an inline reference tag is encountered and name matches a substitutionname in the inline_substitutions section then replace (first remove) the comment with the corresponding substitutiontext. If the name does not match then show warning message MSG011 and leave the comment as-is.

All relative paths in the configuration are interpreted relative to the repository root (the GitHub Actions workspace, GITHUB_WORKSPACE) when running as an action. When running locally for tests, relative paths are interpreted relative to the process working directory. Absolute paths are accepted unchanged.

Before any directory-equality or overlap checks, the tool canonicalizes paths by calling Path.GetFullPath, normalizing separators, removing trailing separators and when possible, resolving symlinks to their targets. Path comparisons use platform-appropriate case sensitivity (case-insensitive on Windows, case-sensitive on Unix). Equality is determined by comparing canonicalized absolute paths; A is considered a subdirectory of B if A begins with B + path separator after canonicalization.

Messages

ID Level File Line Message Template
MSG001 error conf. conf. file line Configuration file unknown top-level key: {key}
MSG002 error conf. conf. file line Invalid key {key}
MSG003 error conf. conf. file line Invalid yaml, reason: {reason}
MSG004 error conf. conf. file line Input directory {input_dir} does not exist
MSG005 error conf. conf. file line Snippets directory {snippets_dir} does not exist
MSG007 error conf. conf. file line Overlapping directory, {directory} is a subdirectory of {directory}
MSG008 warning html output 1 File has a BOM
MSG009 warning html html line Block name {name} not found
MSG010 warning conf. conf. file line File {filename} does not exist
MSG011 warning html html line Substitution name {name} not found
MSG012 warning conf. conf. file line Duplicate name {name} in block {key}
MSG013 error conf. conf. file line Output directory cannot be created, error: {reason}
MSG014 error conf. conf. file line Output directory cannot be cleared, error: {reason}
MSG015 error conf. conf. file line input_dir and output_dir resolve to the same path
MSG016 error conf. conf. file line input_dir and snippets_dir resolve to the same path
MSG017 error conf. conf. file line output_dir and snippets_dir resolve to the same path

For MSG001, MSG002, MSG003, MSG010 and MSG012 use parser node line numbers.

For MSG009 and MSG011 use the line where the comment appears in the input HTML.

When I got the program generated it looked for the snippets in the base directory instead of the snippets directory in the base directory. That might be a design error but it probably was an error made by Github Copilot.

When I asked Google Geminin AI about how to get the generated site seen by Google one suggestion is that the Static Site Generator could create a site map. Therefore that is an additional feature that would be useful for this.