L'art du contenu - Artisanal content blocks for FilamentPHP v4

Maintainers

Package info

github.com/Blackpig/atelier

Homepage

pkg:composer/blackpig-creatif/atelier

Statistics

Installs: 12

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.2 2026-02-25 14:14 UTC

This package is auto-updated.

Last update: 2026-02-25 14:16:24 UTC


README

Latest Version on Packagist Total Downloads

A polymorphic content block builder for FilamentPHP v5 with first-class translation support, EAV attribute storage, and Schema.org structured data generation.

Atelier stores block data as polymorphic EAV (Entity-Attribute-Value) rows, keeping your application schema lean while supporting arbitrary block structures. Translatable fields are stored per-locale, and the schema is scanned at save time to determine translatability automatically.

Requirements

  • PHP 8.2+
  • Laravel 11+
  • FilamentPHP 5.0+

Installation

composer require blackpig-creatif/atelier

Atelier automatically pulls in its companion packages:

Publish and run migrations:

php artisan vendor:publish --tag="atelier-migrations"
php artisan migrate

Publish the config:

php artisan vendor:publish --tag="atelier-config"

Register the Filament plugin in your PanelProvider:

use BlackpigCreatif\Atelier\AtelierPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugins([
            AtelierPlugin::make(),
        ]);
}

Optional Publishables

# Block Blade templates (recommended -- customise frontend rendering)
php artisan vendor:publish --tag="atelier-block-templates"

# Divider SVG components
php artisan vendor:publish --tag="atelier-dividers"

Quick Start

1. Add the trait to your model

use BlackpigCreatif\Atelier\Concerns\HasAtelierBlocks;

class Page extends Model
{
    use HasAtelierBlocks;
}

This gives you:

$page->blocks;            // MorphMany -- all blocks, ordered
$page->publishedBlocks;   // MorphMany -- only is_published = true
$page->renderBlocks();    // string -- rendered HTML of all published blocks
$page->renderBlocks('fr');

2. Add BlockManager to your Filament resource

use BlackpigCreatif\Atelier\Forms\Components\BlockManager;
use BlackpigCreatif\Atelier\Collections\BasicBlocks;

public static function form(Form $form): Form
{
    return $form->schema([
        BlockManager::make('blocks')
            ->blocks(BasicBlocks::class)
            ->collapsible()
            ->reorderable(),
    ]);
}

3. Render blocks in your view

{{-- Blade directive --}}
@renderBlocks($page)

{{-- Or manually --}}
@foreach($page->publishedBlocks as $block)
    {!! $block->render() !!}
@endforeach

{{-- With explicit locale --}}
@foreach($page->publishedBlocks as $block)
    {!! $block->render('fr') !!}
@endforeach

Built-in Blocks

Block Description Schema contribution
HeroBlock Full-width hero with background image, headline, CTAs None
TextBlock Rich text with optional title, subtitle, column layout Article body text
TextWithImageBlock Text + single image, configurable position Article body text + image URL
TextWithTwoImagesBlock Rich text + two images, multiple layout modes Article body text + image URLs
ImageBlock Single image with caption, aspect ratio, lightbox None
VideoBlock YouTube/Vimeo/direct URL embed with auto-detection VideoObject schema
GalleryBlock Grid gallery with configurable columns and lightbox Image URLs for Article
CarouselBlock Image carousel with navigation and autoplay Image URLs for Article
FaqsBlock Accordion FAQ list FAQPage schema

Block Collections

Collections group blocks into reusable sets.

Built-in collections

Collection Blocks
BasicBlocks Hero, Text, TextWithImage
MediaBlocks Image, Video, Gallery, Carousel
AllBlocks All built-in blocks

Usage

// Single collection
->blocks(BasicBlocks::class)

// Multiple collections
->blocks([BasicBlocks::class, MediaBlocks::class])

// Mix collections with individual blocks
->blocks([BasicBlocks::class, CustomBlock::class])

// Closure for dynamic logic
->blocks(fn () => auth()->user()->isAdmin()
    ? AllBlocks::make()
    : BasicBlocks::make()
)

Creating a collection

php artisan atelier:make-collection Ecommerce

Or manually:

namespace App\BlackpigCreatif\Atelier\Collections;

use BlackpigCreatif\Atelier\Abstracts\BaseBlockCollection;
use BlackpigCreatif\Atelier\Blocks\HeroBlock;
use App\BlackpigCreatif\Atelier\Blocks\ProductBlock;

class EcommerceBlocks extends BaseBlockCollection
{
    public function getBlocks(): array
    {
        return [
            HeroBlock::class,
            ProductBlock::class,
        ];
    }

    public static function getLabel(): string
    {
        return 'E-commerce Blocks';
    }
}

Block Configuration

Atelier supports two layers of configuration: field configuration (tweak individual field properties) and schema modification (add, remove, or reorder fields structurally). Both can be applied globally or per-resource.

For the full reference with all helper methods and patterns, see docs/block-configuration.md.

Global Configuration

Register global defaults in a service provider:

namespace App\Providers;

use BlackpigCreatif\Atelier\Blocks\HeroBlock;
use BlackpigCreatif\Atelier\Blocks\TextBlock;
use BlackpigCreatif\Atelier\Support\BlockFieldConfig;
use Filament\Forms\Components\Toggle;
use Illuminate\Support\ServiceProvider;

class AtelierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Configure individual fields
        BlockFieldConfig::configure(TextBlock::class, [
            'subtitle' => ['visible' => false],
            'columns'  => ['options' => ['1' => '1 Column', '2' => '2 Columns']],
        ]);

        // Modify schema structure
        BlockFieldConfig::modifySchema(HeroBlock::class, function ($schema) {
            return BlockFieldConfig::removeFields($schema, ['text_color', 'overlay_opacity']);
        });

        // Add fields
        BlockFieldConfig::modifySchema(HeroBlock::class, function ($schema) {
            return [
                ...$schema,
                Toggle::make('featured')->label('Featured')->default(false),
            ];
        });
    }
}

Register it in bootstrap/providers.php.

Per-Resource Configuration

Override globals on a specific resource:

BlockManager::make('blocks')
    ->blocks([HeroBlock::class, TextBlock::class])

    // Field config (array form)
    ->configureBlock(HeroBlock::class, [
        'headline' => ['maxLength' => 60],
        'ctas'     => ['maxItems' => 5],
    ])

    // Schema modifier (closure form)
    ->configureBlock(HeroBlock::class, fn ($schema) =>
        BlockFieldConfig::removeFields($schema, 'subtitle')
    )

Configuration Priority

Block Default Schema
  -> Global Schema Modifiers
    -> Per-Resource Schema Modifiers
      -> Global Field Configs
        -> Per-Resource Field Configs (wins)

Schema modifiers shape the structure first; field configs tweak properties last. Per-resource always overrides global at the same level.

Fluent API (BlockConfigurator)

For a chainable alternative:

use BlackpigCreatif\Atelier\Support\BlockConfigurator;

BlockConfigurator::for(HeroBlock::class)
    ->hide('overlay_opacity', 'text_color')
    ->remove('height')
    ->configure('ctas', ['maxItems' => 2])
    ->insertAfter('headline', [
        TextInput::make('tagline')->maxLength(100),
    ])
    ->apply();

Creating Custom Blocks

Artisan generator

php artisan atelier:make-block Quote

Creates the block class and Blade template with the correct boilerplate.

Manual creation

namespace App\BlackpigCreatif\Atelier\Blocks;

use BlackpigCreatif\Atelier\Abstracts\BaseBlock;
use BlackpigCreatif\Atelier\Concerns\HasCommonOptions;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Illuminate\Contracts\View\View;

class QuoteBlock extends BaseBlock
{
    use HasCommonOptions;

    public static function getLabel(): string
    {
        return 'Quote';
    }

    public static function getIcon(): string
    {
        return 'heroicon-o-chat-bubble-left-right';
    }

    public static function getSchema(): array
    {
        return [
            static::getPublishedField(),

            Section::make('Content')
                ->schema([
                    Textarea::make('quote')
                        ->required()
                        ->rows(3)
                        ->translatable(),  // must be last in chain

                    TextInput::make('author')
                        ->required()
                        ->translatable(),  // must be last in chain
                ])
                ->collapsible(),

            ...static::getCommonOptionsSchema(),
        ];
    }

    public function render(): View
    {
        return view(static::getViewPath(), $this->getViewData());
    }
}

Key points:

  • Extend BaseBlock and implement getLabel(), getSchema(), render()
  • Use HasCommonOptions for background, spacing, width, and divider controls
  • Call ->translatable() as the last method in a field chain
  • Call static::getPublishedField() at the top of your schema for the publish toggle
  • Call ...static::getCommonOptionsSchema() at the end for display options

Block template

Templates live at resources/views/vendor/atelier/blocks/{block-identifier}.blade.php. See docs/block-templates.md for the full template guide.

@php
    $blockIdentifier = 'atelier-' . $block::getBlockIdentifier();
@endphp

<section class="{{ $blockIdentifier }} {{ $block->getWrapperClasses() }}"
         data-block-type="{{ $block::getBlockIdentifier() }}"
         data-block-id="{{ $block->blockId ?? '' }}">

    <div class="{{ $block->getContainerClasses() }}">
        @if($quote = $block->getTranslated('quote'))
            <blockquote class="text-2xl italic">
                <p>"{{ $quote }}"</p>
            </blockquote>
        @endif

        @if($author = $block->getTranslated('author'))
            <footer class="mt-4 font-semibold">-- {{ $author }}</footer>
        @endif
    </div>

    @if($block->getDividerComponent())
        <x-dynamic-component
            :component="$block->getDividerComponent()"
            :to-background="$block->getDividerToBackground()"
        />
    @endif
</section>

Translation

Atelier provides inline, per-field translation with a global locale switcher in the block modal.

Making fields translatable

Append ->translatable() as the last call in the field chain:

TextInput::make('headline')
    ->required()
    ->maxLength(255)
    ->translatable();

The macro clones the field for each configured locale, wrapping them in a group that responds to the global locale selector via Alpine.js.

Schema scanning

Atelier automatically detects translatable fields by scanning the block schema at save time. You do not need to maintain a getTranslatableFields() method. If you do define one, it is used as a performance optimisation on the frontend to avoid schema scanning on render.

Retrieving translated values

// In templates
$block->getTranslated('headline');         // current locale
$block->getTranslated('headline', 'fr');   // explicit locale

Configuration

In config/atelier.php:

'locales' => [
    'en' => 'English',
    'fr' => 'Francais',
],

'default_locale' => 'en',

Data is stored as one EAV row per locale per field: headline/en, headline/fr.

Call to Actions (CTAs)

The HasCallToActions trait adds a repeater-based CTA system to any block.

Adding CTAs

use BlackpigCreatif\Atelier\Concerns\HasCallToActions;

class HeroBlock extends BaseBlock
{
    use HasCallToActions;

    public static function getSchema(): array
    {
        return [
            // ... content fields

            Section::make('Call to Action')
                ->schema([
                    static::getCallToActionsField()
                        ->maxItems(3),
                ])
                ->collapsible(),
        ];
    }
}

Each CTA item includes: label (translatable), url, icon (Heroicon name), style (from config), new_tab toggle.

Rendering

@if($block->hasCallToActions())
    <div class="flex gap-4">
        @foreach($block->getCallToActions() as $index => $cta)
            <x-atelier::call-to-action
                :cta="$cta"
                :block="$block"
                :index="$index"
            />
        @endforeach
    </div>
@endif

Helper methods

$block->hasCallToActions(): bool
$block->getCallToActions(): array
$block->getCallToActionLabel($cta, ?string $locale): string
$block->getCallToActionStyleClass($cta): string
$block->getCallToActionTarget($cta): string   // '_blank' or '_self'
$block->isExternalUrl(string $url): bool

Button styles

Configured in config/atelier.php:

'features' => [
    'button_styles' => [
        'enabled' => true,
        'options' => [
            'primary'   => ['label' => 'Primary',   'class' => 'btn btn-primary'],
            'secondary' => ['label' => 'Secondary', 'class' => 'btn btn-secondary'],
            'alternate' => ['label' => 'Alternate', 'class' => 'btn btn-alternate'],
        ],
    ],
],

Display Options

All blocks using HasCommonOptions gain a collapsible "Display Options" section with:

Feature Description Config key
Background Predefined background colours (Tailwind classes + admin colour swatch) features.backgrounds
Spacing Balanced (equal py-) or individual (pt- / pb-) vertical padding features.spacing
Width Container, Narrow, Wide, or Full Width content constraint features.width
Dividers Decorative SVG dividers (wave, curve, diagonal, triangle) with colour transition to next section features.dividers
Published Toggle to show/hide on frontend (is_published column) --

In templates, use $block->getWrapperClasses() on the outer <section> (background + spacing) and $block->getContainerClasses() on the inner <div> (width constraint).

Media Handling

Atelier integrates with Chambre Noir for responsive images. Use RetouchMediaUpload in your schema and the HasRetouchMedia trait on your block:

use BlackpigCreatif\ChambreNoir\Concerns\HasRetouchMedia;
use BlackpigCreatif\ChambreNoir\Forms\Components\RetouchMediaUpload;
use BlackpigCreatif\Atelier\Conversions\BlockHeroConversion;

class HeroBlock extends BaseBlock
{
    use HasRetouchMedia;

    public static function getSchema(): array
    {
        return [
            RetouchMediaUpload::make('background_image')
                ->preset(BlockHeroConversion::class)
                ->imageEditor()
                ->maxFiles(1),
        ];
    }
}

In templates:

{!! $block->getPicture('background_image', [
    'alt'           => $block->getTranslated('headline'),
    'class'         => 'w-full h-full object-cover',
    'fetchpriority' => 'high',
]) !!}

Built-in conversion presets

Preset Conversions Use case
BlockHeroConversion thumb, medium, large, desktop, mobile + social (og, twitter) Hero sections, full-width banners
BlockGalleryConversion thumb, medium, large Galleries, content images, carousels

Extracting images from blocks

The HasAtelierMediaExtraction trait (add to your model) provides convenience methods:

use BlackpigCreatif\Atelier\Concerns\HasAtelierMediaExtraction;

class Page extends Model
{
    use HasAtelierBlocks, HasAtelierMediaExtraction;
}

$page->getHeroImageFromBlocks('large');
$page->getImageFromBlock('image', 'medium', ImageBlock::class);

SEO Schema Generation

Atelier exposes three PHP contracts that blocks implement to contribute to Schema.org output. The contracts are deliberately package-agnostic — Atelier has no dependency on Sceau or any other SEO library.

See docs/schema.md for the full reference.

Three schema contracts

BaseBlock implements all three contracts with no-op defaults. Override only what your block needs.

HasCompositeSchema — contributes content or media URLs to a composite schema (e.g. Article) assembled from multiple blocks:

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

public function getCompositeContribution(): array
{
    return [
        'type'    => 'text',
        'content' => strip_tags($this->getTranslated('content') ?? ''),
    ];
}

HasSchemaContribution — declares a typed schema that the active driver converts to a structured data array:

use BlackpigCreatif\Sceau\Enums\SchemaType;

public function getSchemaType(): ?SchemaType
{
    return ! empty($this->get('faqs')) ? SchemaType::FAQPage : null;
}

public function getSchemaData(): array
{
    return ['faqs' => $this->get('faqs', [])];
}

HasStandaloneSchema — legacy escape hatch for blocks that build the full schema array themselves. Prefer the driver pattern for new blocks.

Wiring a driver

The driver is resolved from the container via BlockSchemaDriverInterface. Configure Sceau's driver in your app config:

// config/atelier.php
'schema_driver' => \BlackpigCreatif\Sceau\Schema\Drivers\SceauBlockSchemaDriver::class,

When using Sceau, schema generation is fully automatic — the <x-sceau::head> component calls PageSchemaBuilder::build() for models that carry HasAtelierBlocks.

Configuration Reference

The config/atelier.php file:

return [
    'locales'        => ['en' => 'English', 'fr' => 'Francais'],
    'default_locale' => 'en',

    'modal' => ['width' => '5xl'],

    'table_prefix' => 'atelier_',

    'blocks' => [
        // Default blocks when ->blocks() is called without arguments
    ],

    'schema_driver' => null, // Set to a BlockSchemaDriverInterface class to enable schema generation

    'features' => [
        'backgrounds'   => ['enabled' => true, 'options' => [...]],
        'spacing'       => ['enabled' => true, 'options' => [...]],
        'width'         => ['enabled' => true, 'options' => [...]],
        'dividers'      => ['enabled' => true, 'options' => [...]],
        'button_styles' => ['enabled' => true, 'options' => [...]],
    ],

    'cache' => [
        'enabled' => true,
        'ttl'     => 3600,
        'prefix'  => 'atelier_block_',
    ],
];

Architecture

Atelier uses a polymorphic EAV storage model:

  • atelier_blocks -- polymorphic (blockable_type, blockable_id), stores block type, position, UUID, published status
  • atelier_block_attributes -- stores each field value as a row with key, value, type, locale, translatable, sort_order, collection_name, collection_index

Repeater fields (e.g. CTAs) are stored as collection-based EAV rows, grouped by collection_name and collection_index.

At hydration time, the AtelierBlock model reconstructs the block instance, fills its data array, and caches the result per locale.

Documentation

Testing

composer test

Changelog

See CHANGELOG.

Credits

License

MIT. See LICENSE.