Thelia 3 support, menus addressed by code (2.0.0) - #21
Open
robinallezard wants to merge 19 commits into
Open
Conversation
added 19 commits
September 10, 2026 15:38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat!: Thelia 3 support, menus addressed by code (2.0.0)
What this does
CustomFrontMenu runs on Thelia 3.
module.xmlnow requires<thelia>3.0.0</thelia>and the module version goes to 2.0.0. The Smarty plugin and its templates are gone, the composition screen is rebuilt on thedefault-twigback-office, and the menu tree is exposed through API Platform instead of the OpenApi module, which Thelia 3 no longer ships.19 commits, 46 files, +2463 / -3187. The line count is negative because the old screen carried 1249 lines of hand-written JavaScript that the theme's own tree component replaces.
Breaking changes
A theme built on 1.x will not work as is.
{CustomFrontMenuPlugin menu_id=5}custom_front_menu('header').cfm-menuclassesGET /open_api/custom-front-menu/{id}GET /api/front/custom-front-menus/{code}The id came from an autoincrement shared with the entries, so it differed from one installation to the next and a theme written against it broke on the next shop. A code survives a reinstall, and the composition screen shows the exact call to copy.
The function answers nodes rather than markup because a navigation is where a theme's layout, breakpoints and interaction live. Integrators write the elements they want:
{% for item in custom_front_menu('header') %} <a href="{{ item.href }}">{{ item.title }}</a> {% for child in item.children %} <a href="{{ child.href }}">{{ child.title }}</a> {% endfor %} {% endfor %}Each node is
{id, title, href, children}, nested to any depth.hrefis alwayshttp(s), site-relative, or empty. An unknown code answers an empty list instead of raising, so a menu deleted in the back-office cannot take the storefront down.Migration from 1.2.0
Automatic, no manual step.
update()adds thecodecolumn and its unique index, gives every existing menu a code derived from its title, and clears the links that the 1.x screen let through.Every step asks the database what it looks like instead of trusting the recorded version, and can be run again.
ModuleManagementwrites the new version before it callsupdate(), and MySQL commits that write implicitly on the first DDL, so a failure half-way through would otherwise leave a shop recorded as 2.0.0 with nothing left to finish the job. The same work runs frompostActivation(), which makes deactivating and reactivating the module a real way out.The DDL goes through
information_schemarather thanADD COLUMN IF NOT EXISTS, which is a MariaDB extension that a shop on MySQL 8 would reject.Security
Two holes in 1.2.0 disappear with the files this branch deletes.
Validatorcalledaddslashes()before a Propelsave(), which double-escaped apostrophes in the database and explained thequotePatterngymnastics in the oldmain.js.CustomFrontMenuSaveServicefiltered URLs withFILTER_SANITIZE_URL, which only strips illegal characters and leavesjavascript:intact on a link rendered on every page.Service/MenuLinkreplaces both. It acceptshttp,httpsand site-relative paths, and rejects protocol-relative addresses://evil.comresolves tohttps://evil.comin a browser, and/\evil.comis normalised to the same thing by Chrome and Firefox, so both leave the shop while looking like an internal path.The filter runs on write and on read. Validating on write alone would protect the rows this version writes and nothing else, and a shop coming from 1.2.0 can carry a poisoned link into a menu served by a public API.
Back-office
The screen is rebuilt on
bo-category-tree, the tree component thedefault-twigtheme already ships, so drag and drop, the confirmation modal and the HTMX target field cost no module JavaScript. Entries are reparented either by dragging or from a select on the entry page, which is how you move one back up a level without aiming at a drop zone.Each action saves on its own. The 1.x screen held the whole tree in the browser and saved it by deleting and recreating every row, so a closed tab lost the work and every save renumbered the ids.
A menu code is validated rather than slugified: typing
Header!is refused with a message instead of silently becomingheader. An empty code field at creation derives one from the menu name.Verified
MODULEright on CustomFrontMenu.