From 4d8e98f34bc11306ff03f0d1b79c1f15bae0f151 Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:00:38 +0700
Subject: [PATCH 01/19] feat: add PR_Core_Related_Posts_Provider interface
---
.../interface-related-posts-provider.php | 21 +++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 includes/interfaces/interface-related-posts-provider.php
diff --git a/includes/interfaces/interface-related-posts-provider.php b/includes/interfaces/interface-related-posts-provider.php
new file mode 100644
index 0000000..d999c1d
--- /dev/null
+++ b/includes/interfaces/interface-related-posts-provider.php
@@ -0,0 +1,21 @@
+ Array of WP_Post objects (may be empty).
+ */
+ public function get_posts( int $peptide_id, int $limit ): array;
+}
From a906e1b701e67e9ef21521ce5d7d236ac0c1aba6 Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:00:43 +0700
Subject: [PATCH 02/19] feat: add PR_Core_Internal_Posts_Provider
implementation
---
.../class-pr-core-internal-posts-provider.php | 102 ++++++++++++++++++
1 file changed, 102 insertions(+)
create mode 100644 includes/core/class-pr-core-internal-posts-provider.php
diff --git a/includes/core/class-pr-core-internal-posts-provider.php b/includes/core/class-pr-core-internal-posts-provider.php
new file mode 100644
index 0000000..dc7bb49
--- /dev/null
+++ b/includes/core/class-pr-core-internal-posts-provider.php
@@ -0,0 +1,102 @@
+ Array of WP_Post objects (may be empty).
+ */
+ public function get_posts( int $peptide_id, int $limit ): array {
+ $peptide = get_post( $peptide_id );
+ if ( ! $peptide || 'peptide' !== $peptide->post_type ) {
+ return [];
+ }
+
+ $cache_key = 'pr_core_related_' . $peptide_id;
+ $cached = get_transient( $cache_key );
+ if ( is_array( $cached ) ) {
+ return array_slice( $cached, 0, $limit );
+ }
+
+ // Primary: query posts tagged with peptide's slug as peptide_topic.
+ $posts = $this->query_by_taxonomy( $peptide->post_name, $limit );
+
+ // Fallback: if empty, search by peptide title.
+ if ( empty( $posts ) ) {
+ $posts = $this->query_by_search( $peptide->post_title, $limit );
+ }
+
+ // Cache for 1 hour.
+ set_transient( $cache_key, $posts, HOUR_IN_SECONDS );
+
+ return $posts;
+ }
+
+ /**
+ * Query posts tagged with a specific peptide_topic term.
+ *
+ * @param string $peptide_slug The peptide's post_name (slug).
+ * @param int $limit Maximum posts to return.
+ * @return array
+ */
+ private function query_by_taxonomy( string $peptide_slug, int $limit ): array {
+ $query = new WP_Query( [
+ 'post_type' => 'post',
+ 'posts_per_page' => $limit,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ 'tax_query' => [
+ [
+ 'taxonomy' => 'peptide_topic',
+ 'field' => 'slug',
+ 'terms' => $peptide_slug,
+ ],
+ ],
+ ] );
+
+ return $query->posts ?: [];
+ }
+
+ /**
+ * Fallback: query posts by full-text search on post_title.
+ *
+ * @param string $search_term The peptide's display title.
+ * @param int $limit Maximum posts to return.
+ * @return array
+ */
+ private function query_by_search( string $search_term, int $limit ): array {
+ $query = new WP_Query( [
+ 'post_type' => 'post',
+ 'posts_per_page' => $limit,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ 's' => $search_term,
+ ] );
+
+ return $query->posts ?: [];
+ }
+}
From a21d35ee27215c506c39c2d89c31f13c1fa75308 Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:00:47 +0700
Subject: [PATCH 03/19] feat: add PR_Core_Related_Posts_Section renderer
---
.../class-pr-core-related-posts-section.php | 126 ++++++++++++++++++
1 file changed, 126 insertions(+)
create mode 100644 includes/core/class-pr-core-related-posts-section.php
diff --git a/includes/core/class-pr-core-related-posts-section.php b/includes/core/class-pr-core-related-posts-section.php
new file mode 100644
index 0000000..6d65ada
--- /dev/null
+++ b/includes/core/class-pr-core-related-posts-section.php
@@ -0,0 +1,126 @@
+provider = $provider;
+ }
+
+ /**
+ * Register the action hook.
+ *
+ * @return void
+ */
+ public function register_hooks(): void {
+ add_action( 'pr_core_after_peptide_content', [ $this, 'render' ] );
+ add_action( 'save_post_post', [ $this, 'invalidate_caches' ] );
+ }
+
+ /**
+ * Render the related posts section.
+ *
+ * Called via do_action( 'pr_core_after_peptide_content', $peptide_id ) in templates.
+ * If feature disabled or no posts found, renders nothing.
+ *
+ * @param int $peptide_id The peptide post ID.
+ * @return void
+ */
+ public function render( int $peptide_id ): void {
+ $enabled = (bool) get_option( 'pr_core_related_posts_enabled', true );
+ if ( ! $enabled ) {
+ return;
+ }
+
+ $limit = (int) get_option( 'pr_core_related_posts_limit', 3 );
+ $limit = min( 6, max( 1, $limit ) );
+
+ $posts = $this->provider->get_posts( $peptide_id, $limit + 5 );
+ if ( empty( $posts ) ) {
+ return;
+ }
+
+ $peptide = get_post( $peptide_id );
+ ?>
+
+
+
+ = $limit ) {
+ break;
+ }
+ setup_postdata( $post );
+ get_template_part( 'template-parts/related-posts/card', null, [ 'post' => $post ] );
+ ++$count;
+ }
+ wp_reset_postdata();
+ ?>
+
+ $limit && $peptide ) {
+ $term = get_term_by( 'slug', $peptide->post_name, 'peptide_topic' );
+ if ( $term ) {
+ $archive_url = get_term_link( $term );
+ if ( ! is_wp_error( $archive_url ) ) {
+ ?>
+
+
+ post_title )
+ )
+ );
+ ?>
+
+
+
+
+ get_col(
+ "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'pr_core_related_%'"
+ );
+ foreach ( $cache_keys as $key ) {
+ delete_transient( str_replace( '_transient_', '', $key ) );
+ }
+ }
+}
From 33dc8a183a9111070113195a86e81b56d918befe Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:00:51 +0700
Subject: [PATCH 04/19] feat: add related-posts card template part
---
template-parts/related-posts/card.php | 67 +++++++++++++++++++++++++++
1 file changed, 67 insertions(+)
create mode 100644 template-parts/related-posts/card.php
diff --git a/template-parts/related-posts/card.php b/template-parts/related-posts/card.php
new file mode 100644
index 0000000..57e24d5
--- /dev/null
+++ b/template-parts/related-posts/card.php
@@ -0,0 +1,67 @@
+
+
+
+
+ 'pr-related-posts__image-element',
+ 'loading' => 'lazy',
+ 'decoding' => 'async',
+ ]
+ )
+ );
+ ?>
+
+
+
+
 ); ?>)
+
+
+
+
+
+
+ post_excerpt ) {
+ $excerpt = wp_strip_all_tags( $post->post_excerpt );
+ $excerpt = wp_kses_post( wp_trim_words( $excerpt, 20 ) );
+ ?>
+
+
+
+
+
+
From 6359a18fed4f64eb447021bcdca57a06a25b00cf Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:00:55 +0700
Subject: [PATCH 05/19] feat: add related-posts CSS styles
---
assets/css/related-posts.css | 192 +++++++++++++++++++++++++++++++++++
1 file changed, 192 insertions(+)
create mode 100644 assets/css/related-posts.css
diff --git a/assets/css/related-posts.css b/assets/css/related-posts.css
new file mode 100644
index 0000000..060e561
--- /dev/null
+++ b/assets/css/related-posts.css
@@ -0,0 +1,192 @@
+/**
+ * Related Posts Section Styles
+ * Scoped to .pr-related-posts
+ */
+
+:root {
+ color-scheme: light;
+}
+
+.pr-related-posts {
+ margin-top: 3rem;
+ padding: 2rem 0;
+ border-top: 1px solid var(--wp--preset--color--border, #e5e5e5);
+}
+
+.pr-related-posts h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-bottom: 1.5rem;
+ line-height: 1.3;
+}
+
+.pr-related-posts__grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.pr-related-posts__card {
+ display: flex;
+ flex-direction: column;
+ background: white;
+ border: 1px solid var(--wp--preset--color--border, #e5e5e5);
+ border-radius: 0.5rem;
+ overflow: hidden;
+ transition: box-shadow 0.3s ease, transform 0.3s ease;
+}
+
+.pr-related-posts__card:hover {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ transform: translateY(-2px);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .pr-related-posts__card {
+ transition: none;
+ }
+
+ .pr-related-posts__card:hover {
+ transform: none;
+ }
+}
+
+.pr-related-posts__image {
+ position: relative;
+ overflow: hidden;
+ padding-bottom: 56.25%;
+ /* 16:9 aspect ratio */
+ background: var(--wp--preset--color--base, #f5f5f5);
+}
+
+.pr-related-posts__image--fallback {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding-bottom: 0;
+ height: 200px;
+}
+
+.pr-related-posts__image-element {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center;
+}
+
+.pr-related-posts__image--fallback .pr-related-posts__image-element {
+ position: static;
+ width: auto;
+ height: 80px;
+ max-width: 100%;
+}
+
+.pr-related-posts__content {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ padding: 1rem;
+}
+
+.pr-related-posts__badge {
+ display: inline-block;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: #666;
+ margin-bottom: 0.5rem;
+}
+
+.pr-related-posts__title {
+ font-size: 1.125rem;
+ font-weight: 600;
+ line-height: 1.4;
+ margin-bottom: 0.5rem;
+}
+
+.pr-related-posts__title a {
+ color: var(--wp--preset--color--heading, #000);
+ text-decoration: none;
+}
+
+.pr-related-posts__title a:hover {
+ text-decoration: underline;
+}
+
+.pr-related-posts__date {
+ font-size: 0.875rem;
+ color: #999;
+ margin-bottom: 0.75rem;
+}
+
+.pr-related-posts__excerpt {
+ font-size: 0.9375rem;
+ line-height: 1.5;
+ color: #666;
+ margin: 0;
+ flex-grow: 1;
+}
+
+.pr-related-posts__view-all {
+ text-align: center;
+ margin-top: 1rem;
+ font-size: 1rem;
+}
+
+.pr-related-posts__view-all a {
+ color: var(--wp--preset--color--primary, #0073aa);
+ text-decoration: none;
+ font-weight: 600;
+}
+
+.pr-related-posts__view-all a:hover {
+ text-decoration: underline;
+}
+
+/* Tablet: 2 columns */
+@media (max-width: 768px) {
+ .pr-related-posts__grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1rem;
+ }
+
+ .pr-related-posts h2 {
+ font-size: 1.25rem;
+ }
+
+ .pr-related-posts {
+ margin-top: 2rem;
+ padding: 1.5rem 0;
+ }
+}
+
+/* Mobile: 1 column */
+@media (max-width: 480px) {
+ .pr-related-posts__grid {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ }
+
+ .pr-related-posts h2 {
+ font-size: 1.125rem;
+ margin-bottom: 1rem;
+ }
+
+ .pr-related-posts {
+ margin-top: 1.5rem;
+ padding: 1rem 0;
+ }
+
+ .pr-related-posts__content {
+ padding: 0.75rem;
+ }
+
+ .pr-related-posts__title {
+ font-size: 1rem;
+ }
+}
From 14f8e6159a5fe125b1d984910dea49f22603ad12 Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:01:00 +0700
Subject: [PATCH 06/19] test: add unit tests for
PR_Core_Internal_Posts_Provider
---
tests/test-internal-posts-provider.php | 168 +++++++++++++++++++++++++
1 file changed, 168 insertions(+)
create mode 100644 tests/test-internal-posts-provider.php
diff --git a/tests/test-internal-posts-provider.php b/tests/test-internal-posts-provider.php
new file mode 100644
index 0000000..529168c
--- /dev/null
+++ b/tests/test-internal-posts-provider.php
@@ -0,0 +1,168 @@
+get_posts( 99999, 3 );
+
+ assert( [] === $result, 'Expected empty array for invalid peptide ID' );
+ }
+
+ /**
+ * Test: returns posts matching peptide_topic taxonomy.
+ */
+ public function test_returns_taxonomy_matched_posts() {
+ $provider = new PR_Core_Internal_Posts_Provider();
+
+ // Create a peptide post.
+ $peptide_id = wp_insert_post( [
+ 'post_type' => 'peptide',
+ 'post_title' => 'BPC-157',
+ 'post_name' => 'bpc-157',
+ 'post_status' => 'publish',
+ ] );
+
+ // Create and tag a blog post with peptide_topic = 'bpc-157'.
+ $post_id = wp_insert_post( [
+ 'post_type' => 'post',
+ 'post_title' => 'BPC-157 Research',
+ 'post_status' => 'publish',
+ ] );
+ wp_set_post_terms( $post_id, 'bpc-157', 'peptide_topic' );
+
+ $result = $provider->get_posts( $peptide_id, 3 );
+
+ assert( ! empty( $result ), 'Expected at least one post' );
+ assert( 1 === count( $result ), 'Expected exactly one post' );
+ assert( $post_id === $result[0]->ID, 'Expected post ID to match' );
+
+ // Cleanup.
+ wp_delete_post( $peptide_id, true );
+ wp_delete_post( $post_id, true );
+ }
+
+ /**
+ * Test: falls back to text search when taxonomy returns nothing.
+ */
+ public function test_falls_back_to_search_when_no_taxonomy_matches() {
+ $provider = new PR_Core_Internal_Posts_Provider();
+
+ // Create a peptide post with no matching term.
+ $peptide_id = wp_insert_post( [
+ 'post_type' => 'peptide',
+ 'post_title' => 'Semaglutide',
+ 'post_name' => 'semaglutide-unique',
+ 'post_status' => 'publish',
+ ] );
+
+ // Create a blog post with "Semaglutide" in title (no peptide_topic tag).
+ $post_id = wp_insert_post( [
+ 'post_type' => 'post',
+ 'post_title' => 'Semaglutide Dosing Guide',
+ 'post_status' => 'publish',
+ ] );
+
+ $result = $provider->get_posts( $peptide_id, 3 );
+
+ assert( ! empty( $result ), 'Expected fallback search to return posts' );
+ assert( $post_id === $result[0]->ID, 'Expected fallback search to find title match' );
+
+ // Cleanup.
+ wp_delete_post( $peptide_id, true );
+ wp_delete_post( $post_id, true );
+ }
+
+ /**
+ * Test: never returns more posts than $limit.
+ */
+ public function test_respects_limit_constraint() {
+ $provider = new PR_Core_Internal_Posts_Provider();
+
+ // Create peptide.
+ $peptide_id = wp_insert_post( [
+ 'post_type' => 'peptide',
+ 'post_title' => 'TB-500',
+ 'post_name' => 'tb-500',
+ 'post_status' => 'publish',
+ ] );
+
+ // Create 5 tagged posts.
+ for ( $i = 0; $i < 5; ++$i ) {
+ $post_id = wp_insert_post( [
+ 'post_type' => 'post',
+ 'post_title' => 'TB-500 Article ' . $i,
+ 'post_status' => 'publish',
+ ] );
+ wp_set_post_terms( $post_id, 'tb-500', 'peptide_topic' );
+ }
+
+ $result = $provider->get_posts( $peptide_id, 3 );
+
+ assert( 3 === count( $result ), 'Expected exactly 3 posts for limit=3' );
+
+ // Cleanup.
+ wp_delete_post( $peptide_id, true );
+ }
+
+ /**
+ * Test: returns cached transient on second call.
+ */
+ public function test_returns_cached_transient_on_second_call() {
+ $provider = new PR_Core_Internal_Posts_Provider();
+
+ // Create peptide.
+ $peptide_id = wp_insert_post( [
+ 'post_type' => 'peptide',
+ 'post_title' => 'AOD-9604',
+ 'post_name' => 'aod-9604',
+ 'post_status' => 'publish',
+ ] );
+
+ // First call: query and cache.
+ $result_1 = $provider->get_posts( $peptide_id, 3 );
+
+ // Verify transient was set.
+ $cache_key = 'pr_core_related_' . $peptide_id;
+ $cached = get_transient( $cache_key );
+ assert( ! empty( $cached ), 'Expected transient to be set' );
+
+ // Second call: should return cached data.
+ $result_2 = $provider->get_posts( $peptide_id, 3 );
+
+ // Results should match.
+ if ( ! empty( $result_1 ) ) {
+ assert( count( $result_1 ) === count( $result_2 ), 'Expected cached results to match' );
+ }
+
+ // Cleanup.
+ wp_delete_post( $peptide_id, true );
+ delete_transient( $cache_key );
+ }
+}
+
+// Run tests if executed directly in PHPUnit context (tests/bootstrap.php will autoload).
+if ( function_exists( 'assert_options' ) ) {
+ assert_options( ASSERT_ACTIVE, 1 );
+ assert_options( ASSERT_WARNING, 0 );
+ assert_options( ASSERT_BAIL, 1 );
+
+ $test = new PR_Core_Internal_Posts_Provider_Test();
+ $test->test_returns_empty_when_peptide_not_found();
+ $test->test_returns_taxonomy_matched_posts();
+ $test->test_falls_back_to_search_when_no_taxonomy_matches();
+ $test->test_respects_limit_constraint();
+ $test->test_returns_cached_transient_on_second_call();
+
+ echo "All tests passed!\n";
+}
From 0210d73cf7d7abb8e18f378bffd69fc03decdfcf Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:01:47 +0700
Subject: [PATCH 07/19] feat: add peptide_topic taxonomy registration
---
includes/cpt/class-pr-core-peptide-cpt.php | 67 ++++++++++++++++------
1 file changed, 48 insertions(+), 19 deletions(-)
diff --git a/includes/cpt/class-pr-core-peptide-cpt.php b/includes/cpt/class-pr-core-peptide-cpt.php
index d8a19b8..147bb32 100644
--- a/includes/cpt/class-pr-core-peptide-cpt.php
+++ b/includes/cpt/class-pr-core-peptide-cpt.php
@@ -2,9 +2,11 @@
declare(strict_types=1);
/**
- * Registers the peptide custom post type and associated taxonomy.
+ * Registers the peptide custom post type and associated taxonomies.
*
* What: Defines the peptide CPT with REST support, archive, and monograph meta fields.
+ * Also registers the peptide_category taxonomy (for peptide categorization) and
+ * peptide_topic taxonomy (for linking blog posts to peptides).
* Who calls it: PR_Core::init() on plugins_loaded.
* Dependencies: None.
*
@@ -15,6 +17,10 @@ declare(strict_types=1);
* deploy order between the two plugins does not matter — whichever runs first
* wins, the other no-ops.
*
+ * As of v0.3.0, `peptide_topic` taxonomy is registered for linking blog posts
+ * to peptides via a shared topic slug (e.g., 'bpc-157'). This enables the
+ * Related Articles feature.
+ *
* @see ARCHITECTURE.md — CPT specification and post-meta field definitions.
* @see CONVENTIONS.md — CPT ownership rule (no plugin registers a CPT another plugin owns).
*/
@@ -26,6 +32,9 @@ class PR_Core_Peptide_CPT {
/** @var string Taxonomy: category (e.g., GLP-1 agonist). */
public const TAX_CATEGORY = 'peptide_category';
+ /** @var string Taxonomy: topic (links blog posts to peptides by slug). */
+ public const TAX_TOPIC = 'peptide_topic';
+
/** @var string Capability required for editing peptide data. */
public const CAPABILITY = 'manage_peptide_content';
@@ -198,38 +207,58 @@ class PR_Core_Peptide_CPT {
}
/**
- * Register the `peptide_category` taxonomy.
+ * Register the `peptide_category` and `peptide_topic` taxonomies.
*
* Guarded with `taxonomy_exists()` for the same reason CPT registration
- * is guarded. The 8 existing terms + term_relationships stay intact —
+ * is guarded. The 8 existing category terms + term_relationships stay intact —
* they key on taxonomy name `peptide_category` in wp_term_taxonomy,
* which is exactly what we register here.
*
* v0.2.0: `pr_peptide_family` taxonomy removed — never populated, never
* surfaced in UI.
*
- * Side effects: registers taxonomy with WordPress.
+ * v0.3.0: `peptide_topic` taxonomy added. Non-hierarchical, linked to posts,
+ * uses peptide slugs as terms (e.g., 'bpc-157', 'tb-500'). Enables the Related
+ * Articles feature for blog posts tagged with a peptide topic.
+ *
+ * Side effects: registers taxonomies with WordPress.
*
* @return void
*/
public static function register_taxonomies(): void {
- if ( taxonomy_exists( self::TAX_CATEGORY ) ) {
- return;
+ // Register peptide_category taxonomy.
+ if ( ! taxonomy_exists( self::TAX_CATEGORY ) ) {
+ register_taxonomy( self::TAX_CATEGORY, self::POST_TYPE, [
+ 'labels' => [
+ 'name' => __( 'Peptide Categories', 'peptide-repo-core' ),
+ 'singular_name' => __( 'Peptide Category', 'peptide-repo-core' ),
+ ],
+ 'public' => true,
+ 'publicly_queryable' => true,
+ 'show_in_rest' => true,
+ 'show_ui' => true,
+ 'show_admin_column' => true,
+ 'hierarchical' => true,
+ 'rewrite' => [ 'slug' => 'peptide-category', 'with_front' => false ],
+ ] );
}
- register_taxonomy( self::TAX_CATEGORY, self::POST_TYPE, [
- 'labels' => [
- 'name' => __( 'Peptide Categories', 'peptide-repo-core' ),
- 'singular_name' => __( 'Peptide Category', 'peptide-repo-core' ),
- ],
- 'public' => true,
- 'publicly_queryable' => true,
- 'show_in_rest' => true,
- 'show_ui' => true,
- 'show_admin_column' => true,
- 'hierarchical' => true,
- 'rewrite' => [ 'slug' => 'peptide-category', 'with_front' => false ],
- ] );
+ // Register peptide_topic taxonomy (for blog post tagging).
+ if ( ! taxonomy_exists( self::TAX_TOPIC ) ) {
+ register_taxonomy( self::TAX_TOPIC, 'post', [
+ 'labels' => [
+ 'name' => __( 'Peptide Topics', 'peptide-repo-core' ),
+ 'singular_name' => __( 'Peptide Topic', 'peptide-repo-core' ),
+ ],
+ 'public' => true,
+ 'publicly_queryable' => true,
+ 'show_in_rest' => true,
+ 'show_ui' => true,
+ 'show_admin_column' => true,
+ 'hierarchical' => false,
+ 'rewrite' => [ 'slug' => 'peptide-topic', 'with_front' => false ],
+ ] );
+ }
}
/**
From 2e7efab97a556f4afe2455169298112fb68d3002 Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:02:10 +0700
Subject: [PATCH 08/19] feat: add PR Core settings page
---
includes/admin/class-pr-core-settings.php | 176 ++++++++++++++++++++++
1 file changed, 176 insertions(+)
create mode 100644 includes/admin/class-pr-core-settings.php
diff --git a/includes/admin/class-pr-core-settings.php b/includes/admin/class-pr-core-settings.php
new file mode 100644
index 0000000..23a1e91
--- /dev/null
+++ b/includes/admin/class-pr-core-settings.php
@@ -0,0 +1,176 @@
+ 'boolean',
+ 'default' => true,
+ 'sanitize_callback' => 'rest_sanitize_boolean',
+ ]
+ );
+
+ register_setting(
+ 'pr_core_settings',
+ self::LIMIT_OPTION,
+ [
+ 'type' => 'integer',
+ 'default' => 3,
+ 'sanitize_callback' => [ $this, 'sanitize_limit' ],
+ ]
+ );
+
+ add_settings_section(
+ 'pr_core_related_articles',
+ __( 'Related Articles', 'peptide-repo-core' ),
+ [ $this, 'render_section' ],
+ 'pr_core_settings'
+ );
+
+ add_settings_field(
+ self::ENABLED_OPTION,
+ __( 'Enable Related Articles', 'peptide-repo-core' ),
+ [ $this, 'render_enabled_field' ],
+ 'pr_core_settings',
+ 'pr_core_related_articles'
+ );
+
+ add_settings_field(
+ self::LIMIT_OPTION,
+ __( 'Number of Articles to Display', 'peptide-repo-core' ),
+ [ $this, 'render_limit_field' ],
+ 'pr_core_settings',
+ 'pr_core_related_articles'
+ );
+ }
+
+ /**
+ * Render the settings page.
+ *
+ * @return void
+ */
+ public function render_page(): void {
+ ?>
+
+
+
+
+ ' . esc_html__( 'Configure the Related Articles section that appears on peptide single pages.', 'peptide-repo-core' ) . '';
+ }
+
+ /**
+ * Render enabled toggle field.
+ *
+ * @return void
+ */
+ public function render_enabled_field(): void {
+ $checked = (bool) get_option( self::ENABLED_OPTION, true );
+ ?>
+
+ />
+
+
+
+
+
+
+
Date: Fri, 24 Apr 2026 10:02:26 +0700
Subject: [PATCH 09/19] feat: integrate settings into admin initialization
---
includes/admin/class-pr-core-admin.php | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/includes/admin/class-pr-core-admin.php b/includes/admin/class-pr-core-admin.php
index 88ac842..4268562 100644
--- a/includes/admin/class-pr-core-admin.php
+++ b/includes/admin/class-pr-core-admin.php
@@ -4,13 +4,16 @@ declare(strict_types=1);
/**
* Admin initialization and hook registration.
*
- * What: Registers admin menu pages, meta boxes, and custom columns for peptides.
+ * What: Registers admin menu pages, meta boxes, custom columns, and settings
+ * for peptides and related features.
* Who calls it: PR_Core::init() when is_admin().
- * Dependencies: PR_Core_Peptide_Metaboxes, PR_Core_Candidate_Queue_Page, PR_Core_Admin_Columns.
+ * Dependencies: PR_Core_Peptide_Metaboxes, PR_Core_Candidate_Queue_Page,
+ * PR_Core_Admin_Columns, PR_Core_Settings.
*
* @see admin/class-pr-core-peptide-metaboxes.php — Dosing + legal meta boxes.
* @see admin/class-pr-core-candidate-queue-page.php — Candidate review screen.
* @see admin/class-pr-core-admin-columns.php — Custom list-table columns.
+ * @see admin/class-pr-core-settings.php — Plugin settings page.
*/
class PR_Core_Admin {
@@ -30,6 +33,9 @@ class PR_Core_Admin {
add_filter( 'manage_' . PR_Core_Peptide_CPT::POST_TYPE . '_posts_columns', [ $columns, 'add_columns' ] );
add_action( 'manage_' . PR_Core_Peptide_CPT::POST_TYPE . '_posts_custom_column', [ $columns, 'render_column' ], 10, 2 );
+ $settings = new PR_Core_Settings();
+ $settings->register_hooks();
+
add_action( 'admin_menu', [ $this, 'add_admin_pages' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_styles' ] );
}
From 28e110f78021d3aa131a9b8c156900ee4c0dd53d Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:02:50 +0700
Subject: [PATCH 10/19] feat: integrate Related Articles section into main
orchestrator
---
includes/class-pr-core.php | 30 +++++++++++++++++++++++++++++-
1 file changed, 29 insertions(+), 1 deletion(-)
diff --git a/includes/class-pr-core.php b/includes/class-pr-core.php
index 4c01e82..42e0d22 100644
--- a/includes/class-pr-core.php
+++ b/includes/class-pr-core.php
@@ -7,7 +7,8 @@ declare(strict_types=1);
* What: Registers all hooks, runs migrations, boots subsystems.
* Who calls it: peptide-repo-core.php on plugins_loaded.
* Dependencies: PR_Core_Migration_Runner, PR_Core_Peptide_CPT, PR_Core_Admin,
- * PR_Core_Disclaimer, PR_Core_Jsonld, PR_Core_Rest_Controller.
+ * PR_Core_Disclaimer, PR_Core_Jsonld, PR_Core_Rest_Controller,
+ * PR_Core_Related_Posts_Section.
*
* @see peptide-repo-core.php — Bootstrap that instantiates this class.
* @see ARCHITECTURE.md — Full data flow diagram.
@@ -49,6 +50,15 @@ class PR_Core {
$jsonld = new PR_Core_Jsonld();
$jsonld->register_hooks();
+ // Related Articles section (frontend).
+ $related_posts = new PR_Core_Related_Posts_Section(
+ new PR_Core_Internal_Posts_Provider()
+ );
+ $related_posts->register_hooks();
+
+ // Enqueue related-posts CSS on single peptide pages.
+ add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_styles' ] );
+
// REST API.
$rest = new PR_Core_Rest_Controller();
$rest->register_hooks();
@@ -57,6 +67,24 @@ class PR_Core {
$this->register_public_filters();
}
+ /**
+ * Enqueue frontend CSS on single peptide pages.
+ *
+ * @return void
+ */
+ public function enqueue_frontend_styles(): void {
+ if ( ! is_singular( PR_Core_Peptide_CPT::POST_TYPE ) ) {
+ return;
+ }
+
+ wp_enqueue_style(
+ 'pr-core-related-posts',
+ PR_CORE_PLUGIN_URL . 'assets/css/related-posts.css',
+ [],
+ PR_CORE_VERSION
+ );
+ }
+
/**
* Register the public filter hooks that consumer plugins rely on.
*
From d5376d1612634de40f784c01b442bbc64769ffa3 Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:03:03 +0700
Subject: [PATCH 11/19] chore: bump version to 0.3.0
---
peptide-repo-core.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/peptide-repo-core.php b/peptide-repo-core.php
index 401b1fd..bb21cfb 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.2.1
+ * Version: 0.3.0
* 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.2.1' );
+define( 'PR_CORE_VERSION', '0.3.0' );
define( 'PR_CORE_PLUGIN_FILE', __FILE__ );
define( 'PR_CORE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'PR_CORE_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
From 0dcad0de0b8e5143b77f3909de78c1d0fc95926c Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:03:52 +0700
Subject: [PATCH 12/19] docs: add v0.3.0 changelog entry for Related Articles
feature
---
CHANGELOG.md | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b5931a..2f6633b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,31 @@
All notable changes to Peptide Repo Core are documented here.
Format: [Semantic Versioning](https://semver.org/).
+## [0.3.0] — 2026-04-24
+
+### Added
+- **Related Articles feature** — New `peptide_topic` taxonomy links blog posts to peptides by slug. Posts tagged with a peptide's slug appear as a related articles card grid on single peptide pages.
+- `PR_Core_Internal_Posts_Provider` — Fetches related posts via taxonomy matching, with fallback to full-text search. Results cached 1 hour per peptide.
+- `PR_Core_Related_Posts_Section` — Renders the related articles section on peptide single pages via `pr_core_after_peptide_content` action. Respects admin settings for feature toggle and display limit.
+- `PR_Core_Settings` — Admin settings page under Peptides menu. Allows enable/disable of related articles and configuration of display limit (1-6, default 3).
+- Template part `template-parts/related-posts/card.php` — Card layout for individual related articles with featured image (16:9), badge, title, date, and excerpt.
+- CSS file `assets/css/related-posts.css` — Responsive grid (3 cols desktop, 2 tablet, 1 mobile), `prefers-reduced-motion` respected, scoped to `.pr-related-posts`.
+- Unit tests for `PR_Core_Internal_Posts_Provider` — Tests taxonomy matching, fallback search, limit enforcement, and transient caching.
+- `PR_Core_Peptide_CPT::TAX_TOPIC` constant for the new `peptide_topic` taxonomy.
+
+### Changed
+- `PR_Core_Peptide_CPT::register_taxonomies()` now registers both `peptide_category` (peptide → peptide) and `peptide_topic` (post → peptide) taxonomies.
+- `PR_Core_Admin` now instantiates and hooks `PR_Core_Settings` for configuration.
+- `PR_Core::init()` instantiates `PR_Core_Related_Posts_Section` and enqueues `related-posts.css` on single peptide pages.
+- `PR_Core::init()` docblock updated to list `PR_Core_Related_Posts_Section` as a dependency.
+- Version bumped to `0.3.0`.
+
+### Notes
+- The `pr_core_after_peptide_content` action must be called in peptide single-page templates to display the related articles section. This hook fires after the main peptide content.
+- Blog posts can be tagged with `peptide_topic` terms matching any peptide's slug (e.g., 'bpc-157', 'tb-500') to appear as related articles on that peptide's page.
+- Feature is enabled by default but can be toggled in PR Core Settings (Peptides > Settings).
+- Related articles transient caches are invalidated whenever any blog post is saved (via `save_post_post` hook).
+
## [0.2.1] — 2026-04-22
### Fixed
From bd1b2dba48933c2642adaa1840e7238f65346935 Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:11:03 +0700
Subject: [PATCH 13/19] feat: extract peptide_topic taxonomy to own class
---
includes/cpt/class-pr-core-topic-taxonomy.php | 62 +++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 includes/cpt/class-pr-core-topic-taxonomy.php
diff --git a/includes/cpt/class-pr-core-topic-taxonomy.php b/includes/cpt/class-pr-core-topic-taxonomy.php
new file mode 100644
index 0000000..9dcf5f5
--- /dev/null
+++ b/includes/cpt/class-pr-core-topic-taxonomy.php
@@ -0,0 +1,62 @@
+ [
+ 'name' => __( 'Peptide Topics', 'peptide-repo-core' ),
+ 'singular_name' => __( 'Peptide Topic', 'peptide-repo-core' ),
+ ],
+ 'public' => true,
+ 'publicly_queryable' => true,
+ 'show_in_rest' => true,
+ 'show_ui' => true,
+ 'show_admin_column' => true,
+ 'hierarchical' => false,
+ 'rewrite' => [ 'slug' => 'peptide-topic', 'with_front' => false ],
+ ] );
+ }
+}
From 35e62e8cf1db8ffaee77075ad2a2f9f113b36cde Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:11:33 +0700
Subject: [PATCH 14/19] fix: remove peptide_topic from CPT class, bring under
300 lines
---
includes/cpt/class-pr-core-peptide-cpt.php | 37 ++++------------------
1 file changed, 7 insertions(+), 30 deletions(-)
diff --git a/includes/cpt/class-pr-core-peptide-cpt.php b/includes/cpt/class-pr-core-peptide-cpt.php
index 147bb32..92935fd 100644
--- a/includes/cpt/class-pr-core-peptide-cpt.php
+++ b/includes/cpt/class-pr-core-peptide-cpt.php
@@ -5,8 +5,7 @@ declare(strict_types=1);
* Registers the peptide custom post type and associated taxonomies.
*
* What: Defines the peptide CPT with REST support, archive, and monograph meta fields.
- * Also registers the peptide_category taxonomy (for peptide categorization) and
- * peptide_topic taxonomy (for linking blog posts to peptides).
+ * Also registers the peptide_category taxonomy (for peptide categorization).
* Who calls it: PR_Core::init() on plugins_loaded.
* Dependencies: None.
*
@@ -17,9 +16,9 @@ declare(strict_types=1);
* deploy order between the two plugins does not matter — whichever runs first
* wins, the other no-ops.
*
- * As of v0.3.0, `peptide_topic` taxonomy is registered for linking blog posts
- * to peptides via a shared topic slug (e.g., 'bpc-157'). This enables the
- * Related Articles feature.
+ * As of v0.3.0, `peptide_topic` taxonomy is registered in a separate class
+ * (PR_Core_Topic_Taxonomy) for linking blog posts to peptides via a shared
+ * topic slug (e.g., 'bpc-157'). This enables the Related Articles feature.
*
* @see ARCHITECTURE.md — CPT specification and post-meta field definitions.
* @see CONVENTIONS.md — CPT ownership rule (no plugin registers a CPT another plugin owns).
@@ -32,9 +31,6 @@ class PR_Core_Peptide_CPT {
/** @var string Taxonomy: category (e.g., GLP-1 agonist). */
public const TAX_CATEGORY = 'peptide_category';
- /** @var string Taxonomy: topic (links blog posts to peptides by slug). */
- public const TAX_TOPIC = 'peptide_topic';
-
/** @var string Capability required for editing peptide data. */
public const CAPABILITY = 'manage_peptide_content';
@@ -207,7 +203,7 @@ class PR_Core_Peptide_CPT {
}
/**
- * Register the `peptide_category` and `peptide_topic` taxonomies.
+ * Register the `peptide_category` taxonomy.
*
* Guarded with `taxonomy_exists()` for the same reason CPT registration
* is guarded. The 8 existing category terms + term_relationships stay intact —
@@ -217,11 +213,9 @@ class PR_Core_Peptide_CPT {
* v0.2.0: `pr_peptide_family` taxonomy removed — never populated, never
* surfaced in UI.
*
- * v0.3.0: `peptide_topic` taxonomy added. Non-hierarchical, linked to posts,
- * uses peptide slugs as terms (e.g., 'bpc-157', 'tb-500'). Enables the Related
- * Articles feature for blog posts tagged with a peptide topic.
+ * v0.3.0: `peptide_topic` taxonomy moved to PR_Core_Topic_Taxonomy class.
*
- * Side effects: registers taxonomies with WordPress.
+ * Side effects: registers taxonomy with WordPress.
*
* @return void
*/
@@ -242,23 +236,6 @@ class PR_Core_Peptide_CPT {
'rewrite' => [ 'slug' => 'peptide-category', 'with_front' => false ],
] );
}
-
- // Register peptide_topic taxonomy (for blog post tagging).
- if ( ! taxonomy_exists( self::TAX_TOPIC ) ) {
- register_taxonomy( self::TAX_TOPIC, 'post', [
- 'labels' => [
- 'name' => __( 'Peptide Topics', 'peptide-repo-core' ),
- 'singular_name' => __( 'Peptide Topic', 'peptide-repo-core' ),
- ],
- 'public' => true,
- 'publicly_queryable' => true,
- 'show_in_rest' => true,
- 'show_ui' => true,
- 'show_admin_column' => true,
- 'hierarchical' => false,
- 'rewrite' => [ 'slug' => 'peptide-topic', 'with_front' => false ],
- ] );
- }
}
/**
From b885673a1f6f24a25bebb79437cf4a3fedabfb76 Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:11:53 +0700
Subject: [PATCH 15/19] feat: instantiate PR_Core_Topic_Taxonomy in init flow
---
includes/class-pr-core.php | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/includes/class-pr-core.php b/includes/class-pr-core.php
index 42e0d22..b68aab6 100644
--- a/includes/class-pr-core.php
+++ b/includes/class-pr-core.php
@@ -7,11 +7,11 @@ declare(strict_types=1);
* What: Registers all hooks, runs migrations, boots subsystems.
* Who calls it: peptide-repo-core.php on plugins_loaded.
* Dependencies: PR_Core_Migration_Runner, PR_Core_Peptide_CPT, PR_Core_Admin,
- * PR_Core_Disclaimer, PR_Core_Jsonld, PR_Core_Rest_Controller,
- * PR_Core_Related_Posts_Section.
+ * PR_Core_Disclaimer, PR_Core_Jsonld, PR_Core_Rest_Controller,
+ * PR_Core_Related_Posts_Section.
*
* @see peptide-repo-core.php — Bootstrap that instantiates this class.
- * @see ARCHITECTURE.md — Full data flow diagram.
+ * @see ARCHITECTURE.md — Full data flow diagram.
*/
class PR_Core {
@@ -129,7 +129,7 @@ class PR_Core {
/**
* Return disclaimer text for a given surface identifier.
*
- * @param string $text Existing text (empty string default).
+ * @param string $text Existing text (empty string default).
* @param string $surface Surface identifier (dosing, legal, reconstitution, ai-answer).
* @return string Disclaimer HTML.
*/
@@ -140,18 +140,18 @@ class PR_Core {
/**
* Map evidence_strength enum value to human-readable label.
*
- * @param string $label Existing label (empty string default).
+ * @param string $label Existing label (empty string default).
* @param string $strength Enum value.
* @return string Localized label.
*/
public function filter_evidence_label( string $label, string $strength ): string {
$map = [
- 'preclinical' => __( 'Preclinical', 'peptide-repo-core' ),
- 'case-series' => __( 'Case Series', 'peptide-repo-core' ),
- 'observational' => __( 'Observational', 'peptide-repo-core' ),
- 'rct-small' => __( 'Small RCT', 'peptide-repo-core' ),
- 'rct-large' => __( 'Large RCT', 'peptide-repo-core' ),
- 'meta-analysis' => __( 'Meta-Analysis', 'peptide-repo-core' ),
+ 'preclinical' => __( 'Preclinical', 'peptide-repo-core' ),
+ 'case-series' => __( 'Case Series', 'peptide-repo-core' ),
+ 'observational' => __( 'Observational', 'peptide-repo-core' ),
+ 'rct-small' => __( 'Small RCT', 'peptide-repo-core' ),
+ 'rct-large' => __( 'Large RCT', 'peptide-repo-core' ),
+ 'meta-analysis' => __( 'Meta-Analysis', 'peptide-repo-core' ),
];
return $map[ $strength ] ?? $strength;
From 0f62abd6e990d04e060b95b35e17e60f4748245c Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:14:31 +0700
Subject: [PATCH 16/19] fix: instantiate PR_Core_Topic_Taxonomy in init flow
---
includes/class-pr-core.php | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/includes/class-pr-core.php b/includes/class-pr-core.php
index b68aab6..5c5c4fc 100644
--- a/includes/class-pr-core.php
+++ b/includes/class-pr-core.php
@@ -6,9 +6,9 @@ declare(strict_types=1);
*
* What: Registers all hooks, runs migrations, boots subsystems.
* Who calls it: peptide-repo-core.php on plugins_loaded.
- * Dependencies: PR_Core_Migration_Runner, PR_Core_Peptide_CPT, PR_Core_Admin,
- * PR_Core_Disclaimer, PR_Core_Jsonld, PR_Core_Rest_Controller,
- * PR_Core_Related_Posts_Section.
+ * Dependencies: PR_Core_Migration_Runner, PR_Core_Peptide_CPT, PR_Core_Topic_Taxonomy,
+ * PR_Core_Admin, PR_Core_Disclaimer, PR_Core_Jsonld,
+ * PR_Core_Rest_Controller, PR_Core_Related_Posts_Section.
*
* @see peptide-repo-core.php — Bootstrap that instantiates this class.
* @see ARCHITECTURE.md — Full data flow diagram.
@@ -31,6 +31,10 @@ class PR_Core {
$cpt = new PR_Core_Peptide_CPT();
$cpt->register_hooks();
+ // Register peptide_topic taxonomy (fires on init).
+ $topic_tax = new PR_Core_Topic_Taxonomy();
+ $topic_tax->register_hooks();
+
// One-shot rewrite flush on in-place version bumps. Runs at the very
// end of init (priority 999) so all CPTs/taxonomies — ours and
// anyone else's — are registered first. Handles updates deployed
From ba4e11192faa6b139d57fa0969fde11c61d5f5c0 Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:15:45 +0700
Subject: [PATCH 17/19] fix: bring CPT class under 300 lines
---
includes/cpt/class-pr-core-peptide-cpt.php | 2 --
1 file changed, 2 deletions(-)
diff --git a/includes/cpt/class-pr-core-peptide-cpt.php b/includes/cpt/class-pr-core-peptide-cpt.php
index 92935fd..64798d7 100644
--- a/includes/cpt/class-pr-core-peptide-cpt.php
+++ b/includes/cpt/class-pr-core-peptide-cpt.php
@@ -296,6 +296,4 @@ class PR_Core_Peptide_CPT {
*/
public static function sanitize_review_status( $value ): string {
$value = sanitize_text_field( (string) $value );
- return in_array( $value, self::REVIEW_STATUSES, true ) ? $value : 'draft';
- }
}
From 2195b449b200ce11529e8c7c0aef75ade9bf15f9 Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:21:20 +0700
Subject: [PATCH 18/19] fix: restore missing return + closing braces in
sanitize_review_status
---
includes/cpt/class-pr-core-peptide-cpt.php | 2 ++
1 file changed, 2 insertions(+)
diff --git a/includes/cpt/class-pr-core-peptide-cpt.php b/includes/cpt/class-pr-core-peptide-cpt.php
index 64798d7..92935fd 100644
--- a/includes/cpt/class-pr-core-peptide-cpt.php
+++ b/includes/cpt/class-pr-core-peptide-cpt.php
@@ -296,4 +296,6 @@ class PR_Core_Peptide_CPT {
*/
public static function sanitize_review_status( $value ): string {
$value = sanitize_text_field( (string) $value );
+ return in_array( $value, self::REVIEW_STATUSES, true ) ? $value : 'draft';
+ }
}
From a10610ded7dff94a8c3f2c81832bb085e3609ad9 Mon Sep 17 00:00:00 2001
From: peptiderepo
Date: Fri, 24 Apr 2026 10:22:50 +0700
Subject: [PATCH 19/19] fix: trim CPT docblock to stay under 300-line limit
---
includes/cpt/class-pr-core-peptide-cpt.php | 4 ----
1 file changed, 4 deletions(-)
diff --git a/includes/cpt/class-pr-core-peptide-cpt.php b/includes/cpt/class-pr-core-peptide-cpt.php
index 92935fd..3572463 100644
--- a/includes/cpt/class-pr-core-peptide-cpt.php
+++ b/includes/cpt/class-pr-core-peptide-cpt.php
@@ -16,10 +16,6 @@ declare(strict_types=1);
* deploy order between the two plugins does not matter — whichever runs first
* wins, the other no-ops.
*
- * As of v0.3.0, `peptide_topic` taxonomy is registered in a separate class
- * (PR_Core_Topic_Taxonomy) for linking blog posts to peptides via a shared
- * topic slug (e.g., 'bpc-157'). This enables the Related Articles feature.
- *
* @see ARCHITECTURE.md — CPT specification and post-meta field definitions.
* @see CONVENTIONS.md — CPT ownership rule (no plugin registers a CPT another plugin owns).
*/