From 6bcb8b6983036f75da77b4a107a2339201f9a783 Mon Sep 17 00:00:00 2001 From: mricoul Date: Tue, 30 Jun 2026 12:16:00 +0200 Subject: [PATCH 1/4] fix: Instantly synchronize FAQ question headings Refactors heading level synchronization using `useLayoutEffect` and direct calls. This prevents visual delays and inconsistencies when the parent FAQ block's `isAccordion` or `headingLevel` attributes change in the editor. --- src/faq/edit.js | 110 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/src/faq/edit.js b/src/faq/edit.js index 09d2423..23728e2 100644 --- a/src/faq/edit.js +++ b/src/faq/edit.js @@ -6,7 +6,6 @@ import { useBlockProps, useInnerBlocksProps, InspectorControls, - useBlockEditContext, store as blockEditorStore, } from '@wordpress/block-editor'; import { @@ -30,7 +29,7 @@ import { import { useDispatch, useSelect } from '@wordpress/data'; import { createBlock } from '@wordpress/blocks'; import { __, sprintf } from '@wordpress/i18n'; -import { useEffect } from '@wordpress/element'; +import { useEffect, useLayoutEffect } from '@wordpress/element'; const QUESTION_BLOCK = 'blockparty/faq-question'; const HEADING_LEVELS = [ 2, 3, 4, 5, 6 ]; @@ -53,30 +52,46 @@ function collectQuestionBlocks( blocks ) { } ); } -function useSyncQuestionHeadingLevels( headingLevel, isAccordion ) { - const { clientId } = useBlockEditContext(); - const { updateBlockAttributes } = useDispatch( blockEditorStore ); - const questionBlocks = useSelect( - ( select ) => { - const { getBlocksByClientId } = select( blockEditorStore ); - const [ faqBlock ] = getBlocksByClientId( clientId ); - - return collectQuestionBlocks( faqBlock?.innerBlocks || [] ); - }, - [ clientId ] - ); +function syncQuestionHeadingLevels( + clientId, + headingLevel, + isAccordion, + { getBlocksByClientId, updateBlockAttributes } +) { + if ( ! isAccordion ) { + return; + } - useEffect( () => { - if ( ! isAccordion ) { - return; + const [ faqBlock ] = getBlocksByClientId( clientId ); + const questionBlocks = collectQuestionBlocks( faqBlock?.innerBlocks || [] ); + + questionBlocks.forEach( ( block ) => { + if ( block.attributes.headingLevel !== headingLevel ) { + updateBlockAttributes( block.clientId, { headingLevel } ); } + } ); +} - questionBlocks.forEach( ( block ) => { - if ( block.attributes.headingLevel !== headingLevel ) { - updateBlockAttributes( block.clientId, { headingLevel } ); - } +function useSyncQuestionHeadingLevels( + clientId, + headingLevel, + isAccordion, + blockEditor +) { + const { getBlocksByClientId, updateBlockAttributes } = blockEditor; + + useLayoutEffect( () => { + syncQuestionHeadingLevels( clientId, headingLevel, isAccordion, { + getBlocksByClientId, + updateBlockAttributes, } ); - }, [ headingLevel, isAccordion, questionBlocks, updateBlockAttributes ] ); + }, [ + clientId, + headingLevel, + isAccordion, + getBlocksByClientId, + updateBlockAttributes, + ] ); } export default function Edit( { clientId, attributes, setAttributes } ) { @@ -89,13 +104,19 @@ export default function Edit( { clientId, attributes, setAttributes } ) { } ); const { insertBlock, updateBlockAttributes } = - useDispatch( 'core/block-editor' ); - const { getBlocks } = useSelect( - ( select ) => select( 'core/block-editor' ), + useDispatch( blockEditorStore ); + const blockEditor = useSelect( + ( select ) => select( blockEditorStore ), [] ); + const { getBlocks, getBlocksByClientId } = blockEditor; - useSyncQuestionHeadingLevels( headingLevel, isAccordion ); + useSyncQuestionHeadingLevels( + clientId, + headingLevel, + isAccordion, + blockEditor + ); // Synchronize isAccordion attribute to all child blocks useEffect( () => { @@ -149,9 +170,21 @@ export default function Edit( { clientId, attributes, setAttributes } ) { 'blockparty-faq' ) } checked={ isAccordion } - onChange={ ( value ) => - setAttributes( { isAccordion: value } ) - } + onChange={ ( value ) => { + setAttributes( { isAccordion: value } ); + + if ( value ) { + syncQuestionHeadingLevels( + clientId, + headingLevel, + true, + { + getBlocksByClientId, + updateBlockAttributes, + } + ); + } + } } __nextHasNoMarginBottom /> { isAccordion && ( @@ -167,11 +200,22 @@ export default function Edit( { clientId, attributes, setAttributes } ) { value={ headingLevel } isBlock __next40pxDefaultSize - onChange={ ( value ) => + onChange={ ( value ) => { + const newHeadingLevel = Number( value ); + setAttributes( { - headingLevel: Number( value ), - } ) - } + headingLevel: newHeadingLevel, + } ); + syncQuestionHeadingLevels( + clientId, + newHeadingLevel, + isAccordion, + { + getBlocksByClientId, + updateBlockAttributes, + } + ); + } } > { HEADING_LEVELS.map( ( level ) => ( Date: Tue, 30 Jun 2026 12:18:19 +0200 Subject: [PATCH 2/4] fix: Safely merge and renumber FAQPage schema questions Ensures that new FAQ questions are merged with existing ones in the schema data, rather than overwriting them. Normalizes the structure of existing questions and renumbers all combined questions to maintain valid Schema.org position values. --- includes/Services/Seopress_Seo_Service.php | 47 +++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/includes/Services/Seopress_Seo_Service.php b/includes/Services/Seopress_Seo_Service.php index 0c054b4..55e9514 100644 --- a/includes/Services/Seopress_Seo_Service.php +++ b/includes/Services/Seopress_Seo_Service.php @@ -126,14 +126,59 @@ private function merge_block_faq_into_schema( array $json ): ?array { return null; } + $existing_questions = $this->normalize_main_entity( $json['mainEntity'] ?? [] ); + $merged_questions = array_merge( $existing_questions, $questions ); + $json['@type'] = 'FAQPage'; - $json['mainEntity'] = $questions; + $json['mainEntity'] = $this->renumber_question_positions( $merged_questions ); $json['@context'] = $json['@context'] ?? 'https://schema.org'; $json['inLanguage'] = $json['inLanguage'] ?? get_bloginfo( 'language' ); return $json; } + /** + * Normalize FAQPage mainEntity to a list of Question entries. + * + * @param mixed $main_entity FAQPage mainEntity value. + * + * @return array> + */ + private function normalize_main_entity( $main_entity ): array { + if ( empty( $main_entity ) || ! is_array( $main_entity ) ) { + return []; + } + + if ( isset( $main_entity['@type'] ) ) { + return [ $main_entity ]; + } + + return array_values( + array_filter( + $main_entity, + static fn( $question ) => is_array( $question ) && ! empty( $question ) + ) + ); + } + + /** + * Ensure sequential position values on merged FAQ questions. + * + * @param array> $questions Question entries. + * + * @return array> + */ + private function renumber_question_positions( array $questions ): array { + $position = 1; + + foreach ( $questions as $index => $question ) { + $questions[ $index ]['position'] = $position; + ++$position; + } + + return $questions; + } + /** * Create a schema generator from the current singular post. * From d4eab602f633c53a300c9d2f41aaef6f3f94b442 Mon Sep 17 00:00:00 2001 From: mricoul Date: Tue, 30 Jun 2026 12:22:11 +0200 Subject: [PATCH 3/4] fix: Use data hooks for FAQ heading synchronization Updates the `useSyncQuestionHeadingLevels` hook to leverage `@wordpress/data`'s `useSelect` and `useDispatch` for accessing block editor state and actions. This removes the need to pass the `blockEditor` object as a prop, improving the hook's encapsulation and aligning with modern WordPress block editor development patterns. --- src/faq/edit.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/faq/edit.js b/src/faq/edit.js index 23728e2..ffee6cf 100644 --- a/src/faq/edit.js +++ b/src/faq/edit.js @@ -72,13 +72,12 @@ function syncQuestionHeadingLevels( } ); } -function useSyncQuestionHeadingLevels( - clientId, - headingLevel, - isAccordion, - blockEditor -) { - const { getBlocksByClientId, updateBlockAttributes } = blockEditor; +function useSyncQuestionHeadingLevels( clientId, headingLevel, isAccordion ) { + const { getBlocksByClientId } = useSelect( + ( select ) => select( blockEditorStore ), + [] + ); + const { updateBlockAttributes } = useDispatch( blockEditorStore ); useLayoutEffect( () => { syncQuestionHeadingLevels( clientId, headingLevel, isAccordion, { @@ -111,12 +110,7 @@ export default function Edit( { clientId, attributes, setAttributes } ) { ); const { getBlocks, getBlocksByClientId } = blockEditor; - useSyncQuestionHeadingLevels( - clientId, - headingLevel, - isAccordion, - blockEditor - ); + useSyncQuestionHeadingLevels( clientId, headingLevel, isAccordion ); // Synchronize isAccordion attribute to all child blocks useEffect( () => { From 9bdccb965ea51e9fc9ab9ed806d9d3e60ef8c0ed Mon Sep 17 00:00:00 2001 From: mricoul Date: Tue, 30 Jun 2026 12:24:11 +0200 Subject: [PATCH 4/4] fix: Ensure FAQ heading synchronization reacts to inner block changes Updates the `useSyncQuestionHeadingLevels` hook to explicitly select the inner FAQ question blocks using `useSelect`. By including these blocks in the `useEffect` dependencies, heading levels are now correctly re-synchronized when questions are added, removed, or reordered within the FAQ block. --- src/faq/edit.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/faq/edit.js b/src/faq/edit.js index ffee6cf..1dfe691 100644 --- a/src/faq/edit.js +++ b/src/faq/edit.js @@ -73,6 +73,15 @@ function syncQuestionHeadingLevels( } function useSyncQuestionHeadingLevels( clientId, headingLevel, isAccordion ) { + const questionBlocks = useSelect( + ( select ) => { + const { getBlocksByClientId } = select( blockEditorStore ); + const [ faqBlock ] = getBlocksByClientId( clientId ); + + return collectQuestionBlocks( faqBlock?.innerBlocks || [] ); + }, + [ clientId ] + ); const { getBlocksByClientId } = useSelect( ( select ) => select( blockEditorStore ), [] @@ -88,6 +97,7 @@ function useSyncQuestionHeadingLevels( clientId, headingLevel, isAccordion ) { clientId, headingLevel, isAccordion, + questionBlocks, getBlocksByClientId, updateBlockAttributes, ] );