, 33 min read
Simplified Saaze
Original post is here eklausmeier.goip.de/blog/2021/10-31-simplified-saaze.
-
- 4.1 Static site generator
- 4.2 Dynamic content generation
- 4.3 Single file generation
- 4.4 Specifying an alternate build directory
- 4.5 Turning extract file generation on
- 4.6 Draft-mode
- 4.7 Parallel output
- 4.8 Stealth-mode
- 4.9 Categories and tags
- 4.10 RSS XML Feed
- 4.11 Sitemap
- 4.12 Overview file
- 4.13 Environment variables
- 4.14 Code highlighting
-
- 5.1 Collections
- 5.2 Entries
- 5.3 Special tags
- 5.4 Routing
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
:
extension=yaml
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
phpize
./configure
make
Copy the resulting file module/md4c.so
:
cp modules/md4c.so /usr/lib/php/modules
Activate the extension in php.ini
:
extension=md4c
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
:
extension=ffi
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:
opcache.enable=1
opcache.enable_cli=1
opcache.jit_buffer_size=256M
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
ssaaze/
├── 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:
build
will contain the result of the run when generating static files.content
is where all your Markdown files reside.public
is used for dynamic content. It should show the same content as inbuild
, just without any static files laying around.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:
- Use PHP-executable as web-server
- 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.
Essentially:
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": [
[
"\/blog\/2013\/03-13-screenshots-on-nexus-4-android-4-x",
"2013-03-13 21:54:03",
"Screenshots on Nexus 4 (Android 4.x)"
],
[
"\/blog\/2013\/08-04-google-now-emergency-alert",
"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]
.
$i
is eithercategories
ortags
$j
is the category or tag, e.g., "Android" or "hardware", etc.$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">
<channel>
<title>Elmar Klausmeier's Blog</title>
<description>Elmar Klausmeier's Blog</description>
<lastBuildDate><?=gmdate("r")?></lastBuildDate>
<link>https://eklausmeier.goip.de</link>
<atom:link href="https://eklausmeier.goip.de/feed.xml" rel="self" type="application/rss+xml" />
<generator>Simplified Saaze</generator>
<?php
$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);
printf("\t<item>\n"
. "\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);
}
?>
</channel>
</rss>
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">
<?php
foreach ($collections as $collection) {
sort($collection->entries);
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);
}
}
?>
</urlset>
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">
<url><loc>https://eklausmeier.goip.de/aux/about</loc></url>
<url><loc>https://eklausmeier.goip.de/aux/categories</loc></url>
<url><loc>https://eklausmeier.goip.de/aux/collected-links</loc></url>
<url><loc>https://eklausmeier.goip.de/aux/tags</loc></url>
<url><loc>https://eklausmeier.goip.de/aux/yearOverview</loc></url>
<url><loc>https://eklausmeier.goip.de/blog/2008/01-02-erster-eindruck-von-wordpress</loc></url>
<url><loc>https://eklausmeier.goip.de/blog/2008/01-02-hello-world</loc></url>
<url><loc>https://eklausmeier.goip.de/blog/2008</loc></url>
. . .
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"; ?>
<title>Sitemap</title>
</head>
<body>
<h1>Sitemap</h1>
<ol>
<?php
foreach ($collections as $collection) {
sort($collection->entries);
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']);
}
}
?>
</ol>
</body>
</html>
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:
CONTENT_PATH
: directory of content path, i.e., where your markdown files arePUBLIC_PATH
: path for dynamic modeTEMPLATES_PATH
: where to find template files, which are just PHP filesENTRIES_PER_PAGE
: number of entries per index-pageRBASE
: "relative base", common string to prefix all URLs; example isRBASE=/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:
- 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. - Add
[data-start=36]
to start line-numbering at line 36. See Plugins: Line Numbers. - 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
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.
title
: The title of the collection.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 anindex.md
file. Theindex_route
field can be omitted. In that case no index will be shown.index
: boolean. If true then index is shown. If false then no index is shown. Default is true. See Stealth-mode.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.entry
: boolean. If true then entries are shown. If false then no entries are shown. Default is true. See Stealth-mode.filter
: PHP code used ineval()
. This PHP code shouldreturn
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 theindex
key. It is most useful if you want to have multiple collections, filtering different authors or topics. It is then usually combined withentry
equalfalse
. Example:
filter: return ($entry->data['author'] === 'David Berger');
sort_field
: The entry field used to sort the collection.sort_direction
: The direction to sort entries (eitherasc
ordesc
). Default isasc
.entries_per_page
: The number of excerpts shown in index page. Default is 20.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 reduceentries_per_page
to keep your page not becoming too crowded. Though, both parameters can be set freely.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 anindex.html
in it, i.e.,{slug}/index.html
. Currently only implemented for static site generation, not for dynamic generation. Default is false.rss
: boolean. True if this collection should be part of RSS feed, false if it is ignored in RSS. Default is false.more
: boolean. Show content of the posts in the index up to the<!--more-->
tag. In contrast theexcerpt_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 overrulesexcerpt_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
- CommonMark-compliant
- Very fast
- Handles tables
- 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:
title
: string containing the titledate
: string in format yyyy-mm-dd HH24:mi:ssdraft
: boolean, see Draft-mode, default is falseindex
: boolean, see Stealth-mode, default is trueentry
: boolean, see Stealth-mode, default is trueTwitter
: boolean, whether to add extra JavaScript for TwitterTikTok
: boolean, whether to add extra JavaScript for TikTokMermaid
: boolean, indicating whether Mermaid graphics are usedMathJax
: boolean, indicating whether MathJax CSS and JavaScript is usedprismjs
: boolean, indicating whether PrismJS CSS and JavaScript is usedcategories
: JSON, see Categories and tagstags
: JSONpinned
: 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:
url
: the relative URL of the entry, read-only variable; cannot be used to reposition an entrycontent
: the HTML content of the entry produced by MD4Ccontent_raw
: the unprocessed Markdown content from the file without frontmatterexcerpt
: an excerpt from thecontent
of the entry using PHP functionstrip_tags()
gallery_css
+gallery_js
, only populated when the tag-pair[gallery]
and[/gallery]
are usedmarkmap_css
+markmap_js
, only populated when[markmap]
and[/markmap]
tags are usedwordcount
: number of words in body (=content_raw
), excluding frontmatter; computed via PHP functionstr_word_count()
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] 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>
<script>mermaid.initialize({startOnLoad:true});</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:
https://mysite.com/blog/an-example-post
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:
https://mysite.com/blog/marketing/an-example-post
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:
https://mysite.com/blog
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:
https://mysite.com/blog/marketing
6. Templates
The entry-template has the following variables at its disposal:
title
date
draft
index
entry
Twitter
TikTok
Mermaid
MathJax
prismjs
categories
tags
pinned
url
content
content_raw
excerpt
gallery_css
+gallery_js
markmap_css
+markmap_js
wordcount
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
:
currentPage
, the page number of the current index pageprevPage
, the page number of the previous index pagenextPage
, the page number of the next index pageprevUrl
, the URL of the previous index pagenextUrl
, the URL of the next index pageperPage
, the number of entries per index page; this is$H['global_config_entries_per_page']
totalEntries
, number of entries (= blog posts)totalPages
, the number of index pagesentries
, an PHP array of the entries (=blog posts) for the current index page
See Collecions for a list of variables in the collection
array.
title
index_route
index
entry_route
entry
filter
sort_field
sort_direction
entries_per_page
excerpt_length
uglyURL
rss
7. Example themes
There are already a number of example themes for Simplified Saaze.
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 {
$this->clearBuildDirectory(...);
$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.
9. Release history
Working on Saaze started in May 2021.
- v1.0: 02-Nov-2021, removed unnecessary directories
- v1.1: 08-Nov-2021, fixed PHPStan messages, transparent sections in dynamic mode
- v1.2: 15-Nov-2021, QUERY_STRING handling in dynamic mode => honors web-server rewriting rules
- v1.3: 05-Dec-2021, reduce warning messages in case of dynamic mode
- v1.4: 16-Jan-2021, 404 status, url variable in template
- v1.5: 23-Jan-2022, added draft-mode: enable/disable generation of drafts
- v1.6: 26-Jan-2022, special handling for transparent sections, i.e., index.md
- v1.7: 20-Apr-2022, added gallery support
- v1.8: 03-May-2022, added markmap support for Mindmaps
- v.1.9: 25-Jun-2022, support uglyURL and entries_per_page per yaml file
- v.1.10: 09-Jul-2022, cope for index_route in Saaze.php
- v.1.11: 29-Jul-2022, added parameter excerpt_length in collection.yml
- v.1.12: 01-Aug-2022, removed unused variables
- v.1.13: 07-Aug-2022, moved most of EntryManager to Collection, moved CollectionManager to CollectionArray
- v.1.14: 14-Aug-2022, generate cat_and_tag.json
- v.1.15: 16-Aug-2022, added RSS XML feed generation
- v.1.16: 24-Aug-2022, added sitemap-generation for static+dynamic mode
- v.1.17: 03-Oct-2022, corrected syntax error
- v.1.18: 11-Dec-2022, added help-string
- v.1.19: 02-Jan-2023, added YouTube-LowTech/Light tag
- v.1.20: 17-Jan-2023, added alt-attribute in youtubelt for W3-validator
- v.1.21: 26-Jan-2023, Fixed
\cases{}
TeX issue - v.1.22: 31-Jan-2023,
/index.md
must useDIRECTORY_SEPARATOR
- v.1.23: 18-Mar-2023, added
-o
flag for overview-sitemap - v.1.24: 14-Apr-2023, added overview/sitemap.html handling in dynamic mode
- v.1.25: 25-Apr-2023, added TikTok shortcode, corrected Twitter
- v.1.26: 02-Jun-2023, dropped amplink() as it is wrong
- v.1.27: 22-Jun-2023, added linenumber highlighting in PrismJS
- v.1.28: 01-Jul-2023, added stealth-mode: 'index' + 'entry' booleans in frontmatter
- v.1.29: 08-Jul-2023, added wordcount + minutes_read hashes in entry->data
- v.1.30: 05-Aug-2023, corrected comments, line-highlighting fix
- v.1.31: 19-Sep-2023, DIRECTORY_SEPARATOR instead of /, dynamic mode no longer redirects with / added
- v.1.32: 26-Sep-2023, RBASE env var, [gallery] accepts @-prefix to add rbase to image-dir
- v.1.33: 24-Oct-2023, Pinned entries a la Twitter/X
- v.1.34: 28-Dec-2023, Added MD_FLAG_NOINDENTEDCODEBLOCKS to MD4C
- v.1.35: 17-Feb-2024, Reduced CPU overhead in composer
- v.2.0: 26-Feb-2024, Parallelized template output via pcntl_fork()
- v.2.1: 31-Mar-2024, Stealth mode can be configured in collection yaml file
- v.2.2: 09-Apr-2024,
<!--more-->
tag functionality as in WordPress