From aad882bff762a8dd8a63ef7de22ce7587f722e5e Mon Sep 17 00:00:00 2001 From: Peter Mathis Date: Mon, 21 Sep 2026 11:09:42 +0200 Subject: [PATCH 1/4] add blicca customization training docs --- docs/blicca-custiomization-training/index.md | 104 +++++++ docs/blicca-custiomization-training/intro.md | 61 +++++ .../own-pattern.md | 105 ++++++++ .../pattern-options.md | 86 ++++++ .../production.md | 82 ++++++ .../replace-pattern.md | 159 +++++++++++ .../requirements.txt | 5 + docs/blicca-custiomization-training/setup.md | 134 +++++++++ .../stretch-folder-contents.md | 255 ++++++++++++++++++ .../svelte-override.md | 154 +++++++++++ docs/index.md | 1 + 11 files changed, 1146 insertions(+) create mode 100644 docs/blicca-custiomization-training/index.md create mode 100644 docs/blicca-custiomization-training/intro.md create mode 100644 docs/blicca-custiomization-training/own-pattern.md create mode 100644 docs/blicca-custiomization-training/pattern-options.md create mode 100644 docs/blicca-custiomization-training/production.md create mode 100644 docs/blicca-custiomization-training/replace-pattern.md create mode 100644 docs/blicca-custiomization-training/requirements.txt create mode 100644 docs/blicca-custiomization-training/setup.md create mode 100644 docs/blicca-custiomization-training/stretch-folder-contents.md create mode 100644 docs/blicca-custiomization-training/svelte-override.md diff --git a/docs/blicca-custiomization-training/index.md b/docs/blicca-custiomization-training/index.md new file mode 100644 index 000000000..6c138b6cd --- /dev/null +++ b/docs/blicca-custiomization-training/index.md @@ -0,0 +1,104 @@ +--- +myst: + html_meta: + "description": "Half-day training on customizing the Blicca (formerly Plone Classic UI) JavaScript stack: pattern options, custom patterns, replacing core patterns, and overriding Svelte components." + "property=og:description": "Half-day training on customizing the Blicca (formerly Plone Classic UI) JavaScript stack: pattern options, custom patterns, replacing core patterns, and overriding Svelte components." + "property=og:title": "Blicca JS stack insights — how to customize Mockup" + "keywords": "Plone, Blicca, Classic UI, Mockup, Patternslib, Svelte, JavaScript, training, customization" +--- + +(blicca-label)= + +# Blicca JS stack insights — how to customize Mockup + +Level +: Beginner to intermediate + +Blicca — Plone's server-rendered frontend, formerly known as Classic UI — ships a modern JavaScript stack. +It consists of Patternslib-based patterns, webpack module federation, and Svelte apps such as the content browser. +The stack is very customizable, if you know where the hooks are. + +In this half-day training, we build a small add-on that overrides the stack at every level, without forking `plone.staticresources` or Mockup. +The add-on [`blicca.staticresourceoverride`](https://github.com/collective/blicca.staticresourceoverride) is the reference implementation for this training and your safety net during the exercises. + +## What you will learn + +After this training, you can: + +1. Configure patterns site-wide via the `plone.patternoptions` registry record, without writing any JavaScript. +2. Build and register your own Patternslib pattern, and ship it as a module federation remote bundle. +3. Replace a core pattern using the pattern blacklist, while reusing the original implementation and its options. +4. Override a Svelte component of the content browser via the shared `@plone/registry`. +5. Explain how the Blicca JS stack loads, registers, and shares code, and debug it when it doesn't. + +## Prerequisites + +You need basic Plone knowledge, such as installing add-ons and working with GenericSetup profiles. +JavaScript basics with ES6 syntax and module imports are enough. +No Svelte experience is required. + +Bring a laptop with the following software installed: + +- The prerequisites from the Plone documentation, {doc}`Create a project with Cookieplone `: uv, Make, and Git +- Node.js 22 or later +- pnpm, where `corepack enable` is all it takes, as the version is pinned in the project's `package.json` +- A code editor + +```{tip} +Run the setup from {ref}`blicca-setup-label` before the training. +This warms your package caches and saves conference wifi. +``` + +## How to use the step tags + +The git history of the training repository mirrors the four training chapters. +Each tag is a working state of the add-on after the corresponding chapter: + +| Tag | State after | +| -------- | ----------------------------------------------------------------- | +| `step-1` | Pattern options via the registry only, no JavaScript build yet | +| `step-2` | Own pattern `pat-blicca` plus webpack and module federation setup | +| `step-3` | Core pattern `markspeciallinks` replaced via the blacklist | +| `step-4` | Svelte `SelectedItem` override, identical to `main` | + +If you fall behind, jump to the current step and continue from there: + +```shell +git checkout step-2 +pnpm install && pnpm run build +``` + +Reinstall the add-on, or reimport its profile, after switching steps, so that new registry records are applied. + +## How to update your checkout + +We keep improving the material until the training starts. +The `main` branch only moves forward, but the step tags are re-pointed whenever the material changes, and a plain `git fetch` does not update tags that already exist locally. +Update the source checkout in your project like this: + +```shell +cd backend/sources/blicca.staticresourceoverride +git stash +git fetch --force --tags origin +git checkout main +git reset --hard origin/main +``` + +`git stash` saves your own changes from the exercises, so that you can restore them later with `git stash pop`. +`git reset --hard` discards everything that is not on `origin/main`. +Afterwards, run `pnpm install && pnpm run build` again, and reinstall the add-on. + +```{toctree} +:caption: Chapters +:maxdepth: 1 +:numbered: + +intro +setup +pattern-options +own-pattern +replace-pattern +svelte-override +stretch-folder-contents +production +``` diff --git a/docs/blicca-custiomization-training/intro.md b/docs/blicca-custiomization-training/intro.md new file mode 100644 index 000000000..5abd16de0 --- /dev/null +++ b/docs/blicca-custiomization-training/intro.md @@ -0,0 +1,61 @@ +--- +myst: + html_meta: + "description": "How the Blicca JavaScript stack works: Mockup, the Patternslib registry, module federation, and Svelte components." + "property=og:description": "How the Blicca JavaScript stack works: Mockup, the Patternslib registry, module federation, and Svelte components." + "property=og:title": "The Blicca JS stack in a nutshell" + "keywords": "Plone, Blicca, Mockup, Patternslib, module federation, Svelte, registry" +--- + +(blicca-intro-label)= + +# The Blicca JS stack in a nutshell + +Before we override anything, let's look at the moving parts. +This chapter is a guided tour with the browser's developer tools, not a slide deck. + +## Mockup and the Plone bundle + +[Mockup](https://github.com/plone/mockup) is the JavaScript package behind the Plone bundle. +The bundle is shipped by `plone.staticresources` as `++plone++static/bundle-plone/bundle.min.js`, and registered in the resource registry under the name `plone`. + +## Patterns and the registry + +Patterns are registered in the Patternslib registry. +During the DOM scan, each pattern initializes on the elements that match its trigger, which is a plain CSS selector such as `.pat-tinymce`. + +The registry follows one important rule. +**First registration wins.** +A pattern name can only be registered once, and later attempts are ignored. +Remember this rule for {ref}`blicca-replace-pattern-label`. + +## Module federation + +The Plone bundle is a module federation host. +Add-on bundles are remotes. +The host initializes each remote automatically on document-ready and shares core modules with it, including the Patternslib registry, `@plone/registry`, jQuery, and Bootstrap. +This way, the add-on and the core talk to the same registry instances. + +## Svelte components + +Newer patterns, such as `pat-contentbrowser`, are Svelte apps. +They pull some of their sub-components from the `@plone/registry` component registry. +That is exactly where add-ons can hook in their own components, as we do in {ref}`blicca-svelte-override-label`. + +Why Svelte, and not React? +Svelte compiles components to small, plain JavaScript without a virtual DOM, so the runtime footprint stays small. +That is a good fit for a server-rendered UI that is progressively enhanced with JavaScript. +Components read like HTML, CSS, and JavaScript, which keeps the learning curve flat for integrators. +And Blicca deliberately stays free of React dependencies: it does not need Volto's stack to render a widget. + +## See it live + +Open your browser's developer tools on any Plone page, and observe the stack at work: + +- The `` element carries `data-pat-*` attributes with global pattern options. +- The network tab shows the bundle and its lazily loaded chunks. +- The console logs a message for every initialized module federation bundle: + +```console +Patternslib Module Federation: Loaded and initialized bundle "__patternslib_mf__bliccastaticresourceoverride". +``` diff --git a/docs/blicca-custiomization-training/own-pattern.md b/docs/blicca-custiomization-training/own-pattern.md new file mode 100644 index 000000000..511862d2b --- /dev/null +++ b/docs/blicca-custiomization-training/own-pattern.md @@ -0,0 +1,105 @@ +--- +myst: + html_meta: + "description": "Build and register your own Patternslib pattern for Blicca, and ship it as a module federation remote bundle." + "property=og:description": "Build and register your own Patternslib pattern for Blicca, and ship it as a module federation remote bundle." + "property=og:title": "Your own pattern" + "keywords": "Plone, Blicca, Patternslib, BasePattern, webpack, module federation, bundle" +--- + +(blicca-own-pattern-label)= + +# Your own pattern + +In this chapter, we write a pattern from scratch, and ship it in our own bundle. +At the end, a badge appears on every element that carries the class `pat-blicca`. + +## A class-based pattern + +The file {file}`resources/pat-blicca/blicca.js` contains a pattern in the current Patternslib style: + +```js +import { BasePattern } from "@patternslib/patternslib/src/core/basepattern"; +import Parser from "@patternslib/patternslib/src/core/parser"; +import registry from "@patternslib/patternslib/src/core/registry"; + +export const parser = new Parser("blicca"); +parser.addArgument("color", "#0083be"); +parser.addArgument("label", "Blicca override active"); + +class Pattern extends BasePattern { + static name = "blicca"; + static trigger = ".pat-blicca"; + static parser = parser; + + init() { + const badge = document.createElement("span"); + badge.textContent = `★ ${this.options.label}`; + this.el.style.outline = `2px dashed ${this.options.color}`; + this.el.append(badge); + } +} + +registry.register(Pattern); +``` + +Three things matter here: + +- The `trigger` is a plain CSS selector. +- The `Parser` declares the options, and fills them from `data-pat-blicca` attributes, including inheritance from parent elements. +- `registry.register(Pattern)` makes the registry scan the document, including content that arrives later through modals, `pat-inject`, or the folder contents view. + +## The bundle around it + +The webpack setup comes from `@patternslib/dev`, and the module federation plugin turns the bundle into a remote. +The entry point {file}`resources/index.js` only contains a dynamic import, exported as default: + +```js +export default import("./overrides"); +``` + +The dynamic import creates a split point. +Webpack needs it to negotiate the shared dependencies with the Plone bundle at runtime, before our code runs. +The default export matters, too. +Since Mockup 5.6.13, the module federation helper of the Plone bundle awaits the exported promise of every remote, and the Patternslib registry, since 9.11, waits for that before its initial scan of the page. +Your patterns and components are therefore registered before the first scan, no matter how fast the remote loads. + +The profile registers the built bundle in {file}`profiles/default/registry/bundles.xml`: + +```xml + + True + ++plone++blicca.staticresourceoverride/bundles/blicca.staticresourceoverride-remote.min.js + plone + +``` + +The `depends` value makes sure that the bundle loads after the host. + +## Exercise + +Start the watcher: + +```shell +pnpm run watch +``` + +Edit a page, and give a paragraph the class `pat-blicca` in the TinyMCE source view. +Save and observe the badge and the dashed outline. + +Now pass options through the markup: + +```html +

With option.

+``` + +Then make it your own. +Add a new option to the parser, and use it in `init()`. + +## Checkpoint + +The badge and the outline appear on your paragraph. +Changing `data-pat-blicca` changes the result. diff --git a/docs/blicca-custiomization-training/pattern-options.md b/docs/blicca-custiomization-training/pattern-options.md new file mode 100644 index 000000000..36e6090c8 --- /dev/null +++ b/docs/blicca-custiomization-training/pattern-options.md @@ -0,0 +1,86 @@ +--- +myst: + html_meta: + "description": "Configure Blicca patterns site-wide with the plone.patternoptions registry record, without writing any JavaScript." + "property=og:description": "Configure Blicca patterns site-wide with the plone.patternoptions registry record, without writing any JavaScript." + "property=og:title": "Overrides without JavaScript" + "keywords": "Plone, Blicca, patternoptions, registry, pattern options, markspeciallinks" +--- + +(blicca-pattern-options-label)= + +# Overrides without JavaScript + +Many customizations don't need a single line of JavaScript. +In this chapter, we make external links open in a new window, site-wide, with pure XML. + +## How pattern options travel + +The registry record `plone.patternoptions` is a dictionary that maps a pattern name to a JSON string of options. +Plone renders each entry as a `data-pat-` attribute on the `` element. +The pattern's options parser walks up the DOM tree, so every pattern element inherits these options. + +Plone itself uses this mechanism, and configures `pickadate` and `plone-modal` this way. + +## Exercise: change options through the web + +Open {menuselection}`Site Setup --> Management --> Configuration Registry` and search for `plone.patternoptions`. +Add an entry with the key `markspeciallinks` and the value: + +```json +{ "external_links_open_new_window": "true" } +``` + +Reload the page, and inspect the `` element in the developer tools. +Notice the new `data-pat-markspeciallinks` attribute. +External links in the content area now open in a new window. + +## Persist it in a GenericSetup profile + +A through-the-web change lives in the database only. +We persist it in the add-on's profile instead, in {file}`profiles/default/registry/patternoptions.xml`: + +```xml + + + + + {"external_links_open_new_window": "true"} + + + +``` + +```{important} +Keep `purge="false"`. +Without it, the import wipes the entries of the Plone core and of other add-ons. +``` + +## The trigger class catch + +Options alone don't run a pattern. +The trigger class must be present in the DOM. + +The `pat-markspeciallinks` class on the `` is only rendered when one of Plone's link settings is enabled. +That's why the profile also enables `plone.mark_special_links` in {file}`profiles/default/registry/linksettings.xml`: + +```xml + + + + True + + +``` + +When a pattern "does nothing", first check the trigger, then the options. + +## Precedence + +An entry in `plone.patternoptions` overrides the `` attribute that Plone's `IPatternsSettings` adapters render for the same pattern. +This includes the link settings from the control panel. +This is powerful, so be deliberate. + +## Checkpoint + +External links open in a new tab, and you did not run any build. diff --git a/docs/blicca-custiomization-training/production.md b/docs/blicca-custiomization-training/production.md new file mode 100644 index 000000000..f549bf313 --- /dev/null +++ b/docs/blicca-custiomization-training/production.md @@ -0,0 +1,82 @@ +--- +myst: + html_meta: + "description": "Production notes for Blicca add-on bundles: version pinning, clean uninstall, and known pitfalls." + "property=og:description": "Production notes for Blicca add-on bundles: version pinning, clean uninstall, and known pitfalls." + "property=og:title": "Production notes and pitfalls" + "keywords": "Plone, Blicca, production, version pinning, uninstall, pitfalls" +--- + +(blicca-production-label)= + +# Production notes and pitfalls + +Your overrides work. +This chapter collects what you need to know before you ship them. + +## Version pinning + +The versions of `@plone/mockup` and `@patternslib/patternslib` in your `package.json` must match the Mockup version that `plone.staticresources` ships. +Module federation negotiates shared modules through version ranges. +If the versions drift too far apart, module federation loads two instances, and registrations end up in the wrong registry. + +On every Plone upgrade, align the versions, and rebuild the bundle. +Check the browser console for module federation warnings. + +Some techniques also need a minimum Mockup version in the Plone bundle. +The shared Svelte runtime needs Mockup 5.6.9, the override under the default component key needs Mockup 5.6.11, and this add-on is built against Mockup 5.6.14 with Patternslib 9.11. + +## Clean uninstall + +The uninstall profile removes what the default profile added: + +- Bundle records are removed with `remove="true"` in {file}`profiles/uninstall/registry/bundles.xml`. +- Whole records, such as `plone.mark_special_links`, are reset with a plain value. +- Single dictionary keys, such as our entries in `plone.patternoptions`, cannot be removed declaratively. + The `post_uninstall` handler in {file}`setuphandlers.py` removes them in Python: + +```python +PATTERN_OPTION_KEYS = ("markspeciallinks", "contentbrowser") + + +def post_uninstall(context): + registry = getUtility(IRegistry) + options = dict(registry.get("plone.patternoptions") or {}) + remaining = {k: v for k, v in options.items() if k not in PATTERN_OPTION_KEYS} + if remaining != options: + registry["plone.patternoptions"] = remaining +``` + +## Known pitfalls + +Forgotten build +: `static/bundles/` is empty, and the remote bundle returns a 404 error. +Run `pnpm run build` first, then install. + +Missing trigger class +: Options alone don't run a pattern. +When a pattern "does nothing", first check the trigger, then the options. + +Missing `purge="false"` +: Without it, your `plone.patternoptions` import overwrites the options of the Plone core and of other add-ons. + +Two Svelte runtimes +: The selection list renders an empty slot, and the console shows `Cannot read properties of null (reading 'nodes')`. +The host and the add-on don't share the Svelte runtime. +Either the Plone bundle is older than Mockup 5.6.9, or the `svelte` and `svelte/` shares are missing in your webpack configuration. +See {ref}`blicca-svelte-override-label`. + +The default component wins again +: You registered your component under the default key, but the content browser still renders the original. +The Plone bundle is older than Mockup 5.6.11, which re-registered the default component on every widget initialization. +Register your component under a custom key, and activate it with `componentRegistryKeys`, see {ref}`blicca-svelte-override-scoping-label`. + +Lazy component registration +: With a typo in a custom registry key, the content browser silently falls back to the default component. + +## Where to go next + +- The [Patternslib documentation](https://patternslib.com/) for the pattern API. +- The [Mockup source](https://github.com/plone/mockup) as a cookbook of real-world patterns. +- The [plone.staticresources source](https://github.com/plone/plone.staticresources) for how the core bundle is built and registered. +- {doc}`Mockup and Patternslib in the Plone documentation `. diff --git a/docs/blicca-custiomization-training/replace-pattern.md b/docs/blicca-custiomization-training/replace-pattern.md new file mode 100644 index 000000000..f3a9c0ede --- /dev/null +++ b/docs/blicca-custiomization-training/replace-pattern.md @@ -0,0 +1,159 @@ +--- +myst: + html_meta: + "description": "Replace a Blicca core pattern with your own implementation, using the Patternslib pattern blacklist." + "property=og:description": "Replace a Blicca core pattern with your own implementation, using the Patternslib pattern blacklist." + "property=og:title": "Replacing a core pattern" + "keywords": "Plone, Blicca, Patternslib, blacklist, markspeciallinks, override" +--- + +(blicca-replace-pattern-label)= + +# Replacing a core pattern + +Sometimes tweaking options is not enough, and you want your own implementation of a core pattern. +In this chapter, we replace `markspeciallinks`, and external links get a different icon. + +## Why load order is not enough + +The registry rule from {ref}`blicca-intro-label` applies. +**First registration wins.** +Mockup registers its patterns in an asynchronously loaded chunk, so the exact timing between the host and your remote is not guaranteed. +Racing the host is not a strategy. + +Patternslib provides an official switch instead: the pattern blacklist. + +```{note} +Patternslib 9.11, shipped with Mockup 5.6.12 and later, adds a second switch, the `replace` option. +See {ref}`blicca-replace-option-label` at the end of this chapter. +``` + +## The blacklist preload + +The file {file}`static/pattern-blacklist.js` is a tiny, unbuilt JavaScript file: + +```js +window.__patternslib_patterns_blacklist = ( + window.__patternslib_patterns_blacklist || [] +).concat(["markspeciallinks"]); +``` + +The profile registers it as its own bundle, without a `depends` value: + +```xml + + True + ++plone++blicca.staticresourceoverride/pattern-blacklist.js + +``` + +It renders before the Plone bundle and runs synchronously, while Mockup registers its patterns in an async chunk. +The original pattern therefore never gets registered. + +Two things make this ordering reliable. +First, the `depends` field only knows "after": an empty value means "as early as possible", and `*` means "after all other bundles", so `depends="*"` would be exactly the wrong choice here. +Bundles without dependencies render in the alphabetical order of their registry record names, and `plone.bundles/blicca-preload` sorts before `plone.bundles/plone`. +Second, even a synchronous script that renders after the Plone bundle still runs before Mockup's async pattern chunk. +The blacklist takes effect at registration time, so the position among the synchronous scripts does not matter. + +```{note} +Bundles are just files. +This one needs no build at all. +``` + +## The replacement + +The blacklist blocks any registration under the blocked name, including yours. +Therefore, the replacement in {file}`resources/markspeciallinks/markspeciallinks.js` registers under its own name, but with the original trigger: + +```js +import $ from "jquery"; +import mockupParser from "@patternslib/patternslib/src/core/mockup-parser"; +import MarkSpecialLinks from "@plone/mockup/src/pat/markspeciallinks/markspeciallinks"; + +export default MarkSpecialLinks.extend({ + name: "blicca-markspeciallinks", + trigger: ".pat-markspeciallinks", + parser: null, + + async init() { + this.options = $.extend( + true, + {}, + this.defaults, + mockupParser.getOptions(this.el, "markspeciallinks"), + ); + this.protocol_icon_map = { + ...this.protocol_icon_map, + https: "box-arrow-up-right", + http: "box-arrow-up-right", + }; + return this.constructor.__super__.init.call(this); + }, +}); +``` + +Note the three tricks: + +- We extend the original class, and reuse its whole implementation. +- We register under the name `blicca-markspeciallinks`, with the original trigger `.pat-markspeciallinks`. +- The Mockup parser reads options based on the pattern name, and our name differs. + Therefore, `init()` fetches the original's options itself with `mockupParser.getOptions()`, including the inheritance from the ``. + +(blicca-replace-option-label)= + +## The `replace` option + +Since Patternslib 9.11, shipped with Mockup 5.6.12 and later, the registry can replace a registration: + +```js +import $ from "jquery"; +import registry from "@patternslib/patternslib/src/core/registry"; +import MarkSpecialLinks from "@plone/mockup/src/pat/markspeciallinks/markspeciallinks"; + +export default MarkSpecialLinks.extend({ + // Same name, same trigger, and the options keep working. + name: "markspeciallinks", + trigger: ".pat-markspeciallinks", + replace: true, + + async init() { + this.protocol_icon_map = { + ...this.protocol_icon_map, + https: "box-arrow-up-right", + http: "box-arrow-up-right", + }; + return this.constructor.__super__.init.call(this); + }, +}); +``` + +For class-based patterns, pass the option to the registry: `registry.register(Pattern, Pattern.name, { replace: true })`. + +Compared with the blacklist recipe, two of the three building blocks disappear. +There is no preload bundle, and no options bridge, because the pattern keeps its name and the Mockup parser reads `data-pat-markspeciallinks` as before. +It works because the registry now waits for the module federation remotes before its initial scan, see {ref}`blicca-own-pattern-label`. +The replacement is in place for the first scan, no matter whether the Plone bundle or your add-on registered first. +If a replacement arrives after the registry was initialized, the registry logs a warning: elements that were already initialized keep the previous pattern, only new elements get the replacement. + +The blacklist still wins over a replacement, so both switches can coexist. +This chapter keeps the blacklist recipe as the main path, because it works on every Mockup 5 bundle and because it makes registration, names, and triggers visible. +Once your site runs Mockup 5.6.12 or later, the `replace` option is the shorter way. + +## Exercise + +Change the icon that external links get. +Pick any name from [Bootstrap Icons](https://icons.getbootstrap.com/). +As a bonus, additionally set `rel="noopener noreferrer"` on external links. + +## Checkpoint + +External links show your icon. +The console confirms that the original was skipped: + +```console +registry: Pattern name markspeciallinks is blacklisted. +``` diff --git a/docs/blicca-custiomization-training/requirements.txt b/docs/blicca-custiomization-training/requirements.txt new file mode 100644 index 000000000..8fca1bc5d --- /dev/null +++ b/docs/blicca-custiomization-training/requirements.txt @@ -0,0 +1,5 @@ +plone-sphinx-theme +myst-parser +linkify-it-py +sphinx-copybutton +sphinx-design diff --git a/docs/blicca-custiomization-training/setup.md b/docs/blicca-custiomization-training/setup.md new file mode 100644 index 000000000..465b873de --- /dev/null +++ b/docs/blicca-custiomization-training/setup.md @@ -0,0 +1,134 @@ +--- +myst: + html_meta: + "description": "Set up a Plone project with Blicca and the blicca.staticresourceoverride training add-on." + "property=og:description": "Set up a Plone project with Blicca and the blicca.staticresourceoverride training add-on." + "property=og:title": "Setup" + "keywords": "Plone, Blicca, Cookieplone, mxdev, uv, pnpm, installation, training setup" +--- + +(blicca-setup-label)= + +# Setup + +In this chapter, we create a Plone project with Blicca, and install the training add-on. +At the end, your browser console proves that your first own bundle is loaded. + +## Install the prerequisites + +Install the prerequisites from the Plone documentation, {doc}`Create a project with Cookieplone `: uv, Make, and Git. +The add-on's JavaScript build additionally needs Node.js 22 or later, and pnpm. +Enable pnpm with `corepack enable`, as the version is pinned in the add-on's {file}`package.json`. + +## Create a Plone project with Blicca + +Cookieplone 2.0 no longer has a separate template for Blicca. +Generate a project with the `project` template, and answer the question `Use Volto as frontend?` with `No`: + +```shell +uvx cookieplone project +``` + +Cookieplone asks 18 questions. +The following answers matter for the training, keep the defaults for the rest: + +Plone Version +: `6.2.2` or later. + +Use Volto as frontend? +: `No`, so that Cookieplone generates a Blicca project without a frontend. + +Should we setup a caching server?, Add Ansible playbooks?, Add GitHub Action to Deploy this project?, Would you like to add a documentation scaffold to your project? +: `No`, to keep the project small. + +Then install and start the backend: + +```shell +cd +make install +make backend-start +``` + +The installation creates a virtual environment with uv, and a Plone site named `Plone`. +Your site now runs at `http://localhost:8080/Plone` with the login `admin` and password `admin`. + +## Add the add-on as a source checkout + +The generated project keeps the Plone backend in the {file}`backend` directory, with {file}`mx.ini` and {file}`pyproject.toml`. +Cookieplone projects manage source checkouts with [mxdev](https://github.com/mxstack/mxdev). +Add the training add-on to {file}`backend/mx.ini`, and pin `plone.staticresources` to a release with Mockup 5.6.14 or later in the same file: + +```ini +[settings] +main-package = -e .[test] +version-overrides = + plone.staticresources==3.0.9 + +[blicca.staticresourceoverride] +url = https://github.com/collective/blicca.staticresourceoverride.git +branch = main +``` + +Then add the add-on to the `dependencies` of {file}`backend/pyproject.toml`: + +```toml +dependencies = [ + "Products.CMFPlone==6.2.2", + "blicca.staticresourceoverride", + "plone.api", + "plone.restapi", + "plone.classicui", + "plone.app.caching", + "z3c.jbot", +] +``` + +Run the installation again: + +```shell +make install +``` + +mxdev clones the repository into {file}`backend/sources/blicca.staticresourceoverride`, registers it as an editable package in the `tool.uv.sources` table of {file}`pyproject.toml`, and turns the version override into a uv `override-dependencies` entry. +uv then installs everything. + +```{note} +The `version-overrides` entry is what gives you Mockup 5.6.14, the version this add-on is built against. +Plone 6.2.2 ships `plone.staticresources` 3.0.6 with Mockup 5.6.10, which is enough for the first three chapters. +{ref}`blicca-svelte-override-label` needs the default component key from Mockup 5.6.11, and the stretch goal is written for the row scanning of Mockup 5.6.14. +``` + +## Build the JavaScript + +Build the add-on's JavaScript inside the source checkout: + +```shell +cd backend/sources/blicca.staticresourceoverride +pnpm install +pnpm run build +``` + +Start the backend again with `make backend-start`. +Then install {guilabel}`Blicca Static Resource Override (Training)` in the add-ons control panel. + +## The pnpm caveats + +The package manager is pnpm, the same as Mockup itself uses. +The file `pnpm-workspace.yaml` mirrors the known caveats of [plone/mockup](https://github.com/plone/mockup): + +- `shamefullyHoist: true`, because webpack module resolution needs a flat `node_modules` directory. +- `overrides` that remove the git subdependencies `slick-carousel`, `slides`, and `select2`, because pnpm blocks exotic subdependencies. + Only patterns that this add-on does not import need them. +- An `allowBuilds` allowlist, because pnpm 10 and later block dependency build scripts by default. + +## Success check + +Open any page of your site, and open the browser console. +You should see the following message: + +```console +Patternslib Module Federation: Loaded and initialized bundle "__patternslib_mf__bliccastaticresourceoverride". +``` + +The Plone bundle, the host, has found and initialized your add-on bundle, the remote. +Now we can start overriding things. diff --git a/docs/blicca-custiomization-training/stretch-folder-contents.md b/docs/blicca-custiomization-training/stretch-folder-contents.md new file mode 100644 index 000000000..7093fbc78 --- /dev/null +++ b/docs/blicca-custiomization-training/stretch-folder-contents.md @@ -0,0 +1,255 @@ +--- +myst: + html_meta: + "description": "Stretch goal: customize the row actions of the Blicca folder contents, open edit in a modal, and add an image cropping action." + "property=og:description": "Stretch goal: customize the row actions of the Blicca folder contents, open edit in a modal, and add an image cropping action." + "property=og:title": "Stretch goal: folder contents row actions" + "keywords": "Plone, Blicca, pat-structure, folder contents, action menu, menuOptions, modal, image cropping" +--- + +(blicca-stretch-folder-contents-label)= + +# Stretch goal: folder contents row actions + +This chapter is for fast participants, and it is a real customer request. +The folder contents view, the `pat-structure` pattern, shows an action menu in every row: Open, Edit, and a dropdown with Cut, Copy, Paste, and more. +The customer wants two changes: + +- Edit opens in a modal, instead of leaving the folder contents. +- Images get an additional action that opens the image cropping editor of [plone.app.imagecropping](https://github.com/plone/plone.app.imagecropping), also in a modal. + +It combines {ref}`blicca-replace-pattern-label` with a look under the hood of a Backbone-based pattern. + +## Why the `menuOptions` option is not enough + +The pattern has an option `menuOptions`, and it looks like the answer. +It is not. +Mockup builds the menu per row in {file}`src/pat/structure/js/actionmenu.js`: + +```js +const ActionMenu = function (menu) { + // If an explicit menu was specified as an option to AppView, this + // constructor will not override that. + if (menu.app.menuOptions !== null) { + return menu.app.menuOptions; + } + const model = menu.model.attributes; + ... + result.openItem.url = model.getURL + viewAction; + result.editItem.url = model.getURL + "/edit"; + return result; +}; +``` + +With `menuOptions` set, the generator returns your static definition for every row. +The per-row logic is skipped: the Open and Edit links get no URL, and Paste, Move, and "Set as default page" no longer depend on the item. +You could remove actions that way, but you can't add a per-item link. + +## The recipe + +The menu is generated in the `initialize` method of the `ActionMenuView`. +That is where we hook in. +Module federation shares only a few core modules between the Plone bundle and our add-on, so our add-on ships its own copy of the structure app. +We can patch the `ActionMenuView` of that copy, but only if our copy of the pattern is the one that runs. +This is the blacklist recipe from {ref}`blicca-replace-pattern-label`. + +Add `structure` to {file}`static/pattern-blacklist.js`: + +```js +window.__patternslib_patterns_blacklist = ( + window.__patternslib_patterns_blacklist || [] +).concat(["markspeciallinks", "structure"]); +``` + +Create {file}`resources/structure/structure.js`: + +```js +import $ from "jquery"; +import mockupParser from "@patternslib/patternslib/src/core/mockup-parser"; +import Structure from "@plone/mockup/src/pat/structure/structure"; +import ActionMenuView from "@plone/mockup/src/pat/structure/js/views/actionmenu"; +import utils from "@plone/mockup/src/core/utils"; + +// Mockup resolves menu icons while rendering the row, without awaiting the +// first fetch. Warm the icon cache for our new icon, so that the first row +// already shows it instead of the title text. +utils.resolveIcon("crop"); + +const original_initialize = ActionMenuView.prototype.initialize; +ActionMenuView.prototype.initialize = function (options) { + original_initialize.call(this, options); + + // this.menuOptions is the generated menu for THIS row: Paste, Move and + // "Set as default page" are already filtered, the URLs are resolved. + const item = this.model.attributes; + + // 1. Open the edit form in a modal. + this.menuOptions.editItem.css = "pat-plone-modal"; + + // 2. Add the cropping editor for images, also in a modal. + if (item.portal_type === "Image") { + this.menuOptions.cropItem = { + url: `${item.getURL}/@@croppingeditor`, + title: "Crop image", + category: "button", + icon: "crop", + css: "pat-plone-modal", + modal: false, + }; + } + + // Re-bind the click handlers, in case you add or remove entries with a + // ``method``. Methods must exist in src/pat/structure/js/actions.js. + this.events = this.generate_events(); + this.delegateEvents(); +}; + +export default Structure.extend({ + name: "blicca-structure", + trigger: ".pat-structure", + parser: null, + + async init() { + // Take over the options of the original, ``data-pat-structure``. + this.options = $.extend( + true, + {}, + this.defaults, + mockupParser.getOptions(this.el, "structure"), + ); + return this.constructor.__super__.init.call(this); + }, +}); +``` + +Import it in {file}`resources/overrides.js`, next to the `markspeciallinks` replacement. + +Each menu entry has the same shape: `url`, `title`, `category`, `icon`, `css`, and `modal`. +The category `button` renders the entry next to Open and Edit, `dropdown` puts it into the gear menu. +The `css` classes end up on the link, so `pat-plone-modal` opens it in a modal. +Entries with a `method` call a method of `src/pat/structure/js/actions.js`, such as `cutClicked` or `moveTopClicked`. + +```{note} +The `modal: true` flag looks like the official way, but it has no effect in Mockup 5.6. +The view appends the modal class after it has built the class list of the entry. +Set the `css` class yourself, as the comment in the view suggests. +``` + +## The pnpm caveat + +The structure app imports `pat-select2`, and with it the patched select2 fork that Mockup installs from git. +Our {file}`pnpm-workspace.yaml` removes that fork on purpose, see {ref}`blicca-setup-label`. +For this stretch goal, allow it: + +- Remove the `"select2": "-"` line from `overrides`. +- Set `"@plone/mockup": true` in `allowBuilds`. +- Add `blockExoticSubdeps: false`. + +Then run `pnpm install` and `pnpm run build` again. +Mockup's postinstall script tries to patch select2, and doesn't find it in the pnpm store. +The unpatched fork only differs in how already selected items are highlighted in the related items widget, which the folder contents don't use. + +## A lighter alternative: a pattern on the action menu + +The prototype patch changes the generated menu itself. +If all you need is to decorate the rendered menu, add a class here, add a link there, a small pattern as in {ref}`blicca-own-pattern-label` does the job. +No blacklist, no copy of the structure app, no change to the pnpm configuration. + +The hook is the registry scan that Mockup runs on every rendered row, so that the tooltips and modals on the buttons initialize. +Since Mockup 5.6.14, the row view, {file}`src/pat/structure/js/views/tablerow.js`, scans the row once it is attached to the document, and the table view scans all newly inserted rows: + +```js +this.el.model = this.model; + +const menuview = new ActionMenuView({ app: this.app, model: this.model }); +$(".actionmenu-container", this.$el).append(await menuview.render()); + +// Patterns inherit configuration from ancestors, so initialize only +// after attachment. TableView scans newly inserted rows; rows rendered +// again after context-info updates are already in the document. +if (this.el.isConnected) { + registry.scan(this.$el); +} +``` + +The registry is shared with our add-on, so a pattern of ours with a matching trigger runs for every row, and again whenever the rows re-render on paging, sorting, or a folder change. +The row keeps the Backbone model of the item on its DOM element, so the pattern can read `portal_type` and `getURL` from there. + +```{note} +Before Mockup 5.6.14, the `ActionMenuView` scanned the menu itself, at the end of its `render()` method, while the menu was still detached from the table. +A trigger like `.pat-structure .actionmenu` did not match there, and `closest("tr")` found nothing. +The pattern below works with both versions: its trigger matches the menu element itself, and it waits a tick before it looks for the row. +``` + +Create {file}`resources/folder-contents-actions/actions.js`: + +```js +import { BasePattern } from "@patternslib/patternslib/src/core/basepattern"; +import registry from "@patternslib/patternslib/src/core/registry"; +import utils from "@plone/mockup/src/core/utils"; + +class Pattern extends BasePattern { + static name = "blicca-folder-contents-actions"; + // Match the menu element itself: before Mockup 5.6.14 the menu was + // scanned while still detached from the table, so a descendant selector + // of ".pat-structure" would not match there. + static trigger = ".btn-group.actionmenu"; + + async init() { + // Wait a tick, so that the menu is appended to its row on Mockup + // versions that scan the menu before attaching it. + await new Promise((resolve) => setTimeout(resolve)); + const row = this.el.closest(".pat-structure tr"); + // pat-structure stores the Backbone model of the item on its row. + const item = row?.model?.attributes; + if (!item) { + return; + } + + // 1. Open the edit form in a modal. + const edit = this.el.querySelector("a.editItem"); + if (edit) { + edit.classList.add("pat-plone-modal"); + registry.scan(edit); + } + + // 2. Add the cropping editor for images, also in a modal. + if (item.portal_type === "Image" && edit) { + const crop = document.createElement("a"); + crop.className = "btn btn-sm action cropItem pat-plone-modal"; + crop.href = `${item.getURL}/@@croppingeditor`; + crop.title = "Crop image"; + crop.setAttribute("aria-label", "Crop image"); + crop.innerHTML = await utils.resolveIcon("crop"); + edit.after(crop); + registry.scan(crop); + } + } +} + +registry.register(Pattern); +export default Pattern; +``` + +Import it in {file}`resources/overrides.js` instead of the structure replacement, and rebuild. +`registry.scan(link)` initializes the modal pattern on the changed link, and `utils.resolveIcon()` fetches an icon from Plone's icon resolver by its registered name. + +Use one variant or the other, not both: with both active, the image row gets two crop buttons. +The solution branches are `stretch-folder-contents` for the prototype patch, and `stretch-folder-contents-pattern` for this variant. + +## Checkpoint + +Open the folder contents of a folder with an image. +Edit opens in a modal for every item, and the image row has a crop button that opens the cropping editor in a modal. +Change into a subfolder and back: the rows re-render, and your changes are there again. +With the prototype patch, also cut an item with the gear menu: the folder rows now offer Paste, so the re-bound click handlers work. + +```{note} +The cropping action needs `plone.app.imagecropping` installed in your project. +Without it, the link returns a 404 error. +``` + +```{warning} +The structure app pulls a lot of code into your bundle, around 90 KB for the structure chunk alone, plus its dependencies. +Keep such a customization in the customer's add-on, and check the bundle size before you ship it. +``` diff --git a/docs/blicca-custiomization-training/svelte-override.md b/docs/blicca-custiomization-training/svelte-override.md new file mode 100644 index 000000000..c6c65d963 --- /dev/null +++ b/docs/blicca-custiomization-training/svelte-override.md @@ -0,0 +1,154 @@ +--- +myst: + html_meta: + "description": "Override a Svelte component of the Blicca content browser via the shared @plone/registry, the default component key, and one shared Svelte runtime." + "property=og:description": "Override a Svelte component of the Blicca content browser via the shared @plone/registry, the default component key, and one shared Svelte runtime." + "property=og:title": "Overriding a Svelte component" + "keywords": "Plone, Blicca, Svelte, contentbrowser, plone registry, default key, componentRegistryKeys, module federation" +--- + +(blicca-svelte-override-label)= + +# Overriding a Svelte component + +The content browser is a Svelte app, and it is designed to be extended. +In this chapter, we replace its `SelectedItem` component with our own, and the selection list of every relation field renders with it. + +## Where the hook is + +The `pat-contentbrowser` looks up its `SelectedItem` component in the `@plone/registry` component registry. +It first checks a configurable registry key, and then falls back to the default key `pat-contentbrowser.SelectedItem`. +The lookup happens once per widget, when its selection list mounts. + +Both keys are hooks for an add-on. +A registration under the default key replaces the component site-wide, without any configuration. +A registration under a custom key is activated per widget, or through pattern options. + +Our override consists of two building blocks, plus an optional third one for scoping. + +## Block 1: the component + +The file {file}`resources/contentbrowser/SelectedItem.svelte` contains our variant. +The only hard requirement is the props interface of the original: + +```html + +``` + +The component receives `item`, the selected object with its catalog metadata, and `unselectItem`, a callback that removes it from the selection. +Everything else, such as markup, badges, and styling, is yours. +The same technique applies to other Svelte-based parts of the stack, such as the file manager. + +## Block 2: registration + +In {file}`resources/overrides.js`, we register the component under the default key: + +```js +import plone_registry from "@plone/registry"; +import BliccaSelectedItem from "./contentbrowser/SelectedItem.svelte"; + +plone_registry.registerComponent({ + name: "pat-contentbrowser.SelectedItem", + component: BliccaSelectedItem, +}); +``` + +That is all it takes. +Since Mockup 5.6.11, the pattern registers its own default component only if nothing is registered under that key yet. +The component registry itself overwrites silently. +An add-on registration therefore wins, no matter whether the add-on bundle initializes before or after the pattern. + +```{note} +Before Mockup 5.6.11, the pattern registered the default component in its `init()`, on every widget initialization. +An add-on registration under the default key was reset by the next content browser that initialized. +On such a Plone bundle, use a custom key as described in {ref}`blicca-svelte-override-scoping-label`. +``` + +One detail makes this work. +The host and the add-on share a single Svelte runtime through module federation. +Svelte keeps its reactivity state in module-level variables, so a component compiled against a second copy of the runtime cannot be mounted by the host. +Both webpack configurations therefore declare the same singleton shares: `svelte` for the package itself, and the prefix `svelte/` for the subpath imports of compiled components, such as `svelte/internal/client`. +This is the relevant part of {file}`webpack.config.js`: + +```js +shared: { + svelte: { + singleton: true, + requiredVersion: package_json.dependencies["svelte"], + }, + "svelte/": { + singleton: true, + requiredVersion: package_json.dependencies["svelte"], + }, +}, +``` + +```{important} +The Plone bundle shares its Svelte runtime since Mockup 5.6.9. +With an older bundle, the selection list renders an empty slot, and the console shows `TypeError: Cannot read properties of null (reading 'nodes')`. +``` + +This makes a great live debugging story, if time permits. +Remove the two shares from {file}`webpack.config.js`, rebuild, and watch the error appear. + +(blicca-svelte-override-scoping-label)= + +## Block 3: scoping the override + +Sometimes you don't want to replace the component everywhere. +Register it under a custom key instead: + +```js +plone_registry.registerComponent({ + name: "blicca.SelectedItem", + component: BliccaSelectedItem, +}); +``` + +Then tell the content browser which key to use, with the pattern option `componentRegistryKeys.selectedItem`. +You have three ways to set it. + +Site-wide +: Use the mechanism from {ref}`blicca-pattern-options-label`, in {file}`profiles/default/registry/patternoptions.xml`: + +```xml +{"componentRegistryKeys": {"selectedItem": "blicca.SelectedItem"}} +``` + +Per widget +: Set the `data-pat-contentbrowser` attribute directly on the widget. + +Conditionally +: Write an `IPatternsSettings` adapter, for example to activate the override only on certain content types. + +With a custom key, the default component stays registered, and every widget without the option keeps it. +The override becomes a configuration decision instead of a build decision. + +## Exercise + +Restyle the component in {file}`resources/contentbrowser/SelectedItem.svelte`. +Show the review state with your workflow colors, render bigger preview images, or turn the item into a compact table row. + +Then edit any page, and open {menuselection}`Categorization --> Related Items`. +Select an item, and watch your component render the selection. +Remove the item with your own remove button, and observe that the field value updates. + +Finally, close the circle to {ref}`blicca-pattern-options-label`. +Register the component under a custom key, activate it in `plone.patternoptions`, and rebuild. +Then switch the override off again through the web, in {menuselection}`Site Setup --> Configuration Registry`, without touching the bundle. +The `post_uninstall` handler in {file}`setuphandlers.py` already removes the `contentbrowser` key when the add-on is uninstalled. + +## Checkpoint + +The content browser selection renders with your component, and removing an item clears the field value. + +```{tip} +With the default key, the override needs a Plone bundle built from Mockup 5.6.11 or later. +With an older bundle, the default component wins again, and nothing seems to happen. + +With a custom key, the registration is lazy. +On a wrong key, the content browser silently falls back to the default component. +When "nothing happens", first check the key for typos. +``` diff --git a/docs/index.md b/docs/index.md index e1a102402..96e2b158b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,6 +21,7 @@ customizing-volto-light-theme/index plone-deployment/index migrations/index content-editing/index +blicca-customization-training/index ``` ```{toctree} From 8af2df3211b069af5eb08a13a97a85731423b3b8 Mon Sep 17 00:00:00 2001 From: Peter Mathis Date: Mon, 21 Sep 2026 11:19:10 +0200 Subject: [PATCH 2/4] update index --- docs/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/index.md b/docs/index.md index 96e2b158b..fc16113e1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -67,6 +67,12 @@ documentation/index : How to edit content and manage a Plone site. +## Blicca JS stack insights - how to customize mockup + +{doc}`blicca-customization-training/index` +: How to configure, create and customize mockup patterns and override existing Svelte components in `@plone/registry`. + + ## Other {doc}`migrations/index` From 0c39177e6b3986df1deda2e514b4da14dd4ec40c Mon Sep 17 00:00:00 2001 From: Peter Mathis Date: Mon, 21 Sep 2026 11:48:28 +0200 Subject: [PATCH 3/4] Use label for link --- docs/blicca-custiomization-training/index.md | 2 +- docs/index.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/blicca-custiomization-training/index.md b/docs/blicca-custiomization-training/index.md index 6c138b6cd..f00cf0c86 100644 --- a/docs/blicca-custiomization-training/index.md +++ b/docs/blicca-custiomization-training/index.md @@ -7,7 +7,7 @@ myst: "keywords": "Plone, Blicca, Classic UI, Mockup, Patternslib, Svelte, JavaScript, training, customization" --- -(blicca-label)= +(blicca-js-stack-insights-label)= # Blicca JS stack insights — how to customize Mockup diff --git a/docs/index.md b/docs/index.md index fc16113e1..b7e762986 100644 --- a/docs/index.md +++ b/docs/index.md @@ -67,9 +67,9 @@ documentation/index : How to edit content and manage a Plone site. -## Blicca JS stack insights - how to customize mockup +## Blicca JS stack insights -{doc}`blicca-customization-training/index` +{ref}`blicca-js-stack-insights-label` : How to configure, create and customize mockup patterns and override existing Svelte components in `@plone/registry`. From 4ec41f52cb606af785ec89a9038fee07db619aa0 Mon Sep 17 00:00:00 2001 From: Peter Mathis Date: Tue, 22 Sep 2026 08:04:42 +0200 Subject: [PATCH 4/4] fix broken link --- docs/blicca-custiomization-training/stretch-folder-contents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blicca-custiomization-training/stretch-folder-contents.md b/docs/blicca-custiomization-training/stretch-folder-contents.md index 7093fbc78..4d88b8580 100644 --- a/docs/blicca-custiomization-training/stretch-folder-contents.md +++ b/docs/blicca-custiomization-training/stretch-folder-contents.md @@ -16,7 +16,7 @@ The folder contents view, the `pat-structure` pattern, shows an action menu in e The customer wants two changes: - Edit opens in a modal, instead of leaving the folder contents. -- Images get an additional action that opens the image cropping editor of [plone.app.imagecropping](https://github.com/plone/plone.app.imagecropping), also in a modal. +- Images get an additional action that opens the image cropping editor of [plone.app.imagecropping](https://github.com/collective/plone.app.imagecropping), also in a modal. It combines {ref}`blicca-replace-pattern-label` with a look under the hood of a Backbone-based pattern.