After being frustrated by the complexity and 'time to site' of the most popular templating engines, I found they are not really good fit for any web developers who already have their HTML and just want to wrap content in a repeatable way.
Its designed to be dirt simple to use and satisfy the most basic requirements: Page header, nav bar, content and finally page footer. The project was primarily built to generate the https://pisteapp.com website.
- HTML Templating
- Markdown content
- Asset copying
- Easy to use Functions and macros to automate templated elements
- Context aware variable system
- Automatic image resolution selection/webp fallback
Run Static web generator inside any working directory you want to generate your website in.
cd mysite
StaticSiteGenerator.exe -w -x "https://mysite.com"
cd _www
python3 -m http.server
| Argument | Description |
|---|---|
| -w | Watch the input folder for changes |
| -mi | Dont auto detect multi resolution images |
| -t | Add additional asset file types (DEF: ".css", ".png", ".svg", ".js", ".webp", ".mp4", ".webm") |
| -x "https://mysite.com" | Set the Base URL and generate a sitemap.xml |
Anything HTML will be copied into its relative directory inside the output folder (_www/) so legal/eula.html will become _www/legal/eula.html. Anything thats an asset (.jpg, .png) will also be copied to the releative location so images/dog.png will become _www/images/dog.png Anything you do NOT want copied should start with an underscore e.g _workingSection/mysecret.html will not be copied as its parent folder is exempt.
Before your files are copied a parser scans both HTML and Markdown files for templating syntax. Files and folders starting with an underscore are not copied automatically but can still be referenced using the templating engine. You therefore should put all your reusable components inside the _partial folder, the contents of this special folder is made global and can be included from any other html/md file.
Important Example Structure for a blog:
/_partial/_header.html Includes our CSS and JS
/_partial/_footer.html Includes our copyright etc
/_partial/_navbar.html Includes our navigation bar and menu
/assets/css/ Contains asset files that are copied
/assets/js/ Contains asset files that are copied
/assets/image/ Contains asset files that are copied
/blog/index.html Page showing a list of blog posts
/blog/posts/__template.html The HTML template the blog posts should use
/blog/posts/mypost1.md A blog post content
/blog/posts/mypost2.md A blog post content
When running the tool a new folder called _www will be created, any assets or HTML files will be copied into the relevant directories, and Markdown files will be parsed and exploded into subdirectories
_www/assets/css/
_www/assets/js/
_www/assets/image/
_www/blog/index.html
_www/blog/posts/mypost1/index.html
_www/blog/posts/mypost2/index.html
The templating engine is basic, and primerially designed to help you automate placing partials around the page based on rules. Templates can be used in all HTML files, they cannot be used in markdown files. The most common syntax you'll use is the include("myhtml.html") function to embed other html files into your html file.
Basic Syntax
<div>
{{include('helloworld.html')}}
</div>
Becomes
<div>
<h1>Hello World</h1>
</div>
Nested Functions are evaluated automatically
Function Syntax
The answer is: {{add(multiply(2, 2),10)}}
Becomes
The answer is: 14
You can also use LINQ style syntax where the left hand parameter is deduced from the preceding elements
Linq Syntax
{{list_files('/blog/posts','*.md').take(5)}}
Basic Syntax:
{{take(list_files('/blog/posts','*.md'), 5)}}
Inline Operations
Sometimes its easier to just write operations as you would, remember the operators work left to right so you'll need to use brackets to explicitly define the order of mathmatical operations you may want
{{4*5+8}}
Outputs
28
(3-(1+1))
Outputs
1
Variables can be used to store and retrive data, their type is determed by the function using the variable. But conceptually you can treat them as strings.
The Number is: {{
assign('MyVar', 5);
var('MyVar');
}}
Becomes
The Number is: 5
Variables can also be generated by functions:
assign('myArray, to_array('1,2,3'));
foreach(var(myArray),concat('The number is:',var(foreach.key)));
Becomes
The number is: 1
The number is: 2
The number is: 3
And assigned based on the included file
## Inside index.html
include('myPartial.html')
## Inside myPartial.html
My owner is {{var('input.name')}}
And I am {{var('partial.name')}}
Becomes
My owner is index.html
And I am myPartial.html
When including files, any previously assigned variables are still in scope of the included file.
Here we have a Navigation partial that has a menu, we want the menu to highlight which section of the website we are currently in. Using inheritance the Nagivation partial's current directory variable contains the path of the page that included the navbar, we test the start of that path so no matter how deep into the navigation all the pages get the correct nav bar highlight.
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link {{if(equal(var('directory.path'),''),'active','')}}" href="/">
<span>Home</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {{if(starts_with(var('directory.path'),'/features'),'active')}}" href="/features/">
<span>Features</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {{if(starts_with(var('directory.path'),'/download'),'active')}}" href="/download/">
<span>Download</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {{if(starts_with(var('directory.path'),'/blog'),'active')}}" href="/blog/">
<span>Blog</span>
</a>
</li>
</ul>
</div>One of the most common templating examples is the need to make a page that shows recent blog posts.
Our blog posts are all written out as markdown files and we want to show a collection of the most recent ones for the user to browse though and click, at the root of our Blog we create
/blog/index.html -
We include our headers, a nav bar, a list of recent blog posts and a footer.
{#page.title:Blog Posts#}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Global header includes (JS files, fonts etc) -->
{{include('_header.html')}}
<meta name="description" content="List of blog posts published by Piste">
<title>Piste: {{var(page.title)}}</title>
</head>
<body class="d-flex flex-column min-vh-100">
<!-- Static Top Menu -->
{{include('_navbar.html')}}
<!-- Content -->
<div class="content container my-4">
<h1>New Ski Blog Posts</h1>
<div class="row">
{{foreach(list_files('/blog/posts','*.md'),load_metadata(var('foreach.key'), include('_blogpost_stub.html')))}}
</div>
</div>
<!-- Footer -->
{{include('_footer.html')}}
</body>
</html>We want each posts link to show the title and a small subtext of what the blog post is about to attract the user to click on ti, This is best done as a template stub. But before we include our _blogpost_stub.html, first we need to bring the post's metadata into scope so it can be referenced, we can use load_metadata on our file path (which is stored in the foreach.key variable) to do this. Once we've loaded the meta data we can then run the stub which will pull the variables out of the working scope and put them on screen.
/_partial/_blogpost_stub.html
<div class="row g-0
<div class="col-md-4">
<img src="{{var('post.image')}}" class="blog-image img-fluid rounded-start" alt="Blog Post Image">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">{{var('post.title')}}</h5>
<p class="card-text">{{var('post.caption')}}</p>
<div class="card-text" style="text-align: right;">
<a href="{{get_url(var('foreach.key'))}}"
class="btn btn-primary stretched-link float-right">Continue reading...</a>
<br />
<small class="text-muted">{{var('post.date')}}</small>
</div>
</div>
</div>
</div>Markdown files can be used to ease of content creation without the hassle of authoring individual HTML files. The markdown files get combined with a __template.html file to create a html sub directory.
Traditional templating syntax is not supported in markdown, but the metadata syntax is supported.
e.g mypost.md gets transformed into /mypost/index.html
/blog/posts/mypost1.md An example blog post with meta data prefixed (the metadata is used in the template to describe the page title, post date etc)
{#post.title:Top 5 Ski Runs in Val d'Isère#}
{#post.date:16 September 2024#}
{#post.resort:Tignes Val d'Isere#}
{#post.caption:Did your favorite run make the list?#}
{#post.image:/assets/image/blog/topskirunvaldisere/header.png#}
# Top 5 Ski Runs in Val d'Isère
Heading off to Val d'Isère or Tignes this year? Don't miss these top runs. From beginner green to icy half pipes we think you'll find a favourite in our mix.
## 5. Glacier - Val d'Isere

![float-end inline-image][Map of Glacier Piste](/assets/image/blog/topskirunvaldisere/5_map.png)
Val d'Isere has its own Glacier to rival Tignes's Grand Motte. A high-altitude gem that offers spectacular skiing on pristine snow, thanks to its location atop the Pissaillas Glacier. As a blue run, it provides a wide, gentle descent, making it ideal for intermediate skiers while offering stunning panoramic views of the surrounding peaks. The reliable snow conditions and breathtaking scenery make it a must-visit, particularly in the early season when lower-altitude runs may lack coverage. It’s a smooth, enjoyable ride with excellent snow, perfect for skiers looking to experience glacier skiing.We obviously want to wrap our blog post into an actual HTML page, so we use the directory level __template.html to do this.
/blog/posts/__template.html an example template for all blog posts to compile into
<!DOCTYPE html>
<html lang="en">
<head>
{{include('_header.html')}}
<meta name="description" content="{{var('post.caption')}}">
<title>{{var('post.title')}}</title>
<link href="/assets/css/piste_blog.css" rel="stylesheet">
</head>
<body class="d-flex flex-column min-vh-100">
<!-- Static Top Menu -->
{{include('_navbar.html')}}
<!-- Content -->
<div class="content container my-4">
<!-- Breadcrumb -->
<div class="row">
<div class="col-md-8">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item" aria-current="page"><a href="/blog/">Blog</a></li>
<li class="breadcrumb-item active" aria-current="page">{{var('post.title')}}</li>
</ol>
</nav>
</div>
<div class="col-md-4">
<small class="float-end">Posted {{var('post.date')}}</small>
</div>
</div>
<div class="row">
<div class="col-md-8">
<!-- Insert the markdown body here -->
{{var('content')}}
</div>
<div class="col-md-4 d-none d-md-block">
<h2>Related Posts</h2>
{{
foreach(list_files('/blog/posts','*.md').where(notequal(var('where.key'),var('input.fullname'))).take(5),
load_metadata(var('foreach.key'),include('_blogpost_stub_horizontal.html')))}}
</div>
</div>
</div>
<!-- Footer -->
{{include('_footer.html')}}
</body>
</html>The processor pushes and pops variables as it goes though the directories and files, any variables you declare inside a HTML or md file only exists to sub files that are included in within it. each code block creates a new layer thst is popped when the block ends
This are available everywhere
| Variable | Description |
|---|---|
| root.fullpath | The full on-disk path the current working directory (Root) |
| root.url | The full url of the site root as set by -x |
| root.output | The full on-disk path of the output directory |
Each time a sub directory is entered the following variables are pushed onto the stack, subdirectories hide the parent directories variables.
| Variable | Description |
|---|---|
| directory.fullname | The full on-disk path the current input directory |
| directory.path | The relative "to root" path of the current directory |
| directory.name | The name of the current directory |
Each time a HTML file is processed the following variables are assigned
| Variable | Description |
|---|---|
| input.fullname | The full on-disk path the current input file |
| input.path | The relative "to root" path of the current file |
| input.name | The name of the current file |
| input.createdon | The date the input file was created on |
| input.modifiedon | The date the input file was last modified |
| output.fullname | The full on-disk path the current output for the file |
| output.path | The relative on-disk "to root" path of the output for the file |
| output.url | The relative url of the output file generated |
| output.fullurl | The full url of the output file generated |
Each time a Partial file is included the following variables are assigned
| Variable | Description |
|---|---|
| partial.fullname | The full on-disk path the current input file |
| partial.name | The name of the current partial file |
| Function | Arguments | Returns | Description |
|---|---|---|---|
| Add | number1,number2 | number | Adds two numbers together and returns the result |
| Subtract | number1,number2 | number | Subtracts two numbers together and returns the result |
| Divide | number1,number2 | number | Divides two numbers together and returns the result |
| Multiply | number1,number2 | number | Multiplies two numbers together and returns the result |
| Function | Arguments | Returns | Description |
|---|---|---|---|
| FormatDate | date,outputformat | string | Prints an ISO date (yyyy-MM-dd HH:mm:ss) in the specified Microsoft.Date format |
| Function | Arguments | Returns | Description |
|---|---|---|---|
| Assign | VariableName,value | - | Assigns VariableName to the specified value |
| Var | VariableName | value | Retrives the value stored under the variable name |
Variables can also assigned using metadata flags in HTML and Markdown files, this is particually usefull when you want to get a summary information of a file without nesserially parsing the whole file.
{#VariableName:value#}
| Function | Arguments | Returns | Description |
|---|---|---|---|
| load_metadata | input filename,body | result of body | Loads the metadata variables from a file into the stack then executes the body statement |
| Function | Arguments | Returns | Description |
|---|---|---|---|
| equal | LHS,RHS | bool | Returns true if the result of LHS equals the result of RHS |
| doesnotequal | LHS,RHS | bool | Returns true if the result of LHS does not equal the result of RHS |
| if | bool,body if true, [body if false] | Selected Body | Returns the true body or false body depending on the conditional |
| Function | Arguments | Returns | Description |
|---|---|---|---|
| Concat | Str1,Str2,... | String | Concatinates all the paramaters into one string |
| Startswith | haystack,needle | Bool | Checks if the haystack starts with needle |
| Function | Arguments | Returns | Description |
|---|---|---|---|
| foreach | array,body,[foreachVariableName(def=foreach)] | array result of body | executes the body for each item in an array, the variables foreach.key and foreach.index are assigned before each body is executed |
| join | array,[seperator(def=,)] | concatinated result | Concatinates each item in the array together using the seperator charater DEF: , (CSV) |
| reverse | array | array | Reverses the array |
| shuffle | array | array | Randomises the array |
| skip | array,count | array | Skips the first X items in an array |
| take | array,count | array | Takes the first X items in an array |
| to_array | string | array | Splits a CSV string into an array |
| orderby | array, selector, [desc | asc] | array |
| orderby_metadata | array, metadata_value, [desc | asc] | array |
| where | array, conditional bool, [whereVariableName(def=where.key) | array | Selects items out that match the conditional, a variable where.key is added to the stack before each conditional is executed, return true to include item |
| Function | Arguments | Returns | Description |
|---|---|---|---|
| get_url | input path | string | Finds the resultant URL of an input file once processed though the system i.e /blog/posts/markdown1.md -> /blog/posts/markdown1/index.html |
| include | input path | body | Parses then Prints the included file in place |
| list_files | input_path,[filter=(def:*)] | array | Lists all files within a directory with an optional filter |
| minify_url | url | string | Attempts to remove index.html from paths, i.e blog/posts/markdown1/index.html -> blog/posts/markdown1/ |