# EVO Megamenu JSON An editable megamenu for headless TYPO3. One content element holds the whole menu; the extension publishes it as JSON, the front end renders it. Project-neutral by design — nothing in here knows which site it runs on. ## What the editor gets A plugin called **Megamenu**. Inside it: | Level | Holds | |---|---| | **Entry** | one item in the main bar: label, target page, panel layout | | **Column** | a heading (optionally a link) and its items | | **Item** | label, link, teaser line, badge, thumbnail, “page not ready” flag | | **Teaser card** | kicker, headline, text, link, image — for editorial panels | | **Featured** | one highlighted block per entry, edited on the entry itself | Five panel layouts per entry: `columns` for a curated structure menu, `teasers` for a small link column next to cards (news, stories), `pagetree` for a structure menu built live from the page tree, `catalog` for a data-driven panel whose content comes from the site package, and `spotlight` for a visual panel with a left-rail nav and live content cards (see below). ## Installation ```bash composer require evomedien/evo-megamenu-json vendor/bin/typo3 extension:setup vendor/bin/typo3 database:updateschema "*.add,*.change" ``` In the site configuration, add the set **EVO Megamenu (JSON)** and set: | Setting | Meaning | |---|---| | `megamenu.contentUid` | uid of the Megamenu element to publish on every page | | `megamenu.storagePid` | alternative: folder whose first Megamenu element is used | Leave both uid and pid at 0 and the menu is only rendered where the element sits — useful while building it. ## Payload ```json "megaMenu": { "settings": { "stage": 3, "openOn": "hover", "closeDelay": 140, "columnsPerRow": 4, "panelWidth": "container", "pendingBehaviour": "fallback", "showCounts": true, "showViewAll": true, "viewAllLabel": "View all %s" }, "entries": [ { "title": "Products", "link": "/products", "layout": "columns", "columns": [ { "title": "IP Video Streaming", "link": "/products/ip-video-streaming", "items": [ { "title": "Appliances", "link": "/products/ip-video-streaming/appliances", "pending": true, "teaser": "MGW Diamond, Ace, Pico", "image": { "url": "/fileadmin/...", "alternative": "", "title": "", "width": 600, "height": 400 } } ] } ], "featured": { "kicker": "Datasheet", "title": "MGW Diamond-H", "text": "…", "link": "/product/…", "image": { } } } ] } ``` `settings` travels with the menu on purpose: the front end reads hover behaviour, close delay, column count and labels from the payload instead of hard-coding them, so an editor can change them without a deploy. ### Content stages `stage` decides how much detail is emitted — 1 structure only, 2 adds images, teaser lines and cards, 3 adds the featured block. Later stages only **add** keys, so a front end written for stage 1 keeps working when the stage is raised. ### `pending` An item whose target page does not exist yet carries `"pending": true`. `settings.pendingBehaviour` says what to do with it: `fallback` (link to the column heading instead), `mute` (show, not clickable) or `hide`. This keeps a menu shippable while its deeper pages are still being built. ### Layout `catalog` For panels built from data rather than by hand — a product catalog with categories, groups and items, a search bar, whatever the site needs. The extension stays project-neutral: it dispatches `Evomedien\EvoMegamenuJson\Event\BuildCatalogEvent` for every catalog entry and emits whatever a listener sets via `setCatalog()` verbatim as `catalog` on the entry (`null` without a listener). The entry's teaser cards are emitted as `teasers` as usual and serve as the panel's featured tiles; its columns are ignored. ```php #[AsEventListener(identifier: 'site/megamenu-catalog')] final class ProvideCatalog { public function __invoke(BuildCatalogEvent $event): void { $event->setCatalog(['categories' => [/* … */]]); } } ``` `getEntry()` hands the listener the raw entry row (to tell several catalog entries apart), `getStage()` the content stage. ### Layout `pagetree` A structure panel that can never age against the site: the columns mirror the live subtree of the page the entry links to - one column per child page, its children as the items. Hidden or nav-hidden pages leave the menu with the next cache flush. Curated columns are ignored; teasers and the featured slot stay editorial. From stage 2 a child page with media gets its first image as `image` on the column - attach images in the page properties and the panel turns visual by itself. From stage 2 the items carry `image` (first page media or `null`) and, when set, the page `abstract` as `teaser`. The entry field *Page tree display* picks the presentation and travels as `display`: `columns` (default, columns side by side) or `rail` (the columns as a list on the left, the hovered column's items as image cards on the right) plus `cardSize` (`regular` | `compact`) for `rail`. A front end that ignores `display` keeps rendering columns. ### Layout `spotlight` A visual panel ("rossvideo style"): `nav` is a flat left rail built live from the linked page's children - each item with `title`, `link` and, from stage 2, the page `abstract` as `teaser` plus the first page media as `image`, so the front end can swap the visual area on hover. Next to it the extension emits `spotlight`, filled by the site package: it dispatches `Evomedien\EvoMegamenuJson\Event\BuildSpotlightEvent` per spotlight entry and emits whatever a listener sets via `setSpotlight()` verbatim (`null` without a listener). Curated columns are ignored. The VITEC site package ships `ProvideInsightsSpotlight`, which supplies ```json "spotlight": { "news": [ {"title", "date", "link", "teaser?", "image?"} ], "events": [ {"title", "start", "end", "venue", "booth", "city", "country", "attendanceMode", "link", "teaser?", "image?"} ] } ``` - `news`: the three latest news records (storage pid 11, type 0 - the Solr convention), `link` = `/news/article/` per the NewsDetail route enhancer, image from `fal_media` as processed 640px webp. - `events`: the next two events (`hideonwebsite` = 0, upcoming by eventstart/eventend), `link` = the record's `eventurl` or the events page, image processed the same way. ## Notes * Every link is a resolved URL — page, record, external or file, run through typolink at render time. * Translations are overlaid per record, so a menu can be localised entry by entry. * The renderer never throws: a broken menu returns an empty value rather than taking the page payload down. * Caching: every page carrying the menu is tagged with the menu tables and `tt_content_` of the menu element, so saving any menu record refreshes all of them. Panels built from the page tree (`pagetree`, `catalog`, `spotlight`) pick up page changes with the next cache flush. * Without `friendsoftypo3/headless` the element has no JSON envelope to render into — the extension is built for headless installations.