Changeset 3367502
- Timestamp:
- 09/25/2025 03:42:37 AM (5 months ago)
- Location:
- weatherbot/trunk
- Files:
-
- 8 edited
-
assets/css/weather-bot.css (modified) (14 diffs)
-
blocks/weatherbot/block.json (modified) (2 diffs)
-
blocks/weatherbot/index.js (modified) (4 diffs)
-
readme.txt (modified) (5 diffs)
-
src/Frontend/Renderer.php (modified) (3 diffs)
-
src/Shortcodes/Shortcode.php (modified) (14 diffs)
-
src/Widgets/Weather_Widget.php (modified) (7 diffs)
-
weatherbot.php (modified) (2 diffs)
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) 2 2 - Component-scoped, class-only selectors (no element qualifiers) 3 3 - Predictable typography + color baseline (prevents footer/theme bleed) … … 39 39 ========================= */ 40 40 41 41 42 /* Inherit typography safely for children */ 42 43 43 .roxxi-weather,44 .roxxi-weather[data-widget="weatherBot"] {44 aside.roxxi-weather, 45 aside.roxxi-weather[data-widget="weatherBot"] { 45 46 display: flex; 46 47 gap: 5px; … … 56 57 } 57 58 59 aside.roxxi-weather[role="region"].wb-type-badge p, 60 aside.roxxi-weather[role="region"].wb-type-compact p, 61 aside.roxxi-weather[role="region"].wb-type-inline p { 62 margin: 0; 63 } 64 58 65 59 66 /* ========================= … … 89 96 } 90 97 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 { 92 102 display: flex; 93 103 gap: 8px; … … 176 186 } 177 187 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 { 179 192 display: inline-flex; 180 193 align-items: center; … … 203 216 } 204 217 205 .roxxi-weather .wb-temp,206 .roxxi-weather .wb-unit {207 opacity: 0.9;208 }209 210 218 .roxxi-weather .wb-desc { 211 219 color: var(--wb-neutral); … … 215 223 .roxxi-weather .wb-temp, 216 224 .roxxi-weather .wb-unit { 225 color: var(--wb-dark); 226 margin: 0; 227 font-size: inherit; 217 228 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; 218 235 } 219 236 … … 228 245 font-size: 12px; 229 246 font-weight: 400; 247 /* NEW: Reset default browser margin for the new <p> tag */ 248 margin: 0; 230 249 } 231 250 … … 277 296 .roxxi-weather.wb-text-light { 278 297 color: #ffffff; 298 } 299 300 .roxxi-weather .wb-powered { 301 padding-bottom: 2px; 279 302 } 280 303 … … 369 392 } 370 393 394 371 395 /* Respect user motion preferences */ 396 372 397 @media (prefers-reduced-motion: reduce) { 373 398 .roxxi-weather .conditions.wb-loading .wb-loading-msg::after { … … 389 414 .roxxi-weather [role="button"]:focus-visible { 390 415 outline: 3px solid currentColor; 391 /* ensures ≥3:1 contrast vs background */416 /* ensures >=3:1 contrast vs background */ 392 417 outline-offset: 2px; 393 418 border-radius: 3px; … … 431 456 432 457 @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 { 434 460 display: flex; 435 461 flex-direction: column; … … 466 492 align-items: inherit; 467 493 flex-wrap: wrap; 468 gap: 5px;494 gap: 3px; 469 495 } 470 496 … … 484 510 485 511 /* --- Hard override for dark-text variant in aggressive footers --- */ 512 513 486 514 /* Force container + all descendants to inherit dark text, except links */ 515 487 516 footer .roxxi-weather.roxxi-weather.wb-text-dark, 488 517 footer .roxxi-weather.roxxi-weather.wb-text-dark *:not(a) { 489 color: var(--wb-dark) !important; 490 } 518 color: var(--wb-dark) !important; 519 } 520 491 521 492 522 /* Links: restore brand color explicitly */ 523 493 524 footer .roxxi-weather.roxxi-weather.wb-text-dark a { 494 color: var(--wb-primary) !important; 495 } 525 color: var(--wb-primary) !important; 526 } 527 496 528 497 529 /* If links wrap icons/spans, keep them inheriting from the anchor */ 530 498 531 footer .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 45 45 "type": "string", 46 46 "default": "" 47 }, 48 "headingTag": { 49 "type": "string", 50 "default": "h3" 47 51 } 48 52 }, … … 55 59 "fontColor": "light", 56 60 "type": "badge", 57 "unit": "IMPERIAL" 61 "unit": "IMPERIAL", 62 "headingTag": "h3" 58 63 } 59 64 }, -
weatherbot/trunk/blocks/weatherbot/index.js
r3363509 r3367502 43 43 }), 44 44 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, {55 45 label: 'Display Type', 56 46 value: attributes.type || '', … … 61 51 { label: 'Compact', value: 'compact' }, 62 52 { 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' } 63 63 ] 64 64 }), … … 74 74 }), 75 75 el(SelectControl, { 76 label: ' Font Theme',76 label: 'Theme Color', 77 77 value: attributes.fontColor || '', 78 78 onChange: (v) => setAttributes({ fontColor: v }), 79 79 options: [ 80 80 { 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' } 83 83 ] 84 84 }) … … 141 141 }), 142 142 el(SelectControl, { 143 label: 'Theme ',143 label: 'Theme Color', 144 144 value: attributes.fontColor || '', 145 145 onChange: (v) => setAttributes({ fontColor: v }), 146 146 options: [ 147 147 { 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' } 150 150 ] 151 151 }), -
weatherbot/trunk/readme.txt
r3367429 r3367502 5 5 Tested up to: 6.8 6 6 Requires PHP: 7.4 7 Stable tag: 1.1. 07 Stable tag: 1.1.1 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 15 15 16 16 [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.72 17 73 18 *** … … 86 31 == Features == 87 32 33 * **Automatic WeatherForecast Schema.org (JSON-LD) structured data** generation for SEO and Google rich results. 88 34 * **Live local current weather** via **Google Weather API** 89 35 * **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 99 46 * **Caching tuned for freshness** — weather (~2 minutes per location/unit); geocoding (long-lived) 100 47 * **Manual cache purge** from Settings 101 48 * **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.) 103 50 * **No jQuery on the front end** — built with modern WordPress packages and vanilla JS 104 51 * **COMING SOON:** Our **Pro version** with **weather forecast** and other powerful features like **detailed style controls**. … … 249 196 *** 250 197 198 == Installation == 199 200 = From your WordPress dashboard = 201 202 1. Go to **Plugins → Add New**. 203 2. Search for **WeatherBot**. 204 3. Click **Install Now**, then **Activate**. 205 4. Go to **Settings → WeatherBot**, add your **Google Maps API key** (enable **Weather** and **Geocoding/Places** APIs), and save. 206 5. Add the **WeatherBot Block**, **Widget**, or **Shortcode** to your site and configure the options. 207 208 = Manual installation = 209 210 1. Upload the weatherbot folder to /wp-content/plugins/. 211 2. Activate the plugin in **Plugins**. 212 3. Go to **Settings → WeatherBot**, add your **Google Maps API key** (enable **Weather** and **Geocoding/Places** APIs), and save. 213 4. 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? = 220 Yes. 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? = 223 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. 224 225 = 3. Can I use the shortcode in Widgets, Headers/Footers, or builder modules? = 226 Yes—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? = 230 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**. 231 232 = 5. Does the widget support dark backgrounds? = 233 Yes. 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? = 236 No. 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? = 242 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: 243 244 .roxxi-weather span.wb-temp { 245 font-weight: 700; 246 color: #cc0000; 247 } 248 249 See the Styling & Customization section for more options. 250 251 *** 252 251 253 == Advanced Information == 252 254 … … 393 395 394 396 == 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. 395 404 396 405 = 1.1.0 = -
weatherbot/trunk/src/Frontend/Renderer.php
r3367429 r3367502 12 12 13 13 /** 14 * Frontend\Renderer (v3.1.5 )15 * - Outputs inline-safemarkup for WeatherBot variants.14 * Frontend\Renderer (v3.1.5 - SEO Enhanced) 15 * - Outputs semantic, SEO-friendly markup for WeatherBot variants. 16 16 * - Honors credit visibility via Settings (show_credit) and per-instance `show_credit="1"`. 17 17 * - Does NOT emit page-level no-cache headers (live freshness handled via REST). … … 20 20 class Renderer { 21 21 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, ': ' ) ); 104 111 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 } 321 330 } 322 331 … … 326 335 327 336 328 329 330 337 // EOF -
weatherbot/trunk/src/Shortcodes/Shortcode.php
r3363509 r3367502 3 3 4 4 use RoxxiStudios\WeatherBot\Services\Metrics; 5 6 5 use RoxxiStudios\WeatherBot\Plugin; 7 6 use RoxxiStudios\WeatherBot\Frontend\Renderer; … … 11 10 12 11 if ( ! 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; 17 16 } 18 17 19 18 /** 20 * Shortcode handler for [weather _bot] (v3.1.6)19 * Shortcode handler for [weatherbot] (v3.1.6 - JSON-LD Enhanced) 21 20 */ 22 21 class Shortcode { 23 22 private Plugin $plugin; 24 23 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 } 36 42 37 43 public function render( $raw_atts = [] ): string { … … 59 65 'lat' => null, 60 66 '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' => '', 66 70 'show_pre_text' => $default_show_pre_text, 67 71 '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, 70 75 ], $raw_atts, 'weather_bot' ); 71 76 … … 90 95 $atts['show_credit'] = ( (string) $atts['show_credit'] === '1' ) ? '1' : '0'; 91 96 92 // If author did NOT supply show_credit in the shortcode,93 // remove it so Renderer can fall back to the global setting.94 97 if ( ! array_key_exists( 'show_credit', (array) $raw_atts ) ) { 95 98 unset( $atts['show_credit'] ); 96 99 } 97 100 98 // Apply font color helper class if specified99 101 if ( ! empty( $atts['font_color'] ) ) { 100 102 $color = strtolower( trim( (string) $atts['font_color'] ) ); … … 106 108 } 107 109 108 // Require API key109 110 $apiKey = method_exists( $this->plugin, 'api_key' ) 110 111 ? (string) $this->plugin->api_key() … … 112 113 113 114 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 118 118 if ( is_numeric( $atts['lat'] ?? null ) && is_numeric( $atts['lon'] ?? null ) ) { 119 119 $lat = (float) $atts['lat']; … … 126 126 ]; 127 127 $geo_cache_key = 'rx_weatherbot_geo_' . md5( wp_json_encode( $geo_key_input ) ); 128 129 // TTL (defaults to 1 yr; filterable)130 128 $ttl = (int) apply_filters( 'rx_weatherbot_geocode_ttl', YEAR_IN_SECONDS ); 131 132 129 $latlon = get_transient( $geo_cache_key ); 133 130 $lock_key = $geo_cache_key . '_lock'; 134 135 131 if ( is_array( $latlon ) && isset( $latlon['lat'], $latlon['lon'] ) ) { 136 132 GeocodeCache::index_add( $geo_cache_key ); 137 133 } 138 139 134 if ( false === $latlon ) { 140 135 if ( ! get_transient( $lock_key ) ) { 141 136 set_transient( $lock_key, 1, 30 ); 142 143 137 $geocoder = new Geocoder( $apiKey ); 144 138 $latlon = $geocoder->to_latlon( $atts ); 145 146 139 if ( is_array( $latlon ) && isset( $latlon['lat'], $latlon['lon'] ) ) { 147 140 set_transient( $geo_cache_key, $latlon, $ttl ); 148 141 GeocodeCache::index_add( $geo_cache_key ); 149 142 } 150 151 143 delete_transient( $lock_key ); 152 144 } else { 153 // brief backoff and retry once 154 usleep( 150000 ); // 150ms 145 usleep( 150000 ); 155 146 $latlon = get_transient( $geo_cache_key ); 156 147 } 157 148 } 158 159 149 if ( ! is_array( $latlon ) || ! isset( $latlon['lat'], $latlon['lon'] ) ) { 160 return '<!-- WeatherBot geocode error (cached miss) -->'; 161 } 162 150 return ''; 151 } 163 152 $lat = (float) $latlon['lat']; 164 153 $lon = (float) $latlon['lon']; 165 154 } 166 155 167 Metrics::incr( 'weather_requests' ); 168 // Weather caching with TTL + SWR + coalescing 156 Metrics::incr( 'weather_requests' ); 169 157 $opts = $this->plugin->options(); 170 $ttl = (int) ( $opts['weather_ttl'] ?? 120 );158 $ttl = (int) ( $opts['weather_ttl'] ?? 120 ); 171 159 if ( $ttl < 120 ) { $ttl = 120; } if ( $ttl > 300 ) { $ttl = 300; } 172 160 $swr_window = (int) ( $opts['swr_window'] ?? 900 ); 173 161 if ( $swr_window < 300 ) { $swr_window = 300; } if ( $swr_window > 1800 ) { $swr_window = 1800; } 174 162 $coalesce_on = (string) ( $opts['coalesce_enabled'] ?? '1' ) === '1'; 175 176 163 $weather_key_input = [ 177 164 'lat' => round( $lat, 4 ), … … 183 170 $cached = get_transient( $weather_cache_key ); 184 171 $now = time(); 185 186 // Allow transient to live through TTL+SWR so we can serve stale187 172 $data = null; 188 173 $age = null; … … 191 176 $age = max( 0, $now - (int) $cached['fetched'] ); 192 177 } 193 194 // In-request coalescing to avoid duplicate fetches during one render195 178 static $wb_inreq = []; 196 179 $inreq_key = $weather_cache_key; 197 180 if ( $coalesce_on && isset( $wb_inreq[ $inreq_key ] ) ) { 198 181 $data = $wb_inreq[ $inreq_key ]; 199 $age = 0; // treat as fresh for this render 200 } 201 182 $age = 0; 183 } 202 184 $need_live = true; 203 185 if ( is_array( $data ) && $age !== null ) { 204 186 if ( $age <= $ttl ) { 205 // Fresh → serve and skip live206 187 $need_live = false; 207 188 } elseif ( $age <= ( $ttl + $swr_window ) ) { 208 // SWR: serve stale and refresh in background (if not already locked)209 189 $need_live = false; 210 190 if ( $coalesce_on ) { 211 191 if ( ! get_transient( $lock_key ) ) { 212 set_transient( $lock_key, 1, 20 ); // short lock to avoid stampedes192 set_transient( $lock_key, 1, 20 ); 213 193 if ( ! wp_next_scheduled( 'rx_weatherbot_refresh_weather', [ $weather_cache_key, $lat, $lon, $unit, $ttl, $swr_window ] ) ) { 214 194 wp_schedule_single_event( time() + 1, 'rx_weatherbot_refresh_weather', [ $weather_cache_key, $lat, $lon, $unit, $ttl, $swr_window ] ); … … 222 202 } 223 203 } 224 225 204 if ( $need_live ) { 226 // Cross-request coalescing lock227 205 $got_lock = true; 228 206 if ( $coalesce_on ) { … … 238 216 $data = $live; 239 217 } else { 240 // On live failure, if we have cached, keep using it; else bubble error241 218 if ( ! is_array( $data ) ) { 242 219 $data = $live; … … 245 222 delete_transient( $lock_key ); 246 223 } else { 247 // Another request is fetching; if no cached value, wait briefly once248 224 if ( ! is_array( $data ) ) { 249 usleep( 120000 ); // 120ms225 usleep( 120000 ); 250 226 $cached = get_transient( $weather_cache_key ); 251 227 if ( is_array( $cached ) && isset( $cached['payload'] ) ) { … … 256 232 } 257 233 234 if ( ! isset( $data['error'] ) ) { 235 $this->collect_json_ld_data( $data, $atts, $lat, $lon ); 236 } 237 258 238 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 264 242 if ( $type === 'compact' ) { 265 243 if ( method_exists( $this->renderer, 'render_compact' ) ) { 266 244 return $this->renderer->render_compact( $data, $atts, $lat, $lon ); 267 245 } 268 return $this->renderer->render_badge( $data, $atts, $lat, $lon ); // graceful fallback246 return $this->renderer->render_badge( $data, $atts, $lat, $lon ); 269 247 } 270 248 … … 277 255 } 278 256 279 // default280 257 return $this->renderer->render_badge( $data, $atts, $lat, $lon ); 281 258 } 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 } 282 324 } 283 284 285 286 287 // EOF -
weatherbot/trunk/src/Widgets/Weather_Widget.php
r3366760 r3367502 12 12 13 13 /** 14 * WeatherBot Widget (with align support)14 * WeatherBot Widget (with align and heading_tag support) 15 15 */ 16 16 class Weather_Widget extends WP_Widget { … … 27 27 echo wp_kses_post( $args['before_widget'] ); 28 28 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'; 35 35 $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'; 39 40 40 41 $show_credit = ( isset( $instance['show_credit'] ) && (string) $instance['show_credit'] === '1' ) ? '1' : '0'; … … 49 50 'show_pre_text' => $show_pre, 50 51 'pre_text' => $pre_text, 52 'heading_tag' => $heading_tag, 51 53 ]; 52 54 if ( $city !== '' ) { … … 71 73 $parts[] = $k . '="' . esc_attr( $v ) . '"'; 72 74 } 73 $shortcode = '[weather _bot ' . implode( ' ', $parts ) . ']';75 $shortcode = '[weatherbot ' . implode( ' ', $parts ) . ']'; 74 76 75 77 echo do_shortcode( $shortcode ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped … … 120 122 <label for="<?php echo esc_attr( $this->get_field_id('align') ); ?>"><?php esc_html_e('Align', 'weatherbot'); ?></label> 121 123 <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> 126 128 </select> 127 129 </p> … … 137 139 </p> 138 140 <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> 140 153 <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> 144 157 </select> 145 158 </p> … … 162 175 public function update( $new, $old ) { 163 176 $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'] ?? '' ); 168 181 $clean['unit'] = in_array( $new['unit'] ?? 'IMPERIAL', ['IMPERIAL','CELSIUS'], true ) ? $new['unit'] : 'IMPERIAL'; 169 182 $clean['type'] = in_array( $new['type'] ?? 'inline', ['inline','badge','compact'], true ) ? $new['type'] : 'inline'; 170 183 $clean['align'] = in_array( $new['align'] ?? '', ['', 'left','center','right'], true ) ? $new['align'] : ''; 171 184 $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'] ?? '' ); 173 186 $clean['font_color'] = in_array( $new['font_color'] ?? '', ['', 'light', 'dark'], true ) ? $new['font_color'] : ''; 174 187 $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'; 175 191 176 192 return $clean; 177 193 } 178 194 } 179 180 181 182 183 // EOF -
weatherbot/trunk/weatherbot.php
r3367429 r3367502 4 4 Plugin URI: https://roxxistudios.com/plugins/weatherbot 5 5 Description: 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. 06 Version: 1.1.1 7 7 Requires at least: 6.0 8 8 Requires PHP: 7.4 … … 38 38 * -------------------------------------------------------------------------- */ 39 39 if ( ! defined( 'RX_WEATHERBOT_VERSION' ) ) { 40 define( 'RX_WEATHERBOT_VERSION', '1.1. 0' );40 define( 'RX_WEATHERBOT_VERSION', '1.1.1' ); 41 41 } 42 42 if ( ! defined( 'RX_WEATHERBOT_FILE' ) ) {
Note: See TracChangeset
for help on using the changeset viewer.