In the constant battle of visibility, SEO and speed are important factors. Too often do we neglect these points due to our love for the dynamic web, slick SPAs and client-heavy gimmicks. Next to accessibility, static pages solve for many of these problems while our static site generators are usually monsters of their own.
Today, let's look at a "micro generator" that helps us to manage content dynamically while delivering static HTML pages.
Video-version of this tutorial: YouTube
The concept
The idea is rather simple: let's use the webhook capability of a headless CMS (here blua.blue) to receive content and write/update static HTML files. Next to what we want to achieve, this is a solution for additional challenges:
- it enables us to omit any API necessities
- it enables us to run our CMS locally or within a private network
In this POC, we will use the following folder structure
/assets/
/assets/template.html
/assets/menu.json (not used in the article, but in the video and gist)
/assets/script.js (not used in the article, but in the video and gist)
/assets/style.css
/blog/
receiver.php
.htaccess (if apache)
Preparing the CMS
For development reasons (when you develop locally, you will find yourself having trouble sending webhooks to localhost), I installed blua.blue locally. However, you can develop online (e.g. via FTP) to avoid a bigger setup. Whatever the case, setup your headless CMS to send webhooks to your endpoint. In my case, that was http://localhost/static-blog/receiver.php and using a baerer token for security.
Tip 1:
In blua.blue, you can set webhooks in "Manage" -> "API"
Tip 2:
When using Apache, be sure to include the following in your .htaccess
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
receiver.php
Our receiver should account for the following tasks:
1. Validate calls to ensure the received call is legitimate
2. Account for various events (create, update etc)
3. Write/update static pages
4. Generate a menu
Let's start:
First, let's create our class that we can later initiate with new Receiver('our-webhook-secret');
class Receiver
{
function __construct($mySecret)
{
if (isset($_SERVER['HTTP_AUTHORIZATION']) && substr($_SERVER['HTTP_AUTHORIZATION'], 7) == $mySecret) {
// valid call -> do things
}
}
}
Next, we want want to grab the payload of the call:
private $data;
private function fetchData()
{
$data = file_get_contents('php://input');
if (!empty($data)) {
$this->data = json_decode($data, true);
}
}
We now have the data available as associative array in the property $data. At this point, we should know that the blua.blue call is structured like this:
{
"event": "created",
"payload": {
"name": "article-name",
...
"content": [
{
...
"content": "<p>Content</p>"
}
],
...
}
}
So let's make sure we have that information available in our instance and create a switch-case for the possible events
- created
- updated
- deleted
in our constructor:
function __construct($mySecret)
{
if (isset($_SERVER['HTTP_AUTHORIZATION']) && substr($_SERVER['HTTP_AUTHORIZATION'], 7) == $mySecret) {
$this->fetchData();
switch ($this->data['event']) {
case 'created':
case 'updated':
// here, let's write to a file
$this->writeToFile();
break;
case 'deleted':
break;
}
}
}
You might wonder about the method "writeToFile" as we don't have it yet. but first, we need to account for two prerequisites:
- template-rendering
- template
Using composer, let's include neoan3-apps/ops (composer require neoan3-apps/ops
) and remember that we need the autoloader available (require_once __DIR__ . '/vendor/autoload.php';
) in our receiver.
For now, the template is a primitive HTML file in /assets/template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>[[name]]</title>
<link rel="stylesheet" href="../assets/style.css">
</head>
<body>
<div class="container" id="blog">
<blog-menu>
<ul>
<li n-for="links as link">
<a href="[[link.link]]">[[link.name]]</a>
</li>
</ul>
</blog-menu>
<h1>[[name]]</h1>
<div class="content-part" n-for="content as contentPart">
[[contentPart.content]]
</div>
</div>
</body>
</html>
Ups... blua.blue and dev.to have some issues with parsing this template. The hard brackets should be curlys!
The markup should be intuitive, but feel free to head over to the documentation at: https://packagist.org/packages/neoan3-apps/ops
Finally, let's address our missing function:
private function writeToFile()
{
// let's ignore the article if it is a draft
if ($this->data['payload']['publish_date']) {
// account for \Neoan3\Apps\Ops routing
if(!defined('path')){
define('path', __DIR__);
}
$destination = path . '/blog/' . $this->data['payload']['slug'] . '.html';
$content = \Neoan3\Apps\Ops::embraceFromFile('assets/template.html', $this->data['payload']);
file_put_contents($destination , $content);
}
}
That's it! We can now generate our first static article by using the blua.blue backend.
Now, there is a lot of functionality missing (like delete, menu etc.). Some of it is addressed in the video and you can find a little more advanced version here: https://gist.github.com/sroehrl/c81a1d90a8db87b55307ea7f791c1de7