relaticle/ink
Filament-native content publishing for Laravel: Eloquent models, full admin for posts/categories, SEO + JSON-LD, RSS, search, Blade UI components, and 13 MCP tools for AI agents. Headless by default with optional public blog routes.
The package ships an opt-in public-routes mode. Flip a flag in config and you get:
| Route | Name | Notes |
|---|---|---|
GET /blog |
blog.index |
Paginated post listing |
GET /blog/{slug} |
blog.show |
Single post (only published) |
GET /blog/category/{slug} |
blog.category |
Category archive (paginated) |
GET /blog/preview/{post} |
blog.preview |
Signed-only draft preview, with noindex,nofollow meta |
GET /blog/feed |
blog.feed |
RSS 2.0 feed (gated by features.feed) |
GET /blog/tag/{slug} |
blog.tag |
Tag archive (gated by features.tags) |
'features' => [
'public_routes' => true,
'feed' => true, // optional, enables /blog/feed
'tags' => true, // optional, enables /blog/tag/{slug}
],
'layout' => 'layouts.app', // your host layout the page views extend
That's it. The service provider registers the routes at boot — no Filament panel boot is required, so the public site keeps working for guests who never touch the admin.
The page views extend the layout you set in 'layout'. It must define a [@yield](https://github.com/yield)('content') slot. A minimal example:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title ?? config('app.name') }}</title>
[@stack](https://github.com/stack)('head')
</head>
<body class="bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">
[@yield](https://github.com/yield)('content')
</body>
</html>
If your layout uses a different slot mechanism (e.g. Blade components with {{ $slot }}), publish the page views and adapt them:
php artisan vendor:publish --tag=ink-views
The shipped pages live at:
resources/views/vendor/blog/pages/index.blade.phpresources/views/vendor/blog/pages/show.blade.phpresources/views/vendor/blog/pages/category.blade.phpresources/views/vendor/blog/pages/preview.blade.phpresources/views/vendor/blog/pages/tag.blade.phpresources/views/vendor/blog/pages/feed.blade.phpEdit them freely — once published, the package no longer serves its own copies.
Change 'prefix' => 'blog' in config. All routes pick up the new prefix.
Each feature flag is independent:
'features' => [
'public_routes' => true,
'feed' => false, // no RSS feed
'tags' => false, // no tag archive (admin still works if registered)
],
When a flag is off, requests to that path return 404 (not registered). Route::has(...) returns true (the route is defined) but the controller calls abort_unless($flag, 404). That's a deliberate choice so route ordering stays predictable and you can probe the route name without crashing.
| Headless | Public-routes | |
|---|---|---|
| Routes registered | None | All 6 |
| Controllers | You write | Shipped |
| Views | Components only | Full pages |
| Custom domain logic | Yes (any) | Limited to view overrides |
| Effort to ship a blog | ~2 hours | ~5 minutes |
If you outgrow public-routes mode, flip the flag back to false and write your own controllers — see Frontend Setup (headless).
How can I help you explore Laravel packages today?