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. * diff --git a/src/faq/edit.js b/src/faq/edit.js index 09d2423..1dfe691 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,9 +52,27 @@ function collectQuestionBlocks( blocks ) { } ); } -function useSyncQuestionHeadingLevels( headingLevel, isAccordion ) { - const { clientId } = useBlockEditContext(); - const { updateBlockAttributes } = useDispatch( blockEditorStore ); +function syncQuestionHeadingLevels( + clientId, + headingLevel, + isAccordion, + { getBlocksByClientId, updateBlockAttributes } +) { + if ( ! isAccordion ) { + return; + } + + const [ faqBlock ] = getBlocksByClientId( clientId ); + const questionBlocks = collectQuestionBlocks( faqBlock?.innerBlocks || [] ); + + questionBlocks.forEach( ( block ) => { + if ( block.attributes.headingLevel !== headingLevel ) { + updateBlockAttributes( block.clientId, { headingLevel } ); + } + } ); +} + +function useSyncQuestionHeadingLevels( clientId, headingLevel, isAccordion ) { const questionBlocks = useSelect( ( select ) => { const { getBlocksByClientId } = select( blockEditorStore ); @@ -65,18 +82,25 @@ function useSyncQuestionHeadingLevels( headingLevel, isAccordion ) { }, [ clientId ] ); + const { getBlocksByClientId } = useSelect( + ( select ) => select( blockEditorStore ), + [] + ); + const { updateBlockAttributes } = useDispatch( blockEditorStore ); - useEffect( () => { - if ( ! isAccordion ) { - return; - } - - questionBlocks.forEach( ( block ) => { - if ( block.attributes.headingLevel !== headingLevel ) { - updateBlockAttributes( block.clientId, { headingLevel } ); - } + useLayoutEffect( () => { + syncQuestionHeadingLevels( clientId, headingLevel, isAccordion, { + getBlocksByClientId, + updateBlockAttributes, } ); - }, [ headingLevel, isAccordion, questionBlocks, updateBlockAttributes ] ); + }, [ + clientId, + headingLevel, + isAccordion, + questionBlocks, + getBlocksByClientId, + updateBlockAttributes, + ] ); } export default function Edit( { clientId, attributes, setAttributes } ) { @@ -89,13 +113,14 @@ 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 ); // Synchronize isAccordion attribute to all child blocks useEffect( () => { @@ -149,9 +174,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 +204,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 ) => (