Static content pages - still the fastest web-experience
Tags: staticpages, cms, php
Published: 2019-12-07 16:41:49

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: 

[![IMAGE ALT TEXT](http://img.youtube.com/vi/Mw2YqB83KKI/0.jpg)](http://www.youtube.com/watch?v=Mw2YqB83KKI "Create static content with PHP")

The concept
-----------

The idea is rather simple: let's use the webhook capability of a headless CMS (here [blua.blue](https://blua.blue "go to 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');`

```PHP

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:

```PHP
    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:

```PHP
{
    "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:

```PHP
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

```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:

```PHP
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