sylius/content-bundle
Sylius ContentBundle adds CMS-style content management to Sylius e-commerce applications. It provides entities and admin tools for creating and managing static pages, reusable content blocks, and rich text, helping storefronts publish informational content without custom development.
Orient first: this is a Sylius/Symfony bundle, not a Laravel package. composer require sylius/content-bundle in a Laravel app will pull symfony-cmf/routing, Doctrine ORM mappings, and the Sylius ResourceBundle admin stack — none of which resolve against Eloquent, Laravel's container, or Laravel's router. The repo is also archived and read-only, so there are no upstream fixes and no PHP/Symfony version bumps. For a Laravel developer, the honest first step is deciding which mode you are in:
First 60 minutes, in order:
src/Model/Page.php and src/Model/StaticContent.php plus their *Translation counterparts. That is the whole content model: a slug-bearing page entity with one translation row per locale, and a block entity (static content) with localized body HTML.src/Entity/ mappings — there are no shipped migration scripts, only Doctrine mappings, which tells you the schema is meant to be regenerated per-install and is a hazard to rely on.Resources/config/routing.yml and Resources/config/routing/admin.yml to see the CRUD and admin surface SyliusResourceBundle generates.src/Twig/) — this is where "render a page/block by slug" lives, and it is the smallest, most transferable piece.First use case to port: a marketing site inside a Laravel commerce app — pages (slug + layout + localized body + SEO meta) and blocks (keyed, localized fragments embedded in Blade layouts). That covers ~90% of what anyone actually used this bundle for.
If you genuinely need it in Sylius: require the 1.x line matching your Sylius minor, register in config/bundles.php, import both routing files, declare page and static_content classes under sylius_content, run doctrine:migrations:diff (you must author the migrations yourself), and wire symfony-cmf/routing-bundle's dynamic router to the page repository. Expect Composer conflict resolution to fight you.
1. Port the content model before the code. The transferable shape maps almost 1:1 onto Laravel:
| Sylius/CMF concept | Laravel equivalent |
|---|---|
Page + PageTranslation |
pages (slug, layout, published_at) + page_translations (locale, title, body, meta) |
StaticContent + translation |
blocks (key, type) + block_translations (locale, body) |
| CMF route registry | content_routes (locale, slug, content_type, content_id) |
| CMF dynamic router | Catch-all fallback route + SlugResolver service, cached |
| SyliusResourceBundle CRUD | Resource controller + Filament/Nova resource |
Twig render_page / render_block |
Blade components <x-cms.page slug="terms" />, <x-cms.block key="homepage-hero" /> |
2. Use a separate route registry, not just a slug column. The single most valuable idea in the bundle is that routes are polymorphic records pointing at content, not a column on the page. That gives you slug history, 301 redirects, per-locale slugs, and future non-page content types (products, landing pages) resolved through one path. Implement it as content_routes with a content_type/content_id morph, unique on (locale, slug), and a redirect table beside it.
3. Slug resolution as a fallback route. Replace the CMF router with a Laravel catch-all:
Route::fallback(SlugController::class)->middleware('cache.headers:public;max_age=300');
The controller asks SlugResolver for the current locale's slug, returns a 301 if a redirect row matches, 404s if nothing resolves, and caches the resolved content per (locale, slug). Invalidate on publish via Cache::tags(['cms'])->flush() (Redis/database driver required).
4. Translations: prefer JSON columns for small CMS bodies. The bundle uses Doctrine *Translation entities because Symfony's admin needs per-locale rows. In Laravel, unless editors work in a translation-heavy workflow, json: title, body, meta keyed by locale is simpler, keeps whereRaw('json_extract(...)') search viable, and avoids the N+1 join. Go relational only if you need per-locale drafts or per-locale review states.
5. Twig helpers → two small pieces. The bundle's Twig extension is thin; replicate it as:
Block::keyed('homepage-hero') memoized in a request-scoped repository.6. Admin integration. The Sylius admin CRUD is generated from resource config. In Laravel, this is a Filament resource with two forms — a page form with a translatable repeater, and a block form keyed by string. Add a publish/unpublish action and a Cache::tags('cms')->flush() on save; the bundle relied on Sylius's publish workflow and cache clear subscribers for the same behavior.
7. Borrow the composition idea for blocks. The bundle's static content is a flat key→body lookup, deliberately not a page builder. Keep that constraint. If you need richer layouts, layer Blade components over the block registry rather than building nested block trees — the bundle never supported nesting and the moment you add it, admin complexity expands by an order of magnitude.
Do not attempt to install it in Laravel. The failure mode is a container-compilation error or an unresolvable dependency tree that consumes a day before the conclusion "cannot work" is obvious. Every concept in the bundle is reimplementable in a few hundred lines of Laravel; the dependency tree is not portable and never will be.
Archived means total ownership. No CVE response, no PHP 8.x compatibility work, no issue triage. If you fork, record it in an ADR and assign a named owner. In reference mode there is nothing to own — you only copied patterns, which is why reference mode is the defensible one.
There are no migrations, only mappings. In Sylius that means you generate schema from Doctrine mappings, which is a footgun for upgrades. In Laravel, write real migrations from day one — the schema:update shortcut this bundle relies on is exactly the practice not to import.
Translations and the fallback locale. The bundle resolves _locale from the request and falls back through configured locales. Port the ordering explicitly (requested → app fallback → en), because silent fallback to the wrong locale on published pages is the most common content bug in production. Write a test per locale.
Cache invalidation is a correctness risk, not a performance one. Two failure modes to test explicitly: published content not appearing (missed flush), and drafts leaking to public routes (resolver ignoring published_at or status). The bundle delegated this to Sylius subscribers; in Laravel you must implement it in the model observer and test both directions.
Slug and redirect drift. Changing a page slug without writing a redirect row breaks inbound links silently. The bundle's CMF route registry supported this; a naive slug column does not. Enforce redirect creation in the admin (updating observer on Page), and validate the redirect map on any content migration.
Twig helper semantics to preserve. The bundle's render helpers fail soft — a missing block returns empty rather than throwing. Reproduce that in the Blade component (@if($block) {...} @endif with a Log::warning), otherwise a deleted block takes down the storefront during deployment windows.
Extension points are a trap. The bundle supports subclassing Page/StaticContent and overriding resource config — a real extension point in Sylius, but you are not in Sylius. Do not design Laravel equivalents as "swappable bundled resources"; use a plain model plus a BlockRenderer contract if you truly need variant rendering.
RTL, sanitisation, and media. The bundle stores HTML bodies and assumes editors sanitise. If you port it, put sanitisation (mews/purifier or an HTML sanitizer) in the model mutator and store media references as explicit columns, not inline <img> tags inside the body — otherwise every future asset migration becomes a regex job.
Where to stop. The justified investment is roughly one to two days reading the model and Twig layer, plus a day mapping it to Eloquent. Budgeting time to learn Sylius resources, CMF routing, or Symfony container debugging is spend with zero downstream return in a Laravel shop — reject it explicitly in the design decision.
How can I help you explore Laravel packages today?