Repository navigation
feat: A4 · Custom block scaffolding + reference block - #352
ViewFromTheBox merged 2 commits into
Conversation
Ship the documented pattern + tooling for host apps to register custom blocks under the `artisanpack/*` namespace, plus one reference block (`artisanpack/callout`) that exercises the pattern end to end. PHP - Widen `VisualEditor::registerBlock()` to accept a block.json path, a class implementing the new `ProvidesBlockMetadata` interface, or a closure returning the metadata array - Register the bundled callout via `registerReferenceBlocks()` in the service provider; add it to the default `enabled_blocks` allow-list JS - Add `editor/custom-blocks.ts`: auto-discovery via `import.meta.glob` over `resources/js/visual-editor/blocks/*/index.ts`, `artisanpack` block-category registration, and a `CustomBlockModule` contract that forwards an optional inline-SVG `icon` override to `registerBlockType` (the editor does not bundle `dashicons.css`, so a dashicon-slug icon would render blank) - Wire the discovery call into `registerOnce()` in `editor-app.tsx` Reference block - `blocks/callout/`: `block.json` with severity + icon enum attributes, `edit.tsx` with `InspectorControls` + `RichText`, matching `save.tsx`, scoped CSS, and an inline-SVG inserter icon Renderers - Blade, React, and Vue renderer packages each gain a `blocks/artisanpack/callout` partial mirroring the `save.tsx` markup so server-rendered and client-rendered pages ship identical HTML Tests + docs - PHP: `RegisterBlockTest` covering all three registration forms + error paths; Blade renderer tests for the callout - JS: `custom-blocks.test.tsx` covering the registration pipeline + callout round-trip; React and Vue renderer BlockTree tests for the callout - `docs/custom-blocks.md`: authoring pattern, schema notes, dynamic vs static guidance, per-renderer partial pattern, checklist Closes #346 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
✅ Files skipped from review due to trivial changes (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds a custom-block scaffolding + discovery system and ships a reference block Changes
Sequence Diagram(s)sequenceDiagram
participant Host as Host App\n(editor-app.tsx)
participant Discovery as Auto-Discovery\n(custom-blocks.ts)
participant Resolver as Module\nResolver
participant Registry as Block\nRegistry
participant Editor as Editor\nUI
Host->>Host: registerOnce()
Host->>Discovery: discoverAndRegisterCustomBlocks()
Discovery->>Discovery: import.meta.glob('../blocks/*/index.ts')
Discovery->>Resolver: resolveCustomBlockModule(module)
Resolver->>Resolver: Validate metadata.name\nand edit/save/icon exports
Resolver-->>Discovery: validated module
Discovery->>Discovery: ensureArtisanpackCategory()
Discovery->>Registry: registerBlockType() for each module
Registry->>Registry: Wire edit, save, metadata, icon
Discovery-->>Host: registered block names
Host->>Editor: Initialize with registered blocks
Editor->>Editor: Render inserter menu / blocks
sequenceDiagram
participant EditorUI as Block Editor\n(edit.tsx)
participant State as Block\nAttributes
participant Renderer as Backend\nRenderer
participant Output as Rendered\nHTML
EditorUI->>EditorUI: User changes severity via Inspector
EditorUI->>State: setAttributes({ severity })
State-->>EditorUI: attributes updated
EditorUI->>Renderer: Request render preview (severity, icon, content)
Renderer->>Renderer: Normalize severity/icon\nCompose classes and select SVG path
Renderer-->>Output: HTML with icon, classes, content
Output-->>EditorUI: Display preview in editor
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
resources/js/visual-editor/blocks/callout/edit.tsx (1)
44-50: Derive the icon options from the shared icon registry.
ICON_OPTIONSduplicates the canonical icon names fromicons.tsx, so the inspector can drift from the actual renderer/save support later. Pull the values fromCALLOUT_ICON_NAMES(or a shared options export) and keep only the label mapping here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@resources/js/visual-editor/blocks/callout/edit.tsx` around lines 44 - 50, ICON_OPTIONS currently hardcodes the callout icon names and duplicates the canonical list in icons.tsx; change ICON_OPTIONS to derive its values from the shared CALLOUT_ICON_NAMES (or the exported options array) so the inspector stays in sync with the renderer/save support. Locate the ICON_OPTIONS constant in edit.tsx and replace the hardcoded value array with a map over CALLOUT_ICON_NAMES (importing it from the module that exports it) that returns objects with label (humanized or title-cased) and value set to the canonical name; keep the label mapping only in this file and ensure the import of CALLOUT_ICON_NAMES (or the shared options export) is added and used instead of duplicating names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/custom-blocks.md`:
- Around line 23-31: The markdown uses unlabeled fenced code blocks that trigger
markdownlint MD040; update each of the affected triple-backtick fences in
docs/custom-blocks.md (the blocks showing file trees and file paths such as the
resources/js/visual-editor/blocks/ snippet and the packages/.../callout.*
snippets) to include a fence language (use "text") so the fences read ```text;
ensure you change the fences at the occurrences noted (around the snippets for
resources/js/visual-editor/blocks/ and the three packages/.../callout files)
while preserving the inner content exactly.
In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/callout.blade.php`:
- Around line 17-18: $content is being cast to string from
$attributes['content'] and then emitted with {!! !!}, allowing HTML/script
injection; replace the direct cast with a call to the shared sanitizer (e.g.,
sanitizeHtml or sanitize_block_content) before assigning $content (use the
centralized allow-list sanitizer used by the React/Vue renderers), so $content =
sanitize_block_content($attributes['content'] ?? ''); and ensure the same
sanitizer function is available/imported for use in this Blade render and the
mirrored client renderers.
In `@packages/visual-editor-renderer-react/src/blocks/artisanpack/callout.tsx`:
- Around line 78-81: The JSX for the callout body uses dangerouslySetInnerHTML
with the raw content variable (div.ap-callout__body, dangerouslySetInnerHTML={{
__html: content }}), which is an XSS risk; fix by sanitizing content before
injection (e.g., import and use a sanitizer like DOMPurify and call
DOMPurify.sanitize(content) or run the same sanitizer via a shared helper before
passing into attrString/renderer), and apply the same sanitizer at the renderer
layer for other components that use dangerouslySetInnerHTML (paragraph, heading,
quote, code, preformatted, list-item, figcaption, blockquote, table cells) or
enforce/validate a trusted-HTML contract upstream.
In `@resources/js/visual-editor/blocks/callout/block.json`:
- Around line 28-35: The block.json "supports" enables "anchor" and "spacing"
which causes useBlockProps.save() to inject id and spacing attributes in the
editor but the saved output (see save.tsx and the Blade/React/Vue renderers)
only emits class and data-severity, causing editor attributes to be lost; fix
this by either removing/disabling "anchor" and the "spacing" object from the
"supports" block (so supports no longer includes "anchor" or "spacing") OR
update the frontend renderers and save.tsx to use useBlockProps.save() (or
replicate its behavior) so the rendered output includes the same id and spacing
classes/inline styles as the editor, ensuring attributes like anchor and spacing
are preserved.
In `@resources/js/visual-editor/blocks/callout/callout.css`:
- Around line 49-52: The stylelint error is due to the non-canonical casing of
the color keyword in the .ap-callout__icon svg rule; update the fill value in
the .ap-callout__icon svg CSS rule to use the canonical lowercase keyword
"currentcolor" (replace currentColor with currentcolor) so it satisfies the
linting rule.
In `@resources/js/visual-editor/editor/custom-blocks.ts`:
- Around line 170-176: The current selection logic for candidate prefers any
object-shaped module.default over named exports (variable candidate built from
module and type GlobbedModule), which can skip valid named-export modules;
change the logic in the candidate resolution so you only choose module.default
when it actually matches the expected block shape (validate required block
properties/signature on module.default, e.g., expected keys/functions that
define a block) and otherwise fall back to using module (the named exports).
Update the check around module.default and the usage of GlobbedModule to perform
a defensive shape/feature test rather than a plain typeof object test so valid
named exports are not ignored.
In `@src/VisualEditor.php`:
- Around line 77-83: Trim and normalize the block name before registering:
replace the current check on $metadata['name'] with code that trims the value
once, splits it on '/' and ensures there are exactly two non-empty segments
(namespace and name), throw the same InvalidArgumentException if not valid, then
register the normalized joined string (e.g. $normalizedName = $segments[0] . '/'
. $segments[1]) via $this->registry->register($normalizedName, $metadata)
instead of using the raw $metadata['name'].
---
Nitpick comments:
In `@resources/js/visual-editor/blocks/callout/edit.tsx`:
- Around line 44-50: ICON_OPTIONS currently hardcodes the callout icon names and
duplicates the canonical list in icons.tsx; change ICON_OPTIONS to derive its
values from the shared CALLOUT_ICON_NAMES (or the exported options array) so the
inspector stays in sync with the renderer/save support. Locate the ICON_OPTIONS
constant in edit.tsx and replace the hardcoded value array with a map over
CALLOUT_ICON_NAMES (importing it from the module that exports it) that returns
objects with label (humanized or title-cased) and value set to the canonical
name; keep the label mapping only in this file and ensure the import of
CALLOUT_ICON_NAMES (or the shared options export) is added and used instead of
duplicating names.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 77340152-241c-4d6e-a9dd-3a8e667df701
📒 Files selected for processing (27)
config/visual-editor.phpdocs/custom-blocks.mdpackages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/callout.blade.phppackages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.phppackages/visual-editor-renderer-react/src/blocks/artisanpack/callout.tsxpackages/visual-editor-renderer-react/src/blocks/registerCoreBlocks.tspackages/visual-editor-renderer-react/tests/BlockTree.test.tsxpackages/visual-editor-renderer-vue/src/blocks/artisanpack/callout.tspackages/visual-editor-renderer-vue/src/blocks/registerCoreBlocks.tspackages/visual-editor-renderer-vue/tests/BlockTree.test.tsresources/js/visual-editor/blocks/callout/block.jsonresources/js/visual-editor/blocks/callout/callout.cssresources/js/visual-editor/blocks/callout/edit.tsxresources/js/visual-editor/blocks/callout/icons.tsxresources/js/visual-editor/blocks/callout/index.tsresources/js/visual-editor/blocks/callout/inserter-icon.tsxresources/js/visual-editor/blocks/callout/save.tsxresources/js/visual-editor/editor/__tests__/custom-blocks.test.tsxresources/js/visual-editor/editor/custom-blocks.tsresources/js/visual-editor/editor/editor-app.tsxsrc/Blocks/ProvidesBlockMetadata.phpsrc/Facades/VisualEditor.phpsrc/VisualEditor.phpsrc/VisualEditorServiceProvider.phptests/Fixtures/TestBlockMetadata.phptests/Unit/VisualEditor/EnabledBlockNamesTest.phptests/Unit/VisualEditor/RegisterBlockTest.php
| $content = (string) ( $attributes['content'] ?? '' ); | ||
|
|
There was a problem hiding this comment.
Sanitize content before rendering it as HTML.
$attributes['content'] is only coerced to string here, then emitted with {!! !!} on Line 42. That makes the callout body an HTML injection sink; a crafted block tree can ship script/event-handler markup straight through the Blade renderer. The mirrored React/Vue renderers have the same trust boundary, so this needs a shared sanitizer/allow-list rather than a one-off escape.
Also applies to: 42-42
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/callout.blade.php`
around lines 17 - 18, $content is being cast to string from
$attributes['content'] and then emitted with {!! !!}, allowing HTML/script
injection; replace the direct cast with a call to the shared sanitizer (e.g.,
sanitizeHtml or sanitize_block_content) before assigning $content (use the
centralized allow-list sanitizer used by the React/Vue renderers), so $content =
sanitize_block_content($attributes['content'] ?? ''); and ensure the same
sanitizer function is available/imported for use in this Blade render and the
mirrored client renderers.
| <div | ||
| className="ap-callout__body" | ||
| dangerouslySetInnerHTML={{ __html: content }} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect all raw HTML injection sites in the React renderer package.
rg -n -C3 --type=tsx 'dangerouslySetInnerHTML' packages/visual-editor-renderer-react/src
# 2) Check whether sanitization utilities exist and are used on the renderer path.
rg -n -C3 --type=ts --type=tsx 'sanitize|DOMPurify|xss|escapeHtml|allowedTags|kses' \
packages/visual-editor-renderer-react/src resources/js/visual-editor
# 3) Trace this block's content flow.
rg -n -C3 --type=tsx --type=ts 'CalloutBlock|attributes\.content|block\/callout' \
packages/visual-editor-renderer-react/src resources/js/visual-editorRepository: ArtisanPack-UI/visual-editor
Length of output: 99
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Read the callout.tsx file to see full context
echo "=== Content of callout.tsx ==="
cat -n packages/visual-editor-renderer-react/src/blocks/artisanpack/callout.tsx
echo ""
echo "=== Search for dangerouslySetInnerHTML (no type filter) ==="
rg -n -C3 'dangerouslySetInnerHTML' packages/visual-editor-renderer-react/src --glob='*.tsx' --glob='*.ts'
echo ""
echo "=== Search for sanitization utilities ==="
rg -n 'sanitize|DOMPurify|xss|escapeHtml|allowedTags|kses' packages/visual-editor-renderer-react/src resources/js/visual-editor --glob='*.tsx' --glob='*.ts' --glob='*.js' || true
echo ""
echo "=== Check package.json for security libraries ==="
cat packages/visual-editor-renderer-react/package.json | grep -A 5 -B 5 'depend' || trueRepository: ArtisanPack-UI/visual-editor
Length of output: 20207
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Check attrString implementation ==="
find packages/visual-editor-renderer-react/src -name 'attributes*' -o -name 'support*' | head -20
echo ""
echo "=== Read the attributes support file ==="
cat -n packages/visual-editor-renderer-react/src/support/attributes.ts 2>/dev/null || echo "File not found"
echo ""
echo "=== Check package.json dependencies ==="
cat packages/visual-editor-renderer-react/package.jsonRepository: ArtisanPack-UI/visual-editor
Length of output: 4465
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Check BlockRendererProps type definition ==="
rg -n 'BlockRendererProps' packages/visual-editor-renderer-react/src --glob='*.ts' --glob='*.tsx' -A 5
echo ""
echo "=== Check for block JSON sources in visual-editor ==="
find resources/js/visual-editor -name '*.ts' -o -name '*.tsx' | head -10
echo ""
echo "=== Search for where callout block attributes are set ==="
rg -n 'callout.*content\|attributes\.content' resources/js/visual-editor --glob='*.ts' --glob='*.tsx' --glob='*.js' -B 3 -A 3 || true
echo ""
echo "=== Check if there's HTML sanitization in the editor ==="
rg -n 'DOMPurify\|sanitizeHtml\|sanitize.*content\|allowedTags' resources/ --glob='*.ts' --glob='*.tsx' --glob='*.js' || trueRepository: ArtisanPack-UI/visual-editor
Length of output: 26262
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Check Blade callout renderer ==="
find . -name 'callout.blade.php' 2>/dev/null | head -5
echo ""
echo "=== Search for where block JSON is created/parsed in editor ==="
rg -n 'attributes.*save\|saveBlock\|block.*JSON' resources/js/visual-editor --glob='*.ts' --glob='*.tsx' -B 2 -A 2 | head -50
echo ""
echo "=== Check if there's an API layer that creates block data ==="
find . -name '*.php' -type f | xargs grep -l 'block.*attributes\|dangerouslySetInnerHTML' 2>/dev/null | head -10Repository: ArtisanPack-UI/visual-editor
Length of output: 266
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Read Blade callout renderer ==="
cat -n packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/callout.blade.php
echo ""
echo "=== Check if Blade uses any sanitization in block views ==="
rg -n 'e(\|trim\|Purifier\|sanitize' packages/visual-editor-renderer-blade/resources/views --glob='*.blade.php' -A 2 -B 2 | head -40Repository: ArtisanPack-UI/visual-editor
Length of output: 2104
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Check block edit/save in editor to understand data flow ==="
find resources/js/visual-editor -name 'callout' -type d
echo ""
echo "=== List callout block editor files ==="
ls -la resources/js/visual-editor/_legacy/editor/blocks/callout/ 2>/dev/null || ls -la resources/js/visual-editor/blocks/callout/ 2>/dev/null || echo "Not found in those paths"
echo ""
echo "=== Check for sanitization in any block editor ==="
rg -n 'sanitize\|DOMPurify\|allowedTags' resources/js/visual-editor --glob='*.ts' --glob='*.tsx' --glob='*.js' 2>/dev/null | head -20Repository: ArtisanPack-UI/visual-editor
Length of output: 803
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Check callout block save component ==="
cat -n resources/js/visual-editor/blocks/callout/save.tsx
echo ""
echo "=== Check callout block edit component ==="
cat -n resources/js/visual-editor/blocks/callout/edit.tsx
echo ""
echo "=== Check if any editor blocks sanitize content on save ==="
rg -n 'RichText\|contentToSave\|save.*content' resources/js/visual-editor/blocks --glob='*.tsx' -A 3 -B 1 | head -60Repository: ArtisanPack-UI/visual-editor
Length of output: 6620
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Check registry documentation ==="
cat -n packages/visual-editor-renderer-react/src/registry.ts | head -30
echo ""
echo "=== Check README for security notes ==="
cat packages/visual-editor-renderer-react/README.md 2>/dev/null || echo "No README found"
echo ""
echo "=== Check if block JSON is validated anywhere ==="
rg -n 'validate\|schema\|json.?schema' resources/js/visual-editor/blocks/callout --glob='*.json' --glob='*.ts' --glob='*.tsx' -B 1 -A 1
echo ""
echo "=== Check block.json schema ==="
cat -n resources/js/visual-editor/blocks/callout/block.jsonRepository: ArtisanPack-UI/visual-editor
Length of output: 5441
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Check block.json ==="
cat -n resources/js/visual-editor/blocks/callout/block.json
echo ""
echo "=== Check how BlockTree receives data - look for entry point ==="
cat -n packages/visual-editor-renderer-react/src/BlockTree.tsx | head -80
echo ""
echo "=== Check where block JSON originates from Inertia ==="
rg -n 'post\.content\|BlockTree\|HasBlockContent' --glob='*.tsx' --glob='*.ts' --glob='*.php' -B 2 -A 2 | head -40Repository: ArtisanPack-UI/visual-editor
Length of output: 4665
Sanitize HTML before using dangerouslySetInnerHTML.
Line 80 injects attributes.content as raw HTML without sanitization. The attrString() utility only type-coerces; it does not sanitize. Since block attributes are typed as Record<string, unknown> at runtime with no validation layer, if block JSON can originate from untrusted sources (API, user input), this is an XSS vector.
This pattern is systemic across the React renderer: paragraph, heading, quote, code, preformatted, list-item, figcaption, blockquote, table cells, and more all use dangerouslySetInnerHTML without sanitization. Consider:
- Adding DOMPurify or a similar sanitization library and sanitizing content at the renderer level
- Or, explicitly document and enforce a strict "trusted-HTML-only" contract with validation upstream (e.g., in the editor's save layer or a block JSON validator)
🧰 Tools
🪛 ast-grep (0.42.1)
[warning] 79-79: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/visual-editor-renderer-react/src/blocks/artisanpack/callout.tsx`
around lines 78 - 81, The JSX for the callout body uses dangerouslySetInnerHTML
with the raw content variable (div.ap-callout__body, dangerouslySetInnerHTML={{
__html: content }}), which is an XSS risk; fix by sanitizing content before
injection (e.g., import and use a sanitizer like DOMPurify and call
DOMPurify.sanitize(content) or run the same sanitizer via a shared helper before
passing into attrString/renderer), and apply the same sanitizer at the renderer
layer for other components that use dangerouslySetInnerHTML (paragraph, heading,
quote, code, preformatted, list-item, figcaption, blockquote, table cells) or
enforce/validate a trusted-HTML contract upstream.
| if ( ! isset( $metadata['name'] ) || ! is_string( $metadata['name'] ) || '' === trim( $metadata['name'] ) ) { | ||
| throw new InvalidArgumentException( | ||
| 'Block metadata is missing a non-empty "name" field.' | ||
| ); | ||
| } | ||
|
|
||
| $this->registry->register( $metadata['name'], $metadata ); |
There was a problem hiding this comment.
Validate and normalize the block name before registration.
This branch only checks that name is a non-empty string, but the new API contract and docblock both require namespace/name. Right now values like callout, foo/bar/baz, or ' artisanpack/callout ' will still be registered. Please trim once, require exactly two non-empty segments, and register the normalized value.
Suggested fix
- if ( ! isset( $metadata['name'] ) || ! is_string( $metadata['name'] ) || '' === trim( $metadata['name'] ) ) {
+ $name = is_string( $metadata['name'] ?? null ) ? trim( $metadata['name'] ) : '';
+ $parts = explode( '/', $name );
+
+ if ( '' === $name || 2 !== count( $parts ) || '' === $parts[0] || '' === $parts[1] ) {
throw new InvalidArgumentException(
- 'Block metadata is missing a non-empty "name" field.'
+ 'Block metadata must contain a valid "namespace/name" value.'
);
}
- $this->registry->register( $metadata['name'], $metadata );
+ $metadata['name'] = $name;
+ $this->registry->register( $name, $metadata );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/VisualEditor.php` around lines 77 - 83, Trim and normalize the block name
before registering: replace the current check on $metadata['name'] with code
that trims the value once, splits it on '/' and ensures there are exactly two
non-empty segments (namespace and name), throw the same InvalidArgumentException
if not valid, then register the normalized joined string (e.g. $normalizedName =
$segments[0] . '/' . $segments[1]) via
$this->registry->register($normalizedName, $metadata) instead of using the raw
$metadata['name'].
- docs/custom-blocks.md: label unlabeled file-tree/path fenced blocks with `text` to satisfy markdownlint MD040 - blocks/callout/block.json: drop `anchor` and `spacing` from `supports` — the Blade/React/Vue renderers don't round-trip either of them, so leaving them enabled would lose editor-set values on the frontend - blocks/callout/callout.css: canonicalize `currentColor` → `currentcolor` (stylelint `color-named` rule) - editor/custom-blocks.ts: tighten the default-vs-named export resolution — only prefer `module.default` when it actually carries the block shape (metadata.name + edit function), otherwise fall back to the named exports on the module itself - VisualEditor::registerBlock(): trim the incoming name once and write the normalized value back into the metadata array before registering, so `BlockTypeRegistry` stores a canonical value in both the registry key and the stored definition's own `name` field Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Description
Ships the documented pattern + tooling for host apps to register custom blocks under the
artisanpack/*namespace, plus one reference block (artisanpack/callout) that exercises the pattern end to end. V2 block forking (#331) is not in scope here.Closes: #346
Type of Change
Related Issue
Issue: #346
Motivation and Context
Pre-PR, zero ArtisanPack custom blocks existed.
DynamicBlock/ClosureDynamicBlocksupported server-rendered blocks, but there was no edit-side authoring pattern or reference static block and noartisanpackcategory in the inserter. A4 closes that gap.Changes Made
PHP
VisualEditor::registerBlock()to accept three input shapes: ablock.jsonpath, a class implementing the newProvidesBlockMetadatainterface, or a closure returning the metadata array.registerReferenceBlocks()inVisualEditorServiceProvider; addartisanpack/calloutto the defaultenabled_blocksallow-list.JS
editor/custom-blocks.ts: eagerimport.meta.globauto-discovery overresources/js/visual-editor/blocks/*/index.ts,artisanpackblock-category registration, and an optional inline-SVGiconoverride (the editor does not bundledashicons.css, so dashicon-slug icons render blank).discoverAndRegisterCustomBlocks()intoregisterOnce()ineditor-app.tsx.Reference block
blocks/callout/:block.jsonwith severity + icon enum attributes,edit.tsx(InspectorControls+RichText), matchingsave.tsx, scoped CSS, and an inline-SVG inserter icon.Renderers (Blade / React / Vue)
artisanpack/calloutpartial/component that mirrors thesave.tsxmarkup so server-rendered and client-rendered pages emit identical HTML.Docs
docs/custom-blocks.md: authoring pattern, directory structure, block.json schema, dynamic vs static guidance, per-renderer partial pattern, and a per-block checklist. Feeds into M15 · Docs, website, release #325 M15.How Has This Been Tested?
Testing Environment:
Tests Performed:
Accessibility Tests Run
SelectControlcomponents use the standard Gutenberg focus pattern, inserter entries remain keyboard-navigable.aria-hidden="true"on decorative elements; the RichText body carries the implicit role.Tests Added
Test details:
tests/Unit/VisualEditor/RegisterBlockTest.phpcovers all three registration forms + error paths;BlockRendererTestgains Blade renderer cases for the callout.resources/js/visual-editor/editor/__tests__/custom-blocks.test.tsxcovers category registration, theregisterCustomBlockspipeline (dedupe + icon override + skip-missing-name), and the callout edit/save round-trip; React and Vue rendererBlockTree.testfiles gain callout cases.Documentation
Documentation details:
docs/custom-blocks.mdis the new authoring guide — registration mechanics, schema notes, dynamic vs static, renderer wiring, and an authoring checklist.Screenshots
(available on request; manually verified in the dev app — ArtisanPack category + Callout inserter icon + severity/icon InspectorControls all render as expected.)
Pre-Submission Checklist
npx tsc --noEmitsurfaces only pre-existing@wordpress/*missing-types warnings)custom-blocks.ts)Summary by CodeRabbit
New Features
Documentation
Tests