Layup generates CSS classes dynamically based on page content (column widths, user-added classes, theme utilities). Tailwind's purge process cannot detect these classes in static analysis, so Layup generates a safelist file.

## How it works

The `SafelistCollector` gathers classes from three sources:

1. **Static classes** -- predictable utility classes for column widths, gaps, and flex properties (loaded from `resources/css/safelist-classes.txt`)
2. **Dynamic classes** -- user-added CSS classes from the Advanced tab of any widget, row, or column
3. **Extra classes** -- additional classes defined in `config('layup.safelist.extra_classes')`

All published pages are scanned. The result is written to a text file (one class per line).

## Configuration

```php
'safelist' => [
    'enabled' => true,
    'auto_sync' => true,
    'path' => 'storage/layup-safelist.txt',
    'extra_classes' => [],
],
```

- **auto_sync** -- when `true`, the safelist regenerates on every page save and delete
- **path** -- where the safelist file is written (relative to project root)
- **extra_classes** -- classes to always include, regardless of page content

## Generating manually

```bash
php artisan layup:safelist
```

## Integrating with Tailwind

### Tailwind v4

Add to your CSS file:

```css
@source "../../storage/layup-safelist.txt";
```

### Tailwind v3

Add to `tailwind.config.js`:

```js
module.exports = {
    content: [
        './resources/views/**/*.blade.php',
        './storage/layup-safelist.txt',
    ],
};
```

## The SafelistChanged event

When `SafelistCollector::sync()` detects changes in the class list, it dispatches the `Crumbls\Layup\Events\SafelistChanged` event. The event exposes:

- `$added` -- classes newly added to the safelist
- `$removed` -- classes no longer present
- `$path` -- absolute path to the safelist file

Listen for this when you need to rebuild assets or invalidate CDN caches in response to content changes.

### Trigger a CI rebuild

```php
namespace App\Listeners;

use Crumbls\Layup\Events\SafelistChanged;
use Illuminate\Support\Facades\Http;

class RebuildFrontendAssets
{
    public function handle(SafelistChanged $event): void
    {
        if ($event->added === [] && $event->removed === []) {
            return;
        }

        Http::post(config('services.ci.deploy_hook'), [
            'reason' => 'Layup safelist changed',
            'added' => count($event->added),
            'removed' => count($event->removed),
        ]);
    }
}
```

Register the listener in your `EventServiceProvider`:

```php
protected $listen = [
    \Crumbls\Layup\Events\SafelistChanged::class => [
        \App\Listeners\RebuildFrontendAssets::class,
    ],
];
```

### Invalidate a CDN cache

```php
public function handle(SafelistChanged $event): void
{
    cache()->forget('frontend-css-version');

    // Or post to your CDN's purge API:
    Http::withToken(config('services.cloudflare.token'))
        ->post('https://api.cloudflare.com/client/v4/zones/.../purge_cache', [
            'files' => [config('app.url') . '/build/assets/app.css'],
        ]);
}
```

The event fires from a synchronous code path -- if your handler does I/O, dispatch a job:

```php
public function handle(SafelistChanged $event): void
{
    \App\Jobs\PurgeAssetCdnCache::dispatch($event->added, $event->removed);
}
```

## Using SafelistCollector directly

```php
use Crumbls\Layup\Support\SafelistCollector;

// All classes from all published pages
$classes = SafelistCollector::classes();

// Classes from specific pages
$classes = SafelistCollector::classesForPages($pages);

// Classes from a raw content array
$classes = SafelistCollector::classesFromContent($page->content);

// Static plugin classes only
$classes = SafelistCollector::staticClasses();

// Sync and write the safelist file
SafelistCollector::sync();
```

---

## Documentation

- [Introduction](https://documentation.crumbls.com/layup/-/index.md)
- [Introduction](https://documentation.crumbls.com/layup/-/introduction.md)
- [v1](https://documentation.crumbls.com/layup/-/_index.md)
- [Installation](https://documentation.crumbls.com/layup/-/installation.md)
- [Contributing to Layup](https://documentation.crumbls.com/layup/-/CONTRIBUTING.md)
- [Usage](https://documentation.crumbls.com/layup/-/usage.md)
- [Configuration](https://documentation.crumbls.com/layup/-/configuration.md)
- [Examples](https://documentation.crumbls.com/layup/-/examples.md)
- [Grid System](https://documentation.crumbls.com/layup/-/grid-system.md)
- [Field-only Installation](https://documentation.crumbls.com/layup/-/field-only-installation.md)
- [Embedding the Field](https://documentation.crumbls.com/layup/-/embedding-the-field.md)
- [Widgets](https://documentation.crumbls.com/layup/-/widgets/_index.md)
- [Getting Started](https://documentation.crumbls.com/layup/-/getting-started/_index.md)
- [Customization](https://documentation.crumbls.com/layup/-/customization/_index.md)
- [API Reference](https://documentation.crumbls.com/layup/-/api-reference/_index.md)
- [Upgrade guide](https://documentation.crumbls.com/layup/-/upgrade-guide.md)
- [Troubleshooting](https://documentation.crumbls.com/layup/-/troubleshooting.md)
- [Changelog](https://documentation.crumbls.com/layup/-/CHANGELOG.md)
- [🔐 Security Policy](https://documentation.crumbls.com/layup/-/SECURITY.md)
- [The MIT License (MIT)](https://documentation.crumbls.com/layup/-/LICENSE.md)
- [Layup Widget Development Guide for AI Agents](https://documentation.crumbls.com/layup/-/AGENTS.md)
- [Layup — Sprint TODO (overnight Feb 23-24)](https://documentation.crumbls.com/layup/-/TODO.md)
- [Vision](https://documentation.crumbls.com/layup/-/VISION.md)
- [Content Widgets](https://documentation.crumbls.com/layup/-/widgets/content.md)
- [Media Widgets](https://documentation.crumbls.com/layup/-/widgets/media.md)
- [Interactive Widgets](https://documentation.crumbls.com/layup/-/widgets/interactive.md)
- [Layout Widgets](https://documentation.crumbls.com/layup/-/widgets/layout.md)
- [Advanced Widgets](https://documentation.crumbls.com/layup/-/widgets/advanced.md)
- [Creating Pages](https://documentation.crumbls.com/layup/-/getting-started/creating-pages.md)
- [Adding Widgets](https://documentation.crumbls.com/layup/-/getting-started/adding-widgets.md)
- [Saving and Publishing](https://documentation.crumbls.com/layup/-/getting-started/publishing.md)
- [Disable the Pages Resource](https://documentation.crumbls.com/layup/-/customization/disable-pages-resource.md)
- [Rendering Content](https://documentation.crumbls.com/layup/-/customization/rendering-content.md)
- [Frontend Rendering](https://documentation.crumbls.com/layup/-/customization/frontend-rendering.md)
- [Creating Custom Widgets](https://documentation.crumbls.com/layup/-/customization/custom-widgets.md)
- [Extending Existing Widgets](https://documentation.crumbls.com/layup/-/customization/extending-widgets.md)
- [Livewire-rendered widgets](https://documentation.crumbls.com/layup/-/customization/livewire-widgets.md)
- [Filament Plugin API](https://documentation.crumbls.com/layup/-/customization/filament-plugin-api.md)
- [Theme System](https://documentation.crumbls.com/layup/-/customization/theme-system.md)
- [SEO Meta](https://documentation.crumbls.com/layup/-/customization/seo-meta.md)
- [Page Templates](https://documentation.crumbls.com/layup/-/customization/page-templates.md)
- [Revision History](https://documentation.crumbls.com/layup/-/customization/revision-history.md)
- [Swapping the Page Model](https://documentation.crumbls.com/layup/-/customization/swapping-the-page-model.md)
- [Testing](https://documentation.crumbls.com/layup/-/customization/testing.md)
- [Widget Contract](https://documentation.crumbls.com/layup/-/api-reference/widget-contract.md)
- [Models](https://documentation.crumbls.com/layup/-/api-reference/models.md)
- [Service Provider](https://documentation.crumbls.com/layup/-/api-reference/service-provider.md)
- [Support Classes](https://documentation.crumbls.com/layup/-/api-reference/support-classes.md)
- [Commands](https://documentation.crumbls.com/layup/-/api-reference/commands.md)
