, 33 min read

Simplified Saaze

Original post is here eklausmeier.goip.de/blog/2021/10-31-simplified-saaze.

1. Introduction

Simplified Saaze is a static site generator. I.e., it takes Markdown files as input and generates fixed HTML files. Simplified Saaze is a simplified version of Saaze from Gilbert Pellegrom. Large parts of this document are taken from the Saaze documentation. Simplified Saaze is roughly 90% compatible with Saaze. Simplified Saaze is built on below principles.

1. Easy to run. Simplified Saaze is built in PHP with some small parts in C. PHP is roughly used by 80% of all websites on the internet. Simplified Saaze needs no other PHP framework and only one PECL library.

2. Easy to host. Static sites are great for being fast and easy to deploy. However, sometimes you need dynamic aspects to your site (e.g., contact forms, custom scripts, etc). Simplified Saaze gives you the choice depending on what makes most sense.

3. Easy to edit. Markdown has become the de-facto way to edit content for the internet. It's simple to understand and write. So Simplified Saaze uses Markdown with a sprinkle of Yaml frontmatter to manage your content.

4. Easy to theme. Simplified Saaze uses plain PHP/HTML to theme. Any PHP code is a valid theme and can be checked with php -l.

5. Fast and secure. Simplified Saaze works with ordinay files in your filesystem. No database required. This means less setup and maintenance, better security and more speed. Simplified Saaze is way faster than Hugo or Zola, see Performance Comparison Saaze vs. Hugo vs. Zola.

6. Simple to understand. Simplified Sazze deliberately has a stupidly simple architecture: Everything is a collection of entries. Pages, posts, docs, recipes, whatever. It all works in the same, simple way. Supports multiple blogs under the same URL out of the box.

7. All-inclusive. Developing your site should be painless. No external tools required.

8. No bloat. No Javascript or large CSS added to output. Only if you need Javascript or CSS, then it is added.

2. Installation

1. PHP version 8. Simplified Saaze requires PHP version 8 as a minimum as it uses FFI. Instead of FFI you can use the MD4C PHP extension, see below. To be exact, PHP version 7.4 would be sufficient for FFI, but Simplified Saaze also makes use of "union types" only present in PHP8. Please note that PHP 7 active support ended November 2021, and security support for PHP version 7 ended November 2022.

Checking the PHP version is

php -v

and should show something like

PHP 8.3.4 (cli) (built: Mar 17 2024 09:12:30) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.4, Copyright (c) Zend Technologies
    with Zend OPcache v8.3.4, Copyright (c), by Zend Technologies

The OPcache is highly recommend for speed reasons. See Parallelizing the Output of Simplified Saaze.

2. Yaml extension. Your PHP needs the Yaml extension. Download from PECL. See PECL's Yaml Way Faster Than Symfony's Yaml. It boils down to phpize, configure, make. Check with php -m whether yaml is finally enabled in php.ini:


Alternatively, your Linux distribution provides prebuilt packages. For example, Arch Linux has php-yaml, Ubuntu has php-yaml.

3. Composer. Installation with composer: Create a directory of your liking, change into it, then run

composer create-project eklausme/saaze-example

This will download and install an example blog, and also the actual Simplified Saaze software.

4. MD4C library. The MD4C library must be installed. For example, on Arch Linux you check with

pacman -Qs md4c

5. PHP FFI. Either FFI must be enabled in PHP, or you use the MD4C PHP extension. Check with phpinfo() or

php -m | grep FFI

To compile the FFI you need a C compiler, for example GCC. To compile the FFI to so use

cc -fPIC -Wall -O2 -shared php_md4c_toHtml.c -o php_md4c_toHtml.so -lmd4c-html

C program file php_md4c_toHtml.c is located in vendor/eklausme/saaze.

6. MD4C extension. As an alternative to FFI you can use the MD4C PHP extension. Download the source code from php-md4c. Then run


Copy the resulting file module/md4c.so:

cp modules/md4c.so /usr/lib/php/modules

Activate the extension in php.ini:


7. Configuration. Go to directory vendor/eklausme/saaze and edit Config.php to supply the correct location of php_md4c_toHtml.so in self::$H hash, key is global_ffi. Example configuration:

'global_ffi' => \FFI::cdef("char *md4c_toHtml(const char*);","/srv/http/php_md4c_toHtml.so"),

The so-file can be placed "anywhere".

Double check that FFI is enabled in php.ini:


8. General remark. All these prerequisites are only required to generate the static HTML files. Once the HTML files are generated, your web-server does not need PHP, nor MD4C, etc.

Only if you want to use the dynamic function of Simplified Saaze then your web-server needs PHP and all the above prerequisites.

9. GitHub. Source code is on GitHub: eklausme/saaze. Changing or adapting the source code to ones own requirements should be painless. The entire source code is less than 2 kLines.

In Installing Simplified Saaze on Windows 10 you find a step-by-step write-up for installing on Windows 10. Also see Installing Simplified Saaze on Windows 10 #2.

10. JIT. For performance reasons it is recommended to activate PHP JIT and OPCache. Edit php.ini and change/edit:


Check with:

php -v
php -r 'var_dump(opcache_get_status());'  

See PHP JIT in Depth.

3. Directory structure

Assume you have created a directory ssaaze, i.e., mkdir ssaaze. Then composer would have created

├── build/
├── content/
│   ├── blog/
│   |   └── example-page.md
│   └── blog.yml
├── public/
│   └── index.php
└── templates/
    ├── blog/
    │   ├── entry.php
    │   └── index.php
    ├── index.php
    ├── entry.php
    ├── error.php
    ├── top-layout.php
    └── bottom-layout.php

The directories serve the following:

  1. build will contain the result of the run when generating static files.
  2. content is where all your Markdown files reside.
  3. public is used for dynamic content. It should show the same content as in build, just without any static files laying around.
  4. templates contains PHP files which are used to generate static or static files. They usually contain common HTML elements, which are present on all your web pages. For example, they contain your company logo, Google analytics, etc.

It is very likely that you will have additional directories, for example, for images or PDF documents. They are not touched by Simplified Saaze.

4. Basic usage

4.1 Static site generator

Go to your content directory or any subdirectory therein and create your Markdown file with frontmatter in the beginning. An example is here:

title: An Example Post
date: "2021-10-30"
This is an **example** with some _markdown_ formatting.

Then run

php saaze

That's it. This will populate the build directory with HTML files. Either point your web-server document root directly to this directory, or copy/move files in build to your web-server's document root.

All your Markdown files must have suffix .md. That's what Simplified Saaze is processing. The file name can be arbitrary, except the name index.md is special.

File index.md serves as transparent section. I.e., the content of the index.md file will be shown when the directory will be the ending part in the URL. Assume, for example, directory a/b/c contains index.md. Then the URL for https://.../a/b/c will show the Simplified Saaze'd output of index.md. This is usually for table of content like pages. For example, let's assume blog/2021 contains a number of Markdown files. Then index.md in blog/2021 can serve as a table of content for this directory.

4.2 Dynamic content generation

There are two ways to get content generated dynamically, i.e., on the fly:

  1. Use PHP-executable as web-server
  2. Configure your Apache/NGINX/... web-server

The first way is the PHP-executable method. Use PHP builtin's web-server:

php -S 0:8000 -t ~/yourDirectory/public

This will present your content at URL localhost:8000/. This PHP-executable method is mostly for testing only, as the PHP-executable used in web-server mode is not meant for production usage.

The second way is to put index.php from public directory to your document root of your web-server.

For this dynamic web page generation to work you must have URL rewriting enabled in your web-server! E.g., in Hiawatha you must use something like this:

UrlToolkit {
        ToolkitID = PHP_Routing
        RequestURI isfile Return
        Match ^/*$ Rewrite /index.php?/blog/
        Match ^/(.+) Rewrite /index.php?$1

The important part is the last line with Match in above configuration, telling the web-server to redirect the URL https://example.com/abc/uvw to https://example.com/index.php?/abc/uvw. Without this, dynamic content generation will not work. Rewriting the empty string to /blog/ is just a convenience.

For Lighttpd the configuration is:

server.modules += ( "mod_openssl", ..., "mod_rewrite" )

url.rewrite-if-not-file = (
        "^/*$"  => "/index.php?/blog/",
        "^/(.*)" => "/index.php?$1" 

As before, rewriting the empty string to /blog/ is just a convenience.

An example configuration for NGINX is below:

server {
    listen       80;
    server_name  localhost;
    . . .
    rewrite "^/lemire/blog($|/.*)"  "/rewrite/saaze-lemire/public/index.php?/blog$1" last;

For more details see From Hiawatha to NGINX. There we also describe how to aggressively cache all previously generated Markdown files. This essentially gives you static HTML pages with almost zero generation time.

4.3 Single file generation

Simplified Saaze allows to generate a single file, instead of all files in content directory. Use command-line option -s for this and specify the input Markdown file. E.g.,

php saaze -s content/blog/2021/new-post.md

will just build this single file. This is important when you don't want to run Simplified Saaze for your entire website, but rather just insert or update a single post.

This single file generation can be integration into a Makefile to just generate updated files.

Single file generation does not honor draft-mode or stealth-mode, i.e., an entry marked with draft: true or entry: false will be generated anyway.

4.4 Specifying an alternate build directory

When you add the command-line option -b you can specifiy the directory where the static files will be placed. E.g.,

php saaze -b /tmp/ramdisk/

This will generate the static files in /tmp/ramdisk instead of build.

4.5 Turning extract file generation on

In the single file mode you sometimes also want the excerpt file, such that you can update some table of content file with this excerpt. If you want the excerpt file generated, then add -e.

php saaze -es content/blog/2021/another-post.md

Above example generates one static HTML file for the single Markdown file content/blog/2021/another-post.md, but, in addition, the file excerpt.txt is generated. So obviously, the extract file only makes sense for a single file.

4.6 Draft-mode

Any blog post which contains draft: true in the frontmatter will not be shown in the generated static HTML. If you want draft posts to be generated then specify -f:

php saaze -f

Having draft posts mixed with your normal content allows you to work on some still unfinished posts, without having them on your "productive site".

Draft posts will not be shown in dynamic mode, see 4.2. The reason for this disparity between static and dynamic mode is, that dynamic mode cannot take command-line arguments.

4.7 Parallel output

For performance reasons you can specify how many output processes shall be used by specifying the parameter -p and an integer for the number of allowed processes:

php saaze -p 15

Above command will make use of 15 Unix processes. See Parallelizing the Output of Simplified Saaze.

The number of allowed processes has no influence on the actual generated output. This command-line argument is for speed reasons only.

4.8 Stealth-mode

While draft-mode completely suppresses the creation of an entry and corresponding entry in the index-page, there are two settings in the frontmatter that control their appearance in either the index-page or as entry.

Setting index: false will suppress the appearance in the index-page. I.e., this entry will show as separate entry, i.e., it will occur as content page, but it will not be shown in the index, will not be shown in categories, will not show in tags, and not show in sitemap/overview. Default is index: true. This setting is useful if you want to generate an entry, but do not want it be indexed by Google, Bing, etc. Usually you will provide the proper URL to coworkers. Once your coworkers know the URL they can access the content. So the content should not be secret. But the entry is not advertised in any way. In your blog there will be no link to this entry, unless you explicitly reference this entry. Implicitly, this setting is used for Markdown files named index.md. If you use categories or tags then this entry will be seen in the category and tag overview.

Similarly, entry: false will create the corresponding stuff in the index-page but will not create any entry. Default is entry: true. This setting has its use in case you simply don't need any entry, as the excerpt provided in the index-page is already enough. For example, if the excerpt in the index-page is like a Tweet. One application is Leon Paternoster's link-entry.

These two settings can be set on each entry or in the collection yaml file. The setting in the entry will take precedence over the setting in the collection yaml file. So, for example, you can set entry to false in the collection yaml file. Therefore no entry will be shown. But for some individual entries you can override this collection-global setting.


draft===true <=> (index===false) && (entry===false)

If you need a more fine grained method to decide whether an entry is shown or not, consider filter.

4.9 Categories and tags

If you have more than say 50 blog posts, then organizing them via categories and/or tags becomes beneficial for the reader to find related content. If you want to use categories and tags you add

categories: ["category1", "category2", "category3"]

to your frontmatter. In the same way you add tags to your frontmatter at the top of your post:

tags: ["tag1", "tag2", "tag3"]

Now you use

php saaze -t

to generate a cat_and_tag.json file in the content directory. This file can then be used in your templates. This generated file looks like this:

    "categories": {
        "Android": [
                "2013-03-13 21:54:03",
                "Screenshots on Nexus 4 (Android 4.x)"
                "2013-08-04 18:22:55",
                "Google Now Emergency Alert"

This cat_and_tag variable is a three-dimensional array: $cat_and_tag[$i][$j][$k].

  1. $i is either categories or tags
  2. $j is the category or tag, e.g., "Android" or "hardware", etc.
  3. $k being either 0, 1, 2, i.e., it is a list of 3 elements
    • URL
    • date
    • title

This cat_and_tag.json can only be generated during static site generation, and not during dynamic mode.

One important caveat: the cat_and_tag.json file is generated at the end of the generation. So the very first time, when you use categories and tags, the file will be empty, and you won't see categories and tags. Only after the second time, when you generate your static site, will you see your categories and tags. This is similar to the way TeX or LaTeX works with regard to table of contents or indexes.

4.10 RSS XML Feed

Passing -r as command-line flag will produce a file called feed.xml in the build directory. This file is an Atom 2.0 feed. The actual generation of feed.xml is done by the template code in rss.php. This rss.php looks something like this:

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <title>Elmar Klausmeier's Blog</title>
    <description>Elmar Klausmeier's Blog</description>
    <atom:link href="https://eklausmeier.goip.de/feed.xml" rel="self" type="application/rss+xml" />
    <generator>Simplified Saaze</generator>
$rssRelevant = array();
foreach ($collections as $collection) {
    if ( !($collection->data['rss'] ?? false) ) continue;
    foreach ($collection->entriesSansIndex as $entry) {
        if ($collection->draftOverride == false && ($entry->data['draft'] ?? false)) continue;
        if ( ! ($entry->data['index'] ?? true) ) continue;
        $rssRelevant[$entry->data['date'] . $entry->data['title']] = $entry;
krsort($rssRelevant);	// sort on key=date+title in reverse order
$maxRss = 50;	// number of item's in RSS XML feed
$timeZone = new \DateTimeZone('Europe/Berlin');
foreach ($rssRelevant as $entry) {
    if ($maxRss-- <= 0) break;
    $html = str_replace('<a href="/','<a href="https://eklausmeier.goip.de/',$entry->data['content']);
    $html = str_replace('<img src="/img/','<img src="https://eklausmeier.goip.de/img/',$html);
    $html = str_replace('<img src="/img/','<img src="https://eklausmeier.goip.de/img/',$html);
    $html = str_replace('<img src="/pdf/','<img src="https://eklausmeier.goip.de/pdf/',$html);
    $html = str_replace('<img src="/pdf/','<img src="https://eklausmeier.goip.de/pdf/',$html);
    // RFC-822 format: Wed, 02 Oct 2002 13:00:00 GMT, previously used 'D, j M Y G:i:s'
    $d = date_create($entry->data['date'],$timeZone);
        . "\t\t<link>https://eklausmeier.goip.de%s</link>\n"
        . "\t\t<guid>https://eklausmeier.goip.de%s</guid>\n"
        . "\t\t<title>%s</title>\n"
        . "\t\t<pubDate>%s</pubDate>\n"
        . "\t\t<description><![CDATA[\n%s\n"
        . "\t\t]]></description>\n"
        . "\t</item>\n",
        $entry->data['url'], $entry->data['url'], $entry->data['title'], date_format($d,"r"), $html);

For the RSS feed generation to work you need templates/rss.php. Without that, no RSS. As you can see from above PHP code, your yml-file must set rss: true to include the collection in RSS feed. Also, entries with index: false will be skipped, see Stealth-mode.

4.11 Sitemap

Command-line flag -m creates a file called sitemap.xml. This file covers all collections. The template file for the generation is usually as below:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
foreach ($collections as $collection) {
    foreach ($collection->entries as $entry) {
        $href = isset($collection->data['uglyURL']) ? $entry->data['url'] . '.html' : $entry->data['url'];
        printf("\t<url><loc>https://eklausmeier.goip.de%s</loc></url>\n", $href);

It is advisable to put the URL of the sitemap.xml into the robots.txt file like this:

User-agent: *

Sitemap: https://eklausmeier.goip.de/sitemap.xml

The sitemap.xml looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
. . .

The format of this file is described in sitemaps.org.

4.12 Overview file

Command-line flag -o creates a file called sitemap.html. This file covers all collections. The template file for the generation is usually as below:

<?php $url='/sitemap.html'; ?>
<?php require SAAZE_PATH . "/templates/head.php"; ?>
foreach ($collections as $collection) {
    foreach ($collection->entries as $entry) {
        $href = isset($collection->data['uglyURL']) ? $entry->data['url'] . '.html' : $entry->data['url'];
        printf("\t<li><a href=\".%s\">%s</a></li>\n", $href, $entry->data['url']);

The sitemap.html file is meant for human consumption. While the sitemap.xml is usually meant to be used by search-engines.

For the sitemap generation to work you need templates/overview.php. Without that, no overview-sitemap.

Generating this blog, for example, goes like this:

$ time php saaze -mortb /tmp/build
Building static site in /tmp/build...
        execute(): filePath=/home/klm/php/sndsaaze/content/aux.yml, nentries=5, totalPages=1, entries_per_page=20
        execute(): filePath=/home/klm/php/sndsaaze/content/blog.yml, nentries=370, totalPages=19, entries_per_page=20
        execute(): filePath=/home/klm/php/sndsaaze/content/gallery.yml, nentries=4, totalPages=1, entries_per_page=20
        execute(): filePath=/home/klm/php/sndsaaze/content/music.yml, nentries=25, totalPages=2, entries_per_page=20
        execute(): filePath=/home/klm/php/sndsaaze/content/error.yml, nentries=1, totalPages=1, entries_per_page=20
Finished creating 5 collections, 4 with index, and 419 entries (0.11 secs / 11.66MB)
#collections=5, YamlParser=0.0058/425-5, md2html=0.0099, MathParser=0.0057/419, renderEntry=419, content=419/0, excerpt=0/0
        real 0.13s
        user 0.08s
        sys 0
        swapped 0
        total space 0

It uses command-line flags, -m for XML sitemap, -o for overview, -r for RSS XML feed, -t for categories+tags, and -b for writing results into build directory /tmp/build.

4.13 Environment variables

The following environment variables are read:

  1. CONTENT_PATH: directory of content path, i.e., where your markdown files are
  2. PUBLIC_PATH: path for dynamic mode
  3. TEMPLATES_PATH: where to find template files, which are just PHP files
  4. ENTRIES_PER_PAGE: number of entries per index-page
  5. RBASE: "relative base", common string to prefix all URLs; example is RBASE=/lemire

These environment variables are most useful only during static site generation. In dynamic mode not so much, as these variables would have to be set for the web-server as well.

4.14 Code highlighting

As is usual in CommonMark you can show code fragments with triple backquotes. Alternatively in Commonmark you can use proper indentation (4 spaces or 1 tab), though deliberately deactivated in MD4C used for Simplified Saaze for now.

If you activate PrismJS via switch in front-matter, then PrismJS will look for the programming language after the triple backquote and use this to properly colorize the code fragment. Of course, your templates must include references to PrismJS CSS:

<?php if (isset($entry['prismjs'])) { ?>
        <link href=/jscss/prism.css rel=stylesheet>
<?php } ?>

Also, you must include reference to PrismJS JavaScript:

<?php if (isset($entry['prismjs'])) { ?>
        <script src="/jscss/prism.js"></script>
<?php } ?>

A "special syntax" is used to highlight certain linenumbers within a code block, or start line-numbering at another number than one:

  1. Add [data-line="3,9,11"] after the programm language to highlight line numbers 3, 9, and 11. Similarly, use [data-line="4-7"] to highlight lines 4 to 7, boundaries included.
  2. Add [data-start=36] to start line-numbering at line 36. See Plugins: Line Numbers.
  3. Add [/nonr] if you want no linenumbering. Still, you can highlight certain lines, they are just without their linenumbers. Example: PHP[data-line="1,3"/nonr], will highlight lines 1 and 3, but show no linenumbers.

5. Collections and entries

# Collection 1 ## Entry 1 ### Frontmatter #### date #### title #### draft #### ... ### Markdown #### HTML #### PHP #### Text ## Entry 2 ### Frontmatter ### Markdown ## Entry 3 ### Frontmatter ### Markdown # Collection 2 ## Entry 1 ## Entry 2

5.1 Collections

One of the core concepts of Simplified Saaze is that everything is a collection of entries. From pages, blog posts, navigation menus, users, everything.

Collections are defined by Yaml files in the content directory of your site. A collection will define not only the ID and title of the collection, but also the routes for the collection and how entries are sorted in the collection.

For example, say you wanted to create a blog in Simplified Saaze. You could create a collection file called posts.yml with the following content:

title: Blog
index_route: "/blog"
entry_route: "/blog/{slug}"
sort_field: date
sort_direction: desc

The ID of the collection is defined by the file name, e.g., posts. Below is a description of the available fields in a collection and what they do. With the exception of entry_route, all of these fields are optional if you don't need them.

  1. title: The title of the collection.
  2. index_route: The route of the index for this collection. Normally this page will show a collection archive (a paginated list of entries) but it can also be a single entry if the collection has an index.md file. The index_route field can be omitted. In that case no index will be shown.
  3. index: boolean. If true then index is shown. If false then no index is shown. Default is true. See Stealth-mode.
  4. entry_route: The route of an individual entry for this collection. This value should always contain {slug} which will be replaced by the entry ID when serving your site. This field is mandatory.
  5. entry: boolean. If true then entries are shown. If false then no entries are shown. Default is true. See Stealth-mode.
  6. filter: PHP code used in eval(). This PHP code should return a boolean value. It is used to include (true) or exclude (false) entries from the index of the collection. So it is a more versatile form of the index key. It is most useful if you want to have multiple collections, filtering different authors or topics. It is then usually combined with entry equal false. Example:
filter: return ($entry->data['author'] === 'David Berger');
  1. sort_field: The entry field used to sort the collection.
  2. sort_direction: The direction to sort entries (either asc or desc). Default is asc.
  3. entries_per_page: The number of excerpts shown in index page. Default is 20.
  4. excerpt_length: Number of characters for the excerpt in each index entry. Default is 300 characters. If you set this parameter very high, e.g., 900, then you will probably want to reduce entries_per_page to keep your page not becoming too crowded. Though, both parameters can be set freely.
  5. uglyURL: boolean. True, if ugly URLs should be generated, false if no ugly URLs should be generated. Ugly URLs look like {slug}.html. Non-ugly URLs create a separate directory for each file and an index.html in it, i.e., {slug}/index.html. Currently only implemented for static site generation, not for dynamic generation. Default is false.
  6. rss: boolean. True if this collection should be part of RSS feed, false if it is ignored in RSS. Default is false.
  7. more: boolean. Show content of the posts in the index up to the <!--more--> tag. In contrast the excerpt_length shows that many characters in the index, but does not honor any content otherwise. With the <!--more--> tag you have complete control what is shown. It therefore overrules excerpt_length. See More Block. Default is false.

5.2 Entries

Entries are just Markdown files with frontmatter. Below is an example:

title: Your title goes here
date: "2021-10-31 10:15:30"
Here is the usual Markdown.

Markdown is parsed with MD4C, which is

  1. CommonMark-compliant
  2. Very fast
  3. Handles tables
  4. Provides strikethrough with ~

Although generally known, Markdown can contain verbatim HTML code. In contrast, Hugo's Goldmark does not handle HTML by default. It can be enabled by setting the unsafe option to true in the Goldmark configuration.

The Markdown for Simplified Saaze can also contain PHP code! PHP code within links, i.e., within [](...) must be enclosed in stars (*). PHP code in HTML code can be included verbatim.

Here is an example for using the PHP variable $rbase in the URL part in Markdown.

The Markdown for _Simplified Saaze_ can also [contain PHP code](*<?=$rbase?>*/blog/2023/08-27-mixing-php-into-markdown)!

An example for PHP Code embedded in HTML code:

<p>2021-04-02 <a href="<?=$rbase?>/pkg/jpilot_2.0.1-1_amd64.deb">jpilot_2.0.1-1_amd64.deb</a>

Embedding PHP in ordinary text is also no problem. No escaping with a star (*) is required.

Frontmatter in entries is handed over to the template verbatim. So any key/value pair in the frontmatter can be checked in template code. For example:

title: Blog post
date: "2021-10-30 17:30:00"
prismjs: true
MathJax: true
Your blog post

Here title, date, prismjs, and MathJax can be used in template code like this

<?php if (isset($entry['MathJax'])) { ?>

or like this

<?php if (isset($entry['prismjs'])) { ?>

Entries can have the following "variables" in frontmatter:

  1. title: string containing the title
  2. date: string in format yyyy-mm-dd HH24:mi:ss
  3. draft: boolean, see Draft-mode, default is false
  4. index: boolean, see Stealth-mode, default is true
  5. entry: boolean, see Stealth-mode, default is true
  6. Twitter: boolean, whether to add extra JavaScript for Twitter
  7. TikTok: boolean, whether to add extra JavaScript for TikTok
  8. Mermaid: boolean, indicating whether Mermaid graphics are used
  9. MathJax: boolean, indicating whether MathJax CSS and JavaScript is used
  10. prismjs: boolean, indicating whether PrismJS CSS and JavaScript is used
  11. categories: JSON, see Categories and tags
  12. tags: JSON
  13. pinned: boolean, have a post "pinned" at the top of the index page, default is false; similar to pinned tweets in Twitter/X; you can have multiple pinned posts

The following additional "variables" are present in entries and automatically populated and can be overriden. They are mostly only used in templates:

  1. url: the relative URL of the entry, read-only variable; cannot be used to reposition an entry
  2. content: the HTML content of the entry produced by MD4C
  3. content_raw: the unprocessed Markdown content from the file without frontmatter
  4. excerpt: an excerpt from the content of the entry using PHP function strip_tags()
  5. gallery_css + gallery_js, only populated when the tag-pair [gallery] and [/gallery] are used
  6. markmap_css + markmap_js, only populated when [markmap] and [/markmap] tags are used
  7. wordcount: number of words in body (=content_raw), excluding frontmatter; computed via PHP function str_word_count()
  8. minutes_read: wordcount divided by 225, which is a common reading speed ("words per minutes")

5.3 Special tags

Simplified Saaze defines some special tags for various social media or graphing. Furthermore, MathJax is fully supported.

Nr Function Syntax Example
1 YouTube [youtube] xxx [/youtube] [youtube]nvlAW6P5PmE[/youtube]
2 Vimeo [vimeo] xxx [/vimeo] [vimeo]126529871[/vimeo]
3 TikTok [tiktok] xxx [/tiktok] [tiktok]https://www.tiktok.com/@sumitsinvestmenttakes/video/7215847178873343274[/tiktok]
4 Twitter [twitter] xxx [/twitter] [twitter]https://twitter.com/eklausmeier/status/1352896936051937281[/twitter]
5 CodePen [codepen] user/hash [/codepen] [codepen] thebabydino/eJrPoa [/codepen]
6 WordPress Video [wpvideo] code w=x h=y ] [wpvideo RLkLgz2V w=400 h=224]
7 Mermaid [mermaid] xxx [/mermaid], where xxx is the Mermaid code [mermaid]flowchart LR Start --> Stop[/mermaid]
8 Gallery [gallery] dir /regex/ [/gallery] [gallery] /img/gallery /IMG_20220107_14\.+\.jpg [/gallery]
9 markmap Mindmap [markmap] Headings [/markmap] [markmap] # H1 [/markmap]
10 Inline math $ formula $ $a^2+b^2=c^2$
11 Display math $$ display formula $$ $$ \int_1^\infty {1\over x^2} $$

For Mermaid to work: You have to set Mermaid: true in the frontmatter so that the required JavaScript is loaded. Likewise, for Twitter to work, you have to add Twitter: true to your frontmatter. Similarly, for TikTok add TikTok: true to the frontmatter. Below HTML/PHP code or similar is required in the template file:

<?php if (isset($entry['TikTok'])) { ?>
    <script async src="https://www.tiktok.com/embed.js"></script>
<?php } ?>
<?php if (isset($entry['Twitter'])) { ?>
    <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<?php } ?>
<?php if (isset($entry['Mermaid'])) { ?>
    <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<?php } ?>

See Internal data structure for an example use of Mermaid.

For math: You have to set MathJax: true in the frontmatter to load JavaScript for MathJax. Furthermore your template must include below code:

<?php if (isset($entry['MathJax'])) { ?>
    <script>window.MathJax = { tex: { inlineMath: [['$', '$'], ['\\(', '\\)']] } };</script>
    <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
    <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<?php } ?>

See TeX and LaTeX math delimiters.

For several examples on video embeddings in Simplified Saaze see the post Embedding Content in Simplified Saaze.

For examples for mindmaps see Mindmaps in Saaze.

For examples of galleries see Galleries in Saaze. If the directory part of the gallery-tag is prefixed with @, then $rbase is added to this directory.

5.4 Routing

In a Simplified Saaze site, all of the routes are defined by collections. The index_route and entry_route of each collection will be used to determine how an entry can be accessed by URL. For example, let's say we have posts collection:

title: Blog
index_route: "/blog"
entry_route: "/blog/{slug}"

When you create an entry in a collection, the name of the file (the entry ID) is used as the "slug" for the entry. For example, say we have an entry file at content/posts/an-example-post.md. This post will be accessible at the URL:


Subdirectories work too. For example, say we have an entry file at content/posts/marketing/an-example-post.md. This post will be accessible at the URL:


Index entries: If the ID of an entry is index, this entry will be shown at the index_route instead of the default collection archive page. For example, the entry file content/posts/index.md will be accessible at the URL:


This works for subdirectories too. For example, say we have an entry file at content/posts/marketing/index.md. This post will be accessible at the URL:


6. Templates

The entry-template has the following variables at its disposal:

  1. title
  2. date
  3. draft
  4. index
  5. entry
  6. Twitter
  7. TikTok
  8. Mermaid
  9. MathJax
  10. prismjs
  11. categories
  12. tags
  13. pinned
  14. url
  15. content
  16. content_raw
  17. excerpt
  18. gallery_css + gallery_js
  19. markmap_css + markmap_js
  20. wordcount
  21. minutes_read

See Entries for a list of variables present in the so called "entries".

The collection- or index-template has the following variables in the PHP array pagination:

  1. currentPage, the page number of the current index page
  2. prevPage, the page number of the previous index page
  3. nextPage, the page number of the next index page
  4. prevUrl, the URL of the previous index page
  5. nextUrl, the URL of the next index page
  6. perPage, the number of entries per index page; this is $H['global_config_entries_per_page']
  7. totalEntries, number of entries (= blog posts)
  8. totalPages, the number of index pages
  9. entries, an PHP array of the entries (=blog posts) for the current index page

See Collecions for a list of variables in the collection array.

  1. title
  2. index_route
  3. index
  4. entry_route
  5. entry
  6. filter
  7. sort_field
  8. sort_direction
  9. entries_per_page
  10. excerpt_length
  11. uglyURL
  12. rss

7. Example themes

There are already a number of example themes for Simplified Saaze.

Nr Theme demo GitHub post in this blog
1 Saaze example saaze-example n/a
2 Elmar Klausmeier blogging
3 J-Pilot saaze-jpilot Example Theme for Simplified Saaze: J-Pilot
4 Koehntopp saaze-nukeklaus Example Theme for Simplified Saaze: Koehntopp
5 NukeKlaus saaze-koehntopp Example Theme for Simplified Saaze: nukeKlaus
6 Mobility saaze-mobility Example Theme for Simplified Saaze: Mobility
7 Vonhoff saaze-vonhoff Example Theme for Simplified Saaze: Vonhoff
8 Paternoster saaze-paternoster Example Theme for Simplified Saaze: Paternoster
9 Panorama saaze-panorama Example Theme for Simplified Saaze: Panorama
10 Lemire saaze-lemire Example Theme for Simplified Saaze: Lemire
11 Wendt saaze-wendt Example Theme for Simplified Saaze: Wendt

8. Internal data structure

If you just want to use Simplified Saaze below remarks are not relevant for you. If you want to fully understand the inner working of Simplified Saaze and want to make changes to the source code, then below text will provide helpful information.

Overall logic for building all static pages for all collections:

public function buildAllStatic(string $dest) : void {
    $collections = $this->collectionArray->getCollections();

    foreach ($collections as $collection) {
        $entries    = $collection->getEntries();
        $nentries   = count(...);
        $entries_per_page = ...;
        $totalPages = ceil($nentries / $entries_per_page);

        $this->buildCollectionIndex($collection, ...);

        for ($page=1; $page <= $totalPages; $page++)
            $this->buildCollectionIndex($collection, $page, $dest);

        foreach ($entries as $entry)
            $this->buildEntry($collection, $entry, ...);

Below ER diagram contains all PHP classes, which are held in memory during runtime of Simplified Saaze. Of these, the data in entry_data['content_raw'] is the Markdown, and entry_data['content'] is the HTML after conversion via MD4C.

erDiagram CollectionArray ||--o{ Collection : "has multiple" CollectionArray { array collections bool draftOverride } Collection ||--|| collection_data : contains Collection ||--o{ Entry : "has multiple" Collection { string filePath array collection_data string slug bool draftOverride array entries array entriesSansIndex } collection_data { string title string index_route string entry_route string sort_field string sort_direction int entries_per_page int excerpt_length bool uglyURL bool rss } Entry ||--|| entry_data : contains Entry { Collection collection string filePath array entry_data } entry_data { string title string author string date string template bool draft bool Mermaid bool MathJax bool prismjs JSON categories JSON tags string url string content_raw string content string gallery_css string gallery_js string markmap_css string markmap_js } Config { string global_rbase string global_path_base string global_path_public string global_public string global_path_templates string global_config_entries_per_page string global_excerpt_length string global_ffi } Entry ||--|| MarkdownContentParser : uses MarkdownContentParser { function toHtml } SaazeCli ||--|| BuildCommand : uses SaazeCli { function run } BuildCommand ||--|| TemplateManager : has BuildCommand ||--|| CollectionArray : has BuildCommand { string defaultName string buildDest CollectionArray collectionArray TemplateManager templateManager function buildAllStatic function buildSingleStatic } Saaze ||--|| TemplateManager : has Saaze ||--|| CollectionArray : has Saaze { CollectionArray collectionArray TemplateManager templateManager function run } TemplateManager ||--|| pagination : creates TemplateManager { function renderCollection function renderEntry function renderError function renderGeneral } pagination ||--o{ entry_data : contains pagination { int currentPage int prevPage int nextPage string prevUrl string nextUrl int perPage int totalEntries int totalPages entry_data entries }

9. Release history

Working on Saaze started in May 2021.

  1. v1.0: 02-Nov-2021, removed unnecessary directories
  2. v1.1: 08-Nov-2021, fixed PHPStan messages, transparent sections in dynamic mode
  3. v1.2: 15-Nov-2021, QUERY_STRING handling in dynamic mode => honors web-server rewriting rules
  4. v1.3: 05-Dec-2021, reduce warning messages in case of dynamic mode
  5. v1.4: 16-Jan-2021, 404 status, url variable in template
  6. v1.5: 23-Jan-2022, added draft-mode: enable/disable generation of drafts
  7. v1.6: 26-Jan-2022, special handling for transparent sections, i.e., index.md
  8. v1.7: 20-Apr-2022, added gallery support
  9. v1.8: 03-May-2022, added markmap support for Mindmaps
  10. v.1.9: 25-Jun-2022, support uglyURL and entries_per_page per yaml file
  11. v.1.10: 09-Jul-2022, cope for index_route in Saaze.php
  12. v.1.11: 29-Jul-2022, added parameter excerpt_length in collection.yml
  13. v.1.12: 01-Aug-2022, removed unused variables
  14. v.1.13: 07-Aug-2022, moved most of EntryManager to Collection, moved CollectionManager to CollectionArray
  15. v.1.14: 14-Aug-2022, generate cat_and_tag.json
  16. v.1.15: 16-Aug-2022, added RSS XML feed generation
  17. v.1.16: 24-Aug-2022, added sitemap-generation for static+dynamic mode
  18. v.1.17: 03-Oct-2022, corrected syntax error
  19. v.1.18: 11-Dec-2022, added help-string
  20. v.1.19: 02-Jan-2023, added YouTube-LowTech/Light tag
  21. v.1.20: 17-Jan-2023, added alt-attribute in youtubelt for W3-validator
  22. v.1.21: 26-Jan-2023, Fixed \cases{} TeX issue
  23. v.1.22: 31-Jan-2023, /index.md must use DIRECTORY_SEPARATOR
  24. v.1.23: 18-Mar-2023, added -o flag for overview-sitemap
  25. v.1.24: 14-Apr-2023, added overview/sitemap.html handling in dynamic mode
  26. v.1.25: 25-Apr-2023, added TikTok shortcode, corrected Twitter
  27. v.1.26: 02-Jun-2023, dropped amplink() as it is wrong
  28. v.1.27: 22-Jun-2023, added linenumber highlighting in PrismJS
  29. v.1.28: 01-Jul-2023, added stealth-mode: 'index' + 'entry' booleans in frontmatter
  30. v.1.29: 08-Jul-2023, added wordcount + minutes_read hashes in entry->data
  31. v.1.30: 05-Aug-2023, corrected comments, line-highlighting fix
  32. v.1.31: 19-Sep-2023, DIRECTORY_SEPARATOR instead of /, dynamic mode no longer redirects with / added
  33. v.1.32: 26-Sep-2023, RBASE env var, [gallery] accepts @-prefix to add rbase to image-dir
  34. v.1.33: 24-Oct-2023, Pinned entries a la Twitter/X
  35. v.1.34: 28-Dec-2023, Added MD_FLAG_NOINDENTEDCODEBLOCKS to MD4C
  36. v.1.35: 17-Feb-2024, Reduced CPU overhead in composer
  37. v.2.0: 26-Feb-2024, Parallelized template output via pcntl_fork()
  38. v.2.1: 31-Mar-2024, Stealth mode can be configured in collection yaml file
  39. v.2.2: 09-Apr-2024, <!--more--> tag functionality as in WordPress
  40. v.2.3: 02-Jul-2024, Dynamic mode: handle rbase, add collection to entry-template