kunstmaan/node-bundle
KunstmaanNodeBundle provides a hierarchical, translatable node tree for Symfony applications, forming the foundation of the Kunstmaan CMS. It includes Node and NodeTranslation entities, slug-based routing, and tools for managing nested content pages.
First, a reality check: this is a Symfony 2 bundle, not a Laravel package. There is no service provider, no config/node.php, no facade, and no artisan integration. composer require kunstmaan/node-bundle in a Laravel project will resolve against a Symfony 2.3/2.4-era stack and either fail or drag in a decade-old dependency tree. So before anything else, decide whether you are:
If Symfony: install alongside kunstmaan/admin-bundle and Doctrine, register both bundles in AppKernel, configure kunstmaan_node, and run the bundle's install command. You will need the full Kunstmaan CMS stack for the admin UI to be useful.
Where to look first:
Entity/Node.php — the tree node (internal name, timestamps, deletion flag)Entity/NodeTranslation.php — one row per locale (title, slug, online flag)Entity/Page/ — SinglePage / StructurePage (the layout)Entity/PagePart/ — the content blocks (TextPagePart, ImagePagePart, …)NodeManager.php / NodeRepository.php — the service layer you will actually call day-to-dayFirst use case: define a HomePage as a SinglePage, attach a TextPagePart and an ImagePagePart, create the Node with an English NodeTranslation and a slug, mark it online, and let the bundle's route loader serve /en/home without registering any route by hand.
The mental model is the whole point:
Node (tree position)
└── NodeTranslation (per locale: title, slug, online)
└── Page (single or structure)
└── PageParts (ordered content blocks)
If you're doing this in Laravel, translate that model almost 1:1: Node hasMany NodeTranslation; NodeTranslation morphs to Page; Page hasMany (ordered) PageParts via a pivot or ordered column.
The bundle earns its keep through a small number of conventions. These are the ones you'll touch every day:
1. Never put display fields on Node. Node is structural only. Titles, slugs, and publish state live on NodeTranslation. If you add title to Node you will break locale routing.
2. Page vs PagePart is layout vs content. The Page subclass owns the template/controller; PageParts are ordered blocks attached to the page. Reuse means new page parts, not new page types.
3. Routing is slug-driven. Slugs live on NodeTranslation. The bundle registers a route loader that maps /{slug} to _controller based on the page's controller convention. Do not hand-register routes for content pages — you'll fight the loader.
4. Manage slugs through NodeManager, not by hand. When a title changes, regenerate the slug via the manager/Slugifier service, and handle collisions per locale. Doing this inline in a controller is the most common source of dead links.
5. Use online as your publish flag. It is per-translation, not per-node — which is exactly what you want for "English live, French draft."
A typical Symfony call looks like:
$node = $nodeManager->createNode($locale, $user, 'HomePage', $parent);
$nodeManager->updateNode($node, $request); // title, slug, online, page
6. Extend by adding page parts, not by forking.
# config.yml
kunstmaan_node:
page_parts:
- { name: quote, class: Acme\Bundle\AcmeBundle\Entity\PagePart\QuotePagePart }
7. Admin CRUD comes free from kunstmaan/admin-bundle; extend the form type to surface your custom fields.
Laravel port pattern (if that's your goal):
nodes (id, parent_id, internal_name, deleted_at)node_translations (node_id, locale, title, slug, online)pages (node_translation_id or polymorphic, type, template)page_parts (page_id, type, position, payload json)SlugResolver route macro (Route::get('/{slug}', ...)) that resolves slug → translation → page, with locale prefix handling.This gives you the same flexibility without the Symfony coupling.
Node without a NodeTranslation in the requested locale has no slug and therefore no route. Always create a default translation.online is per translation. A node can be published in English and invisible in French. This surprises people during QA./{_locale}/{slug} vs /{slug}) — changing later means rewriting every translation.AbstractPagePart subclasses, SinglePage/StructurePage subclasses, NodeManagerInterface, NodeRepository::getOnlineNodesForLocale(). Subclassing Node itself is possible but brittle — avoid.nodes, node_translations, and the page-part tables directly; inspect the generated route for a slug; check the translation's online value before blaming the controller.Node → NodeTranslation → Page → PagePart split is a genuinely good CMS model and maps cleanly onto Eloquent — copy the schema, not the code.How can I help you explore Laravel packages today?