Skip to content

Seeding & Blueprints

When a tenant signs up — an event, a place, a store — their site shouldn't start as an empty editor. Blueprints let you define starter content in code and seed it automatically, so every new tenant lands on a real, working site.

There are two layers, and they compose:

Page blueprintSite blueprint
DefinesOne page's starting sectionsA whole starter site: theme, pages, regions
ExtendsAbstractBlueprintSiteBlueprint
Typical useAuto-seeding pages into sitesProvisioning a complete live site per tenant
Entry pointRegistered on the pluginYourStarter::provision($tenant)

Page blueprints

A page blueprint describes one template: its name, a stable slug, and the sections it starts with. Scaffold one:

bash
php artisan make:filamentcraft-blueprint EventHome
php
namespace App\Blueprints;

use FilamentCraft\Blueprints\AbstractBlueprint;
use FilamentCraft\Blueprints\BlueprintSection;
use FilamentCraft\Sections\Builtin\FaqSection;
use FilamentCraft\Sections\Builtin\HeroSection;

final class EventHomeBlueprint extends AbstractBlueprint
{
    public function name(): string
    {
        return 'Home';
    }

    public function slug(): string
    {
        return 'event-home';
    }

    public function sections(): array
    {
        return [
            BlueprintSection::make(HeroSection::class)
                ->settings(['heading' => 'Welcome to the event']),

            BlueprintSection::make(FaqSection::class),
        ];
    }

    public function isHomepage(): bool
    {
        return true;
    }
}

BlueprintSection::make() accepts any section class — built-in or your own. Settings you pass are merged over the section's defaults, so you only specify what differs. More knobs:

  • ->lock() — the section can't be removed or reordered in the editor.
  • ->hide() — seeded hidden; the user can reveal it when ready.
  • ->blocks([...]) — replace the section's default repeated items (block arrays) wholesale; unlike ->settings(), blocks are not merged — pass none to keep the defaults.

To start from one of the section's own presets instead of its defaults, use fromPreset() — the preset's settings and blocks become the overrides:

php
BlueprintSection::fromPreset(HeroSection::class, 'editorial')

The slug is the preset key (the four style families share modern / editorial / bold / elegant). When the section has no preset for the slug, it falls back to plain defaults.

And on the blueprint class itself:

  • isHomepage() — the seeded template claims the site's homepage slot (first one wins).
  • sealed() — hides the "Add section" button for pages seeded from this blueprint, giving tenants a fill-in-the-blanks page instead of a free-form one.
  • type() — defaults to TemplateType::Page.

Registering & auto-seeding

Register page blueprints on the plugin:

php
FilamentCraftPlugin::make()
    ->registerBlueprint(EventHomeBlueprint::class)
    // or discover a whole directory:
    ->discoverBlueprintsIn(app_path('Blueprints'))

discoverBlueprintsIn() scans the top level of the directory only — blueprints in subdirectories must be registered explicitly or given their own discoverBlueprintsIn() call.

By default (filamentcraft.blueprints.auto_seed), every registered blueprint is seeded into each newly created Site as a draft template — the SiteCreated event listener handles it. Each template remembers its blueprint_slug, so re-running never duplicates.

For existing sites, seed from the console:

bash
php artisan filamentcraft:seed-blueprints            # every site
php artisan filamentcraft:seed-blueprints --site=3   # one site
php artisan filamentcraft:seed-blueprints --owner=7  # all sites of an owner
php artisan filamentcraft:seed-blueprints --force    # re-seed even if the templates exist
php artisan filamentcraft:seed-blueprints --dry-run  # preview

Site blueprints — starter presets per tenant

A site blueprint is the full preset: which pages, which theme, what header and footer content, and whether the site goes live immediately. Define one per tenant kind — an event starter, a place starter — and provision it when the tenant is created.

bash
php artisan make:filamentcraft-blueprint EventStarter --site
php
namespace App\Blueprints;

use FilamentCraft\Blueprints\BlueprintRegion;
use FilamentCraft\Blueprints\BlueprintSection;
use FilamentCraft\Blueprints\SiteBlueprint;
use FilamentCraft\Enums\RegionName;
use FilamentCraft\Sections\Builtin\FooterSection;
use FilamentCraft\Sections\Builtin\HeaderSection;

final class EventStarterSiteBlueprint extends SiteBlueprint
{
    public function name(): string
    {
        return 'Event starter';
    }

    public function pages(): array
    {
        return [
            EventHomeBlueprint::class,
            // ...any other page blueprints, built like EventHomeBlueprint above
        ];
    }

    public function regions(): array
    {
        return [
            BlueprintRegion::make(RegionName::Header)->sections([
                BlueprintSection::make(HeaderSection::class),
            ]),
            BlueprintRegion::make(RegionName::Footer)->sections([
                BlueprintSection::make(FooterSection::class),
            ]),
        ];
    }
}

Everything else has sensible defaults you can override:

  • themeSlug() — theme for the new site. null (the default) picks the theme flagged as default, falling back to the first theme when none is flagged; when no Theme rows exist at all, provisioning throws InvalidBlueprintException — run php artisan filamentcraft:sync-themes first.
  • locales() — defaults to ['en']; the first entry becomes the site's default locale.
  • publish() — defaults to true: pages are seeded published and the site goes live immediately. Return false to land everything as drafts for review.
  • siteSettings() — initial global site settings (sites.settings_json).

Provisioning

One line creates the whole thing:

php
$site = EventStarterSiteBlueprint::provision($event);

In a single database transaction this creates the Site (morph-attached to the owner you pass), resolves the theme, seeds every page with a first revision, marks the homepage (isHomepage() page, or the first page as a fallback), writes the region content, and — when publish() is true — publishes every page and flips the site live. The tenant can open the editor or their public URL immediately.

The site name defaults to the owner's name attribute (falling back to the blueprint name), and the slug is derived and de-duplicated automatically. Override either:

php
EventStarterSiteBlueprint::provision($event, name: 'My Conference', slug: 'my-conf');

The owner is optional — provision() with no arguments creates an unowned site, which is exactly what a single-site host wants:

php
EventStarterSiteBlueprint::provision();

You can also resolve the service yourself — useful when picking a preset dynamically:

php
use FilamentCraft\Blueprints\SiteProvisioner;

$blueprint = $tenant->kind === 'place'
    ? PlaceStarterSiteBlueprint::class
    : EventStarterSiteBlueprint::class;

app(SiteProvisioner::class)->provision($blueprint, $tenant);

The tenant-onboarding recipe

Hook provisioning into your tenant lifecycle with a model observer:

php
namespace App\Observers;

use App\Blueprints\EventStarterSiteBlueprint;
use App\Models\Event;

final class EventObserver
{
    public function created(Event $event): void
    {
        EventStarterSiteBlueprint::provision($event);
    }
}

Register the observer on the model:

php
use App\Observers\EventObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;

#[ObservedBy(EventObserver::class)]
class Event extends Model
{
    // ...
}

That's the entire integration — every new tenant gets a complete, live site the moment their row exists.

Themes must exist before the first provision

On a fresh install, provisioning throws InvalidBlueprintException"No themes exist to assign to the provisioned site. Run php artisan filamentcraft:sync-themes first." Make sure php artisan filamentcraft:sync-themes (part of filamentcraft:install) has run in every environment before tenants start signing up.

Provisioning suspends auto-seeding

While provision() runs, the global auto-seed listener is suspended — the new site gets exactly the pages the site blueprint lists, not every registered blueprint. Pages listed in a site blueprint don't need to be registered on the plugin. The exception is sealed() pages: the editor looks blueprints up by slug to honour sealed(), so register those with registerBlueprint() too (and set filamentcraft.blueprints.auto_seed to false if you only ever provision through site blueprints).

Starter sites in one of four style families

You don't have to hand-author a blueprint at all. FilamentCraft ships a ready-made starter that provisions a complete, conversion-ordered homepage in one of four cohesive style families — pick one and the whole site lands on-brand.

FamilyForColor schemeFeel
modernModern SaaSSoftware, apps, SaaSmodern (teal)Clean, confident, product-led
editorialEditorial StudioStudios, agencies, portfoliossilk (warm)Crafted, considered, human
boldBold StartupLaunches, fast-moving startupsvelocity (indigo)High-energy, punchy
elegantElegant PremiumPremium & boutique servicesneutral (slate)Quiet, refined, restrained

The homepage is a seven-section funnel:

Hero → Logo cloud → Features → Stats → Testimonials → Pricing → Call to action

…wrapped by a matching header and footer region.

Seed one when a tenant is created

The whole thing is one line — drop it in your tenant model's created observer:

php
use FilamentCraft\Blueprints\StarterFamily;
use FilamentCraft\Blueprints\StarterSiteBlueprint;

final class TeamObserver
{
    public function created(Team $team): void
    {
        StarterSiteBlueprint::seed(StarterFamily::Modern, $team);
    }
}

seed() provisions a live, published site morph-attached to the tenant — the same single transaction the other site blueprints use (theme, pages, regions, homepage pointer, publish state). Register the observer with #[ObservedBy(TeamObserver::class)] on Team, exactly as in the tenant-onboarding recipe.

Let the tenant pick their own look at sign-up and pass the choice straight through:

php
StarterSiteBlueprint::seed(StarterFamily::fromValue($request->input('style')), $team);

Use fromValue() (not the native StarterFamily::from(), which throws ValueError on tampered input) — it falls back to the default family when the value is missing or unrecognised.

StarterFamily implements Filament's label/color/icon contracts and ships an ::options() helper, so it drops straight into a Select on your onboarding form:

php
Select::make('style')
    ->options(StarterFamily::options())
    ->default(StarterFamily::default()->value);

Seed from the console or a database seeder

For existing tenants, or a quick local site, use the command:

bash
php artisan filamentcraft:starter                       # default family (config)
php artisan filamentcraft:starter --family=bold          # a specific family
php artisan filamentcraft:starter --family=editorial --owner=7   # attach to a tenant
php artisan filamentcraft:starter --name="My Conference"          # explicit site name

Or run the bundled, idempotent seeder from your DatabaseSeeder — re-running is a no-op once a starter home exists:

php
$this->call(\FilamentCraft\Database\Seeders\StarterSiteSeeder::class);

The default family is configurable:

php
// config/filamentcraft.php
'starter' => [
    'family' => env('FILAMENTCRAFT_STARTER_FAMILY', 'modern'),
],

The same families power the editor

Picking a family at provision time and picking a section preset in the editor are the same thing under the hood. When a user opens the preset picker on any section, the four choices they see are these four families — so a modern site stays modern even as they add new sections.

Why class-strings instead of enums?

Sections are an open set (your app adds classes the package can't enumerate), so they're referenced by class-string — with IDE autocompletion, refactor-safe renames, and existence validation at build time — while closed sets use real enums (RegionName, TemplateType, SiteStatus).

Next steps