peptide-repo-core v0.6.0: peptide schema backfill + Yoast-integrated Drug/MedicalWebPage/FAQ (#19)
Backfill psa_*->_pr_* meta (migration 0004 + PubChem fallback); enrich Drug node with {permalink}#drug @id + molecularFormula/Weight/alternateName; rewrite emitter into Yoast schema graph (MedicalWebPage retype + FAQPage, no duplicate nodes).
QA: APPROVE (independent) — qa-reviews/peptide-repo-core/2026-06-13-817690c1b6ec2118ddcf3b8cfe8bc5ea7f3933b3.md
Agent-Session: peptide-schema-backfill v0.6.0 merge
This commit is contained in:
158
includes/frontend/class-pr-core-jsonld-drug.php
Normal file
158
includes/frontend/class-pr-core-jsonld-drug.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Builds the schema.org Drug graph piece for a peptide page.
|
||||
*
|
||||
* What: Assembles a Drug (+ MolecularEntity) schema node from peptide DTO
|
||||
* and the new _pr_* schema-input meta fields added in v0.6.0.
|
||||
*
|
||||
* Who calls it: PR_Core_Jsonld::build_drug_piece() during Yoast graph assembly
|
||||
* or standalone @graph construction.
|
||||
*
|
||||
* Dependencies: PR_Core_Peptide_DTO, WordPress get_post_meta(), get_permalink().
|
||||
*
|
||||
* @see frontend/class-pr-core-jsonld.php — Orchestrator that calls this builder.
|
||||
* @see ARCHITECTURE.md — §2.7 JSON-LD output.
|
||||
*/
|
||||
class PR_Core_Jsonld_Drug {
|
||||
|
||||
/** @var string Meta key: molecular formula sourced by migration 0004. */
|
||||
private const META_FORMULA = '_pr_molecular_formula';
|
||||
|
||||
/** @var string Meta key: molecular weight (float string) sourced by migration 0004. */
|
||||
private const META_WEIGHT = '_pr_molecular_weight';
|
||||
|
||||
/** @var string Meta key: aliases JSON array sourced by migration 0004. */
|
||||
private const META_ALIASES = '_pr_aliases';
|
||||
|
||||
/** @var string Unit text for molecularWeight QuantitativeValue. */
|
||||
private const UNIT_TEXT = 'g/mol';
|
||||
|
||||
/**
|
||||
* Build a schema.org Drug piece array for a peptide.
|
||||
*
|
||||
* @param PR_Core_Peptide_DTO $peptide Peptide DTO.
|
||||
* @return array<string, mixed> Schema.org Drug node.
|
||||
*/
|
||||
public function build( PR_Core_Peptide_DTO $peptide ): array {
|
||||
$permalink = get_permalink( $peptide->id );
|
||||
|
||||
$node = [
|
||||
'@type' => [ 'Drug', 'MolecularEntity' ],
|
||||
'@id' => $permalink . '#drug',
|
||||
'name' => $peptide->display_name ?: $peptide->title,
|
||||
'url' => $permalink,
|
||||
];
|
||||
|
||||
if ( '' !== $peptide->excerpt ) {
|
||||
$node['description'] = $peptide->excerpt;
|
||||
}
|
||||
|
||||
// alternateName: prefer _pr_aliases (v0.6.0), fall back to DTO aliases field.
|
||||
$aliases = $this->get_aliases( $peptide );
|
||||
if ( ! empty( $aliases ) ) {
|
||||
$node['alternateName'] = $aliases;
|
||||
}
|
||||
|
||||
// molecularFormula: prefer _pr_molecular_formula, fall back to DTO field.
|
||||
$formula = $this->get_formula( $peptide );
|
||||
if ( '' !== $formula ) {
|
||||
$node['molecularFormula'] = $formula;
|
||||
}
|
||||
|
||||
// molecularWeight: prefer _pr_molecular_weight, fall back to DTO field.
|
||||
$weight = $this->get_weight( $peptide );
|
||||
if ( $weight > 0.0 ) {
|
||||
$node['molecularWeight'] = [
|
||||
'@type' => 'QuantitativeValue',
|
||||
'value' => $weight,
|
||||
'unitText' => self::UNIT_TEXT,
|
||||
];
|
||||
}
|
||||
|
||||
// External identifiers: CAS and DrugBank omitted in v0.6.0 (no source).
|
||||
// @see ARCHITECTURE.md §v0.6.0 schema sprint — future enrichment.
|
||||
$legal_status = $this->build_legal_status();
|
||||
if ( null !== $legal_status ) {
|
||||
$node['legalStatus'] = $legal_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the Drug schema node for a peptide.
|
||||
*
|
||||
* Known listeners: none.
|
||||
* Registered at: PR_Core::register_public_filters() — carried forward from v0.5.0.
|
||||
*
|
||||
* @param array<string, mixed> $node Drug schema node.
|
||||
* @param PR_Core_Peptide_DTO $peptide Peptide DTO.
|
||||
*/
|
||||
return apply_filters( 'pr_core_jsonld_peptide', $node, $peptide );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aliases array from _pr_aliases meta, falling back to DTO aliases.
|
||||
*
|
||||
* @param PR_Core_Peptide_DTO $peptide Peptide DTO.
|
||||
* @return string[] Sanitized alias strings.
|
||||
*/
|
||||
private function get_aliases( PR_Core_Peptide_DTO $peptide ): array {
|
||||
$raw = get_post_meta( $peptide->id, self::META_ALIASES, true );
|
||||
|
||||
if ( is_string( $raw ) && '' !== $raw ) {
|
||||
$decoded = json_decode( $raw, true );
|
||||
if ( is_array( $decoded ) && ! empty( $decoded ) ) {
|
||||
return array_values( array_filter( array_map( 'sanitize_text_field', $decoded ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to DTO aliases (legacy bare-key meta).
|
||||
return $peptide->aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get molecular formula from _pr_molecular_formula meta, falling back to DTO.
|
||||
*
|
||||
* @param PR_Core_Peptide_DTO $peptide Peptide DTO.
|
||||
* @return string Sanitized formula string, or '' if not available.
|
||||
*/
|
||||
private function get_formula( PR_Core_Peptide_DTO $peptide ): string {
|
||||
$raw = (string) get_post_meta( $peptide->id, self::META_FORMULA, true );
|
||||
if ( '' !== $raw ) {
|
||||
return sanitize_text_field( $raw );
|
||||
}
|
||||
return $peptide->molecular_formula;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get molecular weight float from _pr_molecular_weight meta, falling back to DTO.
|
||||
*
|
||||
* @param PR_Core_Peptide_DTO $peptide Peptide DTO.
|
||||
* @return float Weight in g/mol, or 0.0 if not available.
|
||||
*/
|
||||
private function get_weight( PR_Core_Peptide_DTO $peptide ): float {
|
||||
$raw = (string) get_post_meta( $peptide->id, self::META_WEIGHT, true );
|
||||
if ( '' !== $raw ) {
|
||||
$weight = (float) $raw;
|
||||
if ( $weight > 0.0 ) {
|
||||
return $weight;
|
||||
}
|
||||
}
|
||||
return $peptide->molecular_weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a standard legalStatus node for research compounds.
|
||||
*
|
||||
* Returns null when legalStatus should be omitted (future: when per-peptide
|
||||
* status is available from legal cells).
|
||||
*
|
||||
* @return array<string, string>|null
|
||||
*/
|
||||
private function build_legal_status(): ?array {
|
||||
return [
|
||||
'@type' => 'DrugLegalStatus',
|
||||
'description' => esc_html__( 'Not approved for human therapeutic use; supplied for laboratory research purposes only.', 'peptide-repo-core' ),
|
||||
];
|
||||
}
|
||||
}
|
||||
100
includes/frontend/class-pr-core-jsonld-faq.php
Normal file
100
includes/frontend/class-pr-core-jsonld-faq.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Builds the schema.org FAQPage graph piece for a peptide page.
|
||||
*
|
||||
* What: Reads _pr_faq_items post-meta (JSON array of {question, answer} objects)
|
||||
* and produces a FAQPage node suitable for injection into Yoast's graph.
|
||||
* Emits nothing when the meta is empty or absent.
|
||||
*
|
||||
* Who calls it: PR_Core_Jsonld::build_faq_piece() during graph assembly.
|
||||
* Dependencies: WordPress get_post_meta(), get_permalink().
|
||||
*
|
||||
* @see frontend/class-pr-core-jsonld.php — Orchestrator that calls this builder.
|
||||
* @see cpt/class-pr-core-schema-sanitizers.php — sanitize_faq_items() used on save.
|
||||
* @see ARCHITECTURE.md — §2.7 JSON-LD output.
|
||||
*/
|
||||
class PR_Core_Jsonld_Faq {
|
||||
|
||||
/** @var string Meta key holding the FAQ items JSON array. */
|
||||
public const META_FAQ_ITEMS = '_pr_faq_items';
|
||||
|
||||
/**
|
||||
* Build a FAQPage node for a post, or return null if no items exist.
|
||||
*
|
||||
* @param int $post_id WordPress post ID.
|
||||
* @param string $permalink Canonical permalink for @id construction.
|
||||
* @return array<string, mixed>|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 [
|
||||
'@type' => 'FAQPage',
|
||||
'@id' => $permalink . '#faq',
|
||||
'mainEntity' => array_map( [ $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<string, mixed> Schema.org Question node.
|
||||
*/
|
||||
private function build_question( array $item ): array {
|
||||
return [
|
||||
'@type' => 'Question',
|
||||
'name' => esc_html( $item['question'] ),
|
||||
'acceptedAnswer' => [
|
||||
'@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<int, array{question: string, answer: string}> 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 [];
|
||||
}
|
||||
|
||||
$decoded = json_decode( $raw, true );
|
||||
|
||||
if ( ! is_array( $decoded ) || empty( $decoded ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$valid = [];
|
||||
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[] = [
|
||||
'question' => $question,
|
||||
'answer' => $answer,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $valid;
|
||||
}
|
||||
}
|
||||
134
includes/frontend/class-pr-core-jsonld-webpage.php
Normal file
134
includes/frontend/class-pr-core-jsonld-webpage.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Enriches Yoast's WebPage node to MedicalWebPage for peptide single pages.
|
||||
*
|
||||
* What: Hooks into Yoast's wpseo_schema_webpage_type filter to retype the page
|
||||
* node to MedicalWebPage, and into wpseo_schema_graph to inject
|
||||
* lastReviewed, reviewedBy, and audience enrichments.
|
||||
*
|
||||
* Who calls it: PR_Core_Jsonld::register_hooks() registers both filters.
|
||||
* Dependencies: Yoast SEO (wordpress-seo) ≥ 27.6 when active; graceful no-op otherwise.
|
||||
*
|
||||
* @see frontend/class-pr-core-jsonld.php — Parent orchestrator.
|
||||
* @see ARCHITECTURE.md — §2.7 JSON-LD output, Yoast integration contract.
|
||||
*/
|
||||
class PR_Core_Jsonld_Webpage {
|
||||
|
||||
/** @var string Meta key for last editorial review date (ISO date string). */
|
||||
private const META_LAST_REVIEWED = '_pr_last_reviewed';
|
||||
|
||||
/** @var string Meta key for last source verification date (ISO date string). */
|
||||
private const META_LAST_VERIFIED = '_pr_last_source_verified';
|
||||
|
||||
/**
|
||||
* Return 'MedicalWebPage' as the schema type for peptide single pages.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
public function retype_to_medical_webpage( string $type ): string {
|
||||
if ( is_singular( PR_Core_Peptide_CPT::POST_TYPE ) ) {
|
||||
return 'MedicalWebPage';
|
||||
}
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich Yoast's graph pieces array for peptide single pages.
|
||||
*
|
||||
* Injects lastReviewed, reviewedBy, and audience into the WebPage/MedicalWebPage
|
||||
* node already in Yoast's graph. Does NOT add new WebPage or BreadcrumbList nodes.
|
||||
*
|
||||
* Hooked on: wpseo_schema_graph (Yoast SEO ≥ 19.x), priority 11.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $graph Yoast schema graph pieces array.
|
||||
* @param \WPSEO_Schema_Context $context Yoast schema context object.
|
||||
* @return array<int, array<string, mixed>> Modified graph.
|
||||
*/
|
||||
public function enrich_webpage_piece( array $graph, $context ): array {
|
||||
if ( ! is_singular( PR_Core_Peptide_CPT::POST_TYPE ) ) {
|
||||
return $graph;
|
||||
}
|
||||
|
||||
$post_id = get_the_ID();
|
||||
$enrichments = $this->build_webpage_enrichments( (int) $post_id );
|
||||
|
||||
if ( empty( $enrichments ) ) {
|
||||
return $graph;
|
||||
}
|
||||
|
||||
foreach ( $graph as &$piece ) {
|
||||
if ( ! is_array( $piece ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $piece['@type'] ?? '';
|
||||
if ( 'WebPage' === $type || 'MedicalWebPage' === $type ) {
|
||||
foreach ( $enrichments as $key => $value ) {
|
||||
$piece[ $key ] = $value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build MedicalWebPage enrichment fields for a peptide post.
|
||||
*
|
||||
* @param int $post_id WordPress post ID.
|
||||
* @return array<string, mixed> Enrichment fields to merge into the WebPage node.
|
||||
*/
|
||||
private function build_webpage_enrichments( int $post_id ): array {
|
||||
$enrichments = [];
|
||||
|
||||
// lastReviewed: prefer _pr_last_reviewed, fall back to _pr_last_source_verified,
|
||||
// then post modified date.
|
||||
$last_reviewed = (string) get_post_meta( $post_id, self::META_LAST_REVIEWED, true );
|
||||
if ( '' === $last_reviewed ) {
|
||||
$last_reviewed = (string) get_post_meta( $post_id, self::META_LAST_VERIFIED, true );
|
||||
}
|
||||
if ( '' === $last_reviewed ) {
|
||||
$last_reviewed = get_post_modified_time( 'Y-m-d', false, $post_id ) ?: '';
|
||||
}
|
||||
|
||||
if ( '' !== $last_reviewed ) {
|
||||
$enrichments['lastReviewed'] = sanitize_text_field( $last_reviewed );
|
||||
}
|
||||
|
||||
// reviewedBy: Person only when a medical editor is assigned; otherwise Organization.
|
||||
$editor_id = (int) get_post_meta( $post_id, 'medical_editor_id', true );
|
||||
if ( $editor_id > 0 ) {
|
||||
$editor = get_userdata( $editor_id );
|
||||
if ( $editor ) {
|
||||
$enrichments['reviewedBy'] = [
|
||||
'@type' => 'Person',
|
||||
'name' => sanitize_text_field( $editor->display_name ),
|
||||
'url' => get_author_posts_url( $editor_id ),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! isset( $enrichments['reviewedBy'] ) ) {
|
||||
$enrichments['reviewedBy'] = [
|
||||
'@type' => 'Organization',
|
||||
'name' => 'Peptide Repo',
|
||||
'url' => home_url(),
|
||||
];
|
||||
}
|
||||
|
||||
// audience: MedicalAudience for researchers.
|
||||
$enrichments['audience'] = [
|
||||
'@type' => 'MedicalAudience',
|
||||
'audienceType' => 'Researcher',
|
||||
];
|
||||
|
||||
return $enrichments;
|
||||
}
|
||||
}
|
||||
@@ -4,33 +4,122 @@ declare(strict_types=1);
|
||||
/**
|
||||
* JSON-LD / schema.org structured data emission for peptide pages.
|
||||
*
|
||||
* What: Emits Drug schema on single peptide pages, Dataset schema on archives.
|
||||
* Who calls it: PR_Core::init() registers wp_head hook.
|
||||
* Dependencies: PR_Core_Peptide_Repository for data lookup.
|
||||
* What: Integrates Drug, MedicalWebPage, and FAQPage schema into Yoast's graph
|
||||
* on single peptide pages. When Yoast is inactive, emits a standalone
|
||||
* @graph block via wp_head as a fallback.
|
||||
*
|
||||
* Consumer plugins can override via pr_core_jsonld_peptide filter.
|
||||
* Who calls it: PR_Core::init() calls register_hooks(). Yoast hooks fire on
|
||||
* every frontend page that Yoast processes.
|
||||
*
|
||||
* @see ARCHITECTURE.md — Section 2.7 JSON-LD output.
|
||||
* Dependencies:
|
||||
* - Yoast SEO (wordpress-seo) ≥ 27.6 for integrated emission path.
|
||||
* - PR_Core_Jsonld_Drug, PR_Core_Jsonld_Webpage, PR_Core_Jsonld_Faq (builders).
|
||||
* - PR_Core_Peptide_Repository, PR_Core_Peptide_CPT (data).
|
||||
*
|
||||
* Contract (ratified 2026-06-11, jsonld-contract-v1.md):
|
||||
* - Integrated path: Yoast owns WebPage and BreadcrumbList — we never duplicate.
|
||||
* - Standalone fallback: emits a complete @graph (Drug + FAQPage) only.
|
||||
* - Drug @id: {permalink}#drug (stable for cross-plugin linking).
|
||||
*
|
||||
* @see frontend/class-pr-core-jsonld-drug.php — Drug piece builder.
|
||||
* @see frontend/class-pr-core-jsonld-webpage.php — MedicalWebPage enrichment.
|
||||
* @see frontend/class-pr-core-jsonld-faq.php — FAQPage piece builder.
|
||||
* @see ARCHITECTURE.md — §2.7 JSON-LD output.
|
||||
*/
|
||||
class PR_Core_Jsonld {
|
||||
|
||||
/** @var PR_Core_Jsonld_Drug Drug piece builder. */
|
||||
private PR_Core_Jsonld_Drug $drug_builder;
|
||||
|
||||
/** @var PR_Core_Jsonld_Webpage MedicalWebPage enricher. */
|
||||
private PR_Core_Jsonld_Webpage $webpage_enricher;
|
||||
|
||||
/** @var PR_Core_Jsonld_Faq FAQ piece builder. */
|
||||
private PR_Core_Jsonld_Faq $faq_builder;
|
||||
|
||||
/**
|
||||
* Construct the orchestrator with its piece builders.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->drug_builder = new PR_Core_Jsonld_Drug();
|
||||
$this->webpage_enricher = new PR_Core_Jsonld_Webpage();
|
||||
$this->faq_builder = new PR_Core_Jsonld_Faq();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks for JSON-LD output.
|
||||
*
|
||||
* Registers both the Yoast-integrated path (active when Yoast is loaded)
|
||||
* and the standalone wp_head fallback (active only when Yoast is absent).
|
||||
*
|
||||
* Side effects: calls add_filter(), add_action().
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks(): void {
|
||||
add_action( 'wp_head', [ $this, 'emit_jsonld' ], 99 );
|
||||
// Integrated path: hook into Yoast's schema graph.
|
||||
// wpseo_schema_graph_pieces: filter on the array of schema piece objects.
|
||||
// wpseo_schema_webpage_type: filter returning the WebPage @type string.
|
||||
add_filter( 'wpseo_schema_graph_pieces', [ $this, 'inject_graph_pieces' ], 11, 2 );
|
||||
add_filter( 'wpseo_schema_webpage_type', [ $this->webpage_enricher, 'retype_to_medical_webpage' ] );
|
||||
add_filter( 'wpseo_schema_graph', [ $this->webpage_enricher, 'enrich_webpage_piece' ], 11, 2 );
|
||||
|
||||
// Standalone fallback: emits only when Yoast is not producing output.
|
||||
add_action( 'wp_head', [ $this, 'emit_standalone_fallback' ], 99 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit JSON-LD on peptide single pages.
|
||||
* Inject Drug and FAQPage pieces into Yoast's schema graph.
|
||||
*
|
||||
* Side effects: outputs script tag in wp_head.
|
||||
* Hooked on: wpseo_schema_graph_pieces (priority 11, after Yoast's own pieces).
|
||||
* This filter receives Yoast's piece objects array. We append plain arrays;
|
||||
* Yoast 27.6 accepts both objects and plain arrays in this filter.
|
||||
*
|
||||
* @param array<int, mixed> $pieces Existing graph pieces from Yoast.
|
||||
* @param \WPSEO_Schema_Context $context Yoast schema context object.
|
||||
* @return array<int, mixed> Pieces array with Drug and FAQPage appended.
|
||||
*/
|
||||
public function inject_graph_pieces( array $pieces, $context ): array {
|
||||
if ( ! is_singular( PR_Core_Peptide_CPT::POST_TYPE ) ) {
|
||||
return $pieces;
|
||||
}
|
||||
|
||||
$post_id = get_the_ID();
|
||||
if ( ! $post_id ) {
|
||||
return $pieces;
|
||||
}
|
||||
|
||||
$peptide = ( new PR_Core_Peptide_Repository() )->find_by_id( (int) $post_id );
|
||||
if ( ! $peptide ) {
|
||||
return $pieces;
|
||||
}
|
||||
|
||||
$pieces[] = $this->drug_builder->build( $peptide );
|
||||
|
||||
$faq_piece = $this->faq_builder->build( (int) $post_id, get_permalink( (int) $post_id ) );
|
||||
if ( null !== $faq_piece ) {
|
||||
$pieces[] = $faq_piece;
|
||||
}
|
||||
|
||||
return $pieces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a standalone @graph block via wp_head when Yoast is not active.
|
||||
*
|
||||
* Guard: suppressed if Yoast is loaded (function_exists check). This is the
|
||||
* no-Yoast fallback required by the jsonld-contract. When Yoast is present,
|
||||
* the integrated path (inject_graph_pieces) handles emission.
|
||||
*
|
||||
* Side effects: outputs a script tag in wp_head.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function emit_jsonld(): void {
|
||||
public function emit_standalone_fallback(): void {
|
||||
if ( function_exists( 'YoastSEO' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! is_singular( PR_Core_Peptide_CPT::POST_TYPE ) ) {
|
||||
return;
|
||||
}
|
||||
@@ -40,86 +129,25 @@ class PR_Core_Jsonld {
|
||||
return;
|
||||
}
|
||||
|
||||
$repo = new PR_Core_Peptide_Repository();
|
||||
$peptide = $repo->find_by_id( $post_id );
|
||||
|
||||
$peptide = ( new PR_Core_Peptide_Repository() )->find_by_id( (int) $post_id );
|
||||
if ( ! $peptide ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema = $this->build_drug_schema( $peptide );
|
||||
$permalink = get_permalink( (int) $post_id );
|
||||
$graph = [ $this->drug_builder->build( $peptide ) ];
|
||||
|
||||
/**
|
||||
* Filter the JSON-LD schema for a peptide page.
|
||||
*
|
||||
* @param array<string, mixed> $schema Schema.org data array.
|
||||
* @param PR_Core_Peptide_DTO $peptide The peptide DTO.
|
||||
*/
|
||||
$schema = apply_filters( 'pr_core_jsonld_peptide', $schema, $peptide );
|
||||
|
||||
if ( empty( $schema ) ) {
|
||||
return;
|
||||
$faq_piece = $this->faq_builder->build( (int) $post_id, $permalink );
|
||||
if ( null !== $faq_piece ) {
|
||||
$graph[] = $faq_piece;
|
||||
}
|
||||
|
||||
printf(
|
||||
'<script type="application/ld+json">%s</script>' . "\n",
|
||||
wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT )
|
||||
wp_json_encode(
|
||||
[ '@context' => 'https://schema.org', '@graph' => $graph ],
|
||||
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a schema.org Drug object for a peptide.
|
||||
*
|
||||
* @param PR_Core_Peptide_DTO $peptide Peptide data.
|
||||
* @return array<string, mixed> Schema.org JSON-LD structure.
|
||||
*/
|
||||
private function build_drug_schema( PR_Core_Peptide_DTO $peptide ): array {
|
||||
$schema = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Drug',
|
||||
'name' => $peptide->display_name ?: $peptide->title,
|
||||
'url' => get_permalink( $peptide->id ),
|
||||
'description' => $peptide->excerpt,
|
||||
];
|
||||
|
||||
if ( ! empty( $peptide->aliases ) ) {
|
||||
$schema['alternateName'] = $peptide->aliases;
|
||||
}
|
||||
|
||||
if ( '' !== $peptide->molecular_formula ) {
|
||||
$schema['molecularFormula'] = $peptide->molecular_formula;
|
||||
}
|
||||
|
||||
if ( $peptide->molecular_weight > 0 ) {
|
||||
$schema['molecularWeight'] = [
|
||||
'@type' => 'QuantitativeValue',
|
||||
'value' => $peptide->molecular_weight,
|
||||
'unitText' => 'Da',
|
||||
];
|
||||
}
|
||||
|
||||
// External identifiers as codes.
|
||||
$codes = [];
|
||||
if ( '' !== $peptide->cas_number ) {
|
||||
$codes[] = [
|
||||
'@type' => 'MedicalCode',
|
||||
'codeValue' => $peptide->cas_number,
|
||||
'codingSystem' => 'CAS',
|
||||
];
|
||||
}
|
||||
|
||||
if ( '' !== $peptide->drugbank_id ) {
|
||||
$codes[] = [
|
||||
'@type' => 'MedicalCode',
|
||||
'codeValue' => $peptide->drugbank_id,
|
||||
'codingSystem' => 'DrugBank',
|
||||
];
|
||||
}
|
||||
|
||||
if ( ! empty( $codes ) ) {
|
||||
$schema['code'] = $codes;
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user