Back to all articles
Delivery Playbooks Sep 25, 2025 1 min read

The Filament content ops stack we hand over to marketing teams

Reusable panels, approval queues, and asset automation in under a sprint.

Filament admin panels arranged like a content operations workflow.

The Filament Content Ops Stack We Hand Over to Marketing Teams

Reusable panels, approval queues, and asset automation in under a sprint.

Handing over a new CMS to a marketing team can be a tense moment. As developers, we build powerful, flexible systems. But power and flexibility can translate to complexity and confusion for the non-technical users who will manage the content day-to-day. The ideal handoff is one where marketers feel empowered, not overwhelmed.

This is where Filament truly shines. We've refined a content operations stack built with Filament that not only meets complex business requirements but is also a joy for marketing teams to use. It’s a system designed for efficiency, clarity, and control, enabling us to deliver a complete, production-ready content management solution in under a single development sprint.

This guide will walk you through the exact components we use: reusable content panels, custom approval queues, and powerful asset automation. We’ll show you how to build a system that marketing teams will love, complete with code examples and visuals to guide you.

The Challenge: Bridging the Dev-Marketing Gap

Traditional content management systems often create a divide. Developers build rigid templates, and marketers are forced to work within those constraints. Need a new landing page layout? That’s a new development ticket. Want to A/B test a headline? That might require code changes.

This friction slows down marketing campaigns and pulls development resources away from core product features. We needed a solution that would give marketers the freedom to create and iterate, while developers maintain control over the system's architecture and integrity.

Our goal was to create a content ops stack that delivers:

  • Modularity: Allowing marketers to build diverse page layouts from a library of pre-built, on-brand components.
  • Governance: Implementing a clear approval process to ensure content quality and consistency before it goes live.
  • Automation: Streamlining repetitive tasks like image optimization and metadata generation to save time.

Filament provides the tools to build this stack elegantly and efficiently. Let's dive into how we do it.

1. Reusable Panels with the Repeater and Builder Fields

The cornerstone of our content ops stack is the combination of Filament's Repeater and Builder form fields. This duo allows marketers to construct complex page layouts by adding, reordering, and customizing "blocks" of content without ever writing a line of code.

Think of it like playing with Lego bricks. We provide a box of approved, beautifully styled bricks (content blocks), and the marketing team can assemble them in countless ways to build exactly what they need.

For example, a typical landing page might be composed of a hero section, a features grid, a testimonial slider, and a call-to-action banner. Instead of a single, monolithic form, we create a Builder field that lets the user choose which blocks to add.


The Developer's Side: Building the Blocks

From a development perspective, setting this up is straightforward. First, you define a Builder field in your Filament resource. Each block within the builder is its own set of fields.

Here’s a simplified example of how we might define a content attribute on a Page model.

// In app/Filament/Resources/PageResource.php

use Filament\Forms\Components\Builder;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;

// ... inside the form method

public static function form(Form $form): Form
{
    return $form
        ->schema([
            // ... other fields like title, slug
            
            Builder::make('content')
                ->blocks([
                    Builder\Block::make('hero_section')
                        ->schema([
                            TextInput::make('heading')->required(),
                            Textarea::make('subheading'),
                            FileUpload::make('background_image')->image(),
                        ]),
                        
                    Builder\Block::make('feature_list')
                        ->schema([
                            TextInput::make('title')->default('Features'),
                            Repeater::make('features')
                                ->schema([
                                    TextInput::make('name')->required(),
                                    Textarea::make('description'),
                                ])
                                ->columns(2),
                        ]),
                        
                    Builder\Block::make('call_to_action')
                        ->schema([
                            TextInput::make('cta_text')->required(),
                            TextInput::make('button_url')->url()->required(),
                        ]),
                ])
                ->columnSpanFull(),
        ]);
}

In this code:

  • The Builder field is named content. This will store all our page blocks as a JSON array in the database.
  • We define three Block types: hero_section, feature_list, and call_to_action.
  • Each block has its own schema of fields. Notice the feature_list block even contains a Repeater field, allowing marketers to add an unlimited number of features within that block.

This modular approach empowers marketers to create dynamic, unique layouts for campaigns, landing pages, and blog posts, all while staying within the design and brand guidelines you've established.

2. Approval Queues for Quality Control

Giving marketers creative freedom is great, but content governance is crucial for maintaining brand voice and quality. A simple "Draft" and "Published" status is often not enough. You need a workflow.

We implement a simple yet effective approval queue using a status field and Filament's table actions and filters. This creates a clear process where a content creator can submit work for review, and an editor can approve or reject it.


The Developer's Side: Implementing a Status Workflow

First, add a status column to your model's migration. We typically use a string or enum.

// In your migration file
$table->string('status')->default('draft');

Next, in your Filament resource, define the selectable statuses and create tabs to filter the table view.

// In app/Filament/Resources/PostResource.php

use Filament\Resources\Components\Tab;
use Filament\Forms\Components\Select;
use Filament\Tables\Actions\Action;

// ... inside the form method
Select::make('status')
    ->options([
        'draft' => 'Draft',
        'in_review' => 'In Review',
        'published' => 'Published',
        'rejected' => 'Rejected',
    ])
    ->required(),

// ... inside the table method, add custom tabs
public function getTabs(): array
{
    return [
        'all' => Tab::make(),
        'draft' => Tab::make()->query(fn ($query) => $query->where('status', 'draft')),
        'in_review' => Tab::make()->query(fn ($query) => $query->where('status', 'in_review')),
        'published' => Tab::make()->query(fn ($query) => $query->where('status', 'published')),
    ];
}

The final piece is creating custom actions for the approval workflow. We can add "Submit for Review" and "Approve" buttons that only appear for records with the appropriate status.

// In app/Filament/Resources/PostResource.php -> table method

->actions([
    Tables\Actions\EditAction::make(),
    
    Action::make('submit_for_review')
        ->action(fn (Post $record) => $record->update(['status' => 'in_review']))
        ->requiresConfirmation()
        ->color('warning')
        ->icon('heroicon-o-arrow-up-on-square')
        ->visible(fn (Post $record) => $record->status === 'draft'),
        
    Action::make('approve')
        ->action(fn (Post $record) => $record->update(['status' => 'published']))
        ->requiresConfirmation()
        ->color('success')
        ->icon('heroicon-o-check-circle')
        ->visible(fn (Post $record) => $record->status === 'in_review'),
])

This setup provides a clear, actionable workflow directly within the Filament admin panel. Marketers know exactly what stage their content is in, and editors have a dedicated queue of items to review. You can further enhance this with user roles and permissions, ensuring only authorized editors can approve content.

3. Asset Automation to Eliminate Tedium

Marketing teams handle a lot of media. Images, videos, and documents all need to be uploaded, optimized, and tagged. Manually performing these tasks is time-consuming and prone to error. We use Laravel's event system, combined with packages like Spatie's laravel-medialibrary, to automate these processes.

Our automation stack typically handles:

  • Image Optimization: Automatically compressing and resizing images on upload to ensure fast page loads.
  • Alt Text Generation: Using AI services to suggest alt text for images, improving accessibility and SEO.
  • Metadata Extraction: Pulling EXIF data from images or metadata from documents.


The Developer's Side: Using Observers for Automation

Laravel's model observers are perfect for triggering these automated tasks. When a model with an asset is created or updated, we can fire off jobs to handle the processing in the background.

Let's say we want to automatically generate a WebP version and set alt text for a Post model's featured image. We'll use spatie/laravel-medialibrary for this.

First, set up the media library on your model. Then, create an observer.

php artisan make:observer PostObserver --model=Post

Now, in the PostObserver, we can hook into the created and updated events.

// In app/Observers/PostObserver.php

namespace App\Observers;

use App\Models\Post;
use Spatie\Image\Image;

class PostObserver
{
    public function saved(Post $post): void
    {
        if ($post->hasMedia('featured_image')) {
            $mediaItem = $post->getFirstMedia('featured_image');
            
            // Generate a WebP conversion
            if (!$mediaItem->hasGeneratedConversion('webp')) {
                $mediaItem->addGeneratedConversion('webp', fn (Image $image) => $image->format('webp'));
            }
            
            // If alt text is empty, generate it (pseudo-code)
            if (empty($mediaItem->getCustomProperty('alt'))) {
                //dispatch(new GenerateAltTextJob($mediaItem));
            }
            
            $mediaItem->save();
        }
    }
}

This is a simplified example, but it demonstrates the power of this approach. By moving these tasks into background jobs triggered by observers, the user experience for marketers remains fast and fluid. They upload an image, and Filament takes care of the rest.

A Content Stack That Empowers Everyone

By combining reusable Builder blocks, a clear approval workflow, and automated asset handling, you can deliver a content operations stack that bridges the gap between development and marketing. Marketers gain the freedom to create and manage content efficiently, while developers maintain a robust, scalable, and maintainable system.

Filament provides the perfect foundation for this stack. Its flexibility and developer-friendly API make it possible to build these sophisticated features in a fraction of the time it would take with other tools. The result is a faster time-to-market, a happier marketing team, and more time for developers to focus on what they do best: building great products.


Related articles

Continue exploring Laravel insights and practical delivery strategies.

Laravel consulting

Need senior Laravel help for this topic?

Let's adapt these practices to your product and deliver the next milestone.