Plugin Directory

Changeset 3367502


Ignore:
Timestamp:
09/25/2025 03:42:37 AM (5 months ago)
Author:
RoxxiStudios
Message:

Preparing for version 1.1.1 release.

Location:
weatherbot/trunk
Files:
8 edited

Legend:

Unmodified
Added
Removed
  • weatherbot/trunk/assets/css/weather-bot.css

    r3367429 r3367502  
    1 /* WeatherBot by RoxxiStudios — Public Styles (optimized v3.3.1 + ADA enhancements)
     1/* WeatherBot by RoxxiStudios — Public Styles (optimized v3.3.1 + SEO/ADA enhancements)
    22   - Component-scoped, class-only selectors (no element qualifiers)
    33   - Predictable typography + color baseline (prevents footer/theme bleed)
     
    3939   ========================= */
    4040
     41
    4142/* Inherit typography safely for children */
    4243
    43 .roxxi-weather,
    44 .roxxi-weather[data-widget="weatherBot"] {
     44aside.roxxi-weather,
     45aside.roxxi-weather[data-widget="weatherBot"] {
    4546    display: flex;
    4647    gap: 5px;
     
    5657}
    5758
     59aside.roxxi-weather[role="region"].wb-type-badge p,
     60aside.roxxi-weather[role="region"].wb-type-compact p,
     61aside.roxxi-weather[role="region"].wb-type-inline p {
     62    margin: 0;
     63}
     64
    5865
    5966/* =========================
     
    8996}
    9097
    91 .roxxi-weather.wb-type-badge .badge {
     98
     99/* UPDATED: Corrected .badge to .wb-badge to match renderer */
     100
     101.roxxi-weather.wb-type-badge .wb-badge {
    92102    display: flex;
    93103    gap: 8px;
     
    176186}
    177187
    178 .roxxi-weather.wb-right .wb-badge.conditions {
     188
     189/* UPDATED: Corrected selector for proper targeting */
     190
     191.roxxi-weather.wb-right .wb-badge .conditions {
    179192    display: inline-flex;
    180193    align-items: center;
     
    203216}
    204217
    205 .roxxi-weather .wb-temp,
    206 .roxxi-weather .wb-unit {
    207     opacity: 0.9;
    208 }
    209 
    210218.roxxi-weather .wb-desc {
    211219    color: var(--wb-neutral);
     
    215223.roxxi-weather .wb-temp,
    216224.roxxi-weather .wb-unit {
     225    color: var(--wb-dark);
     226    margin: 0;
     227    font-size: inherit;
    217228    font-weight: 600;
     229}
     230
     231.roxxi-weather.wb-text-light .wb-pre,
     232.roxxi-weather.wb-text-light .wb-temp,
     233.roxxi-weather.wb-text-light .wb-unit {
     234    color: white;
    218235}
    219236
     
    228245    font-size: 12px;
    229246    font-weight: 400;
     247    /* NEW: Reset default browser margin for the new <p> tag */
     248    margin: 0;
    230249}
    231250
     
    277296.roxxi-weather.wb-text-light {
    278297    color: #ffffff;
     298}
     299
     300.roxxi-weather .wb-powered {
     301    padding-bottom: 2px;
    279302}
    280303
     
    369392}
    370393
     394
    371395/* Respect user motion preferences */
     396
    372397@media (prefers-reduced-motion: reduce) {
    373398    .roxxi-weather .conditions.wb-loading .wb-loading-msg::after {
     
    389414.roxxi-weather [role="button"]:focus-visible {
    390415    outline: 3px solid currentColor;
    391     /* ensures ≥3:1 contrast vs background */
     416    /* ensures >=3:1 contrast vs background */
    392417    outline-offset: 2px;
    393418    border-radius: 3px;
     
    431456
    432457@media (max-width: 480px) {
    433     .roxxi-weather.wb-type-badge .badge {
     458    /* UPDATED: Corrected .badge to .wb-badge to match renderer */
     459    .roxxi-weather.wb-type-badge .wb-badge {
    434460        display: flex;
    435461        flex-direction: column;
     
    466492    align-items: inherit;
    467493    flex-wrap: wrap;
    468     gap: 5px;
     494    gap: 3px;
    469495}
    470496
     
    484510
    485511/* --- Hard override for dark-text variant in aggressive footers --- */
     512
     513
    486514/* Force container + all descendants to inherit dark text, except links */
     515
    487516footer .roxxi-weather.roxxi-weather.wb-text-dark,
    488517footer .roxxi-weather.roxxi-weather.wb-text-dark *:not(a) {
    489   color: var(--wb-dark) !important;
    490 }
     518    color: var(--wb-dark) !important;
     519}
     520
    491521
    492522/* Links: restore brand color explicitly */
     523
    493524footer .roxxi-weather.roxxi-weather.wb-text-dark a {
    494   color: var(--wb-primary) !important;
    495 }
     525    color: var(--wb-primary) !important;
     526}
     527
    496528
    497529/* If links wrap icons/spans, keep them inheriting from the anchor */
     530
    498531footer .roxxi-weather.roxxi-weather.wb-text-dark a * {
    499   color: inherit !important;
    500 }
     532    color: inherit !important;
     533}
  • weatherbot/trunk/blocks/weatherbot/block.json

    r3363509 r3367502  
    4545      "type": "string",
    4646      "default": ""
     47    },
     48    "headingTag": {
     49      "type": "string",
     50      "default": "h3"
    4751    }
    4852  },
     
    5559      "fontColor": "light",
    5660      "type": "badge",
    57       "unit": "IMPERIAL"
     61      "unit": "IMPERIAL",
     62      "headingTag": "h3"
    5863    }
    5964  },
  • weatherbot/trunk/blocks/weatherbot/index.js

    r3363509 r3367502  
    4343          }),
    4444          el(SelectControl, {
    45             label: 'Temperature Unit',
    46             value: attributes.unit || '',
    47             onChange: (v) => setAttributes({ unit: v }),
    48             options: [
    49               { label: 'Inherit (plugin default)', value: '' },
    50               { label: 'Imperial (°F)', value: 'IMPERIAL' },
    51               { label: 'Metric (°C)', value: 'METRIC' }
    52             ]
    53           }),
    54           el(SelectControl, {
    5545            label: 'Display Type',
    5646            value: attributes.type || '',
     
    6151              { label: 'Compact', value: 'compact' },
    6252              { label: 'Inline', value: 'inline' }
     53            ]
     54          }),
     55          el(SelectControl, {
     56            label: 'Temperature Unit',
     57            value: attributes.unit || '',
     58            onChange: (v) => setAttributes({ unit: v }),
     59            options: [
     60              { label: 'Inherit (plugin default)', value: '' },
     61              { label: 'Imperial (°F)', value: 'IMPERIAL' },
     62              { label: 'Metric (°C)', value: 'METRIC' }
    6363            ]
    6464          }),
     
    7474          }),
    7575          el(SelectControl, {
    76             label: 'Font Theme',
     76            label: 'Theme Color',
    7777            value: attributes.fontColor || '',
    7878            onChange: (v) => setAttributes({ fontColor: v }),
    7979            options: [
    8080              { label: 'Inherit (plugin default)', value: '' },
    81               { label: 'Light', value: 'light' },
    82               { label: 'Dark', value: 'dark' }
     81              { label: 'Light', value: 'dark' },
     82              { label: 'Dark', value: 'light' }
    8383            ]
    8484          })
     
    141141              }),
    142142              el(SelectControl, {
    143                 label: 'Theme',
     143                label: 'Theme Color',
    144144                value: attributes.fontColor || '',
    145145                onChange: (v) => setAttributes({ fontColor: v }),
    146146                options: [
    147147                  { label: 'Inherit (plugin default)', value: '' },
    148                   { label: 'Light', value: 'light' },
    149                   { label: 'Dark', value: 'dark' }
     148                  { label: 'Light', value: 'dark' },
     149                  { label: 'Dark', value: 'light' }
    150150                ]
    151151              }),
  • weatherbot/trunk/readme.txt

    r3367429 r3367502  
    55Tested up to: 6.8
    66Requires PHP: 7.4
    7 Stable tag: 1.1.0
     7Stable tag: 1.1.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    1515
    1616[WeatherBot](http://roxxistudios.com/weatherbot) displays **live weather** for any location in the world using **Google’s Weather API**, with city/place resolution via **Google Maps Geocoding/Places**. Use with a **Classic Widget**, a **Gutenberg block** (with live preview), or with a **shortcode** anywhere shortcodes are supported. Outputs a clean, accessible UI that works in any theme or builder.
    17 
    18 ***
    19 
    20 == Installation ==
    21 
    22 = From your WordPress dashboard =
    23 
    24 1.  Go to **Plugins → Add New**.
    25 2.  Search for **WeatherBot**.
    26 3.  Click **Install Now**, then **Activate**.
    27 4.  Go to **Settings → WeatherBot**, add your **Google Maps API key** (enable **Weather** and **Geocoding/Places** APIs), and save.
    28 5.  Add the **WeatherBot Block**, **Widget**, or **Shortcode** to your site and configure the options.
    29 
    30 = Manual installation =
    31 
    32 1.  Upload the `weatherbot` folder to `/wp-content/plugins/`.
    33 2.  Activate the plugin in **Plugins**.
    34 3.  Go to **Settings → WeatherBot**, add your **Google Maps API key** (enable **Weather** and **Geocoding/Places** APIs), and save.
    35 4.  Add the **WeatherBot Block**, **Widget**, or **Shortcode** to your site and configure the options.
    36 
    37 ***
    38 
    39 == Frequently Asked Questions ==
    40 
    41 = 1. Does this show live location weather using Google Weather? =
    42 Yes. WeatherBot retrieves **current weather** from **Google’s Weather API** and resolves your `city` input with **Google Maps Geocoding/Places**.
    43 
    44 = 2. Where do I get a Google API key and what do I enable? =
    45 Create a key in the [**Google Maps/Weather API Platform**](https://developers.google.com/maps/documentation/weather/get-api-key) and enable **Weather** and **Geocoding/Places** APIs. Restrict your key per Google’s guidance.
    46 
    47 = 3. Can I use the shortcode in Widgets, Headers/Footers, or builder modules? =
    48 Yes—anywhere WordPress **parses shortcodes**: posts, pages, widgets, the Core/HTML block, and most page builders. In a PHP template you can use:
    49 `<?php echo do_shortcode('[weatherbot city="Lake Arrowhead, CA" type="badge"]'); ?>`
    50 
    51 = 4. How often is the weather updated? Can I clear the cache? =
    52 Weather results are cached briefly (about **2 minutes** per location/unit) to keep results fresh while saving API calls. Geocoding results can be cached long-term. You can **manually purge** caches from **Settings → WeatherBot**.
    53 
    54 = 5. Does the widget support dark backgrounds? =
    55 Yes. Choose **Theme → Light/Dark** (or `font_color` shortcode attribute) to ensure readable contrast.
    56 
    57 = 6. Does this plugin track visitors or send data to third-party servers? =
    58 No. API calls go directly from **your site** to **Google** using **your** key. The plugin does not phone home.
    59 
    60 = 7. Is jQuery Required? =
    61 **No jQuery on the front end.** WeatherBot runs on vanilla JS and WordPress packages. (The WP admin may load standard WordPress scripts.)
    62 
    63 = 8. Can I change the font styles of the weather display? =
    64 Yes. While WeatherBot inherits your theme’s fonts, you can easily override them with custom CSS. For example, to make the temperature bold and red, add this to your theme's Additional CSS panel:
    65 
    66     .roxxi-weather span.wb-temp {
    67         font-weight: 700;
    68         color: #cc0000;
    69     }
    70 
    71 See the Styling & Customization section for more options.
    7217
    7318***
     
    8631== Features ==
    8732
     33* **Automatic WeatherForecast Schema.org (JSON-LD) structured data** generation for SEO and Google rich results.
    8834* **Live local current weather** via **Google Weather API**
    8935* **Google Maps Geocoding/Places** for accurate city/place lookups
    90 * **WordPress weather block** with **live server preview** and full controls (Title, Show/Hide Title, Position, Theme, Unit, Type, City)
    91 * **WordPress weather shortcode** that **works anywhere shortcodes are supported** — posts, pages, widgets, Core/HTML block, most builders, and PHP templates via `echo do_shortcode()`
    92 * **Three display layouts** — `badge` (card), `compact` (mini), `inline` (text-flow)
    93 * **Optional title with on/off toggle** via `show_pre_text` (shortcode & block)
    94 * **Contrast themes** — **Light** / **Dark** for different backgrounds
    95 * **Units** — **Fahrenheit (°F)** or **Celsius (°C)**; can inherit from Settings
    96 * **Classic Widget** support (with **Align** option)
    97 * **Accessibility-minded markup** and focus styles
    98 * **Builder-friendly output** — guards against unwanted `<p>` wrappers and extra spacing
     36* **WordPress weather block** with **live server preview** and full controls
     37* **WordPress weather shortcode** that **works anywhere shortcodes are supported**
     38* **Control the title's HTML tag** (H2-H6, P) for SEO hierarchy
     39* **Three display layouts** — badge (card), compact (mini), inline (text-flow)
     40* **Optional title with on/off toggle** via show_pre_text (shortcode & block)
     41* **Intuitive Theme Selection** (Light Theme / Dark Theme) for readability on any background
     42* **Units** — Fahrenheit (°F) or Celsius (°C); can inherit from Settings
     43* **Classic Widget** support (with **Align** and **Heading Tag** options)
     44* **Semantic and Accessible HTML5 Markup** (aside, headings) for improved SEO and screen reader support.
     45* **Builder-friendly output** — guards against unwanted p wrappers and extra spacing
    9946* **Caching tuned for freshness** — weather (~2 minutes per location/unit); geocoding (long-lived)
    10047* **Manual cache purge** from Settings
    10148* **Optional uninstall cleanup** — delete plugin data on uninstall if enabled
    102 * **CSS variables** for easy theming (`--wb-primary`, `--wb-secondary`, `--wb-neutral`, etc.)
     49* **CSS variables** for easy theming (--wb-primary, --wb-secondary, --wb-neutral, etc.)
    10350* **No jQuery on the front end** — built with modern WordPress packages and vanilla JS
    10451* **COMING SOON:** Our **Pro version** with **weather forecast** and other powerful features like **detailed style controls**.
     
    249196***
    250197
     198== Installation ==
     199
     200= From your WordPress dashboard =
     201
     2021.  Go to **Plugins → Add New**.
     2032.  Search for **WeatherBot**.
     2043.  Click **Install Now**, then **Activate**.
     2054.  Go to **Settings → WeatherBot**, add your **Google Maps API key** (enable **Weather** and **Geocoding/Places** APIs), and save.
     2065.  Add the **WeatherBot Block**, **Widget**, or **Shortcode** to your site and configure the options.
     207
     208= Manual installation =
     209
     2101.  Upload the weatherbot folder to /wp-content/plugins/.
     2112.  Activate the plugin in **Plugins**.
     2123.  Go to **Settings → WeatherBot**, add your **Google Maps API key** (enable **Weather** and **Geocoding/Places** APIs), and save.
     2134.  Add the **WeatherBot Block**, **Widget**, or **Shortcode** to your site and configure the options.
     214
     215***
     216
     217== Frequently Asked Questions ==
     218
     219= 1. Does this show live location weather using Google Weather? =
     220Yes. WeatherBot retrieves **current weather** from **Google’s Weather API** and resolves your `city` input with **Google Maps Geocoding/Places**.
     221
     222= 2. Where do I get a Google API key and what do I enable? =
     223Create a key in the [**Google Maps/Weather API Platform**](https://developers.google.com/maps/documentation/weather/get-api-key) and enable **Weather** and **Geocoding/Places** APIs. Restrict your key per Google’s guidance.
     224
     225= 3. Can I use the shortcode in Widgets, Headers/Footers, or builder modules? =
     226Yes—anywhere WordPress **parses shortcodes**: posts, pages, widgets, the Core/HTML block, and most page builders. In a PHP template you can use:
     227`<?php echo do_shortcode('[weatherbot city="Lake Arrowhead, CA" type="badge"]'); ?>`
     228
     229= 4. How often is the weather updated? Can I clear the cache? =
     230Weather results are cached briefly (about **2 minutes** per location/unit) to keep results fresh while saving API calls. Geocoding results can be cached long-term. You can **manually purge** caches from **Settings → WeatherBot**.
     231
     232= 5. Does the widget support dark backgrounds? =
     233Yes. Choose **Theme → Light/Dark** (or `font_color` shortcode attribute) to ensure readable contrast.
     234
     235= 6. Does this plugin track visitors or send data to third-party servers? =
     236No. API calls go directly from **your site** to **Google** using **your** key. The plugin does not phone home.
     237
     238= 7. Is jQuery Required? =
     239**No jQuery on the front end.** WeatherBot runs on vanilla JS and WordPress packages. (The WP admin may load standard WordPress scripts.)
     240
     241= 8. Can I change the font styles of the weather display? =
     242Yes. While WeatherBot inherits your theme’s fonts, you can easily override them with custom CSS. For example, to make the temperature bold and red, add this to your theme's Additional CSS panel:
     243
     244    .roxxi-weather span.wb-temp {
     245        font-weight: 700;
     246        color: #cc0000;
     247    }
     248
     249See the Styling & Customization section for more options.
     250
     251***
     252
    251253== Advanced Information ==
    252254
     
    393395
    394396== Changelog ==
     397
     398= 1.1.1 =
     399* **New Feature (SEO):** Automatically generates valid `WeatherForecast` schema.org (JSON-LD) structured data for every unique location, making pages eligible for Google's weather rich results.
     400* **New Feature:** Added `heading_tag` attribute to the shortcode, block, and widget to allow control over the title's HTML tag (H2-H6) for better SEO hierarchy.
     401* **Enhancement:** The entire front-end HTML output has been refactored to use semantic tags (`<aside>`, `<h3>`, etc.) for improved accessibility and SEO.
     402* **Enhancement:** Updated the widget's "Theme Color" setting to be more intuitive (e.g., selecting "Dark Theme" correctly applies a light font color).
     403* **Tweak:** Renamed "Inherit" to "Default" in widget dropdowns for clarity.
    395404
    396405= 1.1.0 =
  • weatherbot/trunk/src/Frontend/Renderer.php

    r3367429 r3367502  
    1212
    1313/**
    14  * Frontend\Renderer (v3.1.5)
    15  * - Outputs inline-safe markup for WeatherBot variants.
     14 * Frontend\Renderer (v3.1.5 - SEO Enhanced)
     15 * - Outputs semantic, SEO-friendly markup for WeatherBot variants.
    1616 * - Honors credit visibility via Settings (show_credit) and per-instance `show_credit="1"`.
    1717 * - Does NOT emit page-level no-cache headers (live freshness handled via REST).
     
    2020class Renderer {
    2121
    22     private Plugin $plugin;
    23 
    24     public function __construct( Plugin $plugin ) {
    25         $this->plugin = $plugin;
    26     }
    27 
    28     /** Decide whether to show the credit line for this render. */
    29     private function should_show_credit( array $atts ): bool {
    30         // Per-shortcode override wins: [weather_bot ... show_credit="1" | "0"]
    31         if ( isset( $atts['show_credit'] ) ) {
    32             return (string) $atts['show_credit'] === '1';
    33         }
    34         // Global default from Settings (policy default '0' = hidden)
    35         if ( method_exists( $this->plugin, 'options' ) ) {
    36             $opts   = $this->plugin->options();
    37             $global = isset( $opts['show_credit'] ) ? (string) $opts['show_credit'] : '0';
    38             return $global === '1';
    39         }
    40         // Fallback if nothing provided: hidden
    41         return false;
    42     }
    43 
    44     /** Convert API unit label to symbol */
    45     public function unit_symbol( $unit_label ): string {
    46         $u = strtoupper( (string) $unit_label );
    47         if ( $u === 'FAHRENHEIT' ) { return '°F'; }
    48         if ( $u === 'CELSIUS' )    { return '°C'; }
    49         return '';
    50     }
    51 
    52     /** Convert API unit label to code ('f' or 'c') for data-attribute */
    53     private function unit_code( $unit_label ): string {
    54         $u = strtoupper( (string) $unit_label );
    55         if ( $u === 'FAHRENHEIT' ) { return 'f'; }
    56         if ( $u === 'CELSIUS' )    { return 'c'; }
    57         return 'f'; // sensible default
    58     }
    59 
    60     /** Safe array reader with dot notation */
    61     public function read( $array, string $path, $default = null ) {
    62         if ( ! is_array( $array ) ) { return $default; }
    63         $seg = explode( '.', $path );
    64         $ref = $array;
    65         foreach ( $seg as $s ) {
    66             if ( is_array( $ref ) && array_key_exists( $s, $ref ) ) {
    67                 $ref = $ref[ $s ];
    68             } else {
    69                 return $default;
    70             }
    71         }
    72         return $ref;
    73     }
    74 
    75     /**
    76      * Render the BADGE type (inline-safe markup for taxonomy/content use).
    77      * Does not send no-cache headers; client JS hydrates via REST.
    78      */
    79     public function render_badge( ?array $data, array $atts, ?float $lat = null, ?float $lon = null ): string {
    80         $data = is_array( $data ) ? $data : [];
    81 
    82         $temp       = $this->read( $data, 'temperature.degrees' );
    83         $unit_label = $this->read( $data, 'temperature.unit' );
    84         $desc       = $this->read( $data, 'weatherCondition.description.text' );
    85         $icon_base  = $this->read( $data, 'weatherCondition.iconBaseUri' );
    86 
    87         $temp_text  = is_numeric( $temp ) ? (string) round( (float) $temp ) : '';
    88         $unit_text  = $this->unit_symbol( $unit_label );
    89         $unit_code  = $this->unit_code( $unit_label );
    90 
    91         $classes = [ 'roxxi-weather', 'wb-type-badge' ];
    92         $attributes = [ 'role' => 'weatherBot' ];
    93         if ( ! empty( $atts['align'] ) ) { $classes[] = 'wb-' . $atts['align']; }
    94 
    95         if ( isset( $atts['font_color'] ) ) {
    96             if ( $atts['font_color'] === 'light' ) { $classes[] = 'wb-text-light'; }
    97             if ( $atts['font_color'] === 'dark'  ) { $classes[] = 'wb-text-dark'; }
    98         }
    99         if ( isset( $atts['extra_class'] ) && $atts['extra_class'] !== '' ) {
    100             $classes[] = $atts['extra_class'];
    101         }
    102 
    103         $attrs = [ 'class' => implode( ' ', $classes ) ];
     22    private Plugin $plugin;
     23
     24    public function __construct( Plugin $plugin ) {
     25        $this->plugin = $plugin;
     26    }
     27
     28    /** Decide whether to show the credit line for this render. */
     29    private function should_show_credit( array $atts ): bool {
     30        // Per-shortcode override wins: [weather_bot ... show_credit="1" | "0"]
     31        if ( isset( $atts['show_credit'] ) ) {
     32            return (string) $atts['show_credit'] === '1';
     33        }
     34        // Global default from Settings (policy default '0' = hidden)
     35        if ( method_exists( $this->plugin, 'options' ) ) {
     36            $opts   = $this->plugin->options();
     37            $global = isset( $opts['show_credit'] ) ? (string) $opts['show_credit'] : '0';
     38            return $global === '1';
     39        }
     40        // Fallback if nothing provided: hidden
     41        return false;
     42    }
     43
     44    /** Convert API unit label to symbol */
     45    public function unit_symbol( $unit_label ): string {
     46        $u = strtoupper( (string) $unit_label );
     47        if ( $u === 'FAHRENHEIT' ) { return '°F'; }
     48        if ( $u === 'CELSIUS' )    { return '°C'; }
     49        return '';
     50    }
     51
     52    /** Convert API unit label to code ('f' or 'c') for data-attribute */
     53    private function unit_code( $unit_label ): string {
     54        $u = strtoupper( (string) $unit_label );
     55        if ( $u === 'FAHRENHEIT' ) { return 'f'; }
     56        if ( $u === 'CELSIUS' )    { return 'c'; }
     57        return 'f'; // sensible default
     58    }
     59
     60    /** Safe array reader with dot notation */
     61    public function read( $array, string $path, $default = null ) {
     62        if ( ! is_array( $array ) ) { return $default; }
     63        $seg = explode( '.', $path );
     64        $ref = $array;
     65        foreach ( $seg as $s ) {
     66            if ( is_array( $ref ) && array_key_exists( $s, $ref ) ) {
     67                $ref = $ref[ $s ];
     68            } else {
     69                return $default;
     70            }
     71        }
     72        return $ref;
     73    }
     74
     75    /**
     76     * Render the BADGE type (inline-safe markup for taxonomy/content use).
     77     * Does not send no-cache headers; client JS hydrates via REST.
     78     */
     79    public function render_badge( ?array $data, array $atts, ?float $lat = null, ?float $lon = null ): string {
     80        $data = is_array( $data ) ? $data : [];
     81
     82        $temp       = $this->read( $data, 'temperature.degrees' );
     83        $unit_label = $this->read( $data, 'temperature.unit' );
     84        $desc       = $this->read( $data, 'weatherCondition.description.text' );
     85        $icon_base  = $this->read( $data, 'weatherCondition.iconBaseUri' );
     86
     87        $temp_text = is_numeric( $temp ) ? (string) round( (float) $temp ) : '';
     88        $unit_text = $this->unit_symbol( $unit_label );
     89        $unit_code = $this->unit_code( $unit_label );
     90
     91        $classes = [ 'roxxi-weather', 'wb-type-badge' ];
     92        if ( ! empty( $atts['align'] ) ) { $classes[] = 'wb-' . $atts['align']; }
     93
     94        if ( isset( $atts['font_color'] ) ) {
     95            if ( $atts['font_color'] === 'light' ) { $classes[] = 'wb-text-light'; }
     96            if ( $atts['font_color'] === 'dark' ) { $classes[] = 'wb-text-dark'; }
     97        }
     98        if ( isset( $atts['extra_class'] ) && $atts['extra_class'] !== '' ) {
     99            $classes[] = $atts['extra_class'];
     100        }
     101
     102        $attrs = [ 'class' => implode( ' ', $classes ) ];
     103        if ( $lat !== null ) { $attrs['data-lat'] = (string) $lat; }
     104        if ( $lon !== null ) { $attrs['data-lon'] = (string) $lon; }
     105        $attrs['data-unit'] = $unit_code; // ← allow JS live fetch
     106
     107        // SEO: Add ARIA role and label for accessibility
     108        $pre_text_for_label = ! empty( $atts['pre_text'] ) ? (string) $atts['pre_text'] : __( 'Current Weather', 'weatherbot' );
     109        $attrs['role']       = 'region';
     110        $attrs['aria-label'] = esc_attr( rtrim( $pre_text_for_label, ': ' ) );
    104111       
    105         if ( $lat !== null ) { $attrs['data-lat'] = (string) $lat; }
    106        
    107         if ( $lon !== null ) { $attrs['data-lon'] = (string) $lon; }
    108        
    109         $attrs['data-unit'] = $unit_code; // ← allow JS live fetch
    110        
    111         $attrs['role'] = 'weatherBot';
    112 
    113         $attr_html = '';
    114         foreach ( $attrs as $k => $v ) {
    115             $attr_html .= ' ' . esc_attr( $k ) . '="' . esc_attr( $v ) . '"';
    116         }
    117 
    118         $html  = '<span' . $attr_html . '>';
    119 
    120         // === Start Badge wrapper ===
    121         $html .= '<span class="wb-badge">';
    122 
    123             if ( isset( $atts['show_pre_text'] ) && (string) $atts['show_pre_text'] === '1' && ! empty( $atts['pre_text'] ) ) {
    124                 $html .= '<span class="wb-pre">' . esc_html( (string) $atts['pre_text'] ) . '</span>';
    125             }
    126 
    127             $html .= '<span class="conditions">';
    128            
    129                 $html .= '<span class="temp">';
    130 
    131                     if ( $icon_base ) {
    132                         $alt = $desc ? (string) $desc : '';
    133                         $html .= '<img class="wb-ico" src="' . esc_url( $icon_base . '.png' ) . '" title="' . esc_attr( $alt ) . '" alt="' . esc_attr( $alt ) . '" width="20" height="20" />';
    134                     }
    135 
    136                     $html .= '<span class="degree">';
    137                         if ( $temp_text !== '' ) {
    138                             $html .= '<span class="wb-temp">' . esc_html( $temp_text ) . '</span>';
    139                         }
    140                         if ( $unit_text !== '' ) {
    141                             $html .= '<span class="wb-unit">' . esc_html( $unit_text ) . '</span>';
    142                         }
    143                     $html .= '</span>'; // .degree
    144                    
    145                 $html .= '</span>'; // .temp
    146 
    147                 if ( $desc ) {
    148                     $html .= '<span class="wb-sep">-</span><span class="wb-desc">' . esc_html( (string) $desc ) . '</span>';
    149                 }
    150 
    151             $html .= '</span>'; // .conditions
    152 
    153         $html .= '</span>'; // .badge
    154         // === End Badge wrapper ===
    155 
    156         if ( $this->should_show_credit( $atts ) ) {
    157             $html .= '<span class="wb-powered" role="note">'
    158                    . 'powered by <a class="wb-powered-link" href="https://roxxistudios.com/plugins/weatherbot" target="_blank" rel="noopener" title="'
    159                    . esc_attr__( 'Get the WeatherBot plugin for your site.', 'weatherbot' )
    160                    . '">WeatherBot™</a>'
    161                    . '</span>';
    162         }
    163 
    164         $html .= '</span>';
    165         return trim( $html );
    166     }
    167 
    168     /**
    169      * Render the COMPACT type (inline-safe markup for taxonomy/content use).
    170      * Does not send no-cache headers; client JS hydrates via REST.
    171      */
    172     public function render_compact( ?array $data, array $atts, ?float $lat = null, ?float $lon = null ): string {
    173         $data = is_array( $data ) ? $data : [];
    174 
    175         $temp       = $this->read( $data, 'temperature.degrees' );
    176         $unit_label = $this->read( $data, 'temperature.unit' );
    177         $desc       = $this->read( $data, 'weatherCondition.description.text' );
    178         $icon_base  = $this->read( $data, 'weatherCondition.iconBaseUri' );
    179 
    180         $temp_text  = is_numeric( $temp ) ? (string) round( (float) $temp ) : '';
    181         $unit_text  = $this->unit_symbol( $unit_label );
    182         $unit_code  = $this->unit_code( $unit_label );
    183 
    184         $classes = [ 'roxxi-weather', 'wb-type-compact' ];
    185         $attributes = [ 'role' => 'weatherBot' ];
    186         if ( ! empty( $atts['align'] ) ) { $classes[] = 'wb-' . $atts['align']; }
    187 
    188         if ( isset( $atts['font_color'] ) ) {
    189             if ( $atts['font_color'] === 'light' ) { $classes[] = 'wb-text-light'; }
    190             if ( $atts['font_color'] === 'dark'  ) { $classes[] = 'wb-text-dark'; }
    191         }
    192         if ( isset( $atts['extra_class'] ) && $atts['extra_class'] !== '' ) {
    193             $classes[] = $atts['extra_class'];
    194         }
    195 
    196         $attrs = [ 'class' => implode( ' ', $classes ) ];
    197        
    198         if ( $lat !== null ) { $attrs['data-lat'] = (string) $lat; }
    199        
    200         if ( $lon !== null ) { $attrs['data-lon'] = (string) $lon; }
    201        
    202         $attrs['data-unit'] = $unit_code;
    203        
    204         $attrs['role'] = 'weatherBot';
    205 
    206         $attr_html = '';
    207         foreach ( $attrs as $k => $v ) {
    208             $attr_html .= ' ' . esc_attr( $k ) . '="' . esc_attr( $v ) . '"';
    209         }
    210 
    211         $html  = '<span' . $attr_html . '>';
    212         $html .= '<span class="details">';
    213 
    214             if ( isset( $atts['show_pre_text'] ) && (string) $atts['show_pre_text'] === '1' && ! empty( $atts['pre_text'] ) ) {
    215                 $html .= '<span class="wb-pre">' . esc_html( (string) $atts['pre_text'] ) . '</span>';
    216             }
    217 
    218             $html .= '<span class="conditions">';
    219                 $html .= '<span class="temp">';
    220 
    221                     if ( $icon_base ) {
    222                         $alt = $desc ? (string) $desc : '';
    223                         $html .= '<img class="wb-ico" src="' . esc_url( $icon_base . '.png' ) . '" title="' . esc_attr( $alt ) . '" alt="' . esc_attr( $alt ) . '" width="20" height="20" />';
    224                     }
    225 
    226                     $html .= '<span class="degree">';
    227                         if ( $temp_text !== '' ) {
    228                             $html .= '<span class="wb-temp">' . esc_html( $temp_text ) . '</span>';
    229                         }
    230                         if ( $unit_text !== '' ) {
    231                             $html .= '<span class="wb-unit">' . esc_html( $unit_text ) . '</span>';
    232                         }
    233                     $html .= '</span>';
    234                 $html .= '</span>';
    235 
    236             $html .= '</span>';
    237         $html .= '</span>';
    238 
    239         if ( $this->should_show_credit( $atts ) ) {
    240             $html .= '<span class="wb-powered" role="note">'
    241                    . 'powered by <a class="wb-powered-link" href="https://roxxistudios.com/plugins/weatherbot" target="_blank" rel="noopener" title="'
    242                    . esc_attr__( 'Get the WeatherBot plugin for your site.', 'weatherbot' )
    243                    . '">WeatherBot™</a>'
    244                    . '</span>';
    245         }
    246 
    247         $html .= '</span>';
    248         return trim( $html );
    249     }
    250 
    251     /**
    252      * Render the INLINE type (minimal; no credit).
    253      * Does not send no-cache headers; client JS hydrates via REST.
    254      */
    255     public function render_inline( ?array $data, array $atts, ?float $lat = null, ?float $lon = null ): string {
    256         $data = is_array( $data ) ? $data : [];
    257 
    258         $temp       = $this->read( $data, 'temperature.degrees' );
    259         $unit_label = $this->read( $data, 'temperature.unit' );
    260         $desc       = $this->read( $data, 'weatherCondition.description.text' );
    261         $icon_base  = $this->read( $data, 'weatherCondition.iconBaseUri' );
    262 
    263         $temp_text  = is_numeric( $temp ) ? (string) round( (float) $temp ) : '';
    264         $unit_text  = $this->unit_symbol( $unit_label );
    265         $unit_code  = $this->unit_code( $unit_label );
    266 
    267         $classes = [ 'roxxi-weather', 'wb-type-inline' ];
    268         $attributes = [ 'role' => 'weatherBot' ];
    269         if ( ! empty( $atts['align'] ) ) { $classes[] = 'wb-' . $atts['align']; }
    270 
    271         if ( isset( $atts['font_color'] ) ) {
    272             if ( $atts['font_color'] === 'light' ) { $classes[] = 'wb-text-light'; }
    273             if ( $atts['font_color'] === 'dark'  ) { $classes[] = 'wb-text-dark'; }
    274         }
    275         if ( isset( $atts['extra_class'] ) && $atts['extra_class'] !== '' ) {
    276             $classes[] = $atts['extra_class'];
    277         }
    278 
    279         $attrs = [ 'class' => implode( ' ', $classes ) ];
    280        
    281         if ( $lat !== null ) { $attrs['data-lat'] = (string) $lat; }
    282        
    283         if ( $lon !== null ) { $attrs['data-lon'] = (string) $lon; }
    284        
    285         $attrs['data-unit'] = $unit_code;
    286        
    287         $attrs['role'] = 'weatherBot';
    288 
    289         $attr_html = '';
    290         foreach ( $attrs as $k => $v ) {
    291             $attr_html .= ' ' . esc_attr( $k ) . '="' . esc_attr( $v ) . '"';
    292         }
    293 
    294         $html = '<span' . $attr_html . '>';
    295 
    296         if ( isset( $atts['show_pre_text'] ) && (string) $atts['show_pre_text'] === '1' && ! empty( $atts['pre_text'] ) ) {
    297             $html .= '<span class="wb-pre">' . esc_html( (string) $atts['pre_text'] ) . '</span>';
    298         }
    299 
    300         $html .= '<span class="conditions">';
    301        
    302         if ( $icon_base ) {
    303             $alt = $desc ? (string) $desc : '';
    304             $html .= '<img class="wb-ico" src="' . esc_url( $icon_base . '.png' ) . '" title="' . esc_attr( $alt ) . '" alt="' . esc_attr( $alt ) . '" width="20" height="20" />';
    305         }
    306 
    307         $html .= '<span class="degree">';
    308             if ( $temp_text !== '' ) {
    309                 $html .= '<span class="wb-temp">' . esc_html( $temp_text ) . '</span>';
    310             }
    311             if ( $unit_text !== '' ) {
    312                 $html .= '<span class="wb-unit">' . esc_html( $unit_text ) . '</span>';
    313             }
    314         $html .= '</span>'; // .degree
    315 
    316         $html .= '</span>'; // .conditions
    317 
    318         $html .= '</span>';
    319         return trim( $html );
    320     }
     112        $attr_html = '';
     113        foreach ( $attrs as $k => $v ) {
     114            $attr_html .= ' ' . esc_attr( $k ) . '="' . esc_attr( $v ) . '"';
     115        }
     116
     117        // SEO: Changed main wrapper from <span> to <aside> for semantic accuracy.
     118        $html  = '<aside' . $attr_html . '>';
     119
     120        // === Start Badge wrapper ===
     121        // Note: The inner spans are kept for CSS styling compatibility.
     122        $html .= '<div class="wb-badge">'; // Changed to div as it's inside a block-level <aside>
     123
     124            if ( isset( $atts['show_pre_text'] ) && (string) $atts['show_pre_text'] === '1' && ! empty( $atts['pre_text'] ) ) {
     125                // SEO: Use a heading tag for the title. Allow override via shortcode attribute.
     126                $valid_tags  = ['h2', 'h3', 'h4', 'h5', 'h6', 'p'];
     127                $heading_tag = isset($atts['heading_tag']) && in_array($atts['heading_tag'], $valid_tags) ? $atts['heading_tag'] : 'h3';
     128                $html .= '<' . $heading_tag . ' class="wb-pre">' . esc_html( (string) $atts['pre_text'] ) . '</' . $heading_tag . '>';
     129            }
     130
     131            $html .= '<div class="conditions">'; // Changed to div
     132           
     133                $html .= '<div class="temp">'; // Changed to div
     134
     135                    if ( $icon_base ) {
     136                        $alt = $desc ? (string) $desc : '';
     137                        // SEO: Added role="presentation" for icons that are purely decorative next to text.
     138                        $html .= '<img class="wb-ico" src="' . esc_url( $icon_base . '.png' ) . '" title="' . esc_attr( $alt ) . '" alt="' . esc_attr( $alt ) . '" width="20" height="20" role="presentation" />';
     139                    }
     140
     141                    $html .= '<span class="degree">';
     142                        if ( $temp_text !== '' ) {
     143                            $html .= '<span class="wb-temp">' . esc_html( $temp_text ) . '</span>';
     144                        }
     145                        if ( $unit_text !== '' ) {
     146                            $html .= '<span class="wb-unit">' . esc_html( $unit_text ) . '</span>';
     147                        }
     148                    $html .= '</span>'; // .degree
     149                   
     150                $html .= '</div>'; // .temp
     151
     152                if ( $desc ) {
     153                    $html .= '<span class="wb-sep">-</span><span class="wb-desc">' . esc_html( (string) $desc ) . '</span>';
     154                }
     155
     156            $html .= '</div>'; // .conditions
     157
     158        $html .= '</div>'; // .badge
     159        // === End Badge wrapper ===
     160
     161        if ( $this->should_show_credit( $atts ) ) {
     162            // SEO: Changed credit wrapper from <span> to <p>
     163            $html .= '<p class="wb-powered" role="note">'
     164                    . 'powered by <a class="wb-powered-link" href="https://roxxistudios.com/plugins/weatherbot" target="_blank" rel="noopener" title="'
     165                    . esc_attr__( 'Get the WeatherBot plugin for your site.', 'weatherbot' )
     166                    . '">WeatherBot™</a>'
     167                    . '</p>';
     168        }
     169
     170        $html .= '</aside>';
     171        return trim( $html );
     172    }
     173
     174    /**
     175     * Render the COMPACT type (inline-safe markup for taxonomy/content use).
     176     * Does not send no-cache headers; client JS hydrates via REST.
     177     */
     178    public function render_compact( ?array $data, array $atts, ?float $lat = null, ?float $lon = null ): string {
     179        $data = is_array( $data ) ? $data : [];
     180
     181        $temp       = $this->read( $data, 'temperature.degrees' );
     182        $unit_label = $this->read( $data, 'temperature.unit' );
     183        $desc       = $this->read( $data, 'weatherCondition.description.text' );
     184        $icon_base  = $this->read( $data, 'weatherCondition.iconBaseUri' );
     185
     186        $temp_text = is_numeric( $temp ) ? (string) round( (float) $temp ) : '';
     187        $unit_text = $this->unit_symbol( $unit_label );
     188        $unit_code = $this->unit_code( $unit_label );
     189
     190        $classes = [ 'roxxi-weather', 'wb-type-compact' ];
     191        if ( ! empty( $atts['align'] ) ) { $classes[] = 'wb-' . $atts['align']; }
     192
     193        if ( isset( $atts['font_color'] ) ) {
     194            if ( $atts['font_color'] === 'light' ) { $classes[] = 'wb-text-light'; }
     195            if ( $atts['font_color'] === 'dark' ) { $classes[] = 'wb-text-dark'; }
     196        }
     197        if ( isset( $atts['extra_class'] ) && $atts['extra_class'] !== '' ) {
     198            $classes[] = $atts['extra_class'];
     199        }
     200
     201        $attrs = [ 'class' => implode( ' ', $classes ) ];
     202        if ( $lat !== null ) { $attrs['data-lat'] = (string) $lat; }
     203        if ( $lon !== null ) { $attrs['data-lon'] = (string) $lon; }
     204        $attrs['data-unit'] = $unit_code;
     205
     206        // SEO: Add ARIA role and label for accessibility
     207        $pre_text_for_label = ! empty( $atts['pre_text'] ) ? (string) $atts['pre_text'] : __( 'Current Weather', 'weatherbot' );
     208        $attrs['role']       = 'region';
     209        $attrs['aria-label'] = esc_attr( rtrim( $pre_text_for_label, ': ' ) );
     210
     211        $attr_html = '';
     212        foreach ( $attrs as $k => $v ) {
     213            $attr_html .= ' ' . esc_attr( $k ) . '="' . esc_attr( $v ) . '"';
     214        }
     215
     216        // SEO: Changed main wrapper from <span> to <aside> for semantic accuracy.
     217        $html  = '<aside' . $attr_html . '>';
     218        $html .= '<div class="details">'; // Changed to div
     219
     220            if ( isset( $atts['show_pre_text'] ) && (string) $atts['show_pre_text'] === '1' && ! empty( $atts['pre_text'] ) ) {
     221                // SEO: Use a heading tag for the title. Allow override via shortcode attribute.
     222                $valid_tags  = ['h2', 'h3', 'h4', 'h5', 'h6', 'p'];
     223                $heading_tag = isset($atts['heading_tag']) && in_array($atts['heading_tag'], $valid_tags) ? $atts['heading_tag'] : 'h3';
     224                $html .= '<' . $heading_tag . ' class="wb-pre">' . esc_html( (string) $atts['pre_text'] ) . '</' . $heading_tag . '>';
     225            }
     226
     227            $html .= '<div class="conditions">'; // Changed to div
     228                $html .= '<div class="temp">'; // Changed to div
     229
     230                    if ( $icon_base ) {
     231                        $alt = $desc ? (string) $desc : '';
     232                        // SEO: Added role="presentation" for icons that are purely decorative next to text.
     233                        $html .= '<img class="wb-ico" src="' . esc_url( $icon_base . '.png' ) . '" title="' . esc_attr( $alt ) . '" alt="' . esc_attr( $alt ) . '" width="20" height="20" role="presentation" />';
     234                    }
     235
     236                    $html .= '<span class="degree">';
     237                        if ( $temp_text !== '' ) {
     238                            $html .= '<span class="wb-temp">' . esc_html( $temp_text ) . '</span>';
     239                        }
     240                        if ( $unit_text !== '' ) {
     241                            $html .= '<span class="wb-unit">' . esc_html( $unit_text ) . '</span>';
     242                        }
     243                    $html .= '</span>';
     244                $html .= '</div>';
     245
     246            $html .= '</div>';
     247        $html .= '</div>';
     248
     249        if ( $this->should_show_credit( $atts ) ) {
     250            // SEO: Changed credit wrapper from <span> to <p>
     251            $html .= '<p class="wb-powered" role="note">'
     252                    . 'powered by <a class="wb-powered-link" href="https://roxxistudios.com/plugins/weatherbot" target="_blank" rel="noopener" title="'
     253                    . esc_attr__( 'Get the WeatherBot plugin for your site.', 'weatherbot' )
     254                    . '">WeatherBot™</a>'
     255                    . '</p>';
     256        }
     257
     258        $html .= '</aside>';
     259        return trim( $html );
     260    }
     261
     262    /**
     263     * Render the INLINE type (minimal; no credit).
     264     * NOTE: This render method remains unchanged as it is designed for true inline use,
     265     * where block-level elements like <aside> or <h3> would be inappropriate.
     266     */
     267    public function render_inline( ?array $data, array $atts, ?float $lat = null, ?float $lon = null ): string {
     268        $data = is_array( $data ) ? $data : [];
     269
     270        $temp       = $this->read( $data, 'temperature.degrees' );
     271        $unit_label = $this->read( $data, 'temperature.unit' );
     272        $desc       = $this->read( $data, 'weatherCondition.description.text' );
     273        $icon_base  = $this->read( $data, 'weatherCondition.iconBaseUri' );
     274
     275        $temp_text = is_numeric( $temp ) ? (string) round( (float) $temp ) : '';
     276        $unit_text = $this->unit_symbol( $unit_label );
     277        $unit_code = $this->unit_code( $unit_label );
     278
     279        $classes = [ 'roxxi-weather', 'wb-type-inline' ];
     280        if ( ! empty( $atts['align'] ) ) { $classes[] = 'wb-' . $atts['align']; }
     281
     282        if ( isset( $atts['font_color'] ) ) {
     283            if ( $atts['font_color'] === 'light' ) { $classes[] = 'wb-text-light'; }
     284            if ( $atts['font_color'] === 'dark' ) { $classes[] = 'wb-text-dark'; }
     285        }
     286        if ( isset( $atts['extra_class'] ) && $atts['extra_class'] !== '' ) {
     287            $classes[] = $atts['extra_class'];
     288        }
     289
     290        $attrs = [ 'class' => implode( ' ', $classes ) ];
     291        if ( $lat !== null ) { $attrs['data-lat'] = (string) $lat; }
     292        if ( $lon !== null ) { $attrs['data-lon'] = (string) $lon; }
     293        $attrs['data-unit'] = $unit_code;
     294
     295        // The non-standard role is removed for better standards compliance.
     296        // $attrs['role'] = 'weatherBot';
     297
     298        $attr_html = '';
     299        foreach ( $attrs as $k => $v ) {
     300            $attr_html .= ' ' . esc_attr( $k ) . '="' . esc_attr( $v ) . '"';
     301        }
     302
     303        $html = '<span' . $attr_html . '>';
     304
     305        if ( isset( $atts['show_pre_text'] ) && (string) $atts['show_pre_text'] === '1' && ! empty( $atts['pre_text'] ) ) {
     306            $html .= '<span class="wb-pre">' . esc_html( (string) $atts['pre_text'] ) . '</span>';
     307        }
     308
     309        $html .= '<span class="conditions">';
     310
     311        if ( $icon_base ) {
     312            $alt = $desc ? (string) $desc : '';
     313            $html .= '<img class="wb-ico" src="' . esc_url( $icon_base . '.png' ) . '" title="' . esc_attr( $alt ) . '" alt="' . esc_attr( $alt ) . '" width="20" height="20" />';
     314        }
     315
     316        $html .= '<span class="degree">';
     317            if ( $temp_text !== '' ) {
     318                $html .= '<span class="wb-temp">' . esc_html( $temp_text ) . '</span>';
     319            }
     320            if ( $unit_text !== '' ) {
     321                $html .= '<span class="wb-unit">' . esc_html( $unit_text ) . '</span>';
     322            }
     323        $html .= '</span>'; // .degree
     324
     325        $html .= '</span>'; // .conditions
     326
     327        $html .= '</span>';
     328        return trim( $html );
     329    }
    321330}
    322331
     
    326335
    327336
    328 
    329 
    330337// EOF
  • weatherbot/trunk/src/Shortcodes/Shortcode.php

    r3363509 r3367502  
    33
    44use RoxxiStudios\WeatherBot\Services\Metrics;
    5 
    65use RoxxiStudios\WeatherBot\Plugin;
    76use RoxxiStudios\WeatherBot\Frontend\Renderer;
     
    1110
    1211if ( ! defined( 'ABSPATH' ) ) {
    13     if ( function_exists( 'status_header' ) ) {
    14         status_header( 403 );
    15     }
    16     exit;
     12    if ( function_exists( 'status_header' ) ) {
     13        status_header( 403 );
     14    }
     15    exit;
    1716}
    1817
    1918/**
    20  * Shortcode handler for [weather_bot] (v3.1.6)
     19 * Shortcode handler for [weatherbot] (v3.1.6 - JSON-LD Enhanced)
    2120 */
    2221class Shortcode {
    2322    private Plugin $plugin;
    2423    private Renderer $renderer;
    25 
    26     public function __construct( Plugin $plugin ) {
    27         $this->plugin   = $plugin;
    28         $this->renderer = new Renderer( $plugin );
    29 
    30         // Official shortcode
    31         add_shortcode( 'weather_bot', [ $this, 'render' ] );
    32 
    33         // Alias for convenience
    34         add_shortcode( 'weatherbot', [ $this, 'render' ] );
    35     }
     24    /**
     25     * @var array Holds unique weather data to be rendered as JSON-LD in the header.
     26     */
     27    private static array $json_ld_data = [];
     28
     29    public function __construct( Plugin $plugin ) {
     30        $this->plugin   = $plugin;
     31        $this->renderer = new Renderer( $plugin );
     32
     33        // Official shortcode
     34        add_shortcode( 'weather_bot', [ $this, 'render' ] );
     35
     36        // Alias for convenience
     37        add_shortcode( 'weatherbot', [ $this, 'render' ] );
     38
     39        // Hook into the header for better crawler visibility.
     40        add_action( 'wp_head', [ $this, 'render_json_ld_script' ] );
     41    }
    3642
    3743    public function render( $raw_atts = [] ): string {
     
    5965            'lat'           => null,
    6066            'lon'           => null,
    61 
    62             // IMPORTANT: pull defaults from Settings (fixes "C selected but F persists")
    63             'unit'          => $default_unit,   // accepts: IMPERIAL | METRIC | CELSIUS (alias) | F | C
    64             'type'          => $default_var,    // badge | compact | inline
    65             'align'         => '',              // left | center | right
     67            'unit'          => $default_unit,
     68            'type'          => $default_var,
     69            'align'         => '',
    6670            'show_pre_text' => $default_show_pre_text,
    6771            'pre_text'      => $default_pre_text,
    68             'font_color'    => $default_font_color, // '', 'light', 'dark'
    69             'show_credit'   => $default_show_credit, // per-shortcode override
     72            'heading_tag'   => 'h3',
     73            'font_color'    => $default_font_color,
     74            'show_credit'   => $default_show_credit,
    7075        ], $raw_atts, 'weather_bot' );
    7176
     
    9095        $atts['show_credit']   = ( (string) $atts['show_credit']   === '1' ) ? '1' : '0';
    9196
    92         // If author did NOT supply show_credit in the shortcode,
    93         // remove it so Renderer can fall back to the global setting.
    9497        if ( ! array_key_exists( 'show_credit', (array) $raw_atts ) ) {
    9598            unset( $atts['show_credit'] );
    9699        }
    97100
    98         // Apply font color helper class if specified
    99101        if ( ! empty( $atts['font_color'] ) ) {
    100102            $color = strtolower( trim( (string) $atts['font_color'] ) );
     
    106108        }
    107109
    108         // Require API key
    109110        $apiKey = method_exists( $this->plugin, 'api_key' )
    110111            ? (string) $this->plugin->api_key()
     
    112113
    113114        if ( $apiKey === '' ) {
    114             return '<!-- WeatherBot: Missing Google API key in Settings → WeatherBot -->';
    115         }
    116 
    117         // Resolve location → lat/lon with caching
     115            return '';
     116        }
     117
    118118        if ( is_numeric( $atts['lat'] ?? null ) && is_numeric( $atts['lon'] ?? null ) ) {
    119119            $lat = (float) $atts['lat'];
     
    126126            ];
    127127            $geo_cache_key = 'rx_weatherbot_geo_' . md5( wp_json_encode( $geo_key_input ) );
    128 
    129             // TTL (defaults to 1 yr; filterable)
    130128            $ttl = (int) apply_filters( 'rx_weatherbot_geocode_ttl', YEAR_IN_SECONDS );
    131 
    132129            $latlon   = get_transient( $geo_cache_key );
    133130            $lock_key = $geo_cache_key . '_lock';
    134 
    135131            if ( is_array( $latlon ) && isset( $latlon['lat'], $latlon['lon'] ) ) {
    136132                GeocodeCache::index_add( $geo_cache_key );
    137133            }
    138 
    139134            if ( false === $latlon ) {
    140135                if ( ! get_transient( $lock_key ) ) {
    141136                    set_transient( $lock_key, 1, 30 );
    142 
    143137                    $geocoder = new Geocoder( $apiKey );
    144138                    $latlon   = $geocoder->to_latlon( $atts );
    145 
    146139                    if ( is_array( $latlon ) && isset( $latlon['lat'], $latlon['lon'] ) ) {
    147140                        set_transient( $geo_cache_key, $latlon, $ttl );
    148141                        GeocodeCache::index_add( $geo_cache_key );
    149142                    }
    150 
    151143                    delete_transient( $lock_key );
    152144                } else {
    153                     // brief backoff and retry once
    154                     usleep( 150000 ); // 150ms
     145                    usleep( 150000 );
    155146                    $latlon = get_transient( $geo_cache_key );
    156147                }
    157148            }
    158 
    159149            if ( ! is_array( $latlon ) || ! isset( $latlon['lat'], $latlon['lon'] ) ) {
    160                 return '<!-- WeatherBot geocode error (cached miss) -->';
    161             }
    162 
     150                return '';
     151            }
    163152            $lat = (float) $latlon['lat'];
    164153            $lon = (float) $latlon['lon'];
    165154        }
    166155
    167                 Metrics::incr( 'weather_requests' );
    168         // Weather caching with TTL + SWR + coalescing
     156        Metrics::incr( 'weather_requests' );
    169157        $opts = $this->plugin->options();
    170         $ttl         = (int) ( $opts['weather_ttl'] ?? 120 );
     158        $ttl        = (int) ( $opts['weather_ttl'] ?? 120 );
    171159        if ( $ttl < 120 ) { $ttl = 120; } if ( $ttl > 300 ) { $ttl = 300; }
    172160        $swr_window  = (int) ( $opts['swr_window'] ?? 900 );
    173161        if ( $swr_window < 300 ) { $swr_window = 300; } if ( $swr_window > 1800 ) { $swr_window = 1800; }
    174162        $coalesce_on = (string) ( $opts['coalesce_enabled'] ?? '1' ) === '1';
    175 
    176163        $weather_key_input = [
    177164            'lat'  => round( $lat, 4 ),
     
    183170        $cached            = get_transient( $weather_cache_key );
    184171        $now               = time();
    185 
    186         // Allow transient to live through TTL+SWR so we can serve stale
    187172        $data = null;
    188173        $age  = null;
     
    191176            $age  = max( 0, $now - (int) $cached['fetched'] );
    192177        }
    193 
    194         // In-request coalescing to avoid duplicate fetches during one render
    195178        static $wb_inreq = [];
    196179        $inreq_key = $weather_cache_key;
    197180        if ( $coalesce_on && isset( $wb_inreq[ $inreq_key ] ) ) {
    198181            $data = $wb_inreq[ $inreq_key ];
    199             $age  = 0; // treat as fresh for this render
    200         }
    201 
     182            $age  = 0;
     183        }
    202184        $need_live = true;
    203185        if ( is_array( $data ) && $age !== null ) {
    204186            if ( $age <= $ttl ) {
    205                 // Fresh → serve and skip live
    206187                $need_live = false;
    207188            } elseif ( $age <= ( $ttl + $swr_window ) ) {
    208                 // SWR: serve stale and refresh in background (if not already locked)
    209189                $need_live = false;
    210190                if ( $coalesce_on ) {
    211191                    if ( ! get_transient( $lock_key ) ) {
    212                         set_transient( $lock_key, 1, 20 ); // short lock to avoid stampedes
     192                        set_transient( $lock_key, 1, 20 );
    213193                        if ( ! wp_next_scheduled( 'rx_weatherbot_refresh_weather', [ $weather_cache_key, $lat, $lon, $unit, $ttl, $swr_window ] ) ) {
    214194                            wp_schedule_single_event( time() + 1, 'rx_weatherbot_refresh_weather', [ $weather_cache_key, $lat, $lon, $unit, $ttl, $swr_window ] );
     
    222202            }
    223203        }
    224 
    225204        if ( $need_live ) {
    226             // Cross-request coalescing lock
    227205            $got_lock = true;
    228206            if ( $coalesce_on ) {
     
    238216                    $data = $live;
    239217                } else {
    240                     // On live failure, if we have cached, keep using it; else bubble error
    241218                    if ( ! is_array( $data ) ) {
    242219                        $data = $live;
     
    245222                delete_transient( $lock_key );
    246223            } else {
    247                 // Another request is fetching; if no cached value, wait briefly once
    248224                if ( ! is_array( $data ) ) {
    249                     usleep( 120000 ); // 120ms
     225                    usleep( 120000 );
    250226                    $cached = get_transient( $weather_cache_key );
    251227                    if ( is_array( $cached ) && isset( $cached['payload'] ) ) {
     
    256232        }
    257233
     234        if ( ! isset( $data['error'] ) ) {
     235            $this->collect_json_ld_data( $data, $atts, $lat, $lon );
     236        }
     237
    258238        if ( isset( $data['error'] ) ) {
    259             $detail = isset( $data['status'] ) ? ( ' status=' . (int) $data['status'] ) : '';
    260             return '<!-- WeatherBot API error:' . esc_html( (string) $data['error'] ) . $detail . ' -->';
    261         }
    262 
    263         // Render type (pass lat/lon so the live JS can hydrate)
     239            return '';
     240        }
     241
    264242        if ( $type === 'compact' ) {
    265243            if ( method_exists( $this->renderer, 'render_compact' ) ) {
    266244                return $this->renderer->render_compact( $data, $atts, $lat, $lon );
    267245            }
    268             return $this->renderer->render_badge( $data, $atts, $lat, $lon ); // graceful fallback
     246            return $this->renderer->render_badge( $data, $atts, $lat, $lon );
    269247        }
    270248
     
    277255        }
    278256
    279         // default
    280257        return $this->renderer->render_badge( $data, $atts, $lat, $lon );
    281258    }
     259
     260    /**
     261     * Collects and deduplicates weather data for JSON-LD.
     262     */
     263    private function collect_json_ld_data( array $data, array $atts, float $lat, float $lon ): void {
     264        $location_key = md5( round( $lat, 4 ) . ',' . round( $lon, 4 ) );
     265
     266        if ( isset( self::$json_ld_data[ $location_key ] ) ) {
     267            return;
     268        }
     269
     270        $temp_degrees = $this->renderer->read( $data, 'temperature.degrees' );
     271        $unit_label   = $this->renderer->read( $data, 'temperature.unit' );
     272        $description  = $this->renderer->read( $data, 'weatherCondition.description.text' );
     273
     274        if ( ! is_numeric( $temp_degrees ) || empty( $description ) ) {
     275            return;
     276        }
     277
     278        $location_name = ! empty( $atts['city'] ) ? $atts['city'] : ( ! empty( $atts['pre_text'] ) ? trim( rtrim( $atts['pre_text'], ': ' ) ) : 'Current Weather' );
     279        $unit_code = ( strtoupper( $unit_label ) === 'FAHRENHEIT' ) ? 'FAH' : 'CEL';
     280
     281        // UPDATED: This structure now includes the required properties for Google's Rich Results Test.
     282        self::$json_ld_data[ $location_key ] = [
     283            '@context'           => 'https://schema.org',
     284            '@type'              => 'WeatherForecast',
     285            'location'           => [
     286                '@type' => 'Place',
     287                'name'  => $location_name,
     288                'geo'   => [
     289                    '@type'     => 'GeoCoordinates',
     290                    'latitude'  => (string) $lat,
     291                    'longitude' => (string) $lon,
     292                ],
     293            ],
     294            // Add the current date as the forecastDate.
     295            'forecastDate'       => function_exists('wp_date') ? wp_date('Y-m-d') : date_i18n('Y-m-d', time()),
     296            // Use the current temperature for both high and low to meet requirements.
     297            'temperatureHigh'    => [
     298                '@type'    => 'QuantitativeValue',
     299                'value'    => (string) round( (float) $temp_degrees ),
     300                'unitCode' => $unit_code,
     301            ],
     302            'temperatureLow'     => [
     303                '@type'    => 'QuantitativeValue',
     304                'value'    => (string) round( (float) $temp_degrees ),
     305                'unitCode' => $unit_code,
     306            ],
     307            'weatherCondition'   => $description,
     308        ];
     309    }
     310
     311    /**
     312     * Renders the final JSON-LD script in the header.
     313     */
     314    public function render_json_ld_script(): void {
     315        if ( empty( self::$json_ld_data ) ) {
     316            return;
     317        }
     318
     319        // Loop through each forecast and print a separate script tag for each one.
     320        foreach ( self::$json_ld_data as $forecast_data ) {
     321            echo '<script type="application/ld+json">' . wp_json_encode( $forecast_data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ) . '</script>';
     322        }
     323    }
    282324}
    283 
    284 
    285 
    286 
    287 // EOF
  • weatherbot/trunk/src/Widgets/Weather_Widget.php

    r3366760 r3367502  
    1212
    1313/**
    14  * WeatherBot Widget (with align support)
     14 * WeatherBot Widget (with align and heading_tag support)
    1515 */
    1616class Weather_Widget extends WP_Widget {
     
    2727        echo wp_kses_post( $args['before_widget'] );
    2828
    29         $title       = isset( $instance['title'] )        ? (string) $instance['title']        : '';
    30         $type        = isset( $instance['type'] )         ? (string) $instance['type']         : 'inline';
    31         $city        = isset( $instance['city'] )         ? (string) $instance['city']         : '';
    32         $lat         = isset( $instance['lat'] )          ? (string) $instance['lat']          : '';
    33         $lon         = isset( $instance['lon'] )          ? (string) $instance['lon']          : '';
    34         $unit        = isset( $instance['unit'] )         ? (string) $instance['unit']         : 'IMPERIAL';
     29        $title       = isset( $instance['title'] )       ? (string) $instance['title']       : '';
     30        $type        = isset( $instance['type'] )        ? (string) $instance['type']        : 'inline';
     31        $city        = isset( $instance['city'] )        ? (string) $instance['city']        : '';
     32        $lat         = isset( $instance['lat'] )         ? (string) $instance['lat']         : '';
     33        $lon         = isset( $instance['lon'] )         ? (string) $instance['lon']         : '';
     34        $unit        = isset( $instance['unit'] )        ? (string) $instance['unit']        : 'IMPERIAL';
    3535        $show_pre    = ! empty( $instance['show_pre'] )   ? '1' : '0';
    36         $pre_text    = isset( $instance['pre_text'] )     ? (string) $instance['pre_text']     : '';
    37         $font_color  = isset( $instance['font_color'] )   ? (string) $instance['font_color']   : '';
    38         $align       = isset( $instance['align'] )        ? (string) $instance['align']        : ''; // left|center|right
     36        $pre_text    = isset( $instance['pre_text'] )    ? (string) $instance['pre_text']    : '';
     37        $font_color  = isset( $instance['font_color'] )  ? (string) $instance['font_color']  : '';
     38        $align       = isset( $instance['align'] )       ? (string) $instance['align']       : '';
     39        $heading_tag = isset( $instance['heading_tag'] ) ? (string) $instance['heading_tag'] : 'h3';
    3940
    4041        $show_credit = ( isset( $instance['show_credit'] ) && (string) $instance['show_credit'] === '1' ) ? '1' : '0';
     
    4950            'show_pre_text' => $show_pre,
    5051            'pre_text'      => $pre_text,
     52            'heading_tag'   => $heading_tag,
    5153        ];
    5254        if ( $city !== '' ) {
     
    7173            $parts[] = $k . '="' . esc_attr( $v ) . '"';
    7274        }
    73         $shortcode = '[weather_bot ' . implode( ' ', $parts ) . ']';
     75        $shortcode = '[weatherbot ' . implode( ' ', $parts ) . ']';
    7476
    7577        echo do_shortcode( $shortcode ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
     
    120122          <label for="<?php echo esc_attr( $this->get_field_id('align') ); ?>"><?php esc_html_e('Align', 'weatherbot'); ?></label>
    121123          <select class="widefat" id="<?php echo esc_attr( $this->get_field_id('align') ); ?>" name="<?php echo esc_attr( $this->get_field_name('align') ); ?>">
    122             <option value=""      <?php selected( $get('align',''), '' );      ?>><?php esc_html_e('Inherit', 'weatherbot'); ?></option>
    123             <option value="left"  <?php selected( $get('align',''), 'left' );  ?>><?php esc_html_e('Left', 'weatherbot'); ?></option>
    124             <option value="center"<?php selected( $get('align',''), 'center' );?>><?php esc_html_e('Center', 'weatherbot'); ?></option>
    125             <option value="right" <?php selected( $get('align',''), 'right' ); ?>><?php esc_html_e('Right', 'weatherbot'); ?></option>
     124            <option value=""      <?php selected( $get('align',''), '' );       ?>><?php esc_html_e('Default', 'weatherbot'); ?></option>
     125            <option value="left"  <?php selected( $get('align',''), 'left' );   ?>><?php esc_html_e('Left', 'weatherbot'); ?></option>
     126            <option value="center"<?php selected( $get('align',''), 'center' ); ?>><?php esc_html_e('Center', 'weatherbot'); ?></option>
     127            <option value="right" <?php selected( $get('align',''), 'right' );  ?>><?php esc_html_e('Right', 'weatherbot'); ?></option>
    126128          </select>
    127129        </p>
     
    137139        </p>
    138140        <p>
    139           <label for="<?php echo esc_attr( $this->get_field_id('font_color') ); ?>"><?php esc_html_e('Font color', 'weatherbot'); ?></label>
     141            <label for="<?php echo esc_attr( $this->get_field_id('heading_tag') ); ?>"><?php esc_html_e('Pre-text Heading Tag', 'weatherbot'); ?></label>
     142            <select class="widefat" id="<?php echo esc_attr( $this->get_field_id('heading_tag') ); ?>" name="<?php echo esc_attr( $this->get_field_name('heading_tag') ); ?>">
     143                <option value="h2" <?php selected( $get('heading_tag','h3'), 'h2' ); ?>>H2</option>
     144                <option value="h3" <?php selected( $get('heading_tag','h3'), 'h3' ); ?>>H3 (Default)</option>
     145                <option value="h4" <?php selected( $get('heading_tag','h3'), 'h4' ); ?>>H4</option>
     146                <option value="h5" <?php selected( $get('heading_tag','h3'), 'h5' ); ?>>H5</option>
     147                <option value="h6" <?php selected( $get('heading_tag','h3'), 'h6' ); ?>>H6</option>
     148                <option value="p"  <?php selected( $get('heading_tag','h3'), 'p' );  ?>><?php esc_html_e('Paragraph', 'weatherbot'); ?></option>
     149            </select>
     150        </p>
     151        <p>
     152          <label for="<?php echo esc_attr( $this->get_field_id('font_color') ); ?>"><?php esc_html_e('Theme Color', 'weatherbot'); ?></label>
    140153          <select class="widefat" id="<?php echo esc_attr( $this->get_field_id('font_color') ); ?>" name="<?php echo esc_attr( $this->get_field_name('font_color') ); ?>">
    141             <option value=""      <?php selected( $get('font_color',''), '' );      ?>><?php esc_html_e('Inherit', 'weatherbot'); ?></option>
    142             <option value="light" <?php selected( $get('font_color',''), 'light' ); ?>><?php esc_html_e('Light', 'weatherbot'); ?></option>
    143             <option value="dark"  <?php selected( $get('font_color',''), 'dark' );  ?>><?php esc_html_e('Dark', 'weatherbot'); ?></option>
     154            <option value=""      <?php selected( $get('font_color',''), '' );      ?>><?php esc_html_e('Default', 'weatherbot'); ?></option>
     155            <option value="dark"  <?php selected( $get('font_color',''), 'dark' );  ?>><?php esc_html_e('Light Theme', 'weatherbot'); ?></option>
     156            <option value="light" <?php selected( $get('font_color',''), 'light' ); ?>><?php esc_html_e('Dark Theme', 'weatherbot'); ?></option>
    144157          </select>
    145158        </p>
     
    162175    public function update( $new, $old ) {
    163176        $clean = [];
    164         $clean['title']       = sanitize_text_field( $new['title']       ?? '' );
    165         $clean['city']        = sanitize_text_field( $new['city']        ?? '' );
    166         $clean['lat']         = sanitize_text_field( $new['lat']         ?? '' );
    167         $clean['lon']         = sanitize_text_field( $new['lon']         ?? '' );
     177        $clean['title']       = sanitize_text_field( $new['title']      ?? '' );
     178        $clean['city']        = sanitize_text_field( $new['city']       ?? '' );
     179        $clean['lat']         = sanitize_text_field( $new['lat']        ?? '' );
     180        $clean['lon']         = sanitize_text_field( $new['lon']        ?? '' );
    168181        $clean['unit']        = in_array( $new['unit'] ?? 'IMPERIAL', ['IMPERIAL','CELSIUS'], true ) ? $new['unit'] : 'IMPERIAL';
    169182        $clean['type']        = in_array( $new['type'] ?? 'inline', ['inline','badge','compact'], true ) ? $new['type'] : 'inline';
    170183        $clean['align']       = in_array( $new['align'] ?? '', ['', 'left','center','right'], true ) ? $new['align'] : '';
    171184        $clean['show_pre']    = ! empty( $new['show_pre'] ) ? 1 : 0;
    172         $clean['pre_text']    = sanitize_text_field( $new['pre_text']    ?? '' );
     185        $clean['pre_text']    = sanitize_text_field( $new['pre_text']   ?? '' );
    173186        $clean['font_color']  = in_array( $new['font_color'] ?? '', ['', 'light', 'dark'], true ) ? $new['font_color'] : '';
    174187        $clean['show_credit'] = ( isset( $new['show_credit'] ) && (string)$new['show_credit'] === '1' ) ? 1 : 0;
     188
     189        $valid_tags = ['h2', 'h3', 'h4', 'h5', 'h6', 'p'];
     190        $clean['heading_tag'] = in_array( $new['heading_tag'] ?? 'h3', $valid_tags, true ) ? $new['heading_tag'] : 'h3';
    175191
    176192        return $clean;
    177193    }
    178194}
    179 
    180 
    181 
    182 
    183 // EOF
  • weatherbot/trunk/weatherbot.php

    r3367429 r3367502  
    44Plugin URI: https://roxxistudios.com/plugins/weatherbot
    55Description: WeatherBot — free version is a clean, easy to use weather plugin. Add fast, accessible, location-based weather using the Google Maps Platform (Weather + Geocoding). Includes a shortcode with inline/badge/compact variants and a widget.
    6 Version: 1.1.0
     6Version: 1.1.1
    77Requires at least: 6.0
    88Requires PHP: 7.4
     
    3838 * -------------------------------------------------------------------------- */
    3939if ( ! defined( 'RX_WEATHERBOT_VERSION' ) ) {
    40     define( 'RX_WEATHERBOT_VERSION', '1.1.0' );
     40    define( 'RX_WEATHERBOT_VERSION', '1.1.1' );
    4141}
    4242if ( ! defined( 'RX_WEATHERBOT_FILE' ) ) {
Note: See TracChangeset for help on using the changeset viewer.