|null FAQPage schema node, or null when no items. */ public function build( int $post_id, string $permalink ): ?array { $items = $this->get_validated_items( $post_id ); if ( empty( $items ) ) { return null; } return array( '@type' => 'FAQPage', '@id' => $permalink . '#faq', 'mainEntity' => array_map( array( $this, 'build_question' ), $items ), ); } /** * Build a single Question node from a FAQ item array. * * @param array{question: string, answer: string} $item Sanitized FAQ item. * @return array Schema.org Question node. */ private function build_question( array $item ): array { return array( '@type' => 'Question', 'name' => esc_html( $item['question'] ), 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => esc_html( $item['answer'] ), ), ); } /** * Read and validate _pr_faq_items meta, returning only well-formed items. * * Returns empty array when meta is absent, empty, or malformed. * Does not trust stored values — re-validates each item at read time. * * @param int $post_id WordPress post ID. * @return array Validated items. */ private function get_validated_items( int $post_id ): array { $raw = get_post_meta( $post_id, self::META_FAQ_ITEMS, true ); if ( ! is_string( $raw ) || '' === $raw ) { return array(); } $decoded = json_decode( $raw, true ); if ( ! is_array( $decoded ) || empty( $decoded ) ) { return array(); } $valid = array(); foreach ( $decoded as $item ) { if ( ! is_array( $item ) ) { continue; } $question = sanitize_text_field( (string) ( $item['question'] ?? '' ) ); $answer = sanitize_textarea_field( (string) ( $item['answer'] ?? '' ) ); if ( '' !== $question && '' !== $answer ) { $valid[] = array( 'question' => $question, 'answer' => $answer, ); } } return $valid; } }