litecms/page
LiteCMS Page provides page management for Laravel applications, letting you create, edit, and publish static content pages with a ready-made admin interface, routing, and templating.
litecms/page ships no tutorial-grade docs, so the first hour is source recon, not setup. Install into a sandbox branch you are willing to throw away:
composer require litecms/page
Then read vendor/litecms/page/ in this order — it answers 90% of your questions faster than experimentation:
src/Providers/*ServiceProvider.php — what gets registered: routes, views, migrations, Blade directives, admin menu entries.src/Models/Page.php — $fillable, $casts, slug/route helpers, relationships.database/migrations/* — the real schema. Do not trust the README's field list.routes/*.php — route names and URI prefixes for the admin CRUD and the public show route.resources/views/ — the Blade templates you will end up overriding.Publish and migrate:
php artisan vendor:publish --provider="Litecms\Page\Providers\PageServiceProvider"
php artisan migrate
php artisan route:list | grep -i page
If the provider registers publish tags, publish in separate tagged calls (config, views, migrations, assets) so you can re-publish one without clobbering your edited views. Check php artisan vendor:publish --list for available tags.
First use case — a static page resolved by slug. The canonical flow is: an editor creates a page in the admin, the package persists it, and a public route resolves /{slug} to a Blade view:
// app/Http/Controllers/PageController.php
public function show(string $slug)
{
$page = Page::published()->where('slug', $slug)->firstOrFail();
return view($page->template ?? 'pages.default', compact('page'));
}
{{-- resources/views/pages/default.blade.php --}}
@extends($page->layout ?? 'layouts.app')
@section('meta')
<title>{{ $page->title }}</title>
<meta name="description" content="{{ $page->meta_description }}">
@endsection
@section('content')
{!! $page->body !!}
@endsection
Before wiring your own route, confirm the package hasn't already registered a catch-all — check route:list first to avoid a duplicate/conflicting registration.
Wrap the model behind a contract. Given zero dependents and a single maintainer, do not let Litecms\Page\Models\Page leak into controllers and views. Bind your own interface and return a DTO:
// app/Contracts/PageRepository.php
interface PageRepository
{
public function findBySlug(string $slug): ?PageData;
public function published(): Collection;
}
This is the seam that lets you swap in a native module later without touching the front end.
Cache aggressively, invalidate on write. Page content is read-heavy and cache-friendly, but do not assume the package does this for you. Put it in your own layer using model events:
Page::saved(fn ($page) => Cache::forget("page:{$page->slug}"));
Page::deleted(fn ($page) => Cache::forget("page:{$page->slug}"));
public function findBySlug(string $slug): ?PageData
{
return Cache::rememberForever("page:$slug", fn () =>
PageData::fromModel(Page::published()->where('slug', $slug)->first())
);
}
Draft preview without exposing drafts. Never let the public route serve unpublished rows. Preview via a signed, time-limited URL that sits behind auth:
Route::get('/preview/{page}', fn (Page $page) =>
view($page->template, ['page' => $page])
)->middleware(['web', 'auth', 'signed'])->name('page.preview');
Slug renames need redirects. Editors will change slugs on live pages. On save, if the slug changed, write a 301 record into your own redirects table — the package almost certainly does not track history.
Navigation and menus. Pull published pages once per request (cached) and build nav from them rather than hardcoding links:
$nav = app(PageRepository::class)->published()->where('in_menu', true);
Write your own feature tests on the critical paths — publish state, slug uniqueness, public 404 on drafts, and admin authorization. There is no visible test suite to inherit:
it('hides draft pages from the public route', function () {
$page = Page::factory()->create(['published' => false]);
$this->get("/{$page->slug}")->assertNotFound();
});
Admin customisation. Override the admin views by publishing to resources/views/vendor/page/ and adding fields (SEO, OG image, canonical) via your own migration, not by editing vendor files — the fork should be a composer path repository you control, not edits in vendor/.
/{slug} route registered by the package will shadow any later /{a}/{b} style app routes, producing confusing 404s or accidental content exposure. Always check route:list and constrain the pattern (->where('slug', '[a-z0-9-]+')).{!! $page->body !!} is correct — which means the editor is the trust boundary. Confirm admin routes are behind LiteCMS ACL/Gate middleware; a middleware refactor that drops that check silently turns your CMS into an XSS vector.$fillable before any Page::create($request->all()) — you do not want published or template settable from an untrusted request.Schema::create. Do not re-run them on an existing DB; if you need schema changes, add your own migration rather than editing the vendor one.SoftDeletes, the unique index on slug may still block reuse after deletion. Check the migration.composer.json before anything else: which Laravel major and PHP version it pins determines whether you can upgrade your framework at all. A ^10.0 pin will block your own Laravel 11 upgrade.php artisan route:list --path=page, php artisan vendor:publish --list for tags, php artisan tinker with Page::first() to confirm the schema matches the model's $casts, and Log::info(...) in your repository binding to confirm the container resolves your seam not the vendor class.saved/deleted) for cache invalidation, the repository contract for swapping implementations, and published Blade views for presentation. Avoid editing vendor source; if you must, fork to a private repo with a named owner and CI.PageData DTO free of vendor types so a future native implementation only has to satisfy the interface, not replicate the schema.How can I help you explore Laravel packages today?