Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ssg Laravel Package

statamic/ssg

Statamic SSG is a Laravel-powered addon that generates static HTML from your Statamic site for fast, secure, cache-friendly deployments anywhere. It supports incremental builds, multi-site setups, and familiar content workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and first run. The package is a build-time compiler bolted onto an existing Statamic app — there is no service to run, no dashboard, no migrations.

composer require statamic/ssg --dev
php artisan vendor:publish --tag=ssg-config   # writes config/ssg.php
php please ssg:generate

Output lands in build/ at the project root by default. Add it to .gitignore and serve it with any static server (npx http-server build) before you think about CI.

Where to look first, in this order:

  1. config/ssg.php — this file is the package. Ninety percent of your day-to-day decisions (what gets generated, what is skipped, where it goes) live here.
  2. build/ — the artifact is the source of truth. Open build/index.html, build/about/index.html, and diff the rendered HTML against what your live site serves.
  3. vendor/statamic/ssg/src/CrawlStrategy.php — read this before writing any custom routing. It is short, and it explains exactly why a page you expected is missing.
  4. The ssg:generate command class — confirms which options your installed version actually supports (they vary by major).

First use case: a pre-flight diff, not a deployment. Before touching CI, generate once on a content-complete copy of the site and walk the output tree. The goal of the first hour is not a deployment pipeline — it is a written classification of every page archetype as static-safe, statically degradable, or blocking. Blog posts, marketing pages, and docs pages will be safe. Forms, search, account areas, and image-transform-heavy galleries are where the surprises are. You cannot estimate anything until that list exists.

Publish config that is worth reviewing looks roughly like this — treat the exact key set as version-dependent and diff it against the published stub rather than trusting a blog post:

// config/ssg.php
return [
    'destination' => 'build',

    // Crawled but shouldn't be published / would 404 anyway
    'exclude' => [
        '/404',
    ],

    // Not linked from anywhere — the generator will never find these
    'include' => [
        '/404',
        '/sitemap.xml',
        '/search.json',
    ],

    // Swap this out to replace link-crawling with explicit enumeration
    'strategy' => \Statamic\SSG\CrawlStrategy::class,

    // Files from public/ that must travel with the HTML
    'assets' => [
        // ...
    ],
];

Implementation Patterns

The crawl is your router, so treat the manifest as code. The default strategy starts at / and follows internal links. Anything unreachable by a link does not exist in the output, and nothing will warn you. Keep include under code review alongside route changes, and add the pages that are structurally uncrawlable: the 404 template, sitemap.xml, a JSON search index, feed routes, and any listing page reachable only via a form or a JS-driven filter.

Replace the crawl with enumeration when determinism matters. Link-crawling is elegant and non-deterministic; the moment you need reproducibility or you have 5,000 URLs, swap the strategy. Extend the class named in config and override the method that returns the URL set — open the vendor source first, since the signature has moved between majors:

<?php

namespace App\SSG;

use Statamic\SSG\CrawlStrategy;
use Statamic\Facades\Entry;

class EntryStrategy extends CrawlStrategy
{
    public function pages(): array
    {
        return Entry::query()
            ->where('status', 'published')
            ->get()
            ->map(fn ($entry) => $entry->absoluteUrl())
            ->push('/')
            ->push('/sitemap.xml')
            ->all();
    }
}

Point config/ssg.php at App\SSG\EntryStrategy::class. You lose "it just finds things" and gain "the output is a function of the content, and a missing page is a bug in one readable file."

Keep the dynamic app alive; statically serve only the edge. The pattern that survives contact with editors is: Statamic stays deployed and reachable for authoring, Live Preview, and anyone with a session; the CDN serves the artifact to the public. Live Preview renders server-side and will never reflect the static build, which is correct — but say so explicitly to the editorial team or they will file it as a bug.

Pipeline as a single reviewable script. Keep the whole generation in one place so local and CI cannot drift:

php please stache:clear
php please stache:warm
npm ci && npm run build          # Vite output must land in public/build
php please ssg:generate
test "$(find build -name 'index.html' | wc -l)" -ge "$EXPECTED_MIN"

The stache:clear is not superstition — a stale Stache produces a correct build of wrong content, and that is the failure mode you will actually hit. The count assertion is what catches silent under-generation.

Deploy with --delete, and clean the destination first. Static hosting does not garbage-collect. A deleted page leaves build/blog/old-post/index.html behind forever unless you wipe or sync with deletion:

rsync -a --delete build/ user@host:/var/www/site/
# or
aws s3 sync build/ s3://bucket --delete --cache-control "public,max-age=300"

Islands for the things that cannot be static. Search becomes a generated search.json (added to include) consumed by a client-side library. Forms post to a third-party endpoint or a small serverless function. Anything else that assumed PHP at request time gets the same treatment: build the data, ship it as JSON, resolve it in the browser.

Assert correctness on the artifact, not on the build log. Two cheap gates that pay for themselves: a link integrity check against build/ (catches broken relative URLs after a trailing-slash change) and a byte-diff of the new build against the previous one with timestamps stripped. Any unexpected diff is a signal to investigate, not to rationalise.

Gotchas and Tips

A page that is missing produces no error. This is the defining characteristic of the tool and the source of most production incidents. There is no compiler, no route registry validation, no warning. Mitigate with a post-build URL-count assertion and a sitemap comparison — and require every routing change to be reviewed against config/ssg.php.

The 404 page is never crawled. Nothing links to it. It must be in include or it simply will not exist on the CDN, and every bad URL will fall through to your host's default.

Query strings do not become files. ?page=2, ?tag=foo, and ?sort=date all collapse onto the same path. Pagination is the classic breakage: either configure path-based pagination (/blog/page/2) so each page is a real file, or accept that only page one ships. Verify in build/ — do not assume.

Trailing slashes and directory indexes. The generator writes about/index.html, which most hosts serve at /about/ and redirect /about → /about/. Check internal links, canonical tags, and sitemap entries against whichever form the CDN actually serves, and test on the real CDN, not a local file server, which is more forgiving.

Glide image transforms are runtime routes. An {{ glide }} URL pointing at /img/... resolves through PHP at request time; statically, those URLs 404. Pre-generate the transform cache at build time (or push transforms to an image CDN) and verify a sample of URLs against the artifact. This is the single most common post-launch "images are broken" report.

public/ is not copied wholesale. Your CSS, JS, fonts, and favicons live outside the crawled HTML tree. Either your deploy syncs public/ and the assets config correctly, or every page renders unstyled. Check the assets key in config/ssg.php and then check a real page in a browser with the network tab open.

Generate with production environment and a correct APP_URL. Absolute URLs, canonical tags, and OG images get baked into files permanently. Generating on a developer machine with APP_ENV=local and APP_URL=http://mysite.test produces an artifact full of localhost. Run php please ssg:generate under the same env your production app sees.

Cache and Stache freshness. php please stache:clear before generating in CI; php please stache:warm if the build is slow. Forgetting the clear gives you a perfectly valid build of yesterday's content — and you will diagnose it for twenty minutes before remembering.

Forms, CSRF, and sessions. {{ form:create }} renders markup and a URL, not a working endpoint. Any tag that emits a CSRF token or touches the session is dead weight in the artifact. Audit resources/views for those tags during the spike, not after launch.

{{ nocache }} regions freeze. They render whatever the state was at build time and stay that way. If a region was wrapped in nocache for a reason, that reason is now a design problem.

Extension points worth knowing. The strategy class is the primary hook (swap crawling for enumeration, or wrap it to log discovered URLs), and the assets configuration handles file copying. If you need per-page post-processing, do it as a step in your own build script after generation rather than subclassing deeper — the surface area is small and deliberately so.

Build time scales with page count, linearly and unhappily. A few hundred pages is seconds; a few thousand with heavy Antlers is minutes. If content freshness demands builds on every publish, measure this before committing to a cadence. Full rebuilds are fine for hours-to-days freshness; they are the wrong tool for sub-minute.

Version and environment matrix. 4.1.0 is a Laravel 13 compatibility release with a test-only PHPUnit change — no API or output changes from 4.0. Adopting it is a composer update, but verify the resolved matrix explicitly with composer why-not statamic/ssg 4.1.0 on a branch and review the lockfile diff; a widened illuminate/* constraint can move your PHP floor, which touches CI images and any pinned preview host. Keep "the addon supports Laravel 13" and "we should upgrade to Laravel 13" as separate decisions.

Expect to build your own expertise. The ecosystem signal is thin — modest adoption and effectively no listed dependents — so there is no large body of community troubleshooting to search. Read the source (it is small), and write down the two or three things you learn the hard way. They will be the same two or three things the next developer hits.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo