Saisho Update 20200830 🔜

I don't write nearly as enough as I should, so I've decided that I'm going to share any progress updates and what went into making them of some of my publicly available projects, like Saisho.

Besides general code fixes and improvements, this update is a low-key code refactor that I just had to do.

I'm always looking for ways to improve the code of Saisho keeping in mind simplicity and speed (both server and client side) over adding new features. The whole point of Saisho is to have a semi-static website that doesn't add much overhead over serving pure HTML.

Templates

One of the things new to this update is that the visual side is now separate from the logic side. Despite me wanting to keep Saisho simple and contained in one file, the template side of it made the code a bit messy so out it goes.

It is, perhaps, a very crude implementation of a "templating system" (again, due to me wanting to keep the code as simple and as fast as possible), but it will allow anyone that uses Saisho a simpler way to customize it to their liking.

The core of the "system" is loading a template from TEMPLATE_DIR with the corresponding page type, e.g. page, list, notfound etc:

// Called like so:
$page = (object)['type' => 'page', 'content' => $this->renderPage($pagePath) ];
$this->renderTemplate($page->type, [$page->content]);

private function renderTemplate(string $template, array $args)
{
    ob_start();
    if (!file_exists(TEMPLATE_DIR.DS.$template.'.php')) return (string) "Template $template.php not found!";
    // ...
    include TEMPLATE_DIR.DS.$template.'.php';
    return ob_get_clean();
}

The only HTML left in the main file is in renderPageList(array $list, $filter) as I think this is a core function, but I may just move it to the template and leave a function that outputs an array to be used.

Caching

One of the things I've noticed during the implementation/split of the template code was that I've made a stupid mistake that caused the page you're looking at to be rendered even if a valid, cached page was available.

This happened because the request handler (handleRequest(string $requestedPage)) was executing $this->renderPage($pagePath)1 for the content as part of it's output (which was creating a page object with type and content).

The solution was to modify handleRequest() so that it checks for cache first and only then creates the page object:

$cachePath = CACHE_DIR . DS . $requestedPage . '.html';
if (!$this->tryCache($cachePath)) {
    $page = (object)['type' => 'page', 'content' => $this->renderPage($pagePath) ];
}

tryCache() would return false if the cached page is either invalid or not available, or, in the case that cache is valid and available just return the page and exit.

This resulted in cached page loading time going down from about ~2.145ms to ~0.127ms. You might say this is yak shaving, and you'd be right, but even being a duct tape programmer has it's limits and I want to do this thing properly so that I can also learn a thing or two myself.

Tags

I honestly don't use tags much (I probably should), but this was a good way to use "internal tags" like "hide" and "now" which will omit pages from the list:

define('TAGS_TO_HIDE', ['hide', 'now']);

These tags are filtered using array_filter:

return (array)array_filter($parsedPages, function ($item) use ($filter) {
    if ($filter === true) {
        return !array_intersect(TAGS_TO_HIDE, $item->tags);
    } elseif (is_string($filter)) {
        return in_array($filter, $item->tags);
    } else {
        return 1;
    }
});

This allows you to pass true to filter out the hidden tags, or a string to filter by tag.

Speed

In order to make pages load even quicker, I made the decision to replace all images with a placeholder which will load the original image once clicked.

As I'm not smart enough (also, please keep in mind this is a personal project), the change was done directly in Parsedown's code:

'attributes' => array(
    'src' => '/load.png',
    'data-src' => $Link['element']['attributes']['href'],
    'alt' => $Link['element']['handler']['argument'],
    'loading' => 'lazy',
),

The placeholder image itself is 256 bytes. The switch, once the placeholder is clicked is done in vanilla JS:

const images = document.querySelectorAll("img[data-src]");
images.forEach(function(image) {
    image.addEventListener('click', e => {
        e.target.src = e.target.dataset.src;
        image.removeEventListener('click', e);
    });
});

Other things

What's next?

I'd like to refactor handleRequest() to make it easier to add custom paths, as, for example, to add a projects list requires me to do the following:

case 'projects':
    $list = $this->listPages();
    $page = (object)['type'=>'list', 'content'=>$this->renderPageList($list, 'project')];
break;

You can see how this might not be the best or easiest way.


  1. https://git.sr.ht/~hxii/saisho/tree/master/index.php#L33 ↩