Repository navigation
feat: G4b — re-enable core/archives, core/categories, core/tag-cloud - #426
ViewFromTheBox merged 2 commits into
Conversation
Re-enable the three taxonomy/feed widgets that the M5 audit deferred to
"empty-state — disabled by default" until cms-framework lands. Each
block ships as a `DynamicBlock` subclass under `src/Blocks/Core/` that
reads from cms-framework's `PostCategory`, `PostTag`, and `Post` models
respectively. Registration is gated on `class_exists(BlogManager::class)`
so the package still boots standalone — without cms-framework the
blocks stay deferred and the deny-list keeps them out of the inserter.
Editor preview, Blade renderer, React renderer, and Vue renderer all
resolve through the same `DynamicBlock::render()` path: the renderers
post to `/visual-editor/api/blocks/preview`, the controller routes
through the dynamic-block registry, and the registered block emits
HTML. One implementation, all four surfaces.
The upstream `core/categories`, `core/tag-cloud`, and `core/archives`
Edit components depended on shim surface that does not exist in V1
(`getTaxonomies`, `getTaxonomy('category').labels`, and the
`wp/v2/block-renderer/{name}` route). A new
`taxonomy-archive-block-overrides.tsx` module registers a
`blocks.registerBlockType` filter (BEFORE `registerCoreBlocks()`) that
swaps each block's `edit` with a thin wrapper using the existing
`ServerSideRender` shim, plus a minimal `InspectorControls` panel for
the toggles each block actually persists. Block save() shape stays
untouched so attribute round-trip is unaffected.
The taxonomy entity registration called for in #401's notes is
intentionally not added — the upstream Edit components all render
through `ServerSideRender`, so they never query
`useEntityRecords('taxonomy', 'category')`. Shim entities can be
layered in cheaply later if a future block needs them.
Tests: 484 PHP + 970 JS, all green. New coverage:
- CategoriesBlockTest: flat list, hierarchy, post counts, empty-term
filter, top-level filter, empty shell, XSS escape.
- TagCloudBlockTest: link rendering, font scaling, equal-count
collapse, empty-tag filter, count display, numberOfTags clamp,
empty paragraph, XSS escape.
- ArchivesBlockTest: monthly list, post counts, yearly grouping,
dropdown, unique dropdown ids per render, empty-state notice.
Closes #401
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughRe-enables three core widget blocks ( Changes
Sequence DiagramsequenceDiagram
participant User as Editor User
participant EditorUI as Editor Override<br/>(React Component)
participant SSR as ServerSideRender
participant BlockServer as PHP DynamicBlock<br/>(render endpoint)
participant DB as Database
User->>EditorUI: Change block attributes
EditorUI->>EditorUI: setAttributes()
EditorUI->>SSR: Request preview (attributes)
SSR->>BlockServer: POST render request
BlockServer->>DB: Query posts/categories/tags
DB-->>BlockServer: Return data
BlockServer->>BlockServer: Validate & build HTML
BlockServer-->>SSR: Return rendered HTML
SSR-->>EditorUI: Display preview
EditorUI-->>User: Show live preview
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)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 PHPStan (2.1.51)PHPStan was skipped because the sandbox runner could not parse its output. Review rate limit: 4/5 reviews remaining, refill in 12 minutes. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
resources/js/visual-editor/site-editor/site-editor-app.tsx (1)
161-176:⚠️ Potential issue | 🟠 MajorFilter won't apply if core blocks are already registered before site-editor boots
The
registerTaxonomyAndArchiveBlockOverrides()filter is installed before the conditionalregisterCoreBlocks()call, but filters onblocks.registerBlockTypeonly intercept active registrations. Ifcore/paragraph(or other core blocks) already exist in the registry—whether pre-loaded by a host app or registered via another import—the conditional skipsregisterCoreBlocks()entirely, so the filter never runs for those blocks and the overrideeditcomponents won't be applied.Unlike
editor-app.tsx, which callsregisterCoreBlocks()unconditionally (ensuring the filter always applies), the site-editor's conditional approach creates a window where pre-registered blocks bypass the override mechanism.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@resources/js/visual-editor/site-editor/site-editor-app.tsx` around lines 161 - 176, The issue is that registerTaxonomyAndArchiveBlockOverrides() is run before a conditional registerCoreBlocks() call so blocks already in the registry (e.g., detected by getBlockType('core/paragraph')) never get the override; fix by ensuring core blocks are always processed: either call registerCoreBlocks() unconditionally (remove the getBlockType(...) guard) or, if you must keep the guard, after registering the filter explicitly iterate existing registered blocks and apply the same override logic to any matching core blocks (use the same override function that targets blocks.registerBlockType). Update references: registerTaxonomyAndArchiveBlockOverrides, registerCoreBlocks, getBlockType, and the blocks.registerBlockType override handler so the override always reaches already-registered core blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/Blocks/Core/ArchivesBlock.php`:
- Around line 175-209: renderDropdown currently outputs a <select> with archive
URLs in option values but no client-side navigation, so add an inline onchange
handler on the <select> in renderDropdown (use the $dropdownId/select markup
built in renderDropdown) that navigates when a value is selected (e.g.
onchange="if(this.value) location.href=this.value"), ensuring you still escape
$dropdownId with e() and keep existing escaped option values; alternatively
document that hosts must provide their own navigation script if you prefer not
to inline.
In `@tests/Unit/VisualEditor/Blocks/Core/TagCloudBlockTest.php`:
- Around line 8-16: The test fixture function fakeTag builds a tag object with a
permalink property, but the block renderer expects a url property for anchor
hrefs; update fakeTag to set url (and optionally keep permalink for
compatibility) — replace or add $tag->url = '/blog/tag/' . $slug; (also adjust
the other occurrences around lines 25-31 where tag objects are created) so
assertions exercise the renderer's actual link field.
---
Outside diff comments:
In `@resources/js/visual-editor/site-editor/site-editor-app.tsx`:
- Around line 161-176: The issue is that
registerTaxonomyAndArchiveBlockOverrides() is run before a conditional
registerCoreBlocks() call so blocks already in the registry (e.g., detected by
getBlockType('core/paragraph')) never get the override; fix by ensuring core
blocks are always processed: either call registerCoreBlocks() unconditionally
(remove the getBlockType(...) guard) or, if you must keep the guard, after
registering the filter explicitly iterate existing registered blocks and apply
the same override logic to any matching core blocks (use the same override
function that targets blocks.registerBlockType). Update references:
registerTaxonomyAndArchiveBlockOverrides, registerCoreBlocks, getBlockType, and
the blocks.registerBlockType override handler so the override always reaches
already-registered core blocks.
🪄 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: 2424352f-a2b8-497c-b0b7-c29f759a496d
📒 Files selected for processing (14)
config/visual-editor.phpdocs/block-library-audit.mdresources/js/visual-editor/editor/editor-app.tsxresources/js/visual-editor/editor/taxonomy-archive-block-overrides.tsxresources/js/visual-editor/site-editor/__tests__/site-editor-app.test.tsxresources/js/visual-editor/site-editor/site-editor-app.tsxsrc/Blocks/Core/ArchivesBlock.phpsrc/Blocks/Core/CategoriesBlock.phpsrc/Blocks/Core/TagCloudBlock.phpsrc/VisualEditorServiceProvider.phptests/Unit/VisualEditor/Blocks/Core/ArchivesBlockTest.phptests/Unit/VisualEditor/Blocks/Core/CategoriesBlockTest.phptests/Unit/VisualEditor/Blocks/Core/TagCloudBlockTest.phptests/Unit/VisualEditor/EnabledBlockNamesTest.php
| function fakeTag( int $id, string $name, string $slug, int $count ): object | ||
| { | ||
| $tag = new stdClass(); | ||
| $tag->id = $id; | ||
| $tag->name = $name; | ||
| $tag->slug = $slug; | ||
| $tag->posts_count = $count; | ||
| $tag->permalink = '/blog/tag/' . $slug; | ||
|
|
There was a problem hiding this comment.
Tag fixture shape doesn’t match renderer link field
The test fixture sets permalink, but the block renderer reads url for anchor href. This makes link assertions brittle/failing against the actual render contract.
Proposed fix
function fakeTag( int $id, string $name, string $slug, int $count ): object
{
$tag = new stdClass();
$tag->id = $id;
$tag->name = $name;
$tag->slug = $slug;
$tag->posts_count = $count;
- $tag->permalink = '/blog/tag/' . $slug;
+ $tag->url = '/blog/tag/' . $slug;
+ $tag->permalink = '/blog/tag/' . $slug;
return $tag;
}Also applies to: 25-31
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/Unit/VisualEditor/Blocks/Core/TagCloudBlockTest.php` around lines 8 -
16, The test fixture function fakeTag builds a tag object with a permalink
property, but the block renderer expects a url property for anchor hrefs; update
fakeTag to set url (and optionally keep permalink for compatibility) — replace
or add $tag->url = '/blog/tag/' . $slug; (also adjust the other occurrences
around lines 25-31 where tag objects are created) so assertions exercise the
renderer's actual link field.
CodeRabbit flagged that `renderDropdown()` emits a `<select>` whose options carry archive URLs but no client-side navigation, so picking a value did nothing. Add an inline `onchange` handler that mirrors upstream `core/archives` behaviour: `onchange="if(this.value)document.location.href=this.value"`. Hosts under a strict CSP that forbids inline handlers can rebind this block via the dynamic-block registry to emit a data-attribute hook plus a host-supplied script instead — documented inline. Tests: - New "attaches an inline onchange handler so the dropdown navigates on select" case asserting the literal handler string survives. - Existing dropdown-markup case tightened to require the `onchange` attribute on the `<select>`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/Blocks/Core/ArchivesBlock.php`:
- Around line 126-137: The decorate method is building archive URLs with a
hard-coded '/blog' prefix; update it to use a configurable route prefix instead
of '/blog' so custom blog routes won't 404. Replace the literal '/blog' in the
two url(...) calls inside decorate with a value read from configuration or a
class property (e.g., use config('blog.route', 'blog') or $this->routePrefix)
and ensure padding/formatting (sprintf('/%s/%04d'...) and
sprintf('/%s/%04d/%02d'...)) preserves behavior; update any related class
property or constructor to provide the default fallback if not configured.
🪄 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: 605ea541-2ec5-4a7e-8c9f-f4bfad54ab0c
📒 Files selected for processing (2)
src/Blocks/Core/ArchivesBlock.phptests/Unit/VisualEditor/Blocks/Core/ArchivesBlockTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/Unit/VisualEditor/Blocks/Core/ArchivesBlockTest.php
| protected function decorate( array $bucket ): array | ||
| { | ||
| if ( null === $bucket['month'] ) { | ||
| $bucket['label'] = (string) $bucket['year']; | ||
| $bucket['url'] = url( sprintf( '/blog/%04d', $bucket['year'] ) ); | ||
|
|
||
| return $bucket; | ||
| } | ||
|
|
||
| $bucket['label'] = Carbon::create( $bucket['year'], $bucket['month'], 1 )->translatedFormat( 'F Y' ); | ||
| $bucket['url'] = url( sprintf( '/blog/%04d/%02d', $bucket['year'], $bucket['month'] ) ); | ||
|
|
There was a problem hiding this comment.
Avoid hard-coded archive URLs; this can break links on non-default blog routes.
On Line 130 and Line 136, URLs are hard-coded to /blog/..., but the class doc says route customization should be supported via config/rebinding. On installs with custom archive routes, these links can 404.
💡 Suggested fix (fallback-safe configurable patterns)
protected function decorate( array $bucket ): array
{
+ $yearlyPattern = (string) config( 'visual-editor.archive_url_pattern_yearly', '/blog/%04d' );
+ $monthlyPattern = (string) config( 'visual-editor.archive_url_pattern_monthly', '/blog/%04d/%02d' );
+
if ( null === $bucket['month'] ) {
$bucket['label'] = (string) $bucket['year'];
- $bucket['url'] = url( sprintf( '/blog/%04d', $bucket['year'] ) );
+ $bucket['url'] = url( sprintf( $yearlyPattern, $bucket['year'] ) );
return $bucket;
}
$bucket['label'] = Carbon::create( $bucket['year'], $bucket['month'], 1 )->translatedFormat( 'F Y' );
- $bucket['url'] = url( sprintf( '/blog/%04d/%02d', $bucket['year'], $bucket['month'] ) );
+ $bucket['url'] = url( sprintf( $monthlyPattern, $bucket['year'], $bucket['month'] ) );
return $bucket;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| protected function decorate( array $bucket ): array | |
| { | |
| if ( null === $bucket['month'] ) { | |
| $bucket['label'] = (string) $bucket['year']; | |
| $bucket['url'] = url( sprintf( '/blog/%04d', $bucket['year'] ) ); | |
| return $bucket; | |
| } | |
| $bucket['label'] = Carbon::create( $bucket['year'], $bucket['month'], 1 )->translatedFormat( 'F Y' ); | |
| $bucket['url'] = url( sprintf( '/blog/%04d/%02d', $bucket['year'], $bucket['month'] ) ); | |
| protected function decorate( array $bucket ): array | |
| { | |
| $yearlyPattern = (string) config( 'visual-editor.archive_url_pattern_yearly', '/blog/%04d' ); | |
| $monthlyPattern = (string) config( 'visual-editor.archive_url_pattern_monthly', '/blog/%04d/%02d' ); | |
| if ( null === $bucket['month'] ) { | |
| $bucket['label'] = (string) $bucket['year']; | |
| $bucket['url'] = url( sprintf( $yearlyPattern, $bucket['year'] ) ); | |
| return $bucket; | |
| } | |
| $bucket['label'] = Carbon::create( $bucket['year'], $bucket['month'], 1 )->translatedFormat( 'F Y' ); | |
| $bucket['url'] = url( sprintf( $monthlyPattern, $bucket['year'], $bucket['month'] ) ); | |
| return $bucket; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/Blocks/Core/ArchivesBlock.php` around lines 126 - 137, The decorate
method is building archive URLs with a hard-coded '/blog' prefix; update it to
use a configurable route prefix instead of '/blog' so custom blog routes won't
404. Replace the literal '/blog' in the two url(...) calls inside decorate with
a value read from configuration or a class property (e.g., use
config('blog.route', 'blog') or $this->routePrefix) and ensure
padding/formatting (sprintf('/%s/%04d'...) and sprintf('/%s/%04d/%02d'...))
preserves behavior; update any related class property or constructor to provide
the default fallback if not configured.
Description
Re-enable the three taxonomy/feed widgets the M5 audit deferred to "empty-state — disabled by default" until cms-framework landed. Each block ships as a
DynamicBlocksubclass undersrc/Blocks/Core/that reads from cms-framework'sPostCategory,PostTag, andPostmodels respectively. Registration is gated onclass_exists(BlogManager::class)so the package still boots standalone — without cms-framework the blocks stay deferred and the deny-list keeps them out of the inserter.Closes: #401
Type of Change
Related Issue
Issue: #401
Motivation and Context
#401 is Wave 4 of the V1 ship plan (plan 12 §2.5). G4a verified
core/post-*against G3 entities; this is the matching wire-up for the three taxonomy/feed widgets the audit had been holding in the deny-list.Changes Made
DynamicBlocksubclasses (src/Blocks/Core/{Categories,TagCloud,Archives}Block.php). Each isolates the data fetch in aprotected function fetch*()method so tests can override the data source without exercising the database.class_exists(BlogManager::class)in a newregisterTaxonomyAndArchiveBlocks()boot step.config/visual-editor.php: moved the three blocks fromdisabled_blockstoenabled_blocks; updated the explanatory comment.D2_DISABLED_BLOCKSinsite-editor-app.tsxmirrors the PHP change.EnabledBlockNamesTestsnapshot updated.Editor surface
The upstream
core/categories,core/tag-cloud, andcore/archivesEdit components depend on shim surface that does not exist in V1 (getTaxonomies,getTaxonomy('category').labels, and thewp/v2/block-renderer/{name}route). A newtaxonomy-archive-block-overrides.tsxmodule registers ablocks.registerBlockTypefilter (BEFOREregisterCoreBlocks()) that swaps each block'seditwith a thin wrapper using the existingServerSideRendershim, plus a minimalInspectorControlspanel for the toggles each block actually persists. Block save() shape stays untouched so attribute round-trip is unaffected.Wired into both
editor-app.tsx(post editor) andsite-editor-app.tsx(site editor); idempotent across HMR via a global Symbol guard.Renderer integration
Editor preview, Blade renderer, React renderer, and Vue renderer all resolve through the same
DynamicBlock::render()path. Renderers post to/visual-editor/api/blocks/preview, the controller routes through the dynamic-block registry, and the registered block emits HTML. One implementation, all four surfaces.Documented deviation
The taxonomy entity registration called for in #401's implementation notes (
{kind: 'taxonomy', name: 'category', baseURL: '/post-categories', …}) is intentionally not added in V1. The upstream Edit components all render throughServerSideRender(and our overrides do the same), so they never queryuseEntityRecords('taxonomy', 'category'). Adding entities pointing at non-existent endpoints would be dead code in V1;PostCategory/PostTagare notHasBlockContentmodels so they do not fitWpEntityControllereither. The audit doc captures this — entities can be layered in cheaply later if a future block needs them.How Has This Been Tested?
Testing Environment:
release/1.0Tests Performed:
./vendor/bin/pest): 484 passed (1 pre-existing skip), 1519 assertions.npm test): 970 passed (1 pre-existing skip).CodeRabbit: two-pass local review (
cr review --base release/1.0 --prompt-only) — flagged a hardcodedid="wp-block-archives-dropdown"collision risk, fixed inArchivesBlock::renderDropdown()with auniqid()suffix shared between the<select>and the label'sfor=. Second pass: no findings.Accessibility Tests Run
Details:
core/archivesuses<label class="screen-reader-text" for="wp-block-archives-dropdown-{uniqid}">paired to a matching<select id=...>. The CodeRabbit-flagged hardcoded id was replaced so multiplecore/archivesblocks on a page do not collide on the samefor/idpair.e()(Laravel'shtmlspecialcharswrapper). XSS escape is asserted in CategoriesBlockTest + TagCloudBlockTest.Tests Added
Test details:
CategoriesBlockTest(7 cases): flat list, hierarchy, post counts, empty-term filter, top-level filter, empty shell, XSS escape.TagCloudBlockTest(8 cases): link rendering, font scaling, equal-count collapse, empty-tag filter, count display,numberOfTagsclamp, empty paragraph, XSS escape.ArchivesBlockTest(6 cases): monthly list, post counts, yearly grouping, dropdown markup, unique dropdown ids per render, empty-state notice.EnabledBlockNamesTest: snapshot updated for the three new entries; comment notes G4b promotion.Tests use anonymous-class subclasses that override the fetch methods to inject synthetic data, so the suite proves the rendering pipeline without coupling to cms-framework's models or pulling cms-framework into the dev deps.
Documentation
Documentation details:
docs/block-library-audit.mdremoves the three blocks from the empty-state-deferred table and adds a new "G4b — re-enabled" subsection covering the architecture (DynamicBlock pathway, four-surface render, archive URL pattern, host-override path, deferred-entities deviation).Pre-Submission Checklist
Summary by CodeRabbit
New Features
Documentation
Tests