Starting with v1.2, the artisanpack/related-posts block nests an
artisanpack/post-template as its iteration host — the same hosting
pattern artisanpack/query uses. This change unlocks Query Loop's
WYSIWYG canvas (one editable iteration plus N read-only ghosts),
per-post variants (artisanpack/post-variant), variable grid spans,
and masonry packing for related-posts rails without any per-block
fork.
Saved-content backward compatibility
Pre-#601 Related Posts saves have a flat inner-blocks tree — the
post-title / post-date / post-excerpt children sit directly
under artisanpack/related-posts with no artisanpack/post-template
wrapper.
The QueryInliner::expandRelatedPosts() pre-pass auto-detects this on
the public-render path:
- With a nested
post-template(new content): iterations are expanded under the post-template wrapper, exactly like Query Loop — variants, grid spans, and masonry packing all inherit. - Flat inner blocks (legacy content): the legacy per-iteration
expansion kicks in, producing one
core/post-template-itemper resolved post directly underartisanpack/related-posts. Public render output is unchanged from pre-#601.
No content migration is required. Authors who want to opt into the new capabilities re-save the block (which rewrites the saved tree through the editor template) or insert a new Related Posts block.
Editor changes
- With a nested
artisanpack/post-template, Related Posts doesn't expose layout (list / grid / masonry) or column controls itself — the post template owns those via its existing toolbar + inspector, matching the Query Loop experience. Selecting the inner Post Template surfaces the layout toolbar and column count. - Flat content (no nested post template) lays out its related items
itself (#964): Related Posts shows List / Grid toolbar toggles and,
for a grid, a Columns control (2–6). They write
displayLayout({ type, columns }) and mirror the column count intonumColumns. - Related Posts gains a Post Variants inspector panel — the same
PostVariantsPanelQuery Loop uses — so authors can register position / pattern / meta variants without leaving the parent block. - The Related Posts inspector keeps the query-level controls: number of posts, offset, and order.
- Newly-inserted Related Posts blocks seed an
artisanpack/post-templatewith the same defaultpost-title/post-date/post-excerptchildren. Existing flat saves are not migrated by the editor on load — they keep rendering through the legacy path. - When no host post is in editor scope (e.g. a template-editor view with no preview post selected), the canvas surfaces an info Notice instead of issuing a preview fetch.
- When the host post matches zero related posts, the canvas keeps the
editable iteration template and surfaces a warning Notice. This
replaces the pre-#601
ap-related-posts__preview-emptytext fallback.
Resolver API change
POST /visual-editor/api/query/resolve accepts a new relatedTo
field — an integer host-post id. When present:
- The server loads the host post via the bound
QueryResolverContract. - It derives the host's primary taxonomy and term ids via
HostRelatedTermsResolver::hostRelatedTerms()(categories → tags → generictermsfallback, mirroringQueryInliner::expandRelatedPosts). - The payload is rewritten into a
taxQuery: { taxonomy, terms, operator: 'IN' }related-by-taxonomy query withexclude: [hostId], then forwarded to the resolver. relatedTois mutually exclusive withtaxQueryat the request layer.
When the host post can't be loaded or carries no related-terms signal, the controller short-circuits with an empty paginator envelope (no resolver call for the related query).
Renderer parity
The related-posts wrapper now emits only the ap-related-posts class
across all three renderers (Blade / React / Vue). Layout (grid /
masonry / list) and column count are rendered by the nested
artisanpack/post-template via its existing is-layout-* / columns-N
classes — the same module the standalone Query Loop renders against,
so masonry + grid-spans + variants reuse one CSS module instead of
forking per host.
Flat content is the exception (#964). There the related items are the
synthetic core/post-template-items directly under the wrapper, so a
grid displayLayout puts Post Template's grid classes on the wrapper
itself: ap-related-posts is-layout-grid wp-block-artisanpack-related-posts-is-layout-grid columns-N, with the
column count read from displayLayout.columns, else numColumns,
else 3 (clamped to 1–12). post-template.css styles
.ap-related-posts.is-layout-grid like .wp-block-post-template: the
same tracks, the --wp--style--block-gap gap from Block spacing
(style.spacing.blockGap), and the same collapse (three or more
columns drop to two at 1023px and below, and every grid to one at
767px and below). The React and Vue renderers now print the wrapper's
Block spacing too, as Blade does. With a nested post template the
wrapper ignores displayLayout.
The pre-#601 wrapper classes (ap-related-posts-has-N-columns,
ap-related-posts__item*, etc.) and their styles have been removed —
they only existed for the legacy flat-card preview UX that is no
longer wired up.
Out of scope (intentional)
- Pagination siblings (
artisanpack/query-pagination*) — Related Posts is a finite list capped atnumPosts: 10; pagination has no conceptual meaning. artisanpack/query-titleandartisanpack/query-no-resultssiblings.- Inline editing of individual non-first iterations (variants are the per-post override mechanism, same as Query Loop).
- Changing the "relatedness" rule. Related Posts stays locked to "same post type, sharing at least one term in the host's primary taxonomy".
- The
enhancedPaginationattribute onrelated-posts/block.jsonis preserved for saved-content safety but remains unsurfaced in the editor.

