tightenco/jigsaw
Jigsaw is a simple static site generator powered by Laravel's Blade templating engine. Build fast, framework-free websites using familiar Blade components, Markdown content, and collections, all without needing a database or server.
Jigsaw is a static site generator that compiles Blade templates (plus Markdown) into plain HTML. If you know Blade, you already know 80% of Jigsaw — the mental model is "Laravel views, but the output is files on disk instead of a response."
Minimal setup:
composer require tightenco/jigsaw --dev
./vendor/bin/jigsaw init # choose `starter`, `blog`, or `tailwindcss`
./vendor/bin/jigsaw serve # http://localhost:8000, rebuilds on save
Build for deployment:
./vendor/bin/jigsaw build production # -> build_production/
./vendor/bin/jigsaw build # -> build_local/ (default, "local" env)
Most teams add Composer scripts so nobody memorizes vendor/bin paths:
"scripts": {
"dev": "jigsaw serve",
"build": "jigsaw build production"
}
Where to look first
source/ — your site. Everything here becomes a page unless the folder/file is prefixed with _.source/_layouts/ — Blade layouts (master.blade.php, post.blade.php).source/_partials/ — reusable includes.config.php (project root) — baseUrl, collections, build behavior.bootstrap.php — global hook point for custom Blade directives and build events.webpack.mix.js / package.json — asset pipeline (Laravel Mix), if the starter uses it.First use case: add source/about.blade.php with a front-matter block, extend the master layout, fill a section, run jigsaw serve, and confirm it appears at /about.html (or /about/ with --pretty). Then do the same page as about.md with extends: _layouts.master in YAML front matter — that's the whole Markdown workflow.
Layout + section inheritance. Standard Blade, exactly as in Laravel:
{{-- source/_layouts/master.blade.php --}}
<html>
<head><title>@yield('title', $page->title)</title></head>
<body>@yield('content')</body>
</html>
{{-- source/about.blade.php --}}
---
title: About
---
@extends('_layouts.master')
@section('content')
<h1>{{ $page->title }}</h1>
@endsection
Markdown pages. Front matter is YAML delimited by --- at the very top of the file. extends names the layout and section names the yield it fills:
---
title: Hello world
date: 2026-05-01
extends: _layouts.post
section: content
---
Body text here, with **Markdown** and Blade: {{ $page->title }}.
Collections are the workhorse for blogs, docs, changelogs, and any repeated content type. Declare them in config.php, then they're available as a variable in every view:
// config.php
return [
'baseUrl' => getenv('APP_URL') ?: 'http://localhost:8000',
'collections' => [
'posts' => [
'extends' => '_layouts.post',
'items' => ['posts/*.md'],
'sort' => 'date',
],
],
];
@foreach ($posts->sortByDesc('date') as $post)
<a href="{{ $post->getPath() }}">{{ $post->title }}</a>
@endforeach
Collections are collections in the Laravel sense — filter, where, sortBy, first, take all work. Filtering by front-matter fields ($posts->where('featured', true)) is the standard way to build "featured posts" or tag pages without plugins.
Pagination. Call paginate() on the collection and render links(); override the markup by dropping a paginator view into source/_paginators/.
@foreach ($posts->paginate(5) as $post)
...
@endforeach
{{ $posts->links() }}
Partials as components. @include('_partials.card', ['post' => $post]) is the idiomatic way to pass data — there are no props/slots beyond what Blade gives you, so keep partials dumb and pass everything explicitly.
Custom Blade directives & build hooks. bootstrap.php is loaded before the build and receives $bladeCompiler and $events:
// bootstrap.php
$bladeCompiler->directive('datetime', function ($expression) {
return "<?php echo ($expression)->format('M j, Y'); ?>";
});
$events->listen(\TightenCo\Jigsaw\Events\BuildStarted::class, function ($event) {
// e.g. regenerate a sitemap, copy files, purge a CI cache
});
This is the sanctioned extension point — reach for it before forking a starter.
Assets. Keep source assets in source/_assets/ (underscore = not published), build them with Mix, and reference the compiled output. Jigsaw's Tailwind starter wires this up; follow its structure rather than inventing one.
Environments and deployment. build_local for jigsaw serve, build_production for CI. Set baseUrl from an env var in config.php so canonical URLs, feeds, and pagination links are correct on the deployed host. A typical pipeline is: composer install --no-dev && npm ci && npm run prod && jigsaw build production, then publish build_production/ to Netlify, S3+CloudFront, GitHub Pages, or any static host.
_layouts, _partials, _assets are skipped during the build. Rename a folder and you'll suddenly publish it — this is the most common accidental leak.title and no layout.{{ something }} inside a code sample will be evaluated. Escape it as @{{ something }} (or wrap in @verbatim) — this bites everyone writing docs about Blade.--pretty changes output from about.html to about/index.html, and every internal link has to match. Use $page->getPath() / collection item URLs rather than hand-written hrefs, and apply the flag consistently between serve and the production build. Pagination links in particular rely on this.baseUrl is not cosmetic. Feeds, canonical tags, og: tags, and paginator links all build from it. Hardcoding localhost in config.php and shipping it is a classic post-deploy bug.bootstrap.php changes need a restart of jigsaw serve. The watcher picks up source/ edits, not changes to the PHP bootstrap/config layer.@php dump($posts->pluck('title')); @endphp (or dd()) into a template and read the CLI output. It renders into the artifact, so remove it before committing.sortByDesc('date') is lexicographic unless you normalize front matter to YYYY-MM-DD. Mixed formats (May 1, 2026 vs 2026-05-01) produce scrambled ordering that looks like random behavior.$page is the current page; collection items are their own objects. $page->url is the absolute-ish URL, $page->getPath() the path without extension — pick the right one when building nav and active-link states.source/404.blade.php compiles to 404.html; most static hosts pick it up automatically.build_local/ and build_production/. They're derived artifacts, and committing them turns every deploy into a merge-conflict generator.jigsaw init blog produces a working reference implementation (collections, pagination, feeds, Mix) that's more current than any tutorial — read it before building your own structure from scratch.How can I help you explore Laravel packages today?