How to add dynamic content to Jigsaw

We've been working on a CMS product for a while.

One need that we had was to mix statically rendered pages (flat HTML) with dynamic content (pulling from a database). We really liked the workflow and power of building sites with Jigsaw. But, when it came to rendering dynamic content, we needed to create our own solution.

This is how we did it.

  1. Create placeholders that can be adjusted after Jigsaw compiles content down to HTML.
  2. Using Jigsaw's Event Listeners, swap out the placeholders with PHP.
  3. Convert all files from .html to .php so that the server will interpret the files correctly.

Step 1: Create placeholders

In our blade files, we add a custom tag anywhere we want to inject dynamic content. That tag looks like this:

<?php $settings = $block->settings; ?>

<cms-content type="special-content" settings='@json($settings)' />

Here, we're pulling just a few settings from the CMS and using blade to create a JSON object we can pass along to PHP. The type attribute is arbitrary and eventually references a PHP file that will be responsible to render dynamic content.

Step 2: Use Jigsaw's Event Listeners

After all of our pages compile, we run each of them using the  afterBuild Event Listener that Jigsaw makes available. Event Listeners go in a bootstrap.php file in the root of your Jigsaw project. Here's ours.

<?php

use TightenCo\Jigsaw\Jigsaw;
use App\Listeners\Fetcher;
use App\Listeners\Replacer;


$events->beforeBuild( function(Jigsaw $jigsaw) {
   require_once __DIR__ . '/.env.php';
});

$events->beforeBuild( Fetcher::class );

$events->afterBuild( Replacer::class );
bootstrap.php file in our Jigsaw project

In the Replacer file, we do a few things:

  1. Loop through the pages collection
  2. Process the content of each page
  3. Convert the files from .html to .php

Here's (most) of our Replacer file

<?php

namespace App\Listeners;

use TightenCo\Jigsaw\Jigsaw;

class Replacer
{

    protected $jigsaw;

    public function handle(Jigsaw $jigsaw)
    {
        $this->jigsaw = $jigsaw;
        $this->rewriteToPhp();
    }

    public function rewriteToPhp() {
        $jigsaw = $this->jigsaw;

        $basePath = $jigsaw->getDestinationPath();

        collect( $jigsaw->getOutputPaths() )->each(function ($path) use ($jigsaw, $basePath) {

            if (! $this->isAsset($path)) {
                $content = $jigsaw->readOutputFile($path . '/index.html');

                // Process the content and add helpers to the top of the file
                $updatedContent = $this->cmsHelpers() . PHP_EOL . $this->processContent($content);

                // Delete the HTML file and rename it to .php
                unlink( $basePath . $path . '/index.html');

                $jigsaw->writeOutputFile($path . '/index.php', $updatedContent);
            }
        });
    }

    public function isAsset($path)
    {
        return starts_with($path, '/assets');
    }

    private function processContent($content)
    {
        // Replace content
        $pattern = '|<cms-content\s+(?:type="(?<type>[\w-]*)")?\s+(?:settings=\'(?<settings>.*)\')?\s+\/>|';
        $content = preg_replace_callback( $pattern, function($matches) {
            $type = $matches['type'];
            $settings = json_encode($matches['settings']);
            return $this->cmsContent($type, $settings);
        }, $content);

        return $content;
    }

    public function cmsContent($type, $settings)
    {
        return

<<<CMSCONTENT
<?php \$settings = $settings;?>
<?php require('helpers/{$type}.php'); ?>
<?php \$settings = null;?>
CMSCONTENT;
    }
}

The biggest things to note about this file are:

  1. The use of preg_replace_callback to replace <cms-content /> throughout our rendered pages
  2. How we pass settings to each helper file that actually renders the dynamic content.

Our server has a centralized  helpers project that actually contains the files that are required through the replaced content. Those files are where we fetch remote, dynamic content on page load.

Thanks Jigsaw!

By providing various events (like beforeBuild and afterBuild), Jigsaw has made this type of workflow incredibly easy. Although it took us a bit of time to think outside of our own box, we're very happy with the final solution.