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 Bundle Laravel Package

aropixel/page-bundle

Aropixel Page Bundle brings multilingual page management to Symfony: three page types (CKEditor HTML, drag-and-drop block builder, JSON forms), protected system pages, SEO fields, scheduled publishing, and a PageSavedEvent for cache invalidation. Requires aropixel/admin-bundle.

View on GitHub
Deep Wiki
Context7

Getting Started

Do not try to composer require this into a Laravel app. There is no service provider, no facade, no Blade integration, and no Eloquent anything. It is a Symfony bundle that hard-depends on aropixel/admin-bundle ^3.0 and Doctrine ORM. Installing it in a Laravel project gets you classes that nothing in your app can autoload, configure, or route to.

The realistic day-to-day shape for a Laravel developer is headless: a small standalone Symfony app owns authoring, Laravel owns delivery.

Minimal path:

# separate repo / separate service, NOT inside your Laravel app
composer create-project symfony/skeleton:^7.0 content-service
cd content-service
composer require aropixel/admin-bundle aropixel/page-bundle

Then, in config/routes.yaml:

aropixel_page:
    resource: "@AropixelPageBundle/src/Resources/config/routes.yaml"
    prefix: /admin/page

Run php bin/console make:migration and doctrine:migrations:migrate — the schema is shipped as XML Doctrine mapping, not as bundle migrations, so you generate them yourself.

Look at these first, in this order:

  1. doc/installation.md — entity extension, Doctrine mapping override, bundle config keys. This is the file that decides whether your install works at all.
  2. src/Resources/config/routes.yaml in the bundle — confirms exactly which admin paths exist and what prefix they expect.
  3. doc/usage.md — page types, fixed pages, front-end rendering, the PageSavedEvent.
  4. doc/custom-blocks.md — the block registration contract (JS + YAML).
  5. doc/entities.md — only if you intend to subclass Page.

First use case to build: one fixed homepage plus one builder page, exposed as JSON.

  • Create a page with code = 'homepage' and mark it fixed/protected so nobody deletes it. code is your stable lookup key across environments — use it from Laravel instead of IDs or slugs.
  • Create a second page of TYPE_CUSTOM, drop a section/row/column/text block in the builder, save.
  • Write a 30-line Symfony controller GET /api/pages/{code} returning {title, slug, metaTitle, metaDescription, html, locale}.
  • Consume it in Laravel with Http::get(...) behind a cache.

That's the whole loop. If that works, the package is viable for you. If the install or the render step fights you, stop there — you've spent a day, not a month.

Implementation Patterns

Content service + HTTP contract (recommended). Symfony app serves authoring UI and a read API; Laravel never touches the content database. This survives bundle churn, Doctrine version drift, and an AdminBundle 3.x bug tail. Your integration surface is a JSON shape you control.

Read API design. Because TYPE_CUSTOM pages pre-render HTML at save time, your endpoint is essentially a table read — no rendering per request. Return both htmlContent (pre-rendered body) and the SEO/slug fields, and let Laravel decide. Do not proxy the raw builder JSON to Blade and re-render it; you'd be reimplementing the block renderer in PHP/Blade and it will drift.

Laravel-side consumption:

// app/Services/PageRepository.php
public function byCode(string $code, string $locale): ?PageDto
{
    return Cache::rememberForever("page:{$code}:{$locale}", fn () =>
        PageDto::fromArray(
            Http::baseUrl(config('services.content.url'))
                ->get("/api/pages/{$code}", ['locale' => $locale])
                ->throw()
                ->json()
        )
    );
}

In Blade: {!! $page->html !!} — it's already rendered, already sanitized by the builder's own block templates. Pair with {!! $page->metaTitle !!} in your layout.

Cache invalidation via PageSavedEvent. This is the one seam that matters. Register a Symfony listener that POSTs to a Laravel webhook on save:

#[AsEventListener(event: PageSavedEvent::class)]
final class PurgeContentCache
{
    public function __invoke(PageSavedEvent $event): void
    {
        $this->http->post(config('services.laravel.webhook'), [
            'code' => $event->getPage()->getCode(),
        ]);
    }
}

On the Laravel side, Cache::forget("page:{$code}:*")-style invalidation (use tags, or store a version key per page code). Note the README's wording: the event fires after every page builder save — treat it as "builder pages changed," not "any page changed."

Fixed/protected pages as your routing contract. Declare homepage, contact, legal, etc. with static code values in Symfony config. Laravel routes to them by code (/, /contact, /legal). This is far more robust than slug lookups: slugs are per-locale and editors change them, code is not.

Multilingual. Gedmo Translatable handles per-locale title, slug, htmlContent, metaTitle, metaDescription. Your read API must take a locale explicitly and never fall back silently — return 404 with the available locales so Laravel can build its hreflang map from real data rather than guessing.

Custom blocks live in two places. Block types are registered in the Symfony app via JS + YAML (doc/custom-blocks.md). If you also need Laravel-side knowledge of a block (e.g. extracting a hero image URL for an OG tag), don't reimplement the registry — have the Symfony save listener persist a small denormalized extras JSON alongside htmlContent, or expose it in the read API. One registry, one owner.

Publication scheduling in queries. Pages carry online/offline status plus an optional date range. Filter in your read controller (isOnline AND (startDate IS NULL OR startDate <= now) AND (endDate IS NULL OR endDate >= now)), not in Laravel — otherwise scheduled publishing requires a cache flush at the exact boundary moment.

Warm the cache with an artisan command. php artisan content:warm that iterates the fixed codes × configured locales and repopulates. Run it after deploy and after any block template change (see below), not on every request.

Gotchas and Tips

Pre-rendered HTML is stale HTML. htmlContent is rendered at save time. Change a block's Twig template in Symfony and every existing page still serves the old markup until it is re-saved. There is no "rebuild all pages" console command documented; plan to write one (loop pages, re-dispatch the renderer) or accept that template edits require an editor touch-every-page pass. This is the single biggest operational trap and it will bite you on your first design change.

PageSavedEvent is not a general "page changed" hook. Per the docs it fires after page builder saves. If you also allow TYPE_DEFAULT (CKEditor) or custom-JSON pages, wire your cache purging so those don't silently go stale — either add your own Doctrine listener or route all edits through code that purges.

No in-place upgrade from v2. UPGRADE-3.0.md exists, but v3.0 is a rewrite against AdminBundle 3.0 with no migration path. Any v2 tutorial, blog post, or Stack Overflow answer you find is wrong for your install. Read only the v3.0.0 tag docs.

Pin exact versions. 3.0.0 is the first release of a from-scratch rewrite of a 1-star package with zero dependents. "aropixel/page-bundle": "3.0.0" — not ^3.0. Expect a 3.0.x bug tail and budget for reading source or carrying a small fork patch.

Entity overriding is config-gated and easy to get wrong. If you subclass Page, you must also remap the Doctrine XML and point the bundle config at your class. Do this before generating migrations, or you'll write and rewrite the schema. Prefer not overriding at all in a headless setup.

Doctrine ORM 3 is a real fork in the road. The bundle supports ORM 2 or 3. laravel-doctrine/orm has historically lagged on ORM 3. The headless recommendation exists partly to make this irrelevant — if you're tempted to share the content DB with Eloquent, this is where that plan dies.

Never share write access to the content schema. Two ORMs against one database with Gedmo Translatable and cascade-on-delete rules is a corruption bug waiting for a quiet Friday. Read-only projections at most; ideally a separate database served over HTTP.

Slugs are non-null per locale. slug was explicitly made non-nullable in the ORM mapping. Your page-creation code (or fixture loader) must supply a slug for every configured locale, or inserts fail in a way that looks like a Translatable misconfiguration.

Locale support is six UI languages, not unlimited content languages. French, English, German, Spanish, Italian, Czech ship as interface translations. Content locales are yours to configure, but you're the first person testing any locale outside that set.

Custom block assets need a build step if you fork. JS source lives in assets/, generated output in src/Resources/public/js/. After changes: npm run build, then commit both directories. CI rejects PRs that modify assets/ without the rebuild. Irrelevant if you only consume, critical if you patch.

Don't mount the bundle's routes in production. The /admin/page UI is an editing surface. In the content service, gate it behind auth (AdminBundle's), bind it to an internal network, and expose only your own read API publicly.

One star, zero dependents means you are the QA. There is no community to search when something breaks. Keep notes, keep your Symfony app minimal (no unnecessary bundles), and keep the read API thin so a future replacement of the authoring layer — whether that's Filament, a hosted CMS, or your own builder — costs you a controller rewrite rather than a data migration.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core