31st October 2021

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 web-sites 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.

2. Installation

1. Simplified Saaze requires PHP version 8 as a minimum as it uses FFI. 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 ends November 2021, and security support for PHP version 7 ends November 2022.

5.6 7.0 7.1 7.2 7.3 7.4 8.0 1 Jan 2018 1 Jan 2019 1 Jan 2020 1 Jan 2021 1 Jan 2022 1 Jan 2023 1 Jan 2024 1 Jan 2025
Active support A release that is being actively supported. Reported bugs and security issues are fixed and regular point releases are made.
Security fixes only A release that is supported for critical security issues only. Releases are only made on an as-needed basis.
End of life A release that is no longer supported. Users of this release should upgrade as soon as possible, as they may be exposed to unpatched security vulnerabilities.

Checking the PHP version is

php -v

and should show something like

PHP 8.0.11 (cli) (built: Sep 25 2021 07:52:29) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.11, Copyright (c) Zend Technologies

2. Your PHP needs 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:


3. 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. The MD4C library must be installed. For example, on Arch Linux you check with

pacman -Qs md4c

FFI must be enabled in PHP. 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.

5. Go to directory vendor/eklausme/saaze and edit Config.php to supply the correct location of php_md4c_toHtml.so in GLOBALS['ffi'] variable. Example:

$GLOBALS['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:


6. 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.

7. 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.

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. 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

Either put index.php from public directory to your document root of your web-server.

For testing you can also use PHP builtin's web-server:

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

This will present your content at URL localhost:8000/.

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.

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 web-site, but rather just insert or update a single post.

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

4.4 Specifying an alternate build directory

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

php saaze -d /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 for the single file content/blog/2021/another-post.md but in addition the file excerpt.txt is generated. So obviously, 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. Though, these draft posts will be shown in dynamic mode, see 4.2. 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". The reason for this disparity between static and dynamic mode is, that dynamic mode cannot take command-line arguments.

5. Collections and entries

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.
  3. 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.
  4. sort_field: The entry field used to sort the collection.
  5. sort_direction: The direction to sort entries (either asc or desc). Default is asc.
  6. entries_per_page: The number of excerpts shown in index page. Default is 20.
  7. 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.

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. Strikethrough with ~

Although generally known, Markdown can contain verbatim HTML code. In contrast, Hugo's Goldmark does not handle HTML.

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'])) { ?>

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 Twitter [twitter] xxx [/twitter] [twitter]https://twitter.com/eklausmeier/status/1352896936051937281[/twitter]
4 CodePen [codepen] user/hash [/codepen] [codepen] thebabydino/eJrPoa [/codepen]
5 WordPress Video [wpvideo] code w=x h=y ] [wpvideo RLkLgz2V w=400 h=224]
6 Mermaid [mermaid] xxx [/mermaid], where xxx is the Mermaid code [mermaid]flowchart LR Start --> Stop[/mermaid]
7 Gallery [gallery] dir /regex/ [/gallery] [gallery] /img/gallery /IMG_20220107_14\.+\.jpg [/gallery]
8 markmap Mindmap [markmap] Headings [/markmap] [markmap] # H1 [/markmap]
9 Inline math $ formula $ $a^2+b^2=c^2$
10 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.

For math: You have to set MathJax: true in the frontmatter to load JavaScript for MathJax.

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: