fix(jsonld): accept string|array @type — v0.6.1

Yoast's wpseo_schema_webpage_type filter passes ['WebPage'] (array) on
every singular page. The v0.6.0 retype_to_medical_webpage() signature
was strict string, causing TypeError → HTTP 500 on ALL singulars under
strict_types=1. Secondary bug: enrich_webpage_piece compared
'WebPage' === $piece['@type'] but @type is an array, so MedicalWebPage
enrichment never injected.

Fix:
- retype_to_medical_webpage: string|array param + return (PHP 8.1+ union)
- enrich_webpage_piece: normalise @type to array before in_array() check
- Add tests/unit/test-jsonld-webpage.php: regression tests with array
  input for both code paths; fail against v0.6.0, pass after fix
- Bump version header + PR_CORE_VERSION constant to 0.6.1
- CHANGELOG entry under [0.6.1]

Agent-Session: jsonld-webpage-typefix v0.6.1
This commit is contained in:
peptiderepo
2026-06-13 06:44:00 +00:00
parent e9d97b51e2
commit 077c4b0e51
4 changed files with 192 additions and 9 deletions

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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__ ) );

View File

@@ -0,0 +1,165 @@
<?php
/**
* Regression tests for v0.6.1: Yoast wpseo_schema_webpage_type array contract.
*
* Root cause caught on staging: v0.6.0 retype_to_medical_webpage() was
* strict-typed string, but Yoast passes ['WebPage'] (array) on every singular
* page. strict_types=1 → TypeError → HTTP 500 on all monographs + plain Pages.
* Secondary bug: enrich_webpage_piece compared 'WebPage' === $piece['@type']
* but @type is an array, so enrichment (lastReviewed/reviewedBy/audience)
* never injected on any page.
*
* These tests FAIL against v0.6.0 and PASS after the v0.6.1 fix.
*
* Run: php tests/unit/test-jsonld-webpage.php
* Exit 0 = all pass, 1 = any failure.
*
* @package PeptideRepoCore
*/
declare(strict_types=1);
require_once __DIR__ . '/../bootstrap.php';
// ── Additional stubs ─────────────────────────────────────────────────────
if ( ! defined( 'PR_CORE_TARGET_SCHEMA_VERSION' ) ) {
define( 'PR_CORE_TARGET_SCHEMA_VERSION', 4 );
}
$GLOBALS['pr_test_postmeta'] = [];
$GLOBALS['pr_test_is_singular'] = false;
$GLOBALS['pr_test_singular_type'] = '';
$GLOBALS['pr_test_the_id'] = 0;
function get_post_meta( $post_id, string $key, bool $single = false ) {
if ( $single ) {
return $GLOBALS['pr_test_postmeta'][ (int) $post_id ][ $key ] ?? '';
}
return array_values( $GLOBALS['pr_test_postmeta'][ (int) $post_id ] ?? [] );
}
function get_permalink( $post_id = null ): string {
return 'https://peptiderepo.com/peptides/bpc-157/';
}
function get_the_ID(): int {
return $GLOBALS['pr_test_the_id'];
}
function is_singular( $type = '' ): bool {
if ( '' === $type ) {
return $GLOBALS['pr_test_is_singular'];
}
return $GLOBALS['pr_test_is_singular'] && ( $GLOBALS['pr_test_singular_type'] === $type );
}
function get_post_modified_time( $format, $gmt, $post_id ) {
return '2026-04-25';
}
function get_userdata( int $id ) {
return false;
}
function get_author_posts_url( int $id ): string {
return 'https://peptiderepo.com/author/' . $id;
}
function home_url( string $path = '' ): string {
return 'https://peptiderepo.com' . $path;
}
function apply_filters( string $tag, $value ) {
return $value;
}
// Load class under test.
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-webpage.php';
echo "== JSON-LD WebPage v0.6.1 regression tests ==\n\n";
$enricher = new PR_Core_Jsonld_Webpage();
// ── retype_to_medical_webpage: array input, non-peptide singular ──────────
echo "retype_to_medical_webpage — array input:\n";
// Non-peptide singular: Yoast passes ['WebPage']; must return unchanged, no TypeError.
$GLOBALS['pr_test_is_singular'] = true;
$GLOBALS['pr_test_singular_type'] = 'page'; // plain WP Page, not peptide CPT.
$result = $enricher->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() );