Skip to content

Add the @blocks-schema endpoint and Markdown block docs - #8023

Open
sneridagh wants to merge 21 commits into
mainfrom
blocksschemaendpoint
Open

sneridagh wants to merge 21 commits into
mainfrom
blocksschemaendpoint

Conversation

@sneridagh

@sneridagh sneridagh commented Mar 19, 2026 •

Copy link
Copy Markdown
Member

Merge first:

Adds a /@blocks-schema endpoint to the Volto server. It describes the blocks registered on the site: their documentation, their variations, and the schema of their data. It's meant for agents (for example through the Plone MCP server) and for generating reference docs.

Only the frontend knows which blocks a site has, since core, add-ons and the project configuration all register and change them, so the endpoint lives in the Volto server, not in plone.restapi.

What it does

  • GET /@blocks-schema: ID, title and summary of every block. ?full=1 returns the full details for all of them.
  • GET /@blocks-schema/<id>: one block's docs, schema and variations.
  • Markdown: ?format=md, or an Accept header that asks for text/markdown and not for JSON, returns one Markdown document with a section per block. JSON stays the default.
  • Per content item: GET /<path>/@blocks-schema lists only the blocks the block chooser offers when editing that item. It evaluates each block's restricted with the same context as the editor (content with the navroot expander, content type, current user), using the caller's auth_token cookie.

Details

  • Public and cacheable. Everything it returns already ships in the client bundle. Root responses are public, max-age=300 and vary on Accept and Accept-Language. Per-content responses are private.
  • Schemas are resolved like in the block settings form. It uses applySchemaEnhancer, so the default variation's enhancer and then the block's own one are applied. Each variation is returned as { id, title, isDefault, schema, docs? }, with its own schema.
  • Only JSON in the output. Widget components, functions, React elements and cycles are removed from schemas.
  • One broken block doesn't break the response. A block or variation whose schema can't be resolved comes back with schema: null and an error.
  • restricted: the root list flags blocks as restricted: true, or 'context' when it depends on the content. The check is now a shared isBlockRestricted helper that BlockChooser also uses.
  • Backend errors: a 401, 403 or 404 from the backend for a per-content request is returned with the same status.

Block docs

  • Docs are *.block.md files next to each block. Their frontmatter names the block (block: teaser), and can add a variation (variation: imageGallery). The first paragraph is the block's summary. The docs aren't part of the block configuration, and authors don't register anything.
  • Collected at build time, loaded only on the server. @plone/registry gets createBlocksDocsLoader. It finds the *.block.md files in Volto's src and in every add-on, and writes a module that imports each one with ?raw. Volto's build aliases that module as load-blocks-docs and adds an asset/source rule for *.block.md?raw. Only the @blocks-schema middleware imports it, so the docs never reach the client bundle. When two files document the same block, the add-on loaded later wins, so a project can replace core docs.
  • Placeholders: {{key}} is filled from the block's top-level configuration. The grid docs use {{maxLength}} and {{allowedBlocks}}.
  • Optional tailoring: docs in the block configuration (@plone/types) is now only an optional function, ({ blockConfig, intl, markdown }) => string, for special cases.
  • Core blocks and text block: every core block has a docs.block.md. The text block's (volto-slate) documents the value format, with elements, inline formatting as wrapping elements, links, and plaintext. A test checks that its examples' plaintext matches the editor's serializer.
  • Tests: the core test checks every collected file: each must name a registered block, and its JSON examples must parse and use that block's own @type. Vitest generates the same loader, and CI already builds @plone/registry before the unit tests.

Review comments

Answers to @davisagli's review:

  • Use a generic key instead of llm: it's docs.
  • Markdown instead of structured JSON: docs are plain Markdown files, and the endpoint can return Markdown too.
  • Translations: the docs are English only. Schema titles come back translated into the request's language.
  • A function for dynamic configuration: placeholders fill in values from the block configuration, which the grid docs use, and an optional docs function can tailor the text. Variations are returned from the configuration, not written into the docs.

Related

  • Add a blockSchema to the image, maps, toc, html and leadimage core blocks #8472 adds a blockSchema to the core blocks that didn't have one yet (image, maps, toc, html, leadimage). Until it's merged, those blocks return schema: null here.
  • The docs page is docs/source/blocks/blocks-schema.md.
  • HEAD /@blocks-schema currently returns Volto's HTML page instead of the endpoint's response, because something earlier in Volto's server handles HEAD requests first. It isn't caused by this PR.

Not covered

  • Container blocks: the per-content path doesn't take into account the allowedBlocks of container blocks such as the grid.
  • Site root: a bare /@blocks-schema always returns the unfiltered list, so there's currently no way to ask what's offered at the site root.

@sneridagh
sneridagh requested review from danalvrz and tisto March 19, 2026 14:30
@read-the-docs-community

read-the-docs-community Bot commented Apr 13, 2026 •

Copy link
Copy Markdown

@davisagli davisagli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts:

  • We could potentially also use this info to auto-generate reference docs. So I would put it in a more generic key like docs instead of llm.
  • I wonder if it would be more useful for an agent to produce Markdown output instead of structured json.
  • Do we need to have a way to translate this info?
  • Maybe we should have a function that returns this info instead of just an object in the config, so that it can take into account dynamic configuration.

Comment thread packages/volto/src/config/Blocks.jsx Outdated
description:
'Displays an internal or external image with optional title, description, alt text, alignment, size, and link. Use for standalone images within page content.',
usage_notes:
"For INTERNAL Plone images: set 'url' to the Image content object path (e.g. /my-folder/my-image) and set 'image_field' to 'image'. For EXTERNAL images: set 'url' to the full image URL and omit 'image_field'. Do NOT use @@images URLs for internal images. Do not set 'align' or 'size' when nesting inside anpther block (e.g. gridBlock) since the parent block controls layout.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is ok to use a full URL for internal images also. Either way, it gets converted into a UID-based path when it is stored.

Suggested change
"For INTERNAL Plone images: set 'url' to the Image content object path (e.g. /my-folder/my-image) and set 'image_field' to 'image'. For EXTERNAL images: set 'url' to the full image URL and omit 'image_field'. Do NOT use @@images URLs for internal images. Do not set 'align' or 'size' when nesting inside anpther block (e.g. gridBlock) since the parent block controls layout.",
"For INTERNAL Plone images: set 'url' to the Image content object path (e.g. /my-folder/my-image) and set 'image_field' to 'image'. For EXTERNAL images: set 'url' to the full image URL and omit 'image_field'. Do NOT use @@images URLs for internal images. Do not set 'align' or 'size' when nesting inside another block (e.g. gridBlock) since the parent block controls layout.",

Comment thread packages/volto/src/config/Blocks.jsx Outdated
querystring:
"Required. Object with 'query' (required array of filter criteria), optional 'sort_on', 'sort_order', 'sort_order_boolean', 'b_size' (string, items per page), 'limit' (string, max total results). Multiple criteria are combined with AND logic. Each criterion requires 'i' (index name), 'o' (FULL operation string — see below), 'v' (value). " +
'Indexes: portal_type, path, Title, Description, SearchableText, Creator, getId, review_state, Subject, created, modified. ' +
'Operations (always use the FULL string): ' +

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The available indexes and operations are dynamic based on the configuration in the site's registry. This could refer to the API for getting them instead of including a hardcoded list.

Comment thread packages/volto/src/config/Blocks.jsx Outdated
mostUsed: false,
blockHasOwnFocusManagement: true,
sidebarTab: 0,
llm: {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of this is really specifically useful to LLMs. It's documentation that is also useful to humans. I'd suggest calling this key docs rather than llms (name it what it is, not based on one specific thing that will consume it).

Comment thread packages/volto/src/config/Blocks.jsx Outdated
"sort_on values: 'sortable_title', 'effective', 'created', 'modified', 'getObjPositionInParent', 'start', 'end', 'review_state'. " +
"sort_order: 'ascending' (default) or 'descending'.",
variation:
"Required. 'default' (simple list), 'summary' (list with images), 'imageGallery' (image carousel), 'eventCalendar' (calendar view), 'listDate' (list with date).",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The available variations can be customized in Volto config.

Comment thread packages/volto/src/express-middleware/blocks-schema.test.js
@sneridagh

sneridagh commented Aug 25, 2026 •

Copy link
Copy Markdown
Member Author

@davisagli @danalvrz I took a look at the current state, answering to your main questions:

We could potentially also use this info to auto-generate reference docs. So I would put it in a more generic key like docs instead of llm.

+1 (done)

I wonder if it would be more useful for an agent to produce Markdown output instead of structured json.

+1000 I think we reached a point that LLMs are more powerful with short markdown explanations. Also, it's instant docs for humans too. My take on this is that it feels wrong to have the structured docs in JSON... I see no reason for it, although some that I can't see now related to the MCP (which should not be the case, as I'd decouple the use case from the endpoint).

Do we need to have a way to translate this info?

Which would be the point? Machines talk any language, unless we'd like to generate docs out of this endpoint, which is unlikely... Anyways, you could define a .po with generic keys button_docs as msgid, with the correspondent msgstr.

Maybe we should have a function that returns this info instead of just an object in the config, so that it can take into account dynamic configuration.

I am missing the point of this, if can we elaborate the use case it would be great.
In the case of the plain markdown, you would override the whole docs key, right?

Also, we need docs for this endpoint, since it's a whole new category (frontend endpoint), I guess the question is where in the docs structure.

@sneridagh

Copy link
Copy Markdown
Member Author

@davisagli @danalvrz I'd love to see this happening before the conf. Any feedback from my comment?

@sneridagh

Copy link
Copy Markdown
Member Author

Self-review of the current state, including @davisagli's comments. I think the idea is right: putting each block's contract next to its blocksConfig entry and serving it from the frontend makes sense, because the frontend is the only place that knows which blocks, add-ons and variations are actually installed. Before merging, though, I'd change the following.

Problems in the code

  1. The auth check only checks that a token is present. ensureAuthenticated never validates the token, so Authorization: Bearer x gets through. Either make the endpoint public or validate the token against the backend. I'd make it public: everything it returns is already in the JS bundle that anonymous visitors download. Making it public also lets us cache it per build.
  2. The getRequestAuthToken change in Api.js and APIResourceWithAuth.js changes all server-side rendering, not just this endpoint. A Bearer header from an upstream proxy or gateway now overrides the cookie on every SSR request to Plone. That might be useful (for headless or agent rendering), but it's a separate decision and should go in its own PR. If the endpoint is public, this PR doesn't need it at all.
  3. One broken block config can make ?full=1 return a 500. Only schemaEnhancer is wrapped in try/catch, not the blockSchema(...) call. That call gets fake props ({ data: { '@type': id } }), and add-on schemas that read props.navRoot, props.properties and similar will throw. Each block should be wrapped separately, returning schema: null plus an error field for that block.
  4. It re-implements schema enhancement, and incompletely. Variation-level schemaEnhancers are never applied, so for listing, search and teaser the schema is just the base schema. It should reuse Volto's existing applySchemaEnhancer, and either resolve the schema per variation or at least document that schema doesn't include any variation's changes.
  5. variations is serialized raw. Those objects hold React components and functions, so the output is full of empty objects. It should return only { id, title, isDefault }, plus docs if a variation has any.
  6. Schemas can contain things that aren't JSON (widget components, function defaults). They need a sanitising step, not a bare res.json.
  7. Small things:
    • Use middleware.get, not .all.
    • It ignores restricted and context-dependent allowedBlocks. A context-aware /<path>/@blocks-schema that answers "what can I add here?" would be the natural next step for agents.
    • There's no news fragment and no docs yet.

The docs content

  • The slate block has no docs at all. It's the most-used block and the one whose value format agents get wrong most often. If only one block gets documented, it should be this one.
  • The text reads as LLM prompting, not documentation. For example: "Do NOT…", "REQUIRED", "MUST BE OMITTED", "generated automatically when using the Plone MCP block tools", and the grid notes about not setting align. Since the key is now docs, it should be written so a human can read it too.
  • The search block still hardcodes the index, operation and sort_on lists. They were removed from listing after David's comment, but search kept them. It should point to @querystring there as well.
  • field_hints duplicates what belongs in the schema. Field-level guidance fits better in each schema property's description, which also helps editors in the sidebar. Where it's too technical for editors, a separate docs.fields works, but it should live in one place.

Proposed direction

  • Use Markdown. Make docs a Markdown string, or a function that returns one, with examples as fenced JSON. The endpoint can then return JSON by default, with docs as a string, and a single combined Markdown document for Accept: text/markdown. That works like an llms.txt for the site's blocks and gives us reference docs for free.
  • Keep the function form. It's useful for putting the configured variations into the docs. Right now, though, the listing docs function ignores its arguments, so it doesn't show why the function form is needed.
  • No i18n for docs. English is fine. The schema titles do come back translated, so a response can mix languages, which is acceptable.

Porting to Plone Aurora

It fits well and would be easier there:

  • Shared types: this PR adds docs to BlockConfigBase in @plone/types, which Volto and Aurora share. The shape (a Markdown string) should be settled before merging so Aurora doesn't inherit field_hints or usage_notes.
  • Route: it would be a resource route @blocks-schema, registered with config.registerRoute in @plone/cmsui next to @vocabulary and @queryStringOptions. It'd be a loader with no Express code.
  • Auth: getAuthFromRequest in @plone/react-router only reads the cookie. If the endpoint is public, that doesn't matter. If not, adding Bearer support there is the same cross-cutting decision as point 2.
  • Shared schema resolver: Aurora's block sidebar currently passes blockConfig.blockSchema through as-is, without resolving functions, enhancers or variations. One resolveBlockSchema(blockConfig, { intl, formData }) helper used by both the sidebar and the endpoint would keep the editor and the API from disagreeing. Aurora needs that anyway.
  • Coverage: only Maps, Image and Teaser define a blockSchema in Aurora today. The text (Plate) block's value format is what agents need most, so it should be documented first.

Summary: worth merging once the auth question and the per-block error handling are fixed, with docs as Markdown and the core docs content toned down. For Aurora, the shared schema resolver comes first, then the endpoint port.

The block configuration already ships in the public client bundle, so the
endpoint no longer requires authentication and is cacheable. The SSR change
that let an Authorization header override the auth_token cookie is removed.
A block whose schema or schemaEnhancer throws now comes back with
schema: null and an error, and the other blocks are still returned,
instead of the whole request failing.
The top-level schema now gets the same enhancer chain as the block settings
form (default variation, then the block's own schemaEnhancer). Each variation
is returned as { id, title, isDefault, schema } with its own enhanced schema,
instead of the raw variation config with its components and functions.
Schemas can hold widget components, React elements, functions and cyclic
references. toJSONSafe drops those before the response is sent, so the
output is predictable instead of relying on JSON.stringify side effects.
docs is now a Markdown string, or a function of the block config that
returns one; its first paragraph is the block's summary. Variations can
have their own docs. The core block docs move to a docs.js file next to
each block and are written for human readers too: no hardcoded query
indexes or operations, and the grid docs come from its configuration.
The slate block gets Markdown docs explaining value and plaintext, the
element types, inline formatting as wrapping elements, and links, with
examples whose plaintext is checked against the editor's serializer.
?format=md, or an Accept header that asks for text/markdown and not JSON,
returns one Markdown document: a section per block with its docs, its
variations and its schema. A variation's schema is only included when it
differs from the block's. The detached text block gets short docs of its
own instead of repeating the text block's.
With a content path in front, the endpoint only lists the blocks the block
chooser offers when editing that content: it fetches the content (with the
navroot expander) and the current user using the caller's auth_token
cookie, and evaluates each block's restricted the way BlockChooser does,
now through a shared isBlockRestricted helper. Backend 401, 403 and 404
are passed through, and context responses are private.

The root list flags restricted blocks (true, or 'context' when it depends
on the content). Regenerated the type declarations, which also adds the
ones for the block docs files.
Adds a page on writing a block's docs and on the endpoint: the list and
detail routes, the Markdown format, the context path, errors and caching.
The block settings reference mentions the new docs option.
@sneridagh sneridagh changed the title PoC of /@blocks-schema Add the @blocks-schema endpoint and Markdown block docs Oct 20, 2026
Block docs are now Markdown files next to each block, named *.block.md,
whose frontmatter names the block (and optionally a variation). They are
no longer part of the block configuration.

@plone/registry gets createBlocksDocsLoader, which finds those files in
core and in every add-on at build time and writes a module that imports
them with ?raw. Volto's build aliases it as load-blocks-docs and adds an
asset/source rule for *.block.md?raw. Only the @blocks-schema middleware
imports it, so the docs stay out of the client bundle. Later add-ons
replace the docs of earlier ones.

{{key}} placeholders are filled from the block configuration, which the
grid docs use for its allowed blocks and maximum count. The block's docs
config key remains as an optional function to tailor the result.
…sschemaendpoint

* origin/blocksschemaendpoint: (32 commits)
  Fix webpack duplication and react-aria catalog minimums, add minimumReleaseAge (#8464)
  Restore pnpm store cache fallback and fix volto-razzle CI job (#8463)
  Update packages in pnpm-lock.yaml (#8461)
  Fix invisible keyboard focus on single react-select widgets (#8457)
  Use pnpm/action-setup and setup-node pnpm cache in CI (#8459)
  Don't show the parent's tags and related items below the add form. (#8453)
  Fix adding a field with the default type in the schema editor (#8454)
  Pick the SSR locale only from supportedLanguages (#8448) (#8449)
  Fixed link redirects from `thevalleyofcode.com` to `flaviocopes.com` (#8451)
  Fix release-it from babel-preset-razzle
  Release 19.5.0
  Release @plone/volto-slate 19.1.0
  Release @plone/razzle 1.0.2
  Release @plone/razzle-dev-utils 1.0.1
  Release @plone/scripts 4.0.2
  Release @plone/babel-preset-razzle 1.0.2
  Release @plone/registry 3.0.3
  Release @plone/components 4.2.2
  Release @plone/types 2.1.0
  Fix diff of text block word by word (#8429)
  ...
blocks/documentation.md explains why blocks carry their own docs and how
to write them: *.block.md files, conventions, variations, placeholders
and the optional docs function. reference/blocks-schema-endpoint.md is
the reference of the endpoint. Each links to the other.
@sneridagh

sneridagh commented Oct 20, 2026 •

Copy link
Copy Markdown
Member Author

@davisagli @danalvrz I think this is complete now, and covers all our concerns.
What likes me the most: the ?format=md modificator (or header text/markdown) and the fact that docs do not bloat the config object now and they stay where they belong: .md!

I'd go for it, make the Aurora counterpart from this specs and way to proceed.

@sneridagh
sneridagh requested review from davisagli and ericof October 20, 2026 14:48

@davisagli davisagli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sneridagh I like this a lot!

The only thing I am not sure about is making /@blocks-schema public. Yeah, the blocks config was already part of the frontend bundle, but the block docs weren't. I can imagine that in some cases they might include private instructions for editors, or even contact information.

Request it from the frontend URL, for example `http://localhost:3000/@blocks-schema`.
```

The endpoint is public, because everything it returns is already part of the frontend code that every visitor downloads.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is true. The block docs are not part of the frontend bundle, and they could contain private editing instructions or even contact information.

@sneridagh sneridagh Oct 21, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davisagli after the last interations, I agree that this statement is no longer true (because we improved it). So, yes, could be that returning the endpoint now it's better that it requires auth (any auth).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants