In the we took a look at , the PHP static site generator which is currently the most starred on Github. Today we’re exploring , a tool which promises to bring a Laravel-based approach to the world of PHP static site generators (SSGs). first article of the series Sculpin Jigsaw 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! Jigsaw (~900★) 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 and start from there. official docs Unlike Sculpin, Jigsaw doesn’t provide a skeleton project we can use as quickstart. Instead, we must create a normal project, add as dependency, and use its CLI: tightenco/jigsaw 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 with this code: config.php <?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 , and it’s empty (only some commented-out code), but it hints at how we could use the event system at some point. bootstrap.php We also have Javascript dependencies. I see a , which lets me know that is being used. So I run and in the meantime I’ll explore the docs to try to understand a bit more of what I’m doing 😉. webpack.mix.js Laravel Mix npm install About 10 dependencies and 300MB in 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: node_modules # 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. The directory structure 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 directory! What lies therein? source Being a Laravel-based project, I’m not surprised to find an file, which contains the shiny from above. The directive hints to me that I should go look in the directory for a template, and lo and behold, there it is. Everything seems nice and tidy with the templates, let’s go check how assets are handled. index.blade.php Hello world! @extends('_layouts.master') _layouts master.blade.php Assets and configuration Laravel Mix provides out of the box support for Sass, so 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 /* 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 , which, as the docs state, . Sounds about right for a blog. Remember the file from earlier? Let’s tweak it to tell Jigsaw about our collection of blog posts: collections give you the ability to access your content at an aggregate level config.php <?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, will become , so in that directory let’s create a blog post to show our love for one of the greatest songs of all times. posts source/_posts ---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 , and then let’s create the appropriate template as specified in the part of the front matter: . the-fresh-prince.md extends source/_layouts/post.blade.php @extends('_layouts.master')@section('body')<h1></h1> <p>By - </p> @yield('content')@endsection Let’s run , and we’ll have a nice directory generated with our shiny website, so let’s open the URL (which we still have to build ourselves) and… It works! ./vendor/bin/jigsaw build build_local (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 , thank you.) noice Is Jigsaw Contentful-ready? 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 , and with that page open in the tab, we can give this a try. Remember ? It contained some commented-out code which hinted at how events work, so let’s reopen that: documentation page about event listeners bootstrap.php <?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 about creating a sitemap with Jigsaw, and the original for the events feature, so I have enough to figure this out. Let’s and hack this thing together! article pull request composer require contentful/contentful // 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 , and Jigsaw will know that it must create an instance of that class and execute the method of it. Truth be told, I think there should be an interface in order to have a stronger contract, something like this: $events->beforeBuild() handle 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 ). 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! __invoke This is an overview of what actually does: ContentfulFetcher If the 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 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 , to avoid keeping them in version control (content doesn't belong there). local ./vendor/bin/jigsaw build production .gitignore We create an instance of a client object using the Contentful Delivery SDK for PHP. We iterate through all the defined collections, and we use a convention to map a local collection to a content type ID in Contentful. We then clean the corresponding directory, where we will dump the generated files. We fetch entries for the given content type, and we use the 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). Contentful\Delivery\Resource\ContentType We finally dump the contents of the generated file. We use the Contentful ID to save them, as we can define a pretty URL when configuring the collection. 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. Verdict 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 (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 👏 Matt Stauffer 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 to track progress. He also mentioned that they just launched a new , which I encourage you to check out. Github issue showcase of websites built with Jigsaw 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 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. source Next article will be about , which describes itself as a documentation generator. Stay tuned! Couscous Originally published at www.contentful.com on August 1, 2018.