integrated/page-bundle
integrated/page-bundle is a Laravel package for adding structured page management to your application. It offers a foundation for editable, route-aware pages that integrate with existing Laravel projects, with minimal setup and room to extend the content model to fit your needs.
Confirm what you're installing before you write a line of code. integrated/page-bundle targets the Integrated CMS, which is built on Symfony, not Laravel. There is no ServiceProvider, no publishable config, no facade, no Illuminate contracts anywhere in the package. The bundle ships the Symfony idiom: a DependencyInjection extension with a Configuration tree, tagged service definitions, and bundle-registered routing plus Twig templates. If your application is Laravel, your first task is not installation — it's choosing a posture:
Do not try to shim the bundle into a Laravel container. The impedance mismatch across the container, config tree, router, and Twig inheritance will cost more than option 3.
If you are on Symfony/Integrated, the minimal path is:
// config/bundles.php
Integrated\Bundle\PageBundle\IntegratedPageBundle::class => ['all' => true],
# config/routes.yaml — verify the actual routing file name in the repo
integrated_page:
resource: '@IntegratedPageBundle/Resources/config/routing.xml'
prefix: /
Then run the bundle's migrations (bin/console doctrine:migrations:migrate after checking where they live), and open the DI extension's Configuration.php — that class is your real documentation for what is configurable (templates, page types, publishing states).
If you are on Laravel, minimal reimplementation to first page:
pages table: id, slug (unique), title, template, body, status enum(draft|scheduled|published), published_at, seo_title, seo_description, canonical, updated_at.PageResolver::findPublished(string $slug) that returns 404 for missing, 410 for retired, and honours published_at <= now().Route::get('/{slug}', PageController::class)->where('slug', '.*') registered last, so real application routes win.view($page->template) directly.First use case to validate: create one page per template, publish one and leave one as draft, confirm the draft 404s for anonymous users and previews for an authenticated editor. Then, if you're native, edit it in the front end via website-bundle and confirm the published render actually changes. That loop — create, edit inline, publish, render — is the entire value proposition, and it's the only thing worth spiking first.
Treat the page entity as the join point, not the whole feature. The page row should carry slug, template key, publish state, publish timestamp, and SEO fields. Everything else (blocks, media, relationships) should hang off it. Overloading the page table with structured content is what makes the eventual migration painful.
Route resolution: one catch-all, explicit precedence. The bundle's "conflict-safe resolution" is really slug-collision avoidance. Mirror that discipline in any integration:
// Register real routes first, then:
Route::get('/{slug}', [PageController::class, 'show'])
->where('slug', '^(?!admin|api|login|_debugbar).*$')
->middleware('page.cache');
Reserve prefixes for /admin, /api, /assets, and any locale segment. Decide now whether slugs are global or per-locale — retrofitting that later is a migration plus a redirect map.
Template strategy: whitelist, then map. Keep a config array of allowed template keys to view names. Editor-selected templates are a common path to arbitrary view inclusion.
// config/pages.php
'templates' => [
'default' => 'pages.default',
'landing' => 'pages.landing',
'contact' => 'pages.contact',
],
Throw on an unknown key at save time, not render time. A page with a dangling template should fail in the editor, not with a 500 in production.
Draft/preview pattern. Preview needs three things simultaneously: a signed, time-limited token in the URL; cache bypass (Cache::tags(["page:{$id}"])->flush() or simply no-store); and X-Robots-Tag: noindex plus a robots.txt disallow on the preview path. Never gate preview on "is logged in" alone — editors share links, and that link will eventually be pasted into a public channel.
Front-end editing is a separate render path. With website-bundle, in-context editing injects an editing overlay into the page. Keep the edit surface and the public render path separable: the overlay should be a distinct middleware/route guard, and the write endpoints it calls must be authenticated server-side and CSRF-protected. If the overlay is only hidden by CSS, it is not secured.
Headless sidecar integration (Laravel side). If you go this route, keep the coupling to one class:
final class PageClient
{
public function __construct(private Http $http) {}
public function bySlug(string $slug): ?PageData
{
return Cache::remember("page:{$slug}", 3600, fn () =>
PageData::fromArray(
$this->http->get("/api/pages/{$slug}")->throw()->json()
)
);
}
}
Wire a queued job on a schedule to warm or purge cache by updated_at. Do not call the sidecar synchronously from the public request path — a slow editor session must never slow down a visitor.
Purge on write, tag by page and by collection. Every inline edit must invalidate the page itself and any listing, navigation, or sitemap fragment that embeds it. If the bundle offers no invalidation hooks, you own this layer; build it against a key convention from day one.
Extension points, in order of preference: override Twig templates by placing files in templates/bundles/IntegratedPageBundle/...; listen to the bundle's dispatched events (check Event/ in the repo); decorate services; extend the entity only as a last resort, because subclassing a Doctrine entity drags you into inheritance mapping and makes upgrades harder.
bin/console, Twig, Doctrine, service tags. Budget 1–2 weeks of Symfony ramp-up for a Laravel-fluent team before anyone can confidently patch the bundle — and you will be patching it, because there are zero dependents upstream to catch regressions.Configuration tree does nothing until bin/console cache:clear (and cache:warmup in prod). This is a classic half-hour wasted on "my config is ignored."/api will shadow your API for everything under it.published_at in UTC, evaluate comparisons in UTC, and render dates in the site's display zone. Mixed storage is the usual source of "it published a day early/late" reports.app.url, not from Request::getHost(). Otherwise staging, previews, and www/non-www redirects poison your SEO metadata.dd(). bin/console debug:router, debug:container --tag=..., and the profiler are how you find out whether the page route is actually registered and which service handles it. If the router shows nothing, the bundle is not registered — check bundles.php.page table already present is unrecoverable without a backup.How can I help you explore Laravel packages today?