, 8 min read

Example Theme for Simplified Saaze: Panorama

1. Features. Here is another theme called Panorama for Simplified Saaze. The example content is from Ristorante Panorama. This theme has below properties:

  1. It is geared towards restaurants with menus
  2. Responsive with media-breaks for 1-column, 2-column, 3-column, and printer output
  3. RSS and sitemap
  4. Showcase for post-processing, if needed
  5. Hero image
  6. Background SVG image
  7. Animated images and galleries
  8. Lightweight and easy to use

Its source code is in GitHub: saaze-panorama.

Here is a screenshot: Photo

The original website uses WordPress, Elementor, and Google Site Kit. The original website has a number of major shortcomings:

  1. Terribly slow
  2. Loading web-fonts, which are not used
  3. Loading images, which are not used
  4. Duplicated text on a single webpage
  5. RSS feed empty
  6. Google indexing disabled

In addition there are various minor glitches:

  1. Misspellings
  2. Navigation mishaps: redirecting to same page
  3. Color contrast sometimes bad: green text on black background
  4. favicon icon too small to be human-readable

This theme is the eighth example theme. We had themes migrated from WordPress, from Hugo, from Jekyll. This time again a migration from WordPress with Elementor.

2. Creating restaurant menus with post-processing. A restaurant obviously wants to show its menu. This is done as follows:

## 2. Kalte und warme Vorspeisen<a id=vorpeisen></a>

- Antipasti dela Casa ...... klein €9,50 - groß €12,50
    - mit gegrilltem Gemüse und Fisch
- Shrimps Cocktail `1,b,c,d,n` ...... € 9,50
    - mit Shrimps, Ananas
- Shrimps mit Olivenöl, Cocktailtomaten `1,b,c,n` ...... €12,50
    - mit Knoblauch & Schalotten
- Gebackener Schafskäse `1,b,c,d,n` ...... €10,50
    - mit Tomaten, Oliven und Peperoni

So data entry closely mirrors the output, which looks like this: Photo

The CSS for this "dot-trick" can be found here: Dot Leaders by Bert Bos.

The frontmatter of the Markdown looks like this, indicating that it wants its output to be processed further:

title: "Speisekarte"
date: "2023-07-11 21:00:00"
excerpt: "Italienisch-mediterrane Köstlichkeiten, ausgewählte deutsche Spezialitäten, Steaks und Fisch."
heroimg: "Schweinefleisch.webp"
postproc: true

Above frontmatter also shows how the hero-image is defined.

The actual post-processing is done in the template-file entry.php:

<?php require SAAZE_PATH . "/templates/top-layout.php"; ?>

    <article class=aentry>
    if (!function_exists('postproc')) {
        // Post-processing of MD4C processed Markdown, not really clean,
        //because probably specific to MD4C, but does the job
        function postproc(string $s) : string {
            //return $s;
            $s = str_replace(
                array(PHP_EOL.'<ul class=leaders>',	// add class=leaders to ul
                    PHP_EOL.'<li>',	// strip <p> after <li>
                    '</li>',	// strip </p> before </li>
                    '<ul class=noleaders>'.PHP_EOL,	// 2nd ul must not have leaders but noleaders
                    'class=noleaders>'.PHP_EOL.'<li><code>'),	// Allergene Sonderfall
            // replace ABC ...... UVW with ABC+UVW each enclosed in span's
            // catchword is six dots
            return preg_replace(
                '/(' . PHP_EOL . '<li>)(.+)\s+\.\.\.\.\.\.\s+(.+)(<ul|<\/li>)/',
    echo '<h1>' . $entry['title'] . "</h1>\n";
    if (isset($entry['heroimg']))
        printf("<p><img class=heroimg src=\"%s/img/%s\" alt=\"Hero image\"></p>\n",$rbase,$entry['heroimg']);
    $s = ($entry['postproc'] ?? false) ? postproc($entry['content']) : $entry['content'];
    echo $s;


The post-processing effectively just search-and-replaces certain strings, in our case six dots.

If this theme also wants to mix PHP into Markdown then replace above echo $s with below three PHP lines.

    $s = str_replace('*%3c?','<?',$entry['content']);
    $s = str_replace('?%3e*','?>',$s);
    require 'data:text/plain;base64,'.base64_encode($s);

If you omit above post-processing PHP function postproc() from above template, the template would be pretty simple.

3. Installation. The theme including Simplified Saaze is installed by using composer:

composer create-project eklausme/saaze-panorama

This installs below directory tree:

|-- README.md
|-- composer.json
|-- composer.lock
|-- content
|   |-- auxil
|   |   |-- datenschutzerklaerung.md
|   |   `-- impressum.md
|   |-- auxil.yml
|   |-- blog
|   |   |-- aktuell.md
|   |   |-- biergarten.md
|   |   |-- catering.md
|   |   |-- feiern.md
|   |   |-- mittagstisch.md
|   |   |-- pfifferlinge.md
|   |   |-- ristorante.md
|   |   `-- speisekarte.md
|   `-- blog.yml
|-- public
|   |-- img
|   |   |-- Aussenbereich1.jpg
|   |   |-- Aussenbereich1.webp
|   |   |-- Aussenbereich2.jpg
|   |   |-- . . .
|   |   `-- green-orange-and-yellow-pasta-165844-2000x1200-1.webp
|   `-- index.php
|-- saaze
|-- templates
|   |-- bottom-layout.php
|   |-- entry.php
|   |-- error.php
|   |-- head.php
|   |-- index.php
|   |-- overview.php
|   |-- rss.php
|   |-- sitemap.php
|   `-- top-layout.php
`-- vendor
    |-- autoload.php
    |-- composer
    |   |-- ClassLoader.php
    |   |-- InstalledVersions.php
    |   |-- LICENSE
    |   |-- autoload_classmap.php
    |   |-- autoload_namespaces.php
    |   |-- autoload_psr4.php
    |   |-- autoload_real.php
    |   |-- autoload_static.php
    |   |-- installed.json
    |   |-- installed.php
    |   `-- platform_check.php
    `-- eklausme
        `-- saaze
            |-- BuildCommand.php
            |-- Collection.php
            |-- CollectionArray.php
            |-- Config.php
            |-- Entry.php
            |-- LICENSE
            |-- MarkdownContentParser.php
            |-- README.md
            |-- Saaze.php
            |-- SaazeCli.php
            |-- TemplateManager.php
            |-- composer.json
            |-- php_md4c_toHtml.c
            `-- saaze

11 directories, 137 files

Here are two articles if you want to install Simplified Saaze on Windows:

  1. Installing Simplified Saaze on Windows 10
  2. Installing Simplified Saaze on Windows 10 #2

4. Building and deploying. Change to the directory saaze-panorama. The following commmand builds a static site.

$ time php saaze -morb /tmp/build
Building static site in /tmp/build...
        execute(): filePath=/home/klm/php/saaze-panorama/content/auxil.yml, nentries=2, totalPages=1, entries_per_page=20
        execute(): filePath=/home/klm/php/saaze-panorama/content/blog.yml, nentries=8, totalPages=1, entries_per_page=20
Finished creating 2 collections, 2 with index, and 10 entries (0.02 secs / 1.68MB)
#collections=2, YamlParser=0.0002/12-2, md2html=0.0004, MathParser=0.0003/10, renderEntry=10, content=10/0, excerpt=0/0
        real 0.04s
        user 0.01s
        sys 0
        swapped 0
        total space 0

As can be seen, build time is way below a tenth of a second on a Ryzen 7 5700G. In above scenario we use options -m for generating a sitemap, -o for generating an overview page, -r for generating RSS. Option -b is used to build in /tmp, which on Arch Linux is a RAM disk. Options m, o, and r are entirely optional. I.e., below command would do just as well.

php saaze

The resulting HTML files need to be uploaded to your web-server. Below are the steps to upload to a local web-server assuming you built into /tmp/build. A local web-server is a web-server running on the same machine where you generated the HTML files.

[ -d $DOCROOT ] && rm -rf $DOCROOT
[ -d /tmp/build ] || errorExit "No build directory in /tmp"
mv /tmp/build $DOCROOT

ln -s $SAAZEROOT/public/img

For local development of your website, you use:

php -S 0:8000 -t public/

This starts a web-server and you can immediately see any changes you make. Above command shows Simplified Saaze in dynamic mode. You can also use this dynamic mode with NGINX by using something like below:

server {
    rewrite "^/(aux|blog)($|/.*)"  "...your-directory.../index.php?/$1$2" last;

The dynamic mode of Simplified Saaze has the advantage that you don't need to build any static HTML files. All HTML files are generated on the fly. The disadvantage is that every request will rebuild the requested HTML page, unless you use intensive caching in your web-server.

5. CSS and favicon. The panorama-theme uses a SVG based background image. This was inspired by Matt Visiwig's page on SVG backgrounds. In our case we used "Subtle Prism". We already mentioned the dot-leader CSS for aligned lines of dots.

Generating the favicon was done using the web-page favicon-generator using the two letters 'R' and 'P' with circled background. The favicon is directly embedded into the head.php template. This helps to reduce to number of requests required for the browser to show the web-page.

<link href="data:image/png;base64,iVBORw....ABJRU5ErkJggg==" rel="icon" type="image/png">

I had written on this here: Accelerating Page Load Times by Reducing Requests, Part #2.

The three-column output is realized using CSS grids.

@media screen and (min-width:99rem) {	/* 3 column output */
    .aentry, header, aside, footer { width:var(--klmWidth) }
    .aindex { margin-left:0rem; width:20rem }
    .allcontent { max-width:var(--klmWidth); margin:auto; padding:0rem }
    .agrid-container {
        grid-template-columns: auto auto auto;
        grid-template-areas: 'article article article';
    /* https://www.w3docs.com/snippets/css/how-to-vertically-align-text-next-to-an-image.html */
    .imgcontainer { display:flex; align-items:center }
    .textimg { padding-left:2.5rem }

Printing to an old-fashioned printer is handled by a special media break:

@media print {
    h2 { page-break-before: always }
    h1, h2, h3, h4, h5, h6, ul, li, p { color:black }

Most notably this is for printing out the menu card. This is considered to be of some importance as you can now have a single source of truth: the menu on the web-page, and the printed menu from the web-page.

6. Home page / index. The index-page or landing page of this theme is somewhat special as it shows all blog posts, but singles out the newest one. This newest post is interesting as it might contain offers of the day, special announcements on opening hours or holidays, etc. Photo

    if (count($pagination['entries']) > 0) {
        $entry = array_shift($pagination['entries']);	// 1st element, i.e., newest
        echo "<aside>\n" . $entry['content'] . "</aside>\n";

All other posts are handled as usual:

<?php foreach ($pagination['entries'] as $entry) { ?>
    <article class=aindex>
    <h2><a href="<?= $rbase . $entry['url'] ?>"><?= $entry['title'] ?? 'Unknown title' ?></a></h2>
<?php if (isset($entry['heroimg'])) { ?>
    <div class=ixImgContainer><a href="<?=$rbase.$entry['url']?>"><img class=ixImgZoomIn width=300 src="<?=$rbase?>/img/<?=$entry['heroimg']?>" alt=HeroImg></a></div>
<?php } ?>
    <p><?= $entry['excerpt'] ?? '---' ?></p>
<?php } ?>