Building your website using Jigsaw

Published on by

Building your website using Jigsaw image

As developers, the chance of you having a personal website is pretty high. After all, it is what we do - and most likely, you have rebuilt this countless times. Because we all know that we need to rebuild our website before we finish that blog post we have been meaning to write...

I am guilty as the next developer for this; honestly, there is no shame in it. We use rebuilding our website as a learning experience and a time for us to experiment with new ideas or designs. But, if you ever get to a point where you think, "I just want a simple website so I can write," there is an answer. Jigsaw is a static site generator built by Laravel partners Tighten, and it is what I have been using for the last year or so for my personal website.

The beauty of it is that it allows me to write my blog posts in markdown, has a simple blade view to render pages and posts - and can be built to be deployed on any number of services from GitHub Pages to Netlify and beyond.

Getting started with Jigsaw is much simpler than you might imagine, so let's walk through the setup together. Our first step is to create a directory for your project. I use ~/code/github/JustSteveKing for most of my projects that I do not want to use Laravel Valet for. It allows me to organize projects by GitHub repo and organization easily. So run the following command to create the directory:

mkdir jigsaw-website

Our next step is to enter the directory:

cd $_
// or
cd jigsaw-website

Now we have entered the directory. We need to tell Composer that we want to use Jigsaw, so run the following composer command:

composer require tightenco/jigsaw

Once this process has run, we have a few options. Many templates are available, not just the ones listed in the project's documentation. A Google search will help you find many of them. We can start from scratch or use one of the templates available.

We will use one of the ones built by Tighten themselves called blog. To install this, run the following jigsaw command:

./vendor/bin/jigsaw init blog

This will bootstrap an entire blog for you quickly, setting everything up, so all you need to do is focus on writing - but you also have the option to start styling and modifying the templates. Let's have a look at the website and see what we are dealing with:

./vendor/bin/jigsaw serve

This should start a PHP server for you on http://localhost:8000, so visit it and get familiar with your new blog!

Now we can start thinking about how we might write our content. Open the project inside your code editor of choice, and we can explore what is there.

You can change the configuration for your website inside the config.php file, so if you open it up and change the values such as siteName and siteDescription if you restart the site, you should see your changes reflected.

You will also notice a config option called collections. These are content collections, and you can have as many as you need. You can even use remote collections, where you can fetch data from an API and create records in your built site to view them - cool, right?

We won't concentrate too much on this. Instead, let's start writing blog posts. We first want to delete all the dummy data created by this template - it isn't something we would want on our blog, after all. Delete all the files inside the following directories:

  • source/_categories
  • source/_posts_

Now we can look at creating a new post for our website. Next, create a new file using the command line:

touch source/_posts/

Then open it in your editor, and this will be your first blog post. The markdown files are constructed in two parts. You have what is called front matter, a YAML syntax for providing information about the Markdown file itself. This is where we will add things like the title, author, categories, and when it was created. It should look like this:

section: content
title: My First Blog Post
date: 2022-06-21
description: This will be your meta description, make sure it isn't too long
categories: [writting]

So we have a title of My First Blog Post, a date and description, then categories. The important thing to remember is that categories are not created for you. To add a category, create another new file:

touch source/_categories/

Like the posts, this is also split into two parts. So we will add the following:

extends: _layouts.category
title: Writing
description: All posts that are about writing.
These posts are about writing and stuff.

As you can see, we have two parts to the markdown file, the front matter and the actual content. If we now go back to our blog post, we can add anything we want in the markdown here:

section: content
title: My First Blog Post
date: 2022-06-21
description: This will be your meta description, make sure it isn't too long
categories: [writting]
This is my blog post.
## Here is a heading 2
- here
- is
- a
- list
> Even a blockqute
[And a link](

Now, if we serve the website again, using the jigsaw command:

./vendor/bin/jigsaw serve

When we visit http://localhost:8000/blog/my-first-blog-post we will see the new page we just created.

So we have covered how to write content, which in fairness, wasn't too taxing. What else would we want on our website? Well, this template comes with a way to generate a sitemap, which is pretty helpful!

Jigsaw has a concept called Listeners that runs after specific events during the build process of the website. If we open up bootstrap.php, you should see the following code:


We have two registered listeners, one that will generate a sitemap for us and another that will generate an index for us. The index is used in the website's search function - something that comes as part of the blog template. Let's have a look at this GenerateIndex and see what it is doing:

namespace App\Listeners;
use TightenCo\Jigsaw\Jigsaw;
class GenerateIndex
public function handle(Jigsaw $jigsaw): void
$data = collect($jigsaw->getCollection('posts')
->map(function ($page) use ($jigsaw) {
return [
'title' => $page->title,
'categories' => $page->categories,
'link' => rightTrimPath(
) . $page->getPath(),
'snippet' => $page->getExcerpt(),
$jigsaw->getDestinationPath() . '/index.json',

This class is called and handled, so we get the posts collection, map over each one, and return an array of information we want to add to the search index. Next, we put the contents to a new file JSON encoding it. Kind of cool, and a nice way to build a search index. This data is already public on our website, so adding a publicly accessible JSON file allows us to access it easier through searching.

Let's add an RSS feed to our website now. You could use a few different ways to do this - using a listener or simply as a view. From my experience using Jigsaw, I opted for creating a view for this.

Inside our source directory, we will need to create a new file called source/blog/rss.blade.xml, note the file extension. We will be creating an XML file (shudder), but using blade so that we can have familiar syntax. Inside this file add the following code:

{!! '<'.'?'.'xml version="1.0" encoding="UTF-8" ?>' !!}
<rss version="2.0" xmlns:atom="" xmlns:content="" xmlns:media="">
<title>{{ $page->siteName }}</title>
<link>{{ $page->baseUrl }}</link>
<description><![CDATA[{{ $page->siteDescription }}]]></description>
<atom:link href="{{ $page->getUrl() }}" rel="self" type="application/rss+xml" />
<language>{{ $page->siteLanguage }}</language>
<lastBuildDate>{{ $posts->first()->getDate()->format(DateTime::RSS) }}</lastBuildDate>
@foreach($posts as $post)
<title><![CDATA[{!! $post->title !!}]]></title>
<link>{{ $post->getUrl() }}</link>
<guid isPermaLink="true">{{ $post->getUrl() }}</guid>
<description><![CDATA[{!! $post->description !!}]]></description>
<content:encoded><![CDATA[{!! $post->getContent() !!}]]></content:encoded>
<dc:creator xmlns:dc="">{{ $post->author }}</dc:creator>
<pubDate>{{ $post->getDate()->format(DateTime::RSS) }}</pubDate>

We create a channel with our site information, then we loop through each post and add an item to this channel, creating a valid RSS feed. To add a link to the feed, edit the main layout to add the link in the head of our html . This allows RSS feed readers to auto-discover the feed. Open source/_layouts/main.blade.php and refactor the <head> element to look like the following:

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="description" content="{{ $page->description ?? $page->siteDescription }}">
<meta property="og:title" content="{{ $page->title ? $page->title . ' | ' : '' }}{{ $page->siteName }}"/>
<meta property="og:type" content="{{ $page->type ?? 'website' }}" />
<meta property="og:url" content="{{ $page->getUrl() }}"/>
<meta property="og:description" content="{{ $page->description ?? $page->siteDescription }}" />
<title>{{ $page->title ? $page->title . ' | ' : '' }}{{ $page->siteName }}</title>
<link rel="home" href="{{ $page->baseUrl }}">
<link rel="icon" href="/favicon.ico">
<link href="/blog/feed.atom" type="application/atom+xml" rel="alternate" title="{{ $page->siteName }} Atom Feed">
<link rel="alternate" type="application/rss+xml" title="{{ $page->siteName }}" href="/blog/rss.xml" />
@if ($page->production)
<!-- Insert analytics code here -->
<link href=",300i,400,400i,700,700i,800,800i" rel="stylesheet">
<link rel="stylesheet" href="{{ mix('css/main.css', 'assets/build') }}">

As you can see, we added the following snippet to point to our RSS feed.

<link rel="alternate" type="application/rss+xml" title="{{ $page->siteName }}" href="{{ $page->baseUrl.'/rss.xml' }}" />

So inspecting this code, we already have a feed - the atom feed. This comes as part of the template itself but is an Atom feed over an RSS feed - this isn't a problem, though. Having both options available means that you are more accessible, which is never a bad thing when it comes to your website.

The final step in using Jigsaw is to build your website, so it's ready for deployment, and you can do this using the following jigsaw command:

./vendor/bin/jigsaw build production

This will build your local site into static content inside build_production, a new directory that appears once this command is run. The final step is to deploy your website somewhere. The documentation has instructions on deploying to:

So pick your desired provider for hosting, and set it up. You are now ready to start blogging with minimal effort - and an easy-to-use system to do it on. The fact that it is a static site also means that you do not have to worry about slow queries fetching data or any usual problems you might have with a non-static website.

Steve McDougall photo

Technical writer at Laravel News, Developer Advocate at Treblle. API specialist, veteran PHP/Laravel engineer. YouTube livestreamer.


Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.


Version 4 of Tinkerwell is available now. Get the most popular PHP scratchpad with all its new features and simplify your development workflow today.

Visit Tinkerwell
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo


The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo


Providing innovation and stability to ensure your web application succeeds.

Shift logo


Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Bacancy logo


Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo


The official Laravel job board

Larafast: Laravel SaaS Starter Kit logo

Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes. Available with VILT and TALL stacks.

Larafast: Laravel SaaS Starter Kit
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo


Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies


The latest

View all →
Microsoft Clarity Integration for Laravel image

Microsoft Clarity Integration for Laravel

Read article
Apply Dynamic Filters to Eloquent Models with the Filterable Package image

Apply Dynamic Filters to Eloquent Models with the Filterable Package

Read article
Property Hooks Get Closer to Becoming a Reality in PHP 8.4 image

Property Hooks Get Closer to Becoming a Reality in PHP 8.4

Read article
Asserting Exceptions in Laravel Tests image

Asserting Exceptions in Laravel Tests

Read article
Reversible Form Prompts and a New Exceptions Facade in Laravel 11.4 image

Reversible Form Prompts and a New Exceptions Facade in Laravel 11.4

Read article
Basset is an alternative way to load CSS & JS assets image

Basset is an alternative way to load CSS & JS assets

Read article