manukminasyan/filament-blog
A Filament plugin that adds a complete blog management suite to your Laravel admin panel, with ready-made resources for posts, categories, tags, and comments so you can create, publish, and organize content without building custom CRUD screens.
Ink is Relaticle's Laravel content/blog package — posts with slugs, soft deletes, and a built-in MCP (Model Context Protocol) tool surface so AI agents can read and act on your content.
Minimal setup
composer require relaticle/ink
php artisan vendor:publish --tag="ink-config" # config/ink.php
php artisan vendor:publish --tag="ink-migrations"
php artisan migrate
Where to look first
config/ink.php — controls slug source/suffix behaviour, route prefix, middleware, and MCP registration. Read this end-to-end before writing code; most "unexpected behaviour" questions are answered here.database/migrations/*_create_ink_posts_table.php — confirm the slug column's uniqueness strategy on your DB driver (see Gotchas).Post model and its slug trait — this is where the v2.5.1 soft-delete fix lives.routes/ — the package's default public routes; check whether you're expected to consume them or override them.First use case: publish a post, hit the public route, then edit the title and watch what happens to the slug. That one loop teaches you the slug lifecycle, which is the source of nearly every Ink issue you'll hit.
use Relaticle\Ink\Models\Post;
$post = Post::create([
'title' => 'Hello World',
'body' => '# First post',
]);
$post->slug; // "hello-world"
Let the package own the slug; you own the redirects
Treat slug as derived state. When a title is edited, Ink regenerates the slug, which means published URLs move. Wire a redirect layer rather than fighting the generator:
// In your app's RouteServiceProvider / middleware
Route::get('/blog/{slug}', function (string $slug) {
$post = Post::where('slug', $slug)->firstOrFail();
return view('blog.show', compact('post'));
})->where('slug', '.*');
Keep a slug_history table (or a previous_slugs JSON column on your own side) and 301 old paths. This is the single highest-value integration you can add on top of Ink.
Soft deletes as a publish workflow
A common pattern is to treat deleted_at on posts as an editorial archive rather than a true delete. Because v2.5.1 frees the slug of a trashed post, "archive and rewrite" becomes clean:
// Archive the old take, publish a fresh one with the SAME slug
$old = Post::where('slug', 'release-notes')->firstOrFail();
$old->delete(); // slug "release-notes" is now free
$fresh = Post::create([
'title' => 'Release Notes',
'body' => $newBody,
]); // gets "release-notes", not "release-notes-2"
Gate editorial UI on withTrashed() so editors can find archived drafts, but keep the public query scoped to live posts only.
MCP tools for agent workflows
v2.5.1 adds slug to Ink's MCP tools, which makes the tool surface usable as a stable addressing scheme instead of leaking database IDs into agent prompts:
slug to agent tool calls — it survives re-imports and re-seeds where IDs do not.get_post(slug: "pricing-faq") rather than enumerating posts and filtering.create_post will confuse the model.Testing
Use RefreshDatabase and assert against slug values directly; slugs are the package's observable contract:
it('reuses the slug of a trashed post', function () {
Post::factory()->create(['title' => 'Alpha'])->delete();
$new = Post::factory()->create(['title' => 'Alpha']);
expect($new->slug)->toBe('alpha');
});
Add the mirror-image test (restoring a trashed post whose slug was retaken) so a future package bump can't silently change behaviour on you.
Override, don't fork
If you need custom rendering or field additions, extend the package's Post model (config-driven model swap if supported) and keep a thin subclass. Forking means the v2.5.1 class of fix — a uniqueness-scope bug in the slug generator — never reaches you.
v2.5.1 changed slug uniqueness semantics — audit before upgrading
Freed slugs mean two posts can share the same slug string as long as one is soft-deleted. Consequences:
$post->restore() on a trashed post may now produce a duplicate slug. Wrap restores in a transaction and either re-slug the restored post or reject the restore. Decide your policy explicitly; there is no correct default.unique index on slug, freeing slugs at the application layer won't be enough — the insert will still fail. Postgres users should move to a partial unique index (WHERE deleted_at IS NULL); MySQL users need a composite unique on (slug, deleted_at) with the caveat that NULL values don't collide, so you may need a 0/timestamp sentinel. Check your migration after upgrading — this is the most likely upgrade breakage.unique:posts,slug validation rule in your own FormRequests now disagrees with package behaviour. Update it to ignore trashed rows.Route model binding and trashed models
Default bindings exclude soft-deleted posts, so an old bookmarked URL to an archived post 404s. That's usually correct — but if you want an "archived" banner instead, you need an explicit withTrashed() binding plus a view guard.
Slug collisions with the suffix strategy
The generator's fallback appends -2, -3, … If you're bulk-importing, generate slugs in a single pass rather than one-at-a-time inserts, or you'll get ordering-dependent suffixes that make URL diffs noisy between environments.
MCP tool output is not a rendering pipeline
The slug now exposed via MCP is the raw stored value. Don't assume it's normalized for display; run the same URL-generation helper your views use so agents and humans see identical links.
Caching
If you cache rendered posts keyed by slug, a slug change leaves a stale entry forever. Key your cache on the post's primary key and invalidate on saved/deleted, using the slug only in the cache tag if you must.
Debugging checklist
grep config for the slug source field — a package default of title when you're populating name silently yields empty or UUID slugs.slug? Confirm you're actually on >= 2.5.1 (composer show relaticle/ink); the field is new in this release and older cached MCP schemas in your client will hide it until reconfigured.Extension points worth knowing: the slug generator (swap for transliteration or locale-aware slugs), the config-driven model binding, and MCP tool registration. All three let you adapt Ink to your app without touching vendor code — reach for these first when a requirement seems to demand a fork.
How can I help you explore Laravel packages today?