anomaly/pages-module
Pages module for the Anomaly CMS (Laravel/Streams) that adds structured, editable static pages to your site, with routing, templates, and admin management built in for quickly publishing and maintaining content-driven pages.
anomaly/pages-module is not a standalone Laravel package — it is an addon for the Anomaly Streams Platform (the PyroCMS successor). The day-to-day value only appears once it sits inside a Streams application, so the first step is confirming your host app already requires anomaly/streams-platform and boots a distribution that registers addons.
composer require anomaly/pages-module
php artisan addon:install anomaly.module.pages # runs the stream/field migrations
Then open /admin/pages, create a page with slug about, and hit /about. That round trip (CP entry → wildcard route → presenter → Blade view) is the mental model everything else hangs off.
Read in this order:
composer.json — the anomaly/streams-platform constraint tells you which API surface you're coding against. Streams 1.x has drifted across minors; pin it.src/PagesModuleServiceProvider.php and src/PagesModule.php — migrations, route registration, view namespaces, asset publishing.src/Page/ — PageModel, PageRepository, PagePresenter, PageRouter, and the PageType directory. PageRouter is where a request path is matched against the pages stream and turned into a response; read it before you try to change routing behaviour.src/Page/Migrations/*_create_pages_fields.php — the canonical list of fields (title, slug, path, summary, content, meta_*, theme_layout, parent, enabled, home, roles). Everything you render comes from these.resources/views/ — the default page view you will almost certainly override.First real use case: point the home flag at a page so / resolves through the module, then build one custom page type (e.g. a contact page) instead of hard-coding a route in routes/web.php.
Reading pages from application code. Use the repository rather than raw Eloquent so Streams' locale/stream scoping is respected:
$page = app(\Anomaly\PagesModule\Page\PageRepository::class)
->findBy('slug', 'about');
// or in a Blade/controller context, if your version exposes it:
$page = \Anomaly\PagesModule\Page\PageModel::where('path', 'about')->firstOrFail();
In views, page entries are wrapped in a presenter, so $page->title, $page->path, $page->content, $page->meta_title, $page->layout are available without touching raw attributes. Check PagePresenter for the extra accessors (route(), href(), isHome()) before inventing your own.
Custom page types instead of new routes. This is the idiomatic extension point. Extend the module's PageType class, define a slug/name, and optionally override the route resolution or view it returns:
class ContactPageType extends \Anomaly\PagesModule\Page\PageType\PageType
{
protected $slug = 'contact';
protected $name = 'Contact Page';
public function route(PageModel $page) { /* return Response/Redirect */ }
}
Register it with the module's page-type collection (in PagesModule::boot() or a small provider in your own addon). The page type persists on the entry, so editors pick it in the CP and you get a per-type rendering path without forking the module.
Override output, don't patch the module. Two layers, in preference order:
resources/views/pages/{slug}.blade.php, falling back to the module's page view.theme_layout field selects a theme layout; set it in the CP rather than hard-coding a Blade @extends.If you must change the shared markup, publish the module views (php artisan addon:publish anomaly.module.pages) and edit the published copies so upgrades don't overwrite you.
Extend the stream in your own addon. Never add fields by editing the module's migrations. Create a migration in your addon that calls Streams::make('pages')->fields(...) (or add fields through the Streams admin UI, which is the fastest path for one-off fields). You keep the custom field through a composer update of the module.
Listen to lifecycle events. The module dispatches entry events per page (PageWasCreated, PageWasSaved/PageWasUpdated, PageWasDeleted) under Anomaly\PagesModule\Page\Event\*. Register listeners for cache warming, sitemap invalidation, or pushing a slug change into a redirect table. Note that automatic 301s on slug change come from anomaly/redirect-module listening to these — if you don't have it installed, nothing happens.
Access-gated pages. The roles relation / allowed-roles fields let you scope a page to logged-in groups. Use this instead of writing middleware for member-only marketing pages; use middleware when the rule is dynamic (subscription tier, etc.).
/admin, /api, and your own web.php routes win. If a page 404s, run php artisan route:list and confirm the wildcard is last; if your own routes are being swallowed, they're registered after addon providers — check ordering and route:cache state.cache:clear, route:clear, view:clear) before debugging code.addon:uninstall drops the stream and its data. It is not a rollback. Snapshot the pages, pages_pages_translations, and related field tables before you reinstall to test a migration.title almost always means locale mismatch, not a broken field.enabled and home are just fields. The router filters on enabled (so drafts 404 rather than 403), and home is a single-page flag the CP maintains — direct DB edits can leave two pages marked home, and which one wins is undefined. Set it through the CP or normalise it in your own command.--ignore-platform-reqs or a maintained fork. Verify against the require block rather than the README, and sanity-check that addon:install actually created the pages tables.src/ as the spec: when a method you remember from PyroCMS docs is missing, grep the module (and streams-platform) for the current name instead of trusting blog posts.php artisan tinker and app(PageRepository::class)->findBy('path', 'about') to see exactly what the router sees, plus dd() inside PageRouter when the match succeeds but the response is wrong. Almost every "pages module is broken" report is either a routing-order problem or a cache problem.PageType → view override/publish → event listener → rebinding the repository/presenter in your provider. Forking the module is last because you inherit the maintenance of an already-thin package.How can I help you explore Laravel packages today?