litecms/blog
Blog module for LiteCMS that adds posts, categories, tags, and comments to your Laravel application. Includes an admin CRUD interface, slug-based routing, publishing controls, and Blade views, so you can launch a full-featured blog quickly without building content management from scratch.
Install and publish
composer require litecms/blog
php artisan vendor:publish --provider="Litecms\Blog\BlogServiceProvider"
php artisan migrate
The vendor:publish call is the first thing to inspect: open vendor/litecms/blog/src/BlogServiceProvider.php and read the publishes() array. Tag names are not standardised across LiteCMS packages, so prefer --provider= over guessing --tag=blog-config. You typically get a config file (config/blog.php), migrations, and Blade views.
Where to look first, in order
src/BlogServiceProvider.php — route registration, view namespace, publishable assets, any admin-menu hooks.routes/web.php (or routes/admin.php) — route names and URI prefixes. Run php artisan route:list | grep blog to see what actually got registered.src/Models/Post.php — fillable fields, scopes (look for scopePublished), relationships to Category, Tag, User.database/migrations/ — column names matter more than anything else here; everything downstream (queries, views, SEO fields) depends on them.resources/views/ — the view namespace (e.g. blog::posts.index) determines how you override templates.First use case: render a post index
Wire the package's own routes first to confirm it works end-to-end, then replace the frontend with your own controller:
// routes/web.php
Route::get('/blog', [\App\Http\Controllers\BlogController::class, 'index'])->name('blog.index');
// app/Http/Controllers/BlogController.php
public function index()
{
$posts = \Litecms\Blog\Models\Post::query()
->published()
->with(['author', 'category'])
->latest('published_at')
->paginate(12);
return view('blog.index', compact('posts'));
}
Create your first post through the LiteCMS admin panel (the package almost certainly registers admin CRUD). If no admin UI appears, the package is frontend-only and you own the write path — check the README before assuming a bug.
Model extension over model patching. Never edit vendor/. If you need extra behaviour, subclass and swap the binding:
// app/Models/Post.php
class Post extends \Litecms\Blog\Models\Post
{
public function getRouteKeyName(): string { return 'slug'; }
public function scopeFeatured($q) { return $q->where('is_featured', true); }
}
// AppServiceProvider::register()
$this->app->bind(\Litecms\Blog\Models\Post::class, \App\Models\Post::class);
Verify the package resolves models from the container (it usually does via config('blog.models.post')); if it hardcodes the class, config override is the supported lever.
View overriding. Publish views, then edit resources/views/vendor/blog/.... Keep the override surface small — every overridden file is a merge conflict on upgrade. Prefer wrapping: your blog.index includes <x-blog.post-card :post="$post" /> built from scratch.
Read path vs write path. Split them deliberately. Public read routes are yours (full control over SEO, caching, pagination). Admin write routes stay with the package. This keeps upgrades cheap.
Query hygiene. Blog indexes are the classic N+1 trap — author, category, tags, and cover image all lazy-load per card. Always with() explicitly, and add a ->withCount('comments') if you show counts.
Caching with invalidation. Cache the published list and bust it from a model observer rather than TTL guessing:
Post::observe(new class {
public function saved(Post $post) { Cache::forget('blog.index'); }
public function deleted(Post $post) { Cache::forget('blog.index'); }
});
Scheduled publishing. If there's a published_at column, drive visibility from it (where('published_at', '<=', now())) rather than a boolean flag. That gives you scheduled posts for free and one source of truth.
Headless / API usage. If the frontend is separate, ignore the package views entirely and expose your own PostResource. Check whether the model uses HasMedia/mediable; if so, expose cover_url as a computed accessor so the API shape stays stable if the storage driver changes.
Testing. Add your own factories regardless — packages of this size rarely ship them. RefreshDatabase plus a Post::factory()->published()->create() state is worth the ten minutes.
src/ fully before committing. Pin the version exactly ("litecms/blog": "1.2.*") — a patch release can silently rename a column.unique index on slug plus SoftDeletes means a deleted post's slug stays reserved. Decide early: hard-delete, or suffix slugs on collision (my-post-2).published_at. Store UTC, display in the user's zone. A post appearing "tomorrow" on the frontend but "today" in admin is almost always an uncast datetime column. Add 'published_at' => 'datetime' to $casts if missing./blog/{slug} 404s while the admin works, check route order with php artisan route:list --sort=...; your routes must load before the catch-all.users table and specific foreign-key types. On a fresh DB with UUID users and bigint posts, migrate fails. Check the key types before running.{!! !!}. Sanitise on save (HTMLPurifier or a sanitize mutator) rather than trusting editors — a compromised admin account otherwise becomes stored XSS.php artisan storage:link is a silent prerequisite for cover images; broken image icons after deploy are nearly always this.config/blog.php, run php artisan config:clear. In production, a stale cached config is the most common "the package ignores my setting" cause.php artisan route:list, php artisan vendor:publish --provider=... output, and Telescope's query tab to confirm eager loading actually fired. If a scope "doesn't exist", you're likely on a different model class than the one the package resolves.How can I help you explore Laravel packages today?