From b0cc93669d094449f5bfc304023217fd8276cb60 Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Sun, 19 Apr 2026 17:01:54 +0700 Subject: [PATCH] feat: add Writing Instructions setting for LLM system prompt (v0.4.4) New textarea in Content settings lets admins provide custom instructions appended to the writer's system prompt. Steers style, structure, voice, and formatting without editing code. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 8 ++++++++ includes/admin/class-settings-fields.php | 8 ++++++++ includes/core/class-content-generator.php | 19 +++++++++++-------- includes/models/class-content-request.php | 23 +++++++++++++++-------- prautoblogger.php | 4 ++-- tests/unit/Models/ContentRequestTest.php | 23 ++++++++++++++++++++++- 6 files changed, 66 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb50a3..ef60239 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ 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.4.4] — 2026-04-19 + +### Added +- **Writing Instructions setting.** New textarea in Content settings lets you + provide custom instructions appended to the LLM's system prompt when writing + articles. Use it to steer style, structure, voice, and formatting without + editing code. + ## [0.4.3] — 2026-04-19 ### Fixed diff --git a/includes/admin/class-settings-fields.php b/includes/admin/class-settings-fields.php index 21f4e47..cd80873 100755 --- a/includes/admin/class-settings-fields.php +++ b/includes/admin/class-settings-fields.php @@ -202,6 +202,14 @@ class PRAutoBlogger_Settings_Fields { 'default' => 2000, 'min' => 500, ], + [ + 'id' => 'prautoblogger_writing_instructions', + 'label' => __( 'Writing Instructions', 'prautoblogger' ), + 'type' => 'textarea', + 'section' => 'prautoblogger_content', + 'default' => '', + 'description' => __( 'Custom instructions appended to the LLM\'s system prompt when writing articles. Use this to steer style, structure, voice, and formatting — e.g. "Always open with a hook question. Use short paragraphs. Cite at least two studies."', 'prautoblogger' ), + ], [ 'id' => 'prautoblogger_topic_exclusions', 'label' => __( 'Topic Exclusions', 'prautoblogger' ), diff --git a/includes/core/class-content-generator.php b/includes/core/class-content-generator.php index 232af0a..c60c9ab 100755 --- a/includes/core/class-content-generator.php +++ b/includes/core/class-content-generator.php @@ -32,13 +32,10 @@ class PRAutoBlogger_Content_Generator { * Generate a blog post from an article idea. * * Reads pipeline mode from settings and dispatches to the appropriate method. - * * Side effects: LLM API calls (1-3 depending on mode), cost logging. * * @param PRAutoBlogger_Article_Idea $idea The scored idea to generate content for. - * * @return string Generated HTML content. - * * @throws \RuntimeException On generation failure. */ public function generate( PRAutoBlogger_Article_Idea $idea ): string { @@ -51,7 +48,8 @@ class PRAutoBlogger_Content_Generator { absint( get_option( 'prautoblogger_min_word_count', 800 ) ), absint( get_option( 'prautoblogger_max_word_count', 2000 ) ), get_option( 'prautoblogger_niche_description', '' ), - json_decode( get_option( 'prautoblogger_topic_exclusions', '[]' ), true ) ?: [] + json_decode( get_option( 'prautoblogger_topic_exclusions', '[]' ), true ) ?: [], + get_option( 'prautoblogger_writing_instructions', '' ) ); if ( 'single_pass' === $mode ) { @@ -64,8 +62,7 @@ class PRAutoBlogger_Content_Generator { /** * Single-pass generation: one LLM call produces the complete article. * - * @param PRAutoBlogger_Content_Request $request - * + * @param PRAutoBlogger_Content_Request $request Content request with settings. * @return string Generated HTML content. */ private function generate_single_pass( PRAutoBlogger_Content_Request $request ): string { @@ -249,8 +246,7 @@ class PRAutoBlogger_Content_Generator { /** * Build the system prompt shared across all writing stages. * - * @param PRAutoBlogger_Content_Request $request - * + * @param PRAutoBlogger_Content_Request $request Content request with settings. * @return string */ private function build_writer_system_prompt( PRAutoBlogger_Content_Request $request ): string { @@ -262,6 +258,13 @@ class PRAutoBlogger_Content_Generator { $prompt .= ". Write well-researched, engaging, SEO-friendly content. "; $prompt .= "Use a {$request->get_tone()} tone. "; $prompt .= "Output HTML content only — no markdown, no code fences, no commentary."; + + // Append user-defined writing instructions if configured. + $instructions = trim( $request->get_writing_instructions() ); + if ( '' !== $instructions ) { + $prompt .= "\n\nAdditional instructions:\n" . $instructions; + } + return $prompt; } diff --git a/includes/models/class-content-request.php b/includes/models/class-content-request.php index 6f9ce71..5ee90ef 100755 --- a/includes/models/class-content-request.php +++ b/includes/models/class-content-request.php @@ -21,6 +21,7 @@ class PRAutoBlogger_Content_Request { private int $max_word_count; private string $niche_description; private array $topic_exclusions; + private string $writing_instructions; public function __construct( PRAutoBlogger_Article_Idea $idea, @@ -29,15 +30,17 @@ class PRAutoBlogger_Content_Request { int $min_word_count, int $max_word_count, string $niche_description, - array $topic_exclusions = [] + array $topic_exclusions = [], + string $writing_instructions = '' ) { - $this->idea = $idea; - $this->pipeline_mode = $pipeline_mode; - $this->tone = $tone; - $this->min_word_count = $min_word_count; - $this->max_word_count = $max_word_count; - $this->niche_description = $niche_description; - $this->topic_exclusions = $topic_exclusions; + $this->idea = $idea; + $this->pipeline_mode = $pipeline_mode; + $this->tone = $tone; + $this->min_word_count = $min_word_count; + $this->max_word_count = $max_word_count; + $this->niche_description = $niche_description; + $this->topic_exclusions = $topic_exclusions; + $this->writing_instructions = $writing_instructions; } public function get_idea(): PRAutoBlogger_Article_Idea { @@ -67,4 +70,8 @@ class PRAutoBlogger_Content_Request { public function get_topic_exclusions(): array { return $this->topic_exclusions; } + + public function get_writing_instructions(): string { + return $this->writing_instructions; + } } diff --git a/prautoblogger.php b/prautoblogger.php index bcaeb28..16559b2 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.4.3 + * Version: 0.4.4 * 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.4.3' ); +define( 'PRAUTOBLOGGER_VERSION', '0.4.4' ); define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' ); define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ ); define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); diff --git a/tests/unit/Models/ContentRequestTest.php b/tests/unit/Models/ContentRequestTest.php index 4f0ffbf..8751ce6 100644 --- a/tests/unit/Models/ContentRequestTest.php +++ b/tests/unit/Models/ContentRequestTest.php @@ -25,7 +25,8 @@ class ContentRequestTest extends BaseTestCase { 1000, 2500, 'Technology and innovation', - [ 'politics', 'religion' ] + [ 'politics', 'religion' ], + 'Always open with a hook question.' ); $this->assertSame( $idea, $request->get_idea() ); @@ -35,6 +36,7 @@ class ContentRequestTest extends BaseTestCase { $this->assertSame( 2500, $request->get_max_word_count() ); $this->assertSame( 'Technology and innovation', $request->get_niche_description() ); $this->assertSame( [ 'politics', 'religion' ], $request->get_topic_exclusions() ); + $this->assertSame( 'Always open with a hook question.', $request->get_writing_instructions() ); } /** @@ -63,6 +65,25 @@ class ContentRequestTest extends BaseTestCase { $this->assertIsArray( $request->get_topic_exclusions() ); } + /** + * Test writing_instructions defaults to empty string when omitted. + */ + public function test_writing_instructions_defaults_to_empty(): void { + $idea_data = $this->get_article_idea_fixture(); + $idea = new \PRAutoBlogger_Article_Idea( $idea_data ); + + $request = new \PRAutoBlogger_Content_Request( + $idea, + 'auto', + 'professional', + 1000, + 2500, + 'Tech' + ); + + $this->assertSame( '', $request->get_writing_instructions() ); + } + /** * Test with empty topic exclusions. */