rinvex/laravel-pages
Rinvex Laravel Pages gives Laravel apps a ready-made CMS-style page system: a flexible Page model with multilingual translations, slugs, SEO metadata, polymorphic relations, and route/caching support, so you can add editable static pages without building custom infrastructure.
Status check first: this package is archived and unmaintained. Confirm the
laravel/frameworkconstraint incomposer.jsonbefore you invest time — the last releases target Laravel 5.5–8.x. On Laravel 9+ you'll usually need to fork, bump constraints toilluminate/*, and test the transitiverinvex/laravel-supportdependency yourself.
Minimal install:
composer require rinvex/laravel-pages
php artisan vendor:publish --tag="rinvex-pages:config"
php artisan migrate
Look at these files first, in order:
config/rinvex.pages.php — model bindings and table names (usually rinvex.pages.models.page and rinvex.pages.tables.pages).database/migrations/*_create_pages_table.php — the real contract. Expect uri, title, content, active, timestamps; title/content are often JSON columns for translations.src/Models/Page.php — $fillable, casts, and any traits (HasTranslations, slug/URI handling).src/Providers/PagesServiceProvider.php — whether it registers a catch-all route and under which middleware/prefix.First use case: a small set of static marketing/legal pages ("about", "terms", "privacy") that non-developers edit, resolved by URI at request time.
use Rinvex\Pages\Models\Page;
Page::create([
'uri' => 'about-us',
'title' => 'About Us',
'content' => '<p>Who we are.</p>',
'active' => true,
]);
Render it in a controller or fallback route:
$page = Page::where('uri', ltrim($request->path(), '/'))
->where('active', true)
->firstOrFail();
return view('pages.show', compact('page'));
Let the package own the route, or own it yourself — not both. If the service provider registers a catch-all ({uri} with .*), your app routes must be registered before it; otherwise the catch-all swallows /login, /api, etc. If the package does not ship a route (or you disable it in config), register a fallback deliberately:
Route::fallback(function (Request $request) {
$page = Page::where('uri', $request->path())->where('active', true)->firstOrFail();
return view('pages.show', compact('page'));
});
A fallback route is preferable to a {uri} catch-all: it runs only when nothing else matches, so it can't shadow real routes.
Admin CRUD. Keep the package as the persistence layer and put authorization/policy in your own controller. Normalize the URI in a form request (Str::slug, strip leading/trailing slashes) before saving, then:
$page->update($validated);
Translations. If title/content are JSON-translated columns, mass-assign locale arrays and resolve per request:
Page::create([
'uri' => 'about-us',
'title' => ['en' => 'About Us', 'ar' => 'من نحن'],
'content' => ['en' => '<p>...</p>', 'ar' => '<p>...</p>'],
'active' => true,
]);
$page->getTranslation('title', app()->getLocale());
Wrap this in a locale-aware resolver rather than scattering getTranslation calls through Blade.
Caching. Pages change rarely and are read on every request — cache aggressively and invalidate from an observer, not from controllers:
class PageObserver
{
public function saved(Page $page) { Cache::forget("page:{$page->uri}"); }
public function deleted(Page $page) { Cache::forget("page:{$page->uri}"); }
}
$page = Cache::remember("page:{$uri}", 3600, fn () => Page::where('uri', $uri)->where('active', true)->firstOrFail());
Menu/sitemap integration. Query active pages once per request and feed them to layout/nav builders:
$navPages = Page::where('active', true)->orderBy('title')->get(['uri', 'title']);
Blade rendering. Keep a single pages.show view that switches on a page "template"/"layout" column if you add one (see extension points below), rather than storing full Blade in content.
active is almost certainly not a global scope. Nothing stops a draft page from being served if you forget where('active', true). Centralize the lookup in one repository/query scope and never duplicate it./about, about, and about/ are different strings. Normalize on write (mutator or form request) and on read (same helper).php artisan route:list will reveal it. Move app routes above it or replace it with Route::fallback.active must be in $fillable and cast to bool; otherwise "0"/"false" from forms will round-trip as truthy strings.content (HTMLPurifier, mews/purifier) before render; {!! $page->content !!} is an XSS sink by design.where('title', ...) or orderBy('title') on a translated column across databases; sort in PHP or add a non-translated sort_title.rinvex/laravel-support constraint may pin old illuminate/* versions and block unrelated upgrades. Isolate the fork or vendor the two classes you actually use.template, meta_title, published_at), don't edit the package migration in vendor/; publish it or add a follow-up migration.config/rinvex.pages.php (models.page) and extend Rinvex\Pages\Models\Page; register observers for cache invalidation; override the view/route rather than the service provider; add a templates table or template column if you need more than one layout.RefreshDatabase and Page::create([...]) in setUp, and write a feature test asserting draft pages 404 and active pages 200 — that single test catches the scope and URI-normalization bugs above.Bottom line: as an archived, ~40-star package it's a thin Eloquent model plus a wildcard route — you're adopting the schema and conventions more than the code. For a legacy Laravel 8 app with a handful of CMS pages it's workable; for anything new, a 40-line Page model, migration, Route::fallback, and an observer gives you the same functionality with none of the dependency risk.
How can I help you explore Laravel packages today?