Focal points for images: detected on upload, set by editors in Volto, and crops around them in the aspect ratios of your templates.
Templates show images in fixed aspect ratios: teasers at 16:9, a header square on phones, a listing at 3:2. With object-fit: cover the browser crops them around their center, so heads, buildings or the subject of a photo often fall off. This add-on keeps the subject in view.
- Focal point detection. When an image is uploaded, its focal point is detected and stored on the image: a simple saliency estimate (detail, saturation, skin tones, a bias towards the center) with Pillow only. Where it is unsure, it stays near the center.
- Focal point editor. In the sidebar of blocks with an image, editors click on what must stay in view, with previews of crops. It is saved on the image itself (REST service
@focal-point), so it applies wherever the image is used. In Volto's image and teaser blocks (also in grids) out of the box; for other blocks, see below. - Crops around the focal point.
image_scales(catalog metadata and REST api) gets the focal point and, per aspect ratio of the settingcollective.focalpoint.crops, the largest crop around it with its own scales. Templates ask for the crop they show: smaller files, and the focal point in view. object-position. Volto'sImagecomponent puts the focal point inobject-position, for the cropping thatobject-fit: coverstill does.
- Plone 6.1 or later, Volto 18 or later.
- The
focal_pointoption ofplone.scale.scale.scaleImage, which is not released yet: until then, use the branchfocal-point(Plone 6.2+) or4.x-focal-point(Plone 6.1) of plone.scale, seebackend/mx.ini. - Recommended: plone/plone.namedfile#238, so that images always have a scale as large as the original, and the original (often a much larger file) does not have to be in the
srcset.
Add collective.focalpoint to the backend, install it in the add-ons control panel, and add @plone-collective/volto-focal-point to the add-ons of the frontend. Installing detects the focal points of the existing images and updates image_scales in the catalog; that takes a while on a site with many images.
The registry record collective.focalpoint.crops lists the crops, one per line: an aspect ratio, then the widths of its scales.
16:9 400 800 1200 1600
4:3 400 800 1200
1:1 400 800 1200
Set it to the aspect ratios your templates use, for example in the registry.xml of your policy package. After a change, run collective.focalpoint.setuphandlers.maintenance.update_image_scales() (for example in an upgrade step) so that listings get the new crops.
Volto's Image component gets a prop crop:
<Image item={item} crop="16:9" sizes="(min-width: 1600px) 660px, 45vw" />It uses the scales of that crop when the image has one, and the image itself otherwise. For art direction, imageSources(item, imageField, crop) gives the src, srcSet, width, height and objectPosition of an image or of a crop, for example for a <source> of a <picture> with the square crop on phones.
The setting config.settings.focalPoint.originalInSrcset decides when the original goes in the srcset: "auto" (default, only when no scale is as large as the original) or "never".
Wrap the schema enhancer of the block with withFocalPoint, with a function that gives the item with the image from the block data:
import { withFocalPoint, teaserImageItem } from '@plone-collective/volto-focal-point';
config.blocks.blocksConfig.myBlock.schemaEnhancer = withFocalPoint(
config.blocks.blocksConfig.myBlock.schemaEnhancer,
(data) => data.image, // an object browser value
'default', // the fieldset
);Do the same when your project replaces the schemaEnhancer of the teaser or image block, as that replaces the one of this add-on. In a sidebar of your own, render <FocalPointEditor item={item} />.
- The detection is a heuristic: it misses small subjects in front of busy backgrounds, like cyclists in front of brick façades. Editors can correct it.
- Pages show a new focal point after they are reloaded: the scales of a crop are cached per focal point.
- An operating system that runs all the requirements mentioned.
- uv
- nvm
- Node.js and pnpm 22
- Make
- Git
- Docker (optional)
-
Clone this repository, then change your working directory.
git clone git@github.com:collective/collective-focalpoint.git cd collective-focalpoint -
Install this code base.
make install
-
Create a new Plone site on your first run.
make backend-create-site
-
Start the backend at http://localhost:8080/.
make backend-start
-
In a new shell session, start the frontend at http://localhost:3000/.
make frontend-start
Voila! Your Plone site should be live and kicking! 🎉
Deploy a local Docker Compose environment that includes the following.
- Docker images for Backend and Frontend 🖼️
- A stack with a Traefik router and a PostgreSQL database 🗃️
- Accessible at http://collective-focalpoint.localhost 🌐
Run the following commands in a shell session.
make stack-create-site
make stack-startAnd... you're all set! Your Plone site is up and running locally! 🚀
This monorepo consists of the following distinct sections:
- backend: Houses the API and Plone installation, utilizing pip instead of buildout, and includes a policy package named collective.focalpoint.
- frontend: Contains the React (Volto) package.
- devops: Encompasses Docker stack, Ansible playbooks, and cache settings.
- docs: Scaffold for writing documentation for your project.
- All necessary codebases to run the site are contained within the repository (excluding existing add-ons for Plone and React).
- Specific GitHub Workflows are triggered based on changes in each codebase (refer to .github/workflows).
- Simplifies the creation of Docker images for each codebase.
- Demonstrates Plone installation/setup without buildout.
To check your code against quality standards, run the following shell command.
make checkTo format and rewrite the code base, ensuring it adheres to quality standards, run the following shell command.
make format| Section | Tool | Description | Configuration |
|---|---|---|---|
| backend | Ruff | Python code formatting, imports sorting | backend/pyproject.toml |
| backend | zpretty |
XML and ZCML formatting | -- |
| frontend | ESLint | Fixes most common frontend issues | frontend/.eslintrc.js |
| frontend | prettier | Format JS and Typescript code | frontend/.prettierrc |
| frontend | Stylelint | Format Styles (css, less, sass) | frontend/.stylelintrc |
Formatters can also be run within the backend or frontend folders.
or lint:
make lint| Section | Tool | Description | Configuration |
|---|---|---|---|
| backend | Ruff | Checks code formatting, imports sorting | backend/pyproject.toml |
| backend | Pyroma | Checks Python package metadata | -- |
| backend | check-python-versions | Checks Python version information | -- |
| backend | zpretty |
Checks XML and ZCML formatting | -- |
| frontend | ESLint | Checks JS / Typescript lint | frontend/.eslintrc.js |
| frontend | prettier | Check JS / Typescript formatting | frontend/.prettierrc |
| frontend | Stylelint | Check Styles (css, less, sass) formatting | frontend/.stylelintrc |
Linters can be run individually within the backend or frontend folders.
Generate translation files for Plone and Volto with ease:
make i18nGenerated using Cookieplone (2.0.2) and cookieplone-templates (005674c) on 2026-11-11 18:58:54.235716. A special thanks to all contributors and supporters!