Example Theme for Simplified Saaze: Panorama

· klm's blog

A theme for the static site generator Simplified Saaze. Its main use is for restaurants or bars showing a menu on their website.

Original post is here: eklausmeier.goip.de

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:

 1## 2. Kalte und warme Vorspeisen<a id=vorpeisen></a>
 2
 3- Antipasti dela Casa ...... klein €9,50 - groß €12,50
 4	- mit gegrilltem Gemüse und Fisch
 5- Shrimps Cocktail `1,b,c,d,n` ...... € 9,50
 6	- mit Shrimps, Ananas
 7- Shrimps mit Olivenöl, Cocktailtomaten `1,b,c,n` ...... €12,50
 8	- mit Knoblauch & Schalotten
 9- Gebackener Schafskäse `1,b,c,d,n` ...... €10,50
10	- 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:

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

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

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

 1<?php require SAAZE_PATH . "/templates/top-layout.php"; ?>
 2
 3	<main>
 4	<article class=aentry>
 5<?php
 6	if (!function_exists('postproc')) {
 7		// Post-processing of MD4C processed Markdown, not really clean,
 8		//because probably specific to MD4C, but does the job
 9		function postproc(string $s) : string {
10			//return $s;
11			$s = str_replace(
12				array(PHP_EOL.'<ul>',
13					PHP_EOL.'<li><p>',
14					'</p>'.PHP_EOL.'</li>',
15					'<ul>'.PHP_EOL,
16					'class=leaders>'.PHP_EOL.'<li><code>'),
17				array(PHP_EOL.'<ul class=leaders>',	// add class=leaders to ul
18					PHP_EOL.'<li>',	// strip <p> after <li>
19					'</li>',	// strip </p> before </li>
20					'<ul class=noleaders>'.PHP_EOL,	// 2nd ul must not have leaders but noleaders
21					'class=noleaders>'.PHP_EOL.'<li><code>'),	// Allergene Sonderfall
22				$s);
23			// replace ABC ...... UVW with ABC+UVW each enclosed in span's
24			// catchword is six dots
25			return preg_replace(
26				'/(' . PHP_EOL . '<li>)(.+)\s+\.\.\.\.\.\.\s+(.+)(<ul|<\/li>)/',
27				'$1<span>$2</span><span>$3</span>$4',
28				$s
29			);
30		}
31	}
32	echo '<h1>' . $entry['title'] . "</h1>\n";
33	if (isset($entry['heroimg']))
34		printf("<p><img class=heroimg src=\"%s/img/%s\" alt=\"Hero image\"></p>\n",$rbase,$entry['heroimg']);
35	$s = ($entry['postproc'] ?? false) ? postproc($entry['content']) : $entry['content'];
36	echo $s;
37
38
39?>
40	</article>
41	</main>

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:

1composer create-project eklausme/saaze-panorama

This installs below directory tree:

 1saaze-panorama
 2|-- LICENSE
 3|-- README.md
 4|-- composer.json
 5|-- composer.lock
 6|-- content
 7|   |-- auxil
 8|   |   |-- datenschutzerklaerung.md
 9|   |   `-- impressum.md
10|   |-- auxil.yml
11|   |-- blog
12|   |   |-- aktuell.md
13|   |   |-- biergarten.md
14|   |   |-- catering.md
15|   |   |-- feiern.md
16|   |   |-- mittagstisch.md
17|   |   |-- pfifferlinge.md
18|   |   |-- ristorante.md
19|   |   `-- speisekarte.md
20|   `-- blog.yml
21|-- public
22|   |-- img
23|   |   |-- Aussenbereich1.jpg
24|   |   |-- Aussenbereich1.webp
25|   |   |-- Aussenbereich2.jpg
26|   |   |-- . . .
27|   |   `-- green-orange-and-yellow-pasta-165844-2000x1200-1.webp
28|   `-- index.php
29|-- saaze
30|-- templates
31|   |-- bottom-layout.php
32|   |-- entry.php
33|   |-- error.php
34|   |-- head.php
35|   |-- index.php
36|   |-- overview.php
37|   |-- rss.php
38|   |-- sitemap.php
39|   `-- top-layout.php
40`-- vendor
41    |-- autoload.php
42    |-- composer
43    |   |-- ClassLoader.php
44    |   |-- InstalledVersions.php
45    |   |-- LICENSE
46    |   |-- autoload_classmap.php
47    |   |-- autoload_namespaces.php
48    |   |-- autoload_psr4.php
49    |   |-- autoload_real.php
50    |   |-- autoload_static.php
51    |   |-- installed.json
52    |   |-- installed.php
53    |   `-- platform_check.php
54    `-- eklausme
55        `-- saaze
56            |-- BuildCommand.php
57            |-- Collection.php
58            |-- CollectionArray.php
59            |-- Config.php
60            |-- Entry.php
61            |-- LICENSE
62            |-- MarkdownContentParser.php
63            |-- README.md
64            |-- Saaze.php
65            |-- SaazeCli.php
66            |-- TemplateManager.php
67            |-- composer.json
68            |-- php_md4c_toHtml.c
69            `-- saaze
70
7111 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.

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

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

1[ -d $DOCROOT ] && rm -rf $DOCROOT
2[ -d /tmp/build ] || errorExit "No build directory in /tmp"
3mv /tmp/build $DOCROOT
4
5cd $DOCROOT
6ln -s $SAAZEROOT/public/img

For local development of your website, you use:

1php -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:

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

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.

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

 1@media screen and (min-width:99rem) {	/* 3 column output */
 2	.aentry, header, aside, footer { width:var(--klmWidth) }
 3	.aindex { margin-left:0rem; width:20rem }
 4	.allcontent { max-width:var(--klmWidth); margin:auto; padding:0rem }
 5	.agrid-container {
 6		display:grid;
 7		justify-content:center;
 8		column-gap:2rem;
 9		grid-template-columns: auto auto auto;
10		grid-template-areas: 'article article article';
11	}
12	/* https://www.w3docs.com/snippets/css/how-to-vertically-align-text-next-to-an-image.html */
13	.imgcontainer { display:flex; align-items:center }
14	.textimg { padding-left:2.5rem }
15}

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

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

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

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

All other posts are handled as usual:

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