Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/readme-link-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ jobs:
--exclude https://www.lanku.eus/
--exclude https://medium.com/
--exclude https://www.linkedin.com/
--exclude-path packages/volto
--exclude-path packages/volto-slate
'packages/**/README.md'
'*.md'
61 changes: 56 additions & 5 deletions apps/aurora/app/config/server/content-migrations.server.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it } from 'vitest';
import { PLONE_BLOCK_TYPE } from '@plone/helpers';
import config from '@plone/registry';
import { SOMERSAULT_KEY } from '@plone/plate/constants';
import type { Content, ContentBase } from '@plone/types';
Expand Down Expand Up @@ -65,13 +66,36 @@ describe('content migrations', () => {
});
});

it('moves native blocks into the somersault field as unknown nodes', () => {
it('moves native blocks into the somersault field as ploneBlock nodes', () => {
config.blocks = {
blocksConfig: {
listing: {},
image: {},
configuredWidth: {
defaultBlockWidth: 'layout',
blockSchema: {
title: 'Configured width block',
fieldsets: [],
required: [],
properties: {},
},
},
schemaWidth: {
blockSchema: {
title: 'Schema width block',
fieldsets: [],
required: [],
properties: {
blockWidth: {
widget: 'width',
default: 'full',
styleField: true,
},
},
},
},
},
} as typeof config.blocks;
} as unknown as typeof config.blocks;
installMigrations();

const content: TestContent = {
Expand All @@ -92,13 +116,26 @@ describe('content migrations', () => {
url: '/image',
alt: 'Example image',
},
configuredWidth: {
'@type': 'configuredWidth',
},
schemaWidth: {
'@type': 'schemaWidth',
},
custom: {
'@type': 'custom-unregistered',
foo: 'bar',
},
},
blocks_layout: {
items: ['titleBlock', 'listing', 'image', 'custom'],
items: [
'titleBlock',
'listing',
'image',
'configuredWidth',
'schemaWidth',
'custom',
],
},
};

Expand All @@ -114,19 +151,33 @@ describe('content migrations', () => {
},
{
'@type': 'listing',
blockWidth: 'default',
children: [{ text: '' }],
querystring: {
criteria: [],
},
type: 'unknown',
type: PLONE_BLOCK_TYPE,
},
{
'@type': 'image',
alt: 'Example image',
blockWidth: 'default',
children: [{ text: '' }],
type: 'unknown',
type: PLONE_BLOCK_TYPE,
url: '/image',
},
{
'@type': 'configuredWidth',
blockWidth: 'layout',
children: [{ text: '' }],
type: PLONE_BLOCK_TYPE,
},
{
'@type': 'schemaWidth',
blockWidth: 'full',
children: [{ text: '' }],
type: PLONE_BLOCK_TYPE,
},
],
});
});
Expand Down
33 changes: 32 additions & 1 deletion apps/aurora/app/config/server/migrations.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import config from '@plone/registry';
import {
getStyleFieldsFromBlockSchema,
PLONE_BLOCK_TYPE,
} from '@plone/helpers';
import type { BlockConfigBase, BlocksFormData } from '@plone/types';
import {
migrateLegacyBoldInValue,
migrateLegacyBlockWidthsInValue,
Expand All @@ -23,6 +28,31 @@ const isRegisteredNativeBlock = (block: Record<string, unknown>) => {
return Boolean(blocksConfig?.[blockType]);
};

const DEFAULT_BLOCK_WIDTH = 'default';

const getMigratedPloneBlockWidth = (block: Record<string, unknown>) => {
const blockType = block['@type'];

if (typeof blockType !== 'string') {
return DEFAULT_BLOCK_WIDTH;
}

const blocksConfig = config.blocks?.blocksConfig as
| Record<string, BlockConfigBase>
| undefined;
const blockConfig = blocksConfig?.[blockType];
const styleFields = getStyleFieldsFromBlockSchema(
blockConfig,
block as BlocksFormData,
);

return (
styleFields.blockWidth?.defaultValue ??
blockConfig?.defaultBlockWidth ??
DEFAULT_BLOCK_WIDTH
);
};

export default function install() {
config.registerUtility({
name: 'somersaultBlockMigrationTitle',
Expand Down Expand Up @@ -57,7 +87,8 @@ export default function install() {
? [
{
...block,
type: 'unknown',
blockWidth: getMigratedPloneBlockWidth(block),
type: PLONE_BLOCK_TYPE,
children: [{ text: '' }],
},
]
Expand Down
2 changes: 1 addition & 1 deletion apps/aurora/news/+native-blocks-to-somersault.feature
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Registered Aurora native blocks are now migrated into the Somersault field as `type: 'unknown'` nodes so they can be rendered by the new editor pipeline later. @sneridagh
Registered Aurora native blocks are now migrated into the Somersault field as `type: 'ploneBlock'` nodes with migrated `blockWidth` defaults so they can be rendered by the new editor pipeline later. @sneridagh
1 change: 1 addition & 0 deletions apps/aurora/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"build": "pnpm exec init-loaders && react-router build",
"build:publicui": "REGISTRYCONFIG=registry-publicui.config.ts pnpm exec init-loaders && react-router build",
"start:prod": "react-router-serve ./build/server/index.js",
"check:ts": "react-router typegen && tsc",
"typecheck": "react-router typegen && tsc",
"typegen": "react-router typegen",
"release": "release-it",
Expand Down
115 changes: 115 additions & 0 deletions docs/development/block-anatomy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
myst:
html_meta:
"description": "Block anatomy contract for Plone Aurora"
"property=og:description": "Block anatomy contract for Plone Aurora"
"property=og:title": "Block anatomy"
"keywords": "Plone Aurora, blocks, Plate, block model, anatomy"
---

# Block anatomy

Plone Aurora exposes a shared block anatomy contract for both public rendering and Plate/Somersault rendering.

## Plone Aurora's block model

In the past, the Plone's block engines used different approaches, improving and iterating them over the years.
We identified several of these iterations, and defined what we called the Block Model v3.
It is the model behind Plone Aurora block anatomy contract, and from now on, it is simply referred to as the block model.
Its main goal is to keep view mode and edit mode structurally aligned so the same CSS can work in both places.
Instead of letting each block invent its own wrapper layout, the framework provides a standard two-level structure and leaves the block component focused on content and behavior.

The important ideas are:

- The outer container is responsible for full-width page placement, theme styling, and vertical spacing.
- The inner container controls content width, centering, and block-to-block spacing.
- Block categories drive spacing behavior between adjacent blocks, so spacing decisions stay consistent across the site.
- Blocks should stay simple and render their actual content directly, without adding extra layout wrappers unless they are genuinely needed.
- The model is opt-in, which keeps existing blocks compatible while allowing v3-capable blocks to adopt the shared structure.

In practice, that means the block model defines the structure around a block, while the block itself stays focused on the content it renders.

The outer block element receives:

```html
class="block block-<type> category-<category>"
data-block-type="<type>"
data-block-category="<category>"
```

For example, a teaser block in the teaser category renders as:

```html
<div
class="block block-teaser category-teaser"
data-block-type="teaser"
data-block-category="teaser"
>
<div class="block-inner-container">...</div>
</div>
```

## Where the contract is applied

The anatomy contract is resolved by `resolveBlockAnatomy` in `@plone/helpers`.

It is consumed by:

- `BlockAnatomyPlugin` in `@plone/plate` for Plate-native blocks and registry-backed Plone blocks in Plate/Somersault rendering

This avoids duplicating class-name rules in individual blocks.

Somersault editor rendering goes through Plate and receives the same classes from `BlockAnatomyPlugin`.
Public rendering uses `SomersaultRenderer`, which uses the Plate renderer path and the same anatomy plugin contract.

## Plate-native block categories

Plate-native block categories are configured in `config.blocks.plateBlocksConfig`.

```ts
config.blocks.plateBlocksConfig = {
p: {
category: 'text',
blockWidth: {
defaultWidth: 'narrow',
widths: ['narrow'],
},
},
toc: {
category: 'navigation',
blockWidth: {
defaultWidth: 'default',
widths: ['layout', 'default', 'narrow'],
},
},
};
```

Registry-backed Plone blocks use the `category` from `config.blocks.blocksConfig`.

## Registry-backed Plone blocks in Plate

Registry-backed Plone blocks embedded in Plate use the `ploneBlock` node type.
The underlying Plone block type is stored in `@type`.

```ts
{
type: 'ploneBlock',
'@type': 'image',
children: [{ text: '' }],
}
```

`BlockAnatomyPlugin` resolves this as block type `image`, not `ploneBlock`.

## Style fields are separate

Block anatomy controls DOM classes and data attributes.
Style fields control CSS custom properties.

For example:

- `BlockAnatomyPlugin` adds `.block.block-teaser.category-teaser`
- `StyleFieldsPlugin` adds styles such as `--theme-color` or `--block-width`

Keep these responsibilities separate when adding new styling behavior.
Loading
Loading