Getting Started

Frontend Setup (headless)

Build your own blog frontend using the package's Blade components.
Want a working blog without writing controllers? See Public-routes mode — flip a config flag and you're done. This page covers the headless mode for hosts who want full control.

In headless mode the package ships no routes, no controllers, no page views. You wire your own routing and use the provided Blade components.

Create routes

routes/web.php
use App\Http\Controllers\BlogController;

Route::prefix('blog')->name('blog.')->group(function () {
    Route::get('/', [BlogController::class, 'index'])->name('index');
    Route::get('/feed', [BlogController::class, 'feed'])->name('feed');
    Route::get('/category/{slug}', [BlogController::class, 'category'])->name('category');
    Route::get('/tag/{slug}', [BlogController::class, 'tag'])->name('tag');
    Route::get('/preview/{post}', [BlogController::class, 'preview'])
        ->name('preview')->middleware('signed');
    Route::get('/{slug}', [BlogController::class, 'show'])->name('show');
});

The route names matter — the package's URL helpers and SEO components check for them via Route::has(...) and fall back gracefully when missing.

Create controller

app/Http/Controllers/BlogController.php
use ManukMinasyan\FilamentBlog\Models\Category;
use ManukMinasyan\FilamentBlog\Models\Post;
use ManukMinasyan\FilamentBlog\Models\Tag;

final readonly class BlogController
{
    public function index(): View
    {
        $posts = Post::query()
            ->published()
            ->with(['category', 'author', 'seo'])
            ->latest('published_at')
            ->paginate(config('filament-blog.per_page', 12));

        return view('blog.index', compact('posts'));
    }

    public function show(string $slug): View
    {
        $post = Post::query()
            ->published()
            ->with(['category', 'author', 'seo'])
            ->where('slug', $slug)
            ->firstOrFail();

        $relatedPosts = $post->relatedPosts()->get();

        return view('blog.show', compact('post', 'relatedPosts'));
    }

    public function category(string $slug): View
    {
        $category = Category::where('slug', $slug)->firstOrFail();
        $posts = Post::query()
            ->where('category_id', $category->id)
            ->published()
            ->with(['category', 'author', 'seo'])
            ->latest('published_at')
            ->paginate(config('filament-blog.per_page', 12));

        return view('blog.category', compact('category', 'posts'));
    }

    public function tag(string $slug): View
    {
        $tag = Tag::where('slug', $slug)->firstOrFail();
        $posts = Post::query()
            ->whereHas('tags', fn ($q) => $q->where('blog_tags.id', $tag->id))
            ->published()
            ->with(['category', 'author', 'seo'])
            ->latest('published_at')
            ->paginate(config('filament-blog.per_page', 12));

        return view('blog.tag', compact('tag', 'posts'));
    }

    public function preview(Post $post): View
    {
        return view('blog.preview', ['post' => $post->loadMissing(['category', 'author', 'seo'])]);
    }

    public function feed(): Response
    {
        $posts = Post::query()
            ->published()
            ->with(['category', 'author'])
            ->latest('published_at')
            ->limit(20)
            ->get();

        return response()
            ->view('blog.feed', compact('posts'))
            ->header('Content-Type', 'application/rss+xml; charset=UTF-8');
    }
}

Create views

Use the package's Blade components inside your own page templates:

resources/views/blog/show.blade.php
<x-your-layout>
    @push('head')
        <x-blog::meta-tags :post="$post" />
        <x-blog::feed-link />
    @endpush

    <x-blog::structured-data :post="$post" />

    <x-blog::post-header :post="$post" />
    <x-blog::post-body :post="$post" />
    <x-blog::related-posts :posts="$relatedPosts" />
</x-your-layout>
resources/views/blog/index.blade.php
<x-your-layout>
    @foreach($posts as $post)
        <x-blog::post-card :post="$post" />
    @endforeach

    {{ $posts->links() }}
</x-your-layout>
resources/views/blog/feed.blade.php
<x-blog::feed :posts="$posts" />

Helpers on the Post model

Useful in your views:

$post->readingTime();       // int — minutes
$post->relatedPosts(3);     // Builder of same-category, published, !=$this->id
$post->getUrl();            // route('blog.show', $post->slug) if registered, else fallback

Expected route names

The package checks for these names when generating URLs. If a route is missing, the helper returns #:

NamePurpose
blog.indexListing page
blog.showSingle post (slug)
blog.categoryCategory archive (slug)
blog.tagTag archive (slug)
blog.previewSigned draft preview (post)
blog.feedRSS feed

Promote to public-routes mode any time

If writing all this gets old, flip the flag in config and delete your controller — the package will register equivalent routes automatically. See Public-routes mode.