diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index e843624..3e695be 100755
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -134,6 +134,7 @@ prautoblogger/
│ │ ├── class-semantic-dedup.php # Embedding-based cosine similarity dedup with keyword fallback
│ │ ├── class-content-generator.php# Writer agent — manages the generation pipeline
│ │ ├── class-content-prompts.php # Builds all LLM prompts for content generation (system, stage, linking rules)
+│ │ ├── class-peptide-linker.php # Deterministic post-processor: hyperlinks peptide mentions to /peptides/ pages
│ │ ├── class-chief-editor.php # Editor agent — LLM-powered editorial review
│ │ ├── class-publisher.php # Creates WordPress posts from approved content
│ │ ├── class-post-assembler.php # Post-creation helpers: taxonomy, log linking, images, sanitization
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a10d14f..e94ca96 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,23 @@ All notable changes to PRAutoBlogger will be documented in this file.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project uses [Semantic Versioning](https://semver.org/).
+## [0.6.2] — 2026-04-20
+
+### Added
+- **Deterministic peptide auto-linker.** New `Peptide_Linker` class scans
+ generated HTML and wraps every mention of a known peptide in a hyperlink
+ to its `/peptides/{slug}/` database page. Runs as a post-processing step
+ in Publisher before `wp_insert_post` — no LLM involvement, 100% reliable.
+ Handles case-insensitive matching, hyphen/space variants (e.g., "BPC-157"
+ and "BPC 157"), and skips text already inside `` tags. Gracefully
+ no-ops if PR Core is not active.
+
+### Changed
+- **Removed prompt-based peptide linking.** The system prompt no longer asks
+ the LLM to link peptides (unreliable). Peptide links are now injected
+ deterministically by `Peptide_Linker` after content generation. The prompt
+ still provides article links and the "never fabricate URLs" rule.
+
## [0.6.1] — 2026-04-20
### Added
diff --git a/includes/core/class-content-prompts.php b/includes/core/class-content-prompts.php
index 096699d..ba7a04a 100644
--- a/includes/core/class-content-prompts.php
+++ b/includes/core/class-content-prompts.php
@@ -167,13 +167,10 @@ class PRAutoBlogger_Content_Prompts {
$rules = "--- LINKING RULES ---\n";
$rules .= "NEVER fabricate or invent URLs. You do NOT have access to external sources.\n";
$rules .= "Only use the internal links listed below when linking within the article.\n";
- $rules .= "If no listed page is relevant to a section, do not insert a link.\n";
- $rules .= "IMPORTANT: Whenever ANY peptide name from the peptide database list below\n";
- $rules .= "is mentioned in the article, ALWAYS link the FIRST mention to its database page.\n\n";
+ $rules .= "If no listed article is relevant to a section, do not insert a link.\n";
+ $rules .= "Do NOT add links for peptide names — those are injected automatically after generation.\n\n";
$rules .= self::build_article_links();
- $rules .= "\n";
- $rules .= self::build_peptide_links();
$rules .= "--- END LINKING RULES ---";
@@ -211,42 +208,4 @@ class PRAutoBlogger_Content_Prompts {
return implode( "\n", $lines ) . "\n";
}
- /**
- * Build a reference list of all published peptide database pages.
- *
- * Every peptide mentioned in an article must link to its database page
- * on first mention. This gives the model the real URLs to use.
- *
- * @return string Formatted peptide link list, or empty if CPT unavailable.
- */
- private static function build_peptide_links(): string {
- // PR Core may not be active — guard against missing post type.
- if ( ! post_type_exists( 'pr_peptide' ) ) {
- return '';
- }
-
- $peptides = get_posts( [
- 'post_type' => 'pr_peptide',
- 'post_status' => 'publish',
- 'posts_per_page' => 200,
- 'orderby' => 'title',
- 'order' => 'ASC',
- 'fields' => 'ids',
- ] );
-
- if ( empty( $peptides ) ) {
- return '';
- }
-
- $lines = [ "Peptide database pages — ALWAYS link the first mention of each peptide:\n" ];
- foreach ( $peptides as $peptide_id ) {
- $title = get_the_title( $peptide_id );
- $url = get_permalink( $peptide_id );
- if ( $title && $url ) {
- $lines[] = "- {$title}: {$url}";
- }
- }
-
- return implode( "\n", $lines ) . "\n";
- }
}
diff --git a/includes/core/class-peptide-linker.php b/includes/core/class-peptide-linker.php
new file mode 100644
index 0000000..c45a4ab
--- /dev/null
+++ b/includes/core/class-peptide-linker.php
@@ -0,0 +1,149 @@
+ tag pointing to its /peptides/ page.
+ * Skips text that is already inside an existing tag to avoid nested links.
+ *
+ * Triggered by: PRAutoBlogger_Publisher::create_post() (before wp_insert_post).
+ * Dependencies: PR Core plugin (pr_peptide CPT). Gracefully no-ops if PR Core is inactive.
+ *
+ * @see core/class-publisher.php — Calls inject_links() on post_content.
+ * @see peptide-repo-core/includes/cpt/class-pr-core-peptide-cpt.php — Peptide CPT.
+ */
+class PRAutoBlogger_Peptide_Linker {
+
+ /**
+ * Inject peptide hyperlinks into HTML content.
+ *
+ * Every occurrence of a known peptide name is wrapped in a link to its
+ * database page, except when the text is already inside an tag.
+ *
+ * @param string $html Article HTML content.
+ * @return string HTML with peptide names hyperlinked.
+ */
+ public static function inject_links( string $html ): string {
+ if ( ! post_type_exists( 'pr_peptide' ) ) {
+ return $html;
+ }
+
+ $link_map = self::build_link_map();
+ if ( empty( $link_map ) ) {
+ return $html;
+ }
+
+ // Build a single regex alternation of all peptide names, longest first
+ // so "BPC-157" matches before a hypothetical "BPC" would.
+ $names = array_keys( $link_map );
+ usort( $names, static function ( string $a, string $b ): int {
+ return strlen( $b ) - strlen( $a );
+ } );
+
+ $escaped = array_map( static function ( string $name ): string {
+ return preg_quote( $name, '/' );
+ }, $names );
+
+ // Word-boundary match, case-insensitive.
+ $pattern = '/\b(' . implode( '|', $escaped ) . ')\b/iu';
+
+ // Split HTML into segments: inside … tags vs. outside.
+ // Only replace in segments that are NOT inside an anchor tag.
+ return self::replace_outside_links( $html, $pattern, $link_map );
+ }
+
+ /**
+ * Build a map of peptide display names → permalink URLs.
+ *
+ * Includes the canonical title and common aliases (e.g., "BPC-157" and
+ * "BPC 157") so both hyphenated and space-separated forms get linked.
+ *
+ * @return array Name → URL pairs, case-preserved keys.
+ */
+ private static function build_link_map(): array {
+ $peptides = get_posts( [
+ 'post_type' => 'pr_peptide',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 200,
+ 'orderby' => 'title',
+ 'order' => 'ASC',
+ 'fields' => 'ids',
+ ] );
+
+ $map = [];
+ foreach ( $peptides as $peptide_id ) {
+ $title = get_the_title( $peptide_id );
+ $url = get_permalink( $peptide_id );
+ if ( ! $title || ! $url ) {
+ continue;
+ }
+
+ // Canonical name (e.g., "BPC-157").
+ $map[ $title ] = $url;
+
+ // If the name contains hyphens, also match the space-separated
+ // variant (e.g., "BPC 157" → same URL as "BPC-157").
+ if ( strpos( $title, '-' ) !== false ) {
+ $space_variant = str_replace( '-', ' ', $title );
+ $map[ $space_variant ] = $url;
+ }
+ }
+
+ return $map;
+ }
+
+ /**
+ * Replace peptide names with links, but only outside existing tags.
+ *
+ * Splits HTML into chunks by … boundaries. Replacement only
+ * runs on chunks that are outside anchors, preventing nested tags.
+ *
+ * @param string $html Full HTML content.
+ * @param string $pattern Regex pattern matching peptide names.
+ * @param array $link_map Name → URL map (case-preserved keys).
+ * @return string HTML with replacements applied.
+ */
+ private static function replace_outside_links( string $html, string $pattern, array $link_map ): string {
+ // Split on anchor tags, keeping the delimiters.
+ // This regex captures full … blocks as array elements.
+ $parts = preg_split( '/(]*>.*?<\/a>)/is', $html, -1, PREG_SPLIT_DELIM_CAPTURE );
+
+ if ( false === $parts ) {
+ return $html;
+ }
+
+ // Build a lowercase → URL lookup for case-insensitive matching.
+ $lower_map = [];
+ foreach ( $link_map as $name => $url ) {
+ $lower_map[ mb_strtolower( $name ) ] = $url;
+ }
+
+ $result = '';
+ foreach ( $parts as $part ) {
+ // If this chunk is an anchor tag, pass it through untouched.
+ if ( preg_match( '/^' . esc_html( $matched_text ) . '';
+ },
+ $part
+ );
+ }
+
+ return $result;
+ }
+}
diff --git a/includes/core/class-publisher.php b/includes/core/class-publisher.php
index 772c2b6..879f812 100755
--- a/includes/core/class-publisher.php
+++ b/includes/core/class-publisher.php
@@ -12,6 +12,7 @@ declare(strict_types=1);
* WordPress wp_insert_post(), post meta API.
*
* @see core/class-post-assembler.php — Post-creation helpers (taxonomy, images, logs).
+ * @see core/class-peptide-linker.php — Injects peptide database hyperlinks into content.
* @see core/class-chief-editor.php — Produces the editorial review we consume.
* @see core/class-content-generator.php — Produces the content we publish.
* @see ARCHITECTURE.md — Data flow step 6.
@@ -74,9 +75,15 @@ class PRAutoBlogger_Publisher {
string $post_status,
?string $run_id = null
): int {
+ // Clean LLM artifacts, then inject peptide hyperlinks deterministically
+ // before the content enters WordPress. Peptide linker no-ops gracefully
+ // if PR Core is not active.
+ $clean_content = PRAutoBlogger_Post_Assembler::sanitize_llm_content( $content );
+ $linked_content = PRAutoBlogger_Peptide_Linker::inject_links( $clean_content );
+
$post_data = [
'post_title' => sanitize_text_field( $idea->get_suggested_title() ),
- 'post_content' => wp_kses_post( PRAutoBlogger_Post_Assembler::sanitize_llm_content( $content ) ),
+ 'post_content' => wp_kses_post( $linked_content ),
'post_status' => $post_status,
'post_type' => 'post',
'post_author' => PRAutoBlogger_Post_Assembler::get_default_author_id(),
diff --git a/prautoblogger.php b/prautoblogger.php
index d66adee..542020d 100644
--- a/prautoblogger.php
+++ b/prautoblogger.php
@@ -8,7 +8,7 @@ declare(strict_types=1);
* Plugin Name: PRAutoBlogger
* Plugin URI: https://peptiderepo.com/prautoblogger
* Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics.
- * Version: 0.6.1
+ * Version: 0.6.2
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: PeptideRepo
@@ -35,7 +35,7 @@ if ( ! defined( 'ABSPATH' ) ) {
| and limits without magic strings.
*/
-define( 'PRAUTOBLOGGER_VERSION', '0.6.1' );
+define( 'PRAUTOBLOGGER_VERSION', '0.6.2' );
define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' );
define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ );
define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );