In the first article of the series we took a look at Sculpin, the PHP static site generator which is currently the most starred on Github. Today we’re exploring Jigsaw, a tool which promises to bring a Laravel-based approach to the world of PHP static site generators (SSGs).
I’m gonna be honest with you — I’m not the biggest Laravel user. I appreciate what it’s done for PHP, and how it has helped push forward the world of PHP development in a time where older frameworks were struggling to keep up with innovations coming from other platforms. It’s also a prime example of the good kind of cross pollination happening in the PHP world: it employs packages from the Symfony world, The PHP League, Doctrine, and more. It’s a good lesson in building something cool on top of great foundations, without having to continuously reinvent the wheel. Overall, I think that Laravel is an excellent framework, but I’m definitely inexperienced with it. If you think I have overlooked or misjudged something (both Laravel and Jigsaw-related), please feel free to reach out to me on Twitter: constructive criticism is always welcome!
Just like I did with Sculpin, I’ll jump straight into the action, so let’s create a sample project and play with it! We’ll open the official docs and start from there.
Unlike Sculpin, Jigsaw doesn’t provide a skeleton project we can use as quickstart. Instead, we must create a normal project, add tightenco/jigsaw
as dependency, and use its CLI:
mkdir jigsaw-example && cd jigsaw-examplecomposer require tightenco/jigsaw./vendor/bin/jigsaw init
Let’s go through the default directory structure that was just generated. In the top level directory, we have a config.php
with this code:
<?phpreturn [ 'baseUrl' => '', 'production' => false, 'collections' => [],];
I’m not sure yet what this means, but having a configuration file clearly placed seems like a good approach 👍. The second PHP file is called bootstrap.php
, and it’s empty (only some commented-out code), but it hints at how we could use the event system at some point.
We also have Javascript dependencies. I see a webpack.mix.js
, which lets me know that Laravel Mix is being used. So I run npm install
and in the meantime I’ll explore the docs to try to understand a bit more of what I’m doing 😉.
About 10 dependencies and 300MB in node_modules
later, we’re back. Now, let’s try to build our site. It appears we have 2 ways of doing so: we can use the Jigsaw CLI to build and then serve, or use npm and its live reload capabilities provided by Browsersync:
# With Jigsaw CLI./vendor/bin/jigsaw build./vendor/bin/jigsaw serve # localhost:8000# With npmnpm run watch # localhost:3000
Regardless, both work and you get this!
Naturally, only John Oliver can really convey my reaction to this page.
Now let’s explore the skeleton project that was generated earlier. The order in which we’re exploring parts of the project might appear to be random because, well, it is. That’s the cool thing about discovering something new, right? The exploratory phase, where you peek at this, take a look at that, and oh, a source
directory! What lies therein?
Being a Laravel-based project, I’m not surprised to find an index.blade.php
file, which contains the shiny Hello world! from above. The @extends('_layouts.master')
directive hints to me that I should go look in the _layouts
directory for a master.blade.php
template, and lo and behold, there it is. Everything seems nice and tidy with the templates, let’s go check how assets are handled.
Laravel Mix provides out of the box support for Sass, so source/_assets/sass/main.scss
is your starting point. SCSS might not be the latest and shiniest thing on the CSS landscape, but it’s battle-tested and definitely gets the job done, so I approve of its inclusion. Let’s turn that page into something that will get your attention
/* source/_assets/sass/main.scss */$bg-color: red;body { background-color: $bg-color;}
Let’s hit save and webpack should do its magic and turn our background red, right? Yes, it does! I’ll spare you the screenshot for your eyes’ sake, and now let’s continue our exploration.
I’d like to set up a basic blog, with posts written in Markdown files. To do that, let’s introduce the concept of collections, which, as the docs state, give you the ability to access your content at an aggregate level. Sounds about right for a blog. Remember the config.php
file from earlier? Let’s tweak it to tell Jigsaw about our collection of blog posts:
<?phpreturn [ 'baseUrl' => '', 'production' => false, 'collections' => [ 'posts' => [ 'path' => 'blog/{date|Y-m-d}/{filename}', ], ],];
Jigsaw will look for the corresponding directory for every collection we define. In our example, posts
will become source/_posts
, so in that directory let’s create a blog post to show our love for one of the greatest songs of all times.
---extends: _layouts.posttitle: The Fresh Princeauthor: Will Smithdate: 1990-09-10section: content---
Now this is a story all about howMy life got flipped-turned upside downAnd I'd like to take a minuteJust sit right thereI'll tell you how I became the prince of a town called Bel-Air
Let’s save this file as the-fresh-prince.md
, and then let’s create the appropriate template as specified in the extends
part of the front matter: source/_layouts/post.blade.php
.
@extends('_layouts.master')@section('body')<h1></h1> <p>By - </p> @yield('content')@endsection
Let’s run ./vendor/bin/jigsaw build
, and we’ll have a nice build_local
directory generated with our shiny website, so let’s open the URL (which we still have to build ourselves) and… It works!
(Author’s note: at this point I would like to add more GIFs, but I have to keep it professional. Let’s all just pretend to have a GIF of Jake Peralta saying noice, thank you.)
We’ve covered some of the basics, so now the million-dollar question: is Jigsaw Contentful-ready? With this, of course, I mean how easily content stored in Contentful can be integrated in a Jigsaw-powered website. We open the documentation page about event listeners, and with that page open in the tab, we can give this a try. Remember bootstrap.php
? It contained some commented-out code which hinted at how events work, so let’s reopen that:
<?phpuse TightenCo\Jigsaw\Jigsaw;/** @var $container \Illuminate\Container\Container *//** @var $events \TightenCo\Jigsaw\Events\EventBus *//*** You can run custom code at different stages of the build process by* listening to the 'beforeBuild', 'afterCollections', and 'afterBuild' events.** For example:** $events->beforeBuild(function (Jigsaw $jigsaw) {* // Your code here* });*/
Ok, seems to be reasonably easy. As extra help, after some googling I found an article about creating a sitemap with Jigsaw, and the original pull request for the events feature, so I have enough to figure this out. Let’s composer require contentful/contentful
and hack this thing together!
// In bootstrap.phprequire_once 'contentful_fetcher.php';$events->beforeBuild(ContentfulFetcher::class);// In contentful_fetcher.phpuse Contentful\Delivery\Client;use Contentful\Delivery\Resource\Entry;use TightenCo\Jigsaw\Jigsaw;class ContentfulFetcher{ /** * Jigsaw requires either a callable or the name of * a class which implements a method called "handle" * * @param Jigsaw $jigsaw */ public function handle(Jigsaw $jigsaw): void { // Let's only build for production if ($jigsaw->getEnvironment() === 'local') { return; } $client = $this->getClient($jigsaw); foreach ($jigsaw->getCollections() as $collection) { $this->cleanSourceDir($jigsaw, $collection); $entries = $this->getPlainEntries($client, $collection); foreach ($entries as $entry) { $this->writeEntryToMarkdownFile($jigsaw, $entry, $collection); } } } private function getClient(Jigsaw $jigsaw): Client { return new Client( $jigsaw->getConfig('contentful.accessToken'), $jigsaw->getConfig('contentful.spaceId') ); } /** * Before every run, we remove the previous contents of the source directory. */ private function cleanSourceDir(Jigsaw $jigsaw, string $collection): void { $dir = $jigsaw->getSourcePath().'/_'.$collection; $jigsaw->getFilesystem()->deleteDirectory($dir, true); } /** * We transform objects of type Contentful\Delivery\Resource\Entry * into plain arrays for ease of use later on. */ private function getPlainEntries(Client $client, string $collection): array { // Convention: // Every defined collection corresponds to a content type ID in Contentful $query = (new Contentful\Delivery\Query()) ->setContentType($collection) ; $entries = $client->getEntries($query); if (!count($entries)) { return []; } /** @var Contentful\Delivery\Resource\ContentType $contentType */ $contentType = $entries[0]->getContentType(); return \array_map(function (Entry $entry) use ($contentType) { // Convention: // The content types may contain a field called body, // which will be used as the markdown content of the generated file, // so we give it a default value $plainEntry = [ 'id' => $entry->getId(), 'body' => '', ]; foreach ($contentType->getFields() as $field) { $plainEntry[$field->getId()] = $entry->get($field->getId()); } return $plainEntry; }, $entries->getItems()); } /** * Converts an entry into a markdown file and writes it to disk. */ private function writeEntryToMarkdownFile(Jigsaw $jigsaw, array $entry, string $collection): void { $contents = '---extends: _layouts.'.$collection.'section: content'; $body = ''; foreach ($entry as $field => $value) { if ($field === 'body') { $body = $value; continue; } $contents .= $field.': '.$value."\n"; } $contents .= '---'."\n\n".$body; $path = sprintf('_%s/%s.md', $collection, $entry['id']); $jigsaw->writeSourceFile($path, $contents); }}
This code is more of a proof of concept than complete implementation (it relies a lot on conventions, and it lacks handling of special fields such as locations, links, etc); however, it should get the job done for a basic example.
First of all, we pass a fully-qualified class name (FQCN) to $events->beforeBuild()
, and Jigsaw will know that it must create an instance of that class and execute the handle
method of it. Truth be told, I think there should be an interface in order to have a stronger contract, something like this:
namespace TightenCo\Jigsaw;use TightenCo\Jigsaw\Jigsaw;interface EventHandlerInterface{ public function handle(Jigsaw $jigsaw);}
Or perhaps just requiring any valid callable (thus including objects implementing __invoke
). It works now in its present state but I like strong contracts, and you should know by now that I'm picky—it's in the title of the series!
This is an overview of what ContentfulFetcher
actually does:
local
environment is detected, the whole process will be stopped. This is done to avoid the performance impact due to having to fetch all data from Contentful on every build (which can likely be very often, if running on watch mode). The idea is to run a ./vendor/bin/jigsaw build production
initially to fetch all content, and then operate with what’s been stored locally. Of course, a good idea would be to add the generated files to the local .gitignore
, to avoid keeping them in version control (content doesn't belong there).Contentful\Delivery\Resource\ContentType
object to get a list of fields. With those fields, we build the appropriate YAML front matter section, and then we add the body at the end (string handling here is very basic and could be improved).There’s a lot of gluing going on, but it works on my machine™️. This could be done a thousand times better, with more flexible configuration, but as a proof of concept I’m reasonably satisfied with it.
Before our final verdict, I think it’s only fair to mention that during the process of writing this article, I experienced a couple of issues which have since been fixed. Before publishing, I got in touch with Matt Stauffer (one of the people behind Jigsaw) and explained that I was experiencing some difficulties with the handling of assets and events (misconfiguration for the former, and lack of docs for the latter). Over the course of a weekend, both issues have been fixed 👏
I quite like Jigsaw, but it’s rather barebones in its out-of-the-box configuration. Unlike Sculpin, it makes no assumption over what kind of use you’ll have for it, which is a double-edged sword: it avoids clutter and gives you complete control, but it also creates a higher barrier of entry for simple use cases such as blog.
To solve this issue, Matt revealed to me that they’re building a generator with website skeletons, to address the most common needs (such as a blog, a documentation website, etc), with a Github issue to track progress. He also mentioned that they just launched a new showcase of websites built with Jigsaw, which I encourage you to check out.
As a non-Laravel developer, I was afraid that Jigsaw would be difficult to pick up and use, but it definitely wasn’t the case. It is actively being developed (the events system was added just a few months ago) and is on track to quickly become the most popular PHP static site generator, in terms of stars on Github. It provides very good defaults, and assets are preconfigured to use a modern stack (Sass for CSS, and webpack with Babel for JS).
The only real criticisms I can make are actually the same as when I discussed Sculpin: content and presentation (assets and templates) could be more separate; instead, they belong to the same source
directory, and I would love to be able to have a more defined structure for collections, instead of relying exclusively on the YAML bit before the actual Markdown contents.
Next article will be about Couscous, which describes itself as a documentation generator. Stay tuned!
Originally published at www.contentful.com on August 1, 2018.