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. */