Search by

trail / freeze

dodgycoffee

Static site cache for trail

Package info

source.tube/brindly/trail-freeze

Issues

Type:trail-addon

pkg:composer/trail/freeze

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

0.2.2 2026-10-11 23:35 UTC

This package is auto-updated.

Last update: 2026-10-11 23:36:57 UTC


README

Static site cache for trail framework.

install

composer require trail/freeze

It works. Now every public page freezes on first visit, and thaws when you tell it to

what gets frozen

This is how its determined what to freeze

  • GET request + no cookie bypass (explained in config)
  • response is 200 with text/html
  • response sets no cookie and no Cache-Control: no-store
  • path isnt in skip config list
  • no query string
  • folder isnt full (perFolder option)

/about and /about/ would both be about/index.html. Anything with a query string skips the cache, so /posts?page=2 never hits page 1

exclusions

'skip' => ['/admin*', '/pastebin*', '/search'],

Orrrr you can do it per route just cause

use Trail\Freeze\Gates\NeverFreeze;

Route::get('/form', ShowForm::class)->gate(NeverFreeze::class);

Route::group('/admin', [NeverFreeze::class], static function (): void {
    ...
});

You can also do this if headers are your thing: Response::html($body)->header('Cache-Control', 'no-store')

logged in users

if a request has a cookie from cookies config list, it'll skip the cache. trail/sessions registers its own cookie. anyone with a session (signed in or not) gets uncached responses. auth pages send no-store so they never freeze

'cookies' => ['my_cookie'],

Other addons can register their own using boot()

Freeze::bypass('my_cookie');

deletus

use Trail\Freeze\Freeze;

Freeze::thaw();                  // everything
Freeze::thaw('/posts/hello');    // one page
Freeze::thaw('/posts/*');        // glob

Cli

trail/trail freeze::clear
trail/trail freeze::clear /posts/*

If you were to make say... a cms, you could call thaw() on save for an entry and pages attached to it and it would invalidate them all on

Expired pages refreeze on next hit automatically so nothing to do there

manual freeze

Cli

trail/trail freeze::build
trail/trail freeze::build --clear

not cli

'pages' => static fn (): array => array_map(
    static fn (string $slug): string => '/posts/' . $slug,
    Posts::slugs(),
),

For a full static deploy you can just build the freez + copy the folder wherever

config [trail/config/app.php, optional]

Lives in the freeze section of app.php

return [
    '*' => [
        'freeze' => [
            'enabled' => true,                         // turn it on/off
            'dir' => null,                             // storage/cache/cooler
            'expires' => null,                         // seconds. null is forever
            'stamp' => 'cached at {time}, baba booey', // stamp at end. {time}, {path}, null for none
            'skip' => ['/admin*'],
            'perFolder' => 1000,                       // freeze cap per folder. null for no cap
            'cookies' => [],
            'pages' => static fn (): array => [],
        ],
    ],
    'dev' => ['freeze' => ['enabled' => false]],
];

web server in charge

Out of the box trail serves the cached files directly. Less performant, but works with no fiddling and does have a measurable benefit. But its still hitting php every time.

If you wanna skip php completely, you can configure a few things to let caddy/nginx serve the files directly

nginx

'dir' => Config::get('dir.project') . '/web/cooler',   // or whatever

nginx conf

# frozen pages only for GET/HEAD with no query string
map "$request_method:$args" $frozen {
    default "/nowhere";
    "GET:"  "/cooler";
    "HEAD:" "/cooler";
}

server {
    server_name example.com;
    root /var/www/example/web;
    index index.php;

    # file, frozen page, then trail
    location / {
        try_files $uri $frozen$uri/index.html /index.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

caddy

Caddy is cooler it can read the files from the default location. Nginx is more work.

caddyfile

example.com {
    root * /var/www/example/web

    # frozen page for GET/HEAD with no query string
    @frozen {
        method GET HEAD
        expression {query} == ""
        file {
            root /var/www/example/trail/storage/cache/cooler
            try_files {path}/index.html
        }
    }
    handle @frozen {
        root * /var/www/example/trail/storage/cache/cooler
        rewrite * {path}/index.html
        file_server
    }

    # everything else is trail
    php_fastcgi unix//run/php/php8.4-fpm.sock
    file_server
}

expires and cookies only work when trail is serving cache (cause php checks it). But you can run freeze::clear on a cron to replace expirations, and probably some config tweaking to handle cookie stuff