Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Page Module Laravel Package

idavoll/page-module

A Laravel package for adding CMS-style pages to your application, with page management, routing, and Blade rendering. It integrates with Eloquent and Laravel conventions to help you create, organize, and serve editable content pages.

View on GitHub
Deep Wiki
Context7

Getting Started

This is an AsgardCMS module — it is not standalone. It expects the AsgardCMS platform (asgardcms/platform) and its satellite modules to already be installed and configured. Treat the package as an add-on to an existing Asgard install, not a greenfield dependency.

Minimal setup:

composer require idavoll/page-module

Then:

  1. Enable the module. Asgard uses config/modules_statuses.json (and the Module facade) rather than Laravel's auto-discovery. Either run php artisan module:enable page or set "page": true in that file. The module's service provider will be loaded from there.
  2. Migrate. php artisan module:migrate page (or plain php artisan migrate if the platform is already set to run module migrations globally). This creates the pages and page-translation tables.
  3. Check the sidebar. If the module registered its navigation correctly, you'll see a Pages entry in the Asgard admin at /admin. That is your first real use case: create a page, pick a template, hit "Save", then browse the generated slug on the frontend.

Where to look first in the source tree:

  • Providers/PageServiceProvider.php — registers routes, views, translations, config, permissions, sidebar, and (in older versions) Doctrine bindings. Read this before anything else; it tells you exactly what the module assumes exists.
  • Http/routes.php (or Routes/) — two route groups usually exist: an admin group under /admin/page/... and a frontend catch-all group for rendering a page by slug.
  • Entities/Page.php + Entities/PageTranslation.php — the model shape (id, parent_id, template, is_home, slug, title, body, status, timestamps).
  • Repositories/PageRepository.php — the sanctioned query API (findBySlug, getAll, create, etc.). Prefer this over hitting the entity directly.
  • config/page.php — template list and any feature toggles.

First concrete use case: create two or three pages in the admin, set one as is_home, and confirm that GET / renders it via the frontend controller. This validates migrations, routing, translation fallback, and theming all in one pass.

Implementation Patterns

Inject the repository, not the model

The module ships a repository contract (Interfaces/PageRepository). Consumers should resolve that binding. This survives the move between Doctrine and Eloquent backends that Asgard went through — anything that reaches for Page::where(...) directly will break across those versions.

use Modules\Page\Repositories\PageRepository;

public function __construct(PageRepository $pages)
{
    $this->pages = $pages;
}

public function index()
{
    return view('home', [
        'pages' => $this->pages->getAll(),     // active pages, translated
        'home'  => $this->pages->findBySlug('home'),
    ]);
}

For custom queries, inject the repository and access the underlying query builder via its ->all() / ->where(...) helpers rather than the entity class.

Rendering pages from your own controllers or middleware

The frontend controller typically exposes Page::findBySlug($slug) per locale. Two integration styles show up in practice:

  • Override the route. Point /{slug} at your own controller that resolves the page through the repository, decorates it (breadcrumbs, menus, parent_id chain), and returns the module's template view.
  • Hook the route params. If the module uses a route parameter for slug, bind it via Route::bind('slug', fn($s) => app(PageRepository::class)->findBySlug($s)).

If you need a specific layout per section of the site, don't fight the module: create a page row per section with its own template, and let the module pick the view.

Templating

The module stores a template string on each page and dispatches the render through a view that matches it. In practice you'll add a blade per template:

resources/views/vendor/page/templates/default.blade.php
resources/views/vendor/page/templates/landing.blade.php

You publish/override module views with the standard Laravel vendor:publish --tag=views (or, on Asgard, place them in your Theme under views/page/…). Register new templates in config/page.php:

'templates' => [
    'default' => 'Default',
    'landing' => 'Landing page',
    'contact' => 'Contact page',
],

Translations

Pages are translatable. The pattern is:

$page = $this->pages->create((object)[
    'template' => 'default',
    'is_home'  => false,
    'status'   => 1, // published
    'en' => ['title' => 'About', 'slug' => 'about', 'body' => '…'],
    'fr' => ['title' => 'À propos', 'slug' => 'a-propos', 'body' => '…'],
]);

When reading, use $page->translate(config('app.locale')) and fall back to app()->getLocale() default. Asgard stores the title/slug/body on the translation row, so a page without a translation in the current locale will either return null or the base row depending on version — always test both.

Hooking page lifecycle with events

The module fires a standard Asgard event set: PageIsCreating, PageIsUpdating, PageWasCreated, PageWasUpdated, PageWasDeleted, PageIsDeleting. Register listeners in your own EventServiceProvider:

protected $listen = [
    \Modules\Page\Events\PageWasCreated::class => [FlushSitemapCache::class],
    \Modules\Page\Events\PageWasUpdated::class => [FlushSitemapCache::class],
    \Modules\Page\Events\PageIsCreating::class => [ForceDefaultTemplate::class],
];

This is the correct extension point for sitemaps, search indexing, static-site generation, and slug collision handling — do not monkey-patch the repository.

Admin-side customisation

  • Extra fields: extend the page migration with your own columns and add fields to the admin form partial. The admin views are publishable, so prefer overriding the partial over editing vendor code.
  • Permissions: register a new permission in your own service provider if you add a new page-admin action; the module registers page.pages.index / create / edit / destroy via the platform's permission registry, and admin access is gated on those.
  • Menu integration: the platform's Menu module is where page→menu wiring lives; listen to PageWasUpdated and refresh menu entries there rather than embedding menu logic in this module.

Composing with Media

Featured images are stored through the Mediable trait on the page entity. If you want images on pages, ensure idavoll/media-module (or the Asgard media module for your platform version) is enabled, then call $page->files()->first() or use the module's media picker partial in the admin form.

Gotchas and Tips

  • Do not install this without the platform. "0 dependents" on the index reflects that it's consumed transitively by AsgardCMS bundles, but the module hard-depends on asgardcms/platform, Menu, Media, Setting, and Translation conventions. A bare Laravel app will fail to boot on the service provider.

  • Version drift is real. Early Asgard releases used Doctrine entities; later ones use Eloquent. The repository interface is stable across that boundary, but any code touching the model directly is not. Check the composer.json constraint on asgardcms/platform first, then check which repository contract is present, and pin your Asgard version in composer.json.

  • Catch-all route ordering. The frontend route typically registers /{slug?} to render any page. If you add your own routes (a blog, an API) after the module's routes are loaded, they'll be shadowed. Either register your routes in a service provider with ->before('page routes') semantics, or constrain the catch-all via where('slug', '^(?!blog|api).*'). Debugging this is a two-second php artisan route:list — do that first when a page 404s for no obvious reason.

  • Slug uniqueness. The module relies on the admin form (or PageIsCreating listeners) to normalise slugs. Programmatic creation with $repository->create() will happily insert duplicate slugs unless you check first. Add a listener on PageIsCreating that appends -2, -3, etc.

  • is_home is global, not per-locale. Setting a second page to is_home can leave two "home" rows. The repository's getHome() may then return either one. Always unset the previous home explicitly in your save handler.

  • Config caching. Adding a template to config/page.php while php artisan config:cache is active will silently do nothing. Clear config after any module config change — a very common "why doesn't my new template appear?" symptom.

  • Translation fallback surprises. If you inspect $page->title directly on the entity, you get the base row's value, not the current locale. Use $page->translate($locale) or the module's present() helper. This bites when writing frontend blade files: always pull through the translation object.

  • Event timing on update. PageWasUpdated fires after the slug has changed. Anything cached under the old slug (sitemap, static generator, CDN) needs the old slug — capture it in PageIsUpdating and stash it on the request or in a short-lived cache keyed by page ID.

  • Debugging empty frontend pages. In order: (1) route:list for the catch-all, (2) php artisan tinker to confirm findBySlug('your-slug') returns a row, (3) check the row's status column — 0 rows are filtered by the repository's default query and silently return null, (4) check the translation row exists for the current locale, (5) confirm the template key in the DB exists in config/page.php.

  • Permissions in tests. Feature tests for admin routes must seed the page permissions, or Auth will 403 before your assertion runs. Use the platform's permission seeder, not a raw factory.

  • Extension points to prefer, in order: events → published views → repository decoration → forking. Forking this module is cheap (it's small), but you lose upstream fixes and, given the project's activity level, you will be maintaining it yourself regardless — so decorate the repository contract in your app if you can, and only fork if you need to change the schema.

  • License caution. The repo reports NOASSERTION, meaning GitHub couldn't detect a recognised license file. For a production project, get written clarification from the maintainer (or treat it as all-rights-reserved) before shipping. This is the single biggest practical blocker for adopting this module today.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route