Add Display settings: article font, font size, table borders

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 <noreply@anthropic.com>
This commit is contained in:
peptiderepo
2026-04-20 12:48:57 +07:00
parent 86ceea0da1
commit a37fb843a3
9 changed files with 355 additions and 60 deletions

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/**
* Static registry of available image generation models.
*
* What: Returns a hardcoded list of image models for the admin model picker.
* No API discovery — updated manually when providers add/remove models.
* Who calls it: PRAutoBlogger_Admin_Page (model picker UI) and
* PRAutoBlogger_Image_Pipeline (model validation).
* Dependencies: None.
*
* @see admin/class-settings-fields-extended.php — Image settings reference this registry.
* @see core/class-image-pipeline.php — Validates selected model against this list.
*/
class PRAutoBlogger_Image_Model_Registry {
/**
* Get all available image generation models.
*
* Each entry contains:
* - id: Model identifier used in API calls.
* - name: Human-readable display name.
* - provider: 'openrouter' or 'cloudflare'.
* - cost_per_image: Estimated USD cost per generation.
* - capabilities: Array of capability tags.
* - description: Short description for the admin UI.
*
* @return array<int, array<string, mixed>>
*/
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' ),
],
];
}
}

View File

@@ -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<int, array<string, mixed>>
*/
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',

View File

@@ -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',

View File

@@ -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',
];

View File

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

View File

@@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
/**
* Injects frontend typography and table styles for PRAutoBlogger-generated posts.
*
* What: Outputs inline CSS on single post pages that have the
* _prautoblogger_generated meta flag, applying user-configured
* font family, font size, and table border styles.
* Who calls it: PRAutoBlogger::register_frontend_hooks() hooks this
* into wp_head.
* Dependencies: Reads wp_options for typography settings.
*
* @see class-prautoblogger.php — Registers the wp_head hook.
* @see class-settings-fields.php — Defines the Display settings section.
* @see admin/class-admin-page.php — Renders the settings form.
*/
class PRAutoBlogger_Article_Typography {
/** Option key for article font family. */
private const OPT_FONT_FAMILY = 'prautoblogger_article_font_family';
/** Option key for article body font size in pixels. */
private const OPT_FONT_SIZE = 'prautoblogger_article_font_size';
/** Option key for table border toggle. */
private const OPT_TABLE_BORDERS = 'prautoblogger_table_borders';
/**
* Output inline CSS in <head> 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 <style> block.
*
* @return void
*/
public function on_wp_head(): void {
if ( ! is_singular( 'post' ) ) {
return;
}
$post_id = get_queried_object_id();
if ( '1' !== get_post_meta( $post_id, '_prautoblogger_generated', true ) ) {
return;
}
$css = $this->build_css();
if ( '' === $css ) {
return;
}
printf(
"\n<style id=\"prautoblogger-typography\">\n%s\n</style>\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<string, string> 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.
);
}
}
}