From a37fb843a37bcaa360fb00ce3082f6aa0623aea3 Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Mon, 20 Apr 2026 12:48:57 +0700 Subject: [PATCH] Add Display settings: article font, font size, table borders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New "Display" admin section with three settings: - Article Font (7 choices + theme default, Google Fonts loaded on demand) - Article Font Size (px, 0 = theme default) - Table Borders (toggle, on by default — adds borders, padding, striping) Inline CSS injected via wp_head only on singular posts with _prautoblogger_generated meta — zero impact on other pages. Extracted Image_Model_Registry to keep settings-fields-extended under 300 lines. Bump 0.6.3 → 0.7.0. Co-Authored-By: Claude Opus 4.6 --- ARCHITECTURE.md | 7 +- CHANGELOG.md | 16 ++ includes/admin/class-image-model-registry.php | 83 ++++++++ .../admin/class-settings-fields-extended.php | 95 ++++----- includes/admin/class-settings-fields.php | 5 + includes/class-activator.php | 3 + includes/class-prautoblogger.php | 6 +- .../frontend/class-article-typography.php | 196 ++++++++++++++++++ prautoblogger.php | 4 +- 9 files changed, 355 insertions(+), 60 deletions(-) create mode 100644 includes/admin/class-image-model-registry.php create mode 100644 includes/frontend/class-article-typography.php diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3e695be..8338a78 100755 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -116,7 +116,8 @@ prautoblogger/ │ ├── admin/ │ │ ├── class-admin-page.php # Main settings page (tabbed SaaS-style UI) │ │ ├── class-settings-fields.php # Declarative settings: sections + core fields (API/Models/Content/Sources) -│ │ ├── class-settings-fields-extended.php # Operational fields: schedule, publishing, analytics, images +│ │ ├── class-settings-fields-extended.php # Operational fields: schedule, publishing, display, analytics, images +│ │ ├── class-image-model-registry.php # Static list of image generation models for the model picker │ │ ├── class-admin-notices.php # Onboarding notices, error alerts, budget warnings │ │ ├── class-dashboard-widget.php # WP Dashboard widget showing generation status │ │ ├── class-post-metabox.php # Metabox on posts showing generation metadata @@ -177,6 +178,7 @@ prautoblogger/ │ │ └── (Phase 3: class-cloudflare-workers-ai-model-registry.php) │ │ │ ├── frontend/ +│ │ ├── class-article-typography.php # Inline CSS for font, size, table borders on generated posts │ │ └── class-posts-widget.php # [prautoblogger_posts] shortcode + REST endpoint │ │ │ └── models/ @@ -430,6 +432,9 @@ All prefixed with `prautoblogger_`: | `prautoblogger_cloudflare_ai_token` | Encrypted Cloudflare Workers AI API token | | `prautoblogger_cloudflare_account_id` | Cloudflare account UUID (plaintext — identifier, not secret) | | `prautoblogger_image_model` | Image model alias: `flux-1-schnell` (default) or `flux-1-dev` | +| `prautoblogger_article_font_family` | Font family key: 'default', 'inter', 'georgia', 'merriweather', 'lora', 'open_sans', 'roboto', 'system' | +| `prautoblogger_article_font_size` | Body font size in px (0 = theme default). Recommended: 16–18. | +| `prautoblogger_table_borders` | Toggle: add borders/padding/striping to tables (default: '1') | | `prautoblogger_image_style_suffix` | Text appended to every image prompt (default: CEO-locked 90s infomercial prompt) | | `prautoblogger_openrouter_model_registry` | Normalized OpenRouter model list (JSON array, daily refresh, serves as durable cache) | | `prautoblogger_openrouter_model_registry_fetched_at` | Unix timestamp of last successful model registry refresh | diff --git a/CHANGELOG.md b/CHANGELOG.md index 3760ae6..206cd6c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ 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.7.0] — 2026-04-20 + +### Added +- **Display settings section.** New "Display" tab in admin with three + settings that control how generated articles render on the frontend: + - **Article Font** — choose from 7 font families (Inter, Georgia, + Merriweather, Lora, Open Sans, Roboto, System) or keep theme default. + Google Fonts are loaded only on PRAutoBlogger posts that need them. + - **Article Font Size** — set body text size in pixels (0 = theme + default). Recommended 16–18px for comfortable reading. + - **Table Borders** — adds visible borders, cell padding, header + background, and alternating row colors to tables. Enabled by default. +- **Article_Typography frontend class.** Injects inline CSS only on + singular post pages with `_prautoblogger_generated` meta — zero + impact on non-PRAutoBlogger pages. + ## [0.6.3] — 2026-04-20 ### Fixed diff --git a/includes/admin/class-image-model-registry.php b/includes/admin/class-image-model-registry.php new file mode 100644 index 0000000..92401e2 --- /dev/null +++ b/includes/admin/class-image-model-registry.php @@ -0,0 +1,83 @@ +> + */ + public static function get_models(): array { + return [ + [ + 'id' => 'google/gemini-2.5-flash-image', + 'name' => 'Gemini 2.5 Flash Image (Nano Banana)', + 'provider' => 'openrouter', + 'cost_per_image' => 0.005, + 'capabilities' => [ 'image_generation' ], + 'description' => __( 'Good quality, low cost. Recommended.', 'prautoblogger' ), + ], + [ + 'id' => 'google/gemini-3.1-flash-image-preview', + 'name' => 'Gemini 3.1 Flash Image (Nano Banana 2)', + 'provider' => 'openrouter', + 'cost_per_image' => 0.008, + 'capabilities' => [ 'image_generation' ], + 'description' => __( 'Latest Google. Better quality.', 'prautoblogger' ), + ], + [ + 'id' => 'google/gemini-3-pro-image-preview', + 'name' => 'Gemini 3 Pro Image (Nano Banana Pro)', + 'provider' => 'openrouter', + 'cost_per_image' => 0.03, + 'capabilities' => [ 'image_generation' ], + 'description' => __( 'Highest quality Google. Premium.', 'prautoblogger' ), + ], + [ + 'id' => 'openai/gpt-5-image-mini', + 'name' => 'GPT-5 Image Mini', + 'provider' => 'openrouter', + 'cost_per_image' => 0.02, + 'capabilities' => [ 'image_generation' ], + 'description' => __( 'OpenAI budget image model.', 'prautoblogger' ), + ], + [ + 'id' => 'openai/gpt-5-image', + 'name' => 'GPT-5 Image', + 'provider' => 'openrouter', + 'cost_per_image' => 0.08, + 'capabilities' => [ 'image_generation' ], + 'description' => __( 'OpenAI premium. Photorealistic.', 'prautoblogger' ), + ], + [ + 'id' => 'flux-1-schnell', + 'name' => 'FLUX.1 schnell (Cloudflare)', + 'provider' => 'cloudflare', + 'cost_per_image' => 0.0007, + 'capabilities' => [ 'image_generation' ], + 'description' => __( 'Cheapest. Low quality, 4-step.', 'prautoblogger' ), + ], + ]; + } +} diff --git a/includes/admin/class-settings-fields-extended.php b/includes/admin/class-settings-fields-extended.php index 26fdeeb..e4fce83 100644 --- a/includes/admin/class-settings-fields-extended.php +++ b/includes/admin/class-settings-fields-extended.php @@ -2,75 +2,29 @@ declare(strict_types=1); /** - * Extended settings fields: schedule, publishing, analytics, and images. + * Extended settings fields: schedule, publishing, analytics, display, and images. * * What: Declarative field definitions for operational settings sections. * Who calls it: PRAutoBlogger_Settings_Fields::get_fields() merges these in. - * Dependencies: PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL, PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX constants. + * Dependencies: PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL, PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX constants, + * PRAutoBlogger_Article_Typography (font choices), PRAutoBlogger_Image_Model_Registry. * - * @see admin/class-settings-fields.php — Core fields and sections; calls get_extended_fields(). - * @see admin/class-admin-page.php — Renders fields registered from these definitions. - * @see CONVENTIONS.md — "How To: Add a New Admin Setting". + * @see admin/class-settings-fields.php — Core fields and sections; calls get_extended_fields(). + * @see admin/class-admin-page.php — Renders fields registered from these definitions. + * @see admin/class-image-model-registry.php — Static image model list extracted for 300-line compliance. + * @see frontend/class-article-typography.php — Reads Display settings on the frontend. + * @see CONVENTIONS.md — "How To: Add a New Admin Setting". */ class PRAutoBlogger_Settings_Fields_Extended { /** * Static image model list for the admin model picker. - * No registry API for these — they're hardcoded here and updated manually. + * Delegates to Image_Model_Registry (extracted for 300-line file limit). * * @return array> */ public static function get_image_models(): array { - return [ - [ - 'id' => 'google/gemini-2.5-flash-image', - 'name' => 'Gemini 2.5 Flash Image (Nano Banana)', - 'provider' => 'openrouter', - 'cost_per_image' => 0.005, - 'capabilities' => [ 'image_generation' ], - 'description' => __( 'Good quality, low cost. Recommended.', 'prautoblogger' ), - ], - [ - 'id' => 'google/gemini-3.1-flash-image-preview', - 'name' => 'Gemini 3.1 Flash Image (Nano Banana 2)', - 'provider' => 'openrouter', - 'cost_per_image' => 0.008, - 'capabilities' => [ 'image_generation' ], - 'description' => __( 'Latest Google. Better quality.', 'prautoblogger' ), - ], - [ - 'id' => 'google/gemini-3-pro-image-preview', - 'name' => 'Gemini 3 Pro Image (Nano Banana Pro)', - 'provider' => 'openrouter', - 'cost_per_image' => 0.03, - 'capabilities' => [ 'image_generation' ], - 'description' => __( 'Highest quality Google. Premium.', 'prautoblogger' ), - ], - [ - 'id' => 'openai/gpt-5-image-mini', - 'name' => 'GPT-5 Image Mini', - 'provider' => 'openrouter', - 'cost_per_image' => 0.02, - 'capabilities' => [ 'image_generation' ], - 'description' => __( 'OpenAI budget image model.', 'prautoblogger' ), - ], - [ - 'id' => 'openai/gpt-5-image', - 'name' => 'GPT-5 Image', - 'provider' => 'openrouter', - 'cost_per_image' => 0.08, - 'capabilities' => [ 'image_generation' ], - 'description' => __( 'OpenAI premium. Photorealistic.', 'prautoblogger' ), - ], - [ - 'id' => 'flux-1-schnell', - 'name' => 'FLUX.1 schnell (Cloudflare)', - 'provider' => 'cloudflare', - 'cost_per_image' => 0.0007, - 'capabilities' => [ 'image_generation' ], - 'description' => __( 'Cheapest. Low quality, 4-step.', 'prautoblogger' ), - ], - ]; + return PRAutoBlogger_Image_Model_Registry::get_models(); } /** @@ -210,6 +164,35 @@ class PRAutoBlogger_Settings_Fields_Extended { 'description' => __( 'Paste the full JSON key file for a service account with Analytics read access.', 'prautoblogger' ), ], + // ── Display ──────────────────────────────────────────────── + [ + 'id' => 'prautoblogger_article_font_family', + 'label' => __( 'Article Font', 'prautoblogger' ), + 'type' => 'select', + 'section' => 'prautoblogger_display', + 'default' => 'default', + 'options' => PRAutoBlogger_Article_Typography::get_font_choices(), + 'description' => __( 'Font family used for generated article body text. Serif fonts improve long-form readability.', 'prautoblogger' ), + ], + [ + 'id' => 'prautoblogger_article_font_size', + 'label' => __( 'Article Font Size (px)', 'prautoblogger' ), + 'type' => 'number', + 'section' => 'prautoblogger_display', + 'default' => 0, + 'min' => 0, + 'max' => 32, + 'description' => __( 'Base font size for article body text. 0 uses the theme default (13px). Recommended: 16–18px for comfortable reading.', 'prautoblogger' ), + ], + [ + 'id' => 'prautoblogger_table_borders', + 'label' => __( 'Table Borders', 'prautoblogger' ), + 'type' => 'toggle', + 'section' => 'prautoblogger_display', + 'default' => '1', + 'description' => __( 'Add visible borders, padding, and alternating row colors to tables in generated articles.', 'prautoblogger' ), + ], + // ── Images ───────────────────────────────────────────────── [ 'id' => 'prautoblogger_image_enabled', diff --git a/includes/admin/class-settings-fields.php b/includes/admin/class-settings-fields.php index 96a3fb4..9f6ff3b 100755 --- a/includes/admin/class-settings-fields.php +++ b/includes/admin/class-settings-fields.php @@ -60,6 +60,11 @@ class PRAutoBlogger_Settings_Fields { 'icon' => 'dashicons-chart-area', 'description' => __( 'Connect Google Analytics 4 for post performance scoring.', 'prautoblogger' ), ], + 'prautoblogger_display' => [ + 'title' => __( 'Display', 'prautoblogger' ), + 'icon' => 'dashicons-editor-textcolor', + 'description' => __( 'Control how generated articles look on the frontend — fonts, sizes, and table styling.', 'prautoblogger' ), + ], 'prautoblogger_images' => [ 'title' => __( 'Images', 'prautoblogger' ), 'icon' => 'dashicons-format-image', diff --git a/includes/class-activator.php b/includes/class-activator.php index 317f08a..cd5103e 100755 --- a/includes/class-activator.php +++ b/includes/class-activator.php @@ -169,6 +169,9 @@ class PRAutoBlogger_Activator { 'prautoblogger_max_word_count' => 2000, 'prautoblogger_topic_exclusions' => '[]', 'prautoblogger_enabled_sources' => '["reddit"]', + 'prautoblogger_article_font_family' => 'default', + 'prautoblogger_article_font_size' => 0, + 'prautoblogger_table_borders' => '1', 'prautoblogger_schedule_time' => '03:00', 'prautoblogger_log_level' => 'info', ]; diff --git a/includes/class-prautoblogger.php b/includes/class-prautoblogger.php index d70b42b..50774f0 100644 --- a/includes/class-prautoblogger.php +++ b/includes/class-prautoblogger.php @@ -83,11 +83,15 @@ class PRAutoBlogger { add_filter( 'site_transient_update_plugins', [ $this, 'filter_block_false_updates' ] ); } - /** Register frontend hooks (shortcode + REST for posts widget). */ + /** Register frontend hooks (shortcode, REST, typography). */ private function register_frontend_hooks(): void { $posts_widget = new PRAutoBlogger_Posts_Widget(); add_action( 'init', [ $posts_widget, 'on_register_shortcode' ] ); add_action( 'rest_api_init', [ $posts_widget, 'on_register_rest_route' ] ); + + $typography = new PRAutoBlogger_Article_Typography(); + add_action( 'wp_head', [ $typography, 'on_wp_head' ] ); + add_action( 'wp_enqueue_scripts', [ $typography, 'on_enqueue_fonts' ] ); } /** Register cron-triggered hooks for scheduled generation and metrics. */ diff --git a/includes/frontend/class-article-typography.php b/includes/frontend/class-article-typography.php new file mode 100644 index 0000000..b01fd20 --- /dev/null +++ b/includes/frontend/class-article-typography.php @@ -0,0 +1,196 @@ + for PRAutoBlogger posts. + * + * Only outputs on singular post pages generated by PRAutoBlogger. + * The CSS targets .post-content (PR Theme's article body class) + * and overrides the theme's CSS custom properties so the cascade + * works naturally with headings and sub-elements. + * + * Side effects: echoes a \n", + $css // Already escaped — contains only sanitized option values. + ); + } + + /** + * Build the CSS string from current settings. + * + * Reads each setting at call time (never cached) so changes + * take effect immediately without cache invalidation. + * + * @return string CSS rules, or empty string if all defaults. + */ + private function build_css(): string { + $rules = []; + + $font_family = $this->get_font_family_stack(); + if ( '' !== $font_family ) { + $rules[] = sprintf( + ".post-content,\n.post-content p,\n.post-content li {\n\tfont-family: %s;\n}", + $font_family + ); + } + + $font_size = absint( get_option( self::OPT_FONT_SIZE, 0 ) ); + if ( $font_size > 0 ) { + $rules[] = sprintf( + ".post-content,\n.post-content p,\n.post-content li {\n\tfont-size: %dpx;\n\tline-height: 1.7;\n}", + $font_size + ); + } + + $table_borders = get_option( self::OPT_TABLE_BORDERS, '1' ); + if ( '1' === $table_borders ) { + $rules[] = implode( "\n", [ + '.post-content table {', + "\tborder-collapse: collapse;", + "\twidth: 100%;", + "\tmargin: 1.5em 0;", + '}', + '.post-content th,', + '.post-content td {', + "\tborder: 1px solid #d1d5db;", + "\tpadding: 0.6em 1em;", + "\ttext-align: left;", + '}', + '.post-content thead th {', + "\tbackground: #f3f4f6;", + "\tfont-weight: 600;", + '}', + '.post-content tbody tr:nth-child(even) {', + "\tbackground: #f9fafb;", + '}', + ] ); + } + + return implode( "\n\n", $rules ); + } + + /** + * Map the stored font-family key to a full CSS font stack. + * + * Returns empty string for 'default' (let the theme decide). + * + * @return string CSS font-family value, or empty for theme default. + */ + private function get_font_family_stack(): string { + $key = sanitize_text_field( get_option( self::OPT_FONT_FAMILY, 'default' ) ); + + $stacks = [ + 'default' => '', + 'inter' => '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + 'georgia' => '"Georgia", "Times New Roman", Times, serif', + 'merriweather' => '"Merriweather", Georgia, "Times New Roman", serif', + 'lora' => '"Lora", Georgia, "Times New Roman", serif', + 'open_sans' => '"Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + 'roboto' => '"Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + 'system' => '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif', + ]; + + return $stacks[ $key ] ?? ''; + } + + /** + * Get available font choices for the settings UI. + * + * @return array Map of key => display label. + */ + public static function get_font_choices(): array { + return [ + 'default' => __( 'Theme Default (Inter)', 'prautoblogger' ), + 'inter' => __( 'Inter (Sans-serif)', 'prautoblogger' ), + 'georgia' => __( 'Georgia (Serif)', 'prautoblogger' ), + 'merriweather' => __( 'Merriweather (Serif)', 'prautoblogger' ), + 'lora' => __( 'Lora (Serif)', 'prautoblogger' ), + 'open_sans' => __( 'Open Sans (Sans-serif)', 'prautoblogger' ), + 'roboto' => __( 'Roboto (Sans-serif)', 'prautoblogger' ), + 'system' => __( 'System Default', 'prautoblogger' ), + ]; + } + + /** + * Enqueue Google Fonts stylesheet when a web font is selected. + * + * Only loads on singular PRAutoBlogger posts that use a Google-hosted font. + * + * Side effects: enqueues a remote stylesheet. + * + * @return void + */ + public function on_enqueue_fonts(): void { + if ( ! is_singular( 'post' ) ) { + return; + } + + $post_id = get_queried_object_id(); + if ( '1' !== get_post_meta( $post_id, '_prautoblogger_generated', true ) ) { + return; + } + + $font = sanitize_text_field( get_option( self::OPT_FONT_FAMILY, 'default' ) ); + + // Map font keys to Google Fonts URLs. Inter is already loaded by the theme. + $google_fonts = [ + 'merriweather' => 'https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&display=swap', + 'lora' => 'https://fonts.googleapis.com/css2?family=Lora:wght@400;700&display=swap', + 'open_sans' => 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap', + 'roboto' => 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap', + ]; + + if ( isset( $google_fonts[ $font ] ) ) { + wp_enqueue_style( + 'prautoblogger-google-font', + $google_fonts[ $font ], + [], + null // No version for external CDN URLs. + ); + } + } +} diff --git a/prautoblogger.php b/prautoblogger.php index a2bef4b..d13a9c0 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.3 + * Version: 0.7.0 * 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.3' ); +define( 'PRAUTOBLOGGER_VERSION', '0.7.0' ); define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' ); define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ ); define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );