diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c433ac..f409319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to Peptide Repo Core are documented here. Format: [Semantic Versioning](https://semver.org/). +## [0.6.1] — 2026-06-13 + +### Fixed +- Fix: Yoast `wpseo_schema_webpage_type` passes `string|array` on singulars — accept `string|array` in `retype_to_medical_webpage()` signature; return type updated to match (PHP 8.1+ union). Eliminates `TypeError` → HTTP 500 on all singular pages (monographs + plain Pages) under `strict_types=1`. +- Fix: `enrich_webpage_piece` compared `'WebPage' === $piece['@type']` but Yoast sets `@type` as an array on singular pages. Normalise `@type` to array before `in_array()` check; `lastReviewed`, `reviewedBy`, and `audience` now inject correctly. +- Add regression test `tests/unit/test-jsonld-webpage.php`: exercises `retype_to_medical_webpage()` with array input for peptide and non-peptide contexts (no TypeError), and `enrich_webpage_piece` with `@type` as array (enrichment injects). Tests fail against v0.6.0, pass after fix. + ## [0.6.0] — 2026-06-13 ### Added diff --git a/includes/frontend/class-pr-core-jsonld-webpage.php b/includes/frontend/class-pr-core-jsonld-webpage.php index 4fd85d7..4b51e87 100644 --- a/includes/frontend/class-pr-core-jsonld-webpage.php +++ b/includes/frontend/class-pr-core-jsonld-webpage.php @@ -28,10 +28,17 @@ class PR_Core_Jsonld_Webpage { * Hooked on: wpseo_schema_webpage_type (Yoast SEO ≥ 19.x). * Passes through unchanged on non-peptide pages. * - * @param string $type Existing Yoast page type (e.g., 'WebPage'). - * @return string 'MedicalWebPage' on peptide singles, $type otherwise. + * Yoast contract: passes a plain string (e.g., 'CollectionPage') for + * non-singular special types, but an ARRAY (e.g., ['WebPage']) for every + * singular page — including plain Pages and monographs. Under strict_types=1 + * a string-only signature throws TypeError on all singulars. This method + * therefore accepts string|array and returns the same union type so callers + * (Yoast's filter chain) receive the value in the shape they passed it. + * + * @param string|array $type Existing Yoast page type — string or string[]. + * @return string|array 'MedicalWebPage' (string) on peptide singles; $type unchanged otherwise. */ - public function retype_to_medical_webpage( string $type ): string { + public function retype_to_medical_webpage( string|array $type ): string|array { if ( is_singular( PR_Core_Peptide_CPT::POST_TYPE ) ) { return 'MedicalWebPage'; } @@ -55,8 +62,8 @@ class PR_Core_Jsonld_Webpage { return $graph; } - $post_id = get_the_ID(); - $enrichments = $this->build_webpage_enrichments( (int) $post_id ); + $post_id = get_the_ID(); + $enrichments = $this->build_webpage_enrichments( (int) $post_id ); if ( empty( $enrichments ) ) { return $graph; @@ -67,8 +74,12 @@ class PR_Core_Jsonld_Webpage { continue; } - $type = $piece['@type'] ?? ''; - if ( 'WebPage' === $type || 'MedicalWebPage' === $type ) { + // Yoast may set @type as a plain string OR an array of strings. + // Normalise to array so in_array() works regardless of shape. + $raw_type = $piece['@type'] ?? ''; + $types = is_array( $raw_type ) ? $raw_type : [ $raw_type ]; + + if ( in_array( 'WebPage', $types, true ) || in_array( 'MedicalWebPage', $types, true ) ) { foreach ( $enrichments as $key => $value ) { $piece[ $key ] = $value; } diff --git a/peptide-repo-core.php b/peptide-repo-core.php index 1b95b45..40ba309 100644 --- a/peptide-repo-core.php +++ b/peptide-repo-core.php @@ -3,7 +3,7 @@ * Plugin Name: Peptide Repo Core * Plugin URI: https://peptiderepo.com * Description: Canonical peptide schema — shared data layer for the peptiderepo.com ecosystem. Provides the peptide CPT, dosing rows, legal status cells, AI candidate queue, disclaimer component, and JSON-LD output. - * Version: 0.6.0 + * Version: 0.6.1 * Author: peptiderepo * Author URI: https://peptiderepo.com * License: GPL-2.0-or-later @@ -22,7 +22,7 @@ if ( ! defined( 'ABSPATH' ) ) { /* ── Constants ────────────────────────────────────────────────────────── */ -define( 'PR_CORE_VERSION', '0.6.0' ); +define( 'PR_CORE_VERSION', '0.6.1' ); define( 'PR_CORE_PLUGIN_FILE', __FILE__ ); define( 'PR_CORE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PR_CORE_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); diff --git a/tests/unit/test-jsonld-webpage.php b/tests/unit/test-jsonld-webpage.php new file mode 100644 index 0000000..1539792 --- /dev/null +++ b/tests/unit/test-jsonld-webpage.php @@ -0,0 +1,165 @@ +retype_to_medical_webpage( [ 'WebPage' ] ); +pr_assert_equals( [ 'WebPage' ], $result, 'non-peptide singular: array returned unchanged' ); + +// Peptide singular: Yoast passes ['WebPage']; must return 'MedicalWebPage' (string). +$GLOBALS['pr_test_is_singular'] = true; +$GLOBALS['pr_test_singular_type'] = 'peptide'; + +$result = $enricher->retype_to_medical_webpage( [ 'WebPage' ] ); +pr_assert_equals( 'MedicalWebPage', $result, 'peptide singular: returns MedicalWebPage (string)' ); + +// String input still works (non-singular special types like CollectionPage). +$GLOBALS['pr_test_is_singular'] = false; +$GLOBALS['pr_test_singular_type'] = ''; + +$result = $enricher->retype_to_medical_webpage( 'CollectionPage' ); +pr_assert_equals( 'CollectionPage', $result, 'non-singular string: passes through unchanged' ); + +// ── enrich_webpage_piece: @type as array injects enrichments ───────────── + +echo "\nenrich_webpage_piece — @type as array:\n"; + +$GLOBALS['pr_test_is_singular'] = true; +$GLOBALS['pr_test_singular_type'] = 'peptide'; +$GLOBALS['pr_test_the_id'] = 42; +$GLOBALS['pr_test_postmeta'][42] = [ + '_pr_last_reviewed' => '2026-06-01', +]; + +// Graph piece with @type as an array (real Yoast shape on singulars). +$graph = [ + [ + '@type' => [ 'WebPage' ], + '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage', + 'name' => 'BPC-157', + ], +]; + +$result_graph = $enricher->enrich_webpage_piece( $graph, new stdClass() ); +$piece = $result_graph[0] ?? []; + +pr_assert( isset( $piece['lastReviewed'] ), '@type=array: lastReviewed injected' ); +pr_assert_equals( '2026-06-01', $piece['lastReviewed'], '@type=array: lastReviewed value correct' ); +pr_assert( isset( $piece['reviewedBy'] ), '@type=array: reviewedBy injected' ); +pr_assert( isset( $piece['audience'] ), '@type=array: audience injected' ); + +// Also works when @type is 'MedicalWebPage' (string — after our retype fires first). +$graph_retyed = [ + [ + '@type' => 'MedicalWebPage', + '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage', + 'name' => 'BPC-157', + ], +]; + +$result_retyped = $enricher->enrich_webpage_piece( $graph_retyed, new stdClass() ); +pr_assert( isset( $result_retyped[0]['lastReviewed'] ), 'MedicalWebPage string @type: enrichment injects' ); + +// Non-peptide page: enrichment must NOT fire even with array @type. +$GLOBALS['pr_test_is_singular'] = true; +$GLOBALS['pr_test_singular_type'] = 'page'; + +$graph_non_peptide = [ + [ + '@type' => [ 'WebPage' ], + '@id' => 'https://peptiderepo.com/about/#webpage', + ], +]; + +$result_non_peptide = $enricher->enrich_webpage_piece( $graph_non_peptide, new stdClass() ); +pr_assert( ! isset( $result_non_peptide[0]['lastReviewed'] ), 'non-peptide: lastReviewed NOT injected' ); + +// ── Summary ────────────────────────────────────────────────────────────── + +exit( pr_test_summary() );