v1.1.0
This commit is contained in:
parent
9b856d1c3d
commit
ddaaff71ad
23 changed files with 879 additions and 798 deletions
38
CHANGELOG.md
38
CHANGELOG.md
|
|
@ -1,38 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to OpenGraph (by ROBOTSTXT) will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2026-02-18
|
||||
|
||||
### Added
|
||||
- Automatic format detection for AVIF, WebP, GIF, BMP, SVG, and TIFF featured images
|
||||
- HTTP HEAD-based resolution of compatible JPEG/PNG alternatives
|
||||
- Postmeta caching (`_og_image_fallback_url`) with automatic invalidation on featured image change
|
||||
- Negative caching via transients (1-hour TTL) to avoid repeated failed HEAD requests
|
||||
- Open Graph meta tags: `og:image`, `og:image:secure_url`, `og:image:width`, `og:image:height`, `og:image:type`
|
||||
- Direct `wp_head` tag injection (when no SEO plugin is active)
|
||||
- Yoast SEO integration via `wpseo_opengraph_image` filter
|
||||
- RankMath integration via `rank_math/opengraph/facebook/og_image` filter
|
||||
- Global fallback image setting (configurable in Settings > OpenGraph)
|
||||
- Taxonomy archive image support via `robotstxt_og_taxonomy_image` filter
|
||||
- Admin settings page (Settings > OpenGraph) with three tabs: Settings, Tools, Diagnostics
|
||||
- WP-CLI commands: `wp og-fallback resolve` and `wp og-fallback clear-cache`
|
||||
- REST API: `POST /wp-json/robotstxt-og/v1/resolve/{post_id}` and `GET /wp-json/robotstxt-og/v1/status/{post_id}`
|
||||
- Developer filters: `robotstxt_og_external_image_enabled`, `robotstxt_og_external_image_timeout`, `robotstxt_og_taxonomy_image`, `robotstxt_og_enable_logging`
|
||||
- Auto-updater via Gitea (`robotstxt-updater.php`)
|
||||
- Multisite compatible with per-site configuration
|
||||
- Full internationalization support (text domain: `robotstxt-og`)
|
||||
- `uninstall.php` with optional data deletion on plugin removal
|
||||
|
||||
### Security
|
||||
- Nonce verification on all forms and state-changing actions
|
||||
- `manage_options` capability check on all admin pages, REST endpoints, and WP-CLI commands
|
||||
- Input sanitization via WordPress APIs throughout
|
||||
- Output escaping on all dynamic HTML
|
||||
- WordPress database APIs exclusively (no direct SQL writes; `$wpdb->prepare()` for reads)
|
||||
- CSRF protection on all state-changing actions
|
||||
|
||||
[1.0.0]: https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/releases/tag/1.0.0
|
||||
132
README.md
132
README.md
|
|
@ -1,132 +0,0 @@
|
|||
# OpenGraph (by ROBOTSTXT)
|
||||
|
||||
> Intelligent Open Graph image fallback for social media crawlers.
|
||||
|
||||
Automatically detects when a post's featured image uses a modern format (AVIF, WebP) that social media crawlers cannot render, and resolves a compatible JPEG/PNG alternative to use in `og:image` meta tags.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **WordPress:** 6.7+
|
||||
- **PHP:** 8.2–8.5
|
||||
- **WP-CLI:** 2.x (optional, for CLI tools)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# From the plugin directory
|
||||
composer install
|
||||
```
|
||||
|
||||
Activate the plugin through the WordPress admin or WP-CLI:
|
||||
|
||||
```bash
|
||||
wp plugin activate robotstxt-og --allow-root
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
robotstxt-og/
|
||||
├── robotstxt-og.php # Main plugin file, constants, bootstrap
|
||||
├── includes/
|
||||
│ ├── class-robotstxt-og-image-fallback.php # Singleton, orchestration
|
||||
│ ├── class-robotstxt-og-image-resolver.php # Image detection, HEAD requests, caching
|
||||
│ ├── class-robotstxt-og-tags.php # OG tag injection, SEO plugin filters
|
||||
│ ├── class-robotstxt-og-cli.php # WP-CLI commands
|
||||
│ └── class-robotstxt-og-rest-api.php # REST API endpoints
|
||||
├── admin/
|
||||
│ ├── class-robotstxt-og-admin-settings.php # Admin page, settings, cache actions
|
||||
│ └── views/settings-page.php # Settings page template (tabbed)
|
||||
├── assets/
|
||||
│ ├── admin.js # Media uploader integration
|
||||
│ └── admin.css # Admin styles
|
||||
├── languages/
|
||||
│ └── robotstxt-og.pot # Translation template
|
||||
├── docs/
|
||||
│ ├── FILTERS-HOOKS.md # Developer filter/hook reference
|
||||
│ ├── WP-CLI.md # WP-CLI command reference
|
||||
│ └── IDEA.md # Original concept document
|
||||
├── bin/
|
||||
│ └── deploy.sh # Build and ZIP packaging script
|
||||
├── robotstxt-updater.php # Auto-updater (Gitea-based)
|
||||
├── uninstall.php # Data cleanup on uninstall
|
||||
├── readme.txt # WordPress.org plugin readme
|
||||
└── changelog.txt # Full changelog (WordPress.org format)
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Code Standards
|
||||
|
||||
```bash
|
||||
# Lint
|
||||
vendor/bin/phpcs .
|
||||
|
||||
# Auto-fix
|
||||
vendor/bin/phpcbf .
|
||||
|
||||
# PHP compatibility check (8.2–8.5)
|
||||
vendor/bin/phpcs -p . --standard=PHPCompatibility --runtime-set testVersion 8.2-8.5
|
||||
```
|
||||
|
||||
### Regenerate translation template
|
||||
|
||||
```bash
|
||||
wp i18n make-pot . languages/robotstxt-og.pot --domain=robotstxt-og --exclude=vendor,node_modules,tests --allow-root
|
||||
```
|
||||
|
||||
### Build release ZIP
|
||||
|
||||
```bash
|
||||
bash bin/deploy.sh
|
||||
```
|
||||
|
||||
The ZIP is created at `../robotstxt-og-{version}.zip` (i.e. in `wp-content/plugins/`).
|
||||
|
||||
## WP-CLI Commands
|
||||
|
||||
```bash
|
||||
# Resolve fallback for a single post
|
||||
wp og-fallback resolve 123
|
||||
|
||||
# Re-resolve all posts with featured images
|
||||
wp og-fallback resolve --all
|
||||
|
||||
# Dry-run (no changes saved)
|
||||
wp og-fallback resolve --all --dry-run
|
||||
|
||||
# Clear cache for a single post
|
||||
wp og-fallback clear-cache 123
|
||||
|
||||
# Clear all caches
|
||||
wp og-fallback clear-cache --all
|
||||
```
|
||||
|
||||
## REST API
|
||||
|
||||
All endpoints require the `manage_options` capability.
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/wp-json/robotstxt-og/v1/resolve/{post_id}` | Force re-resolve fallback image |
|
||||
| `GET` | `/wp-json/robotstxt-og/v1/status/{post_id}` | Get current cache status |
|
||||
|
||||
## Filters
|
||||
|
||||
| Filter | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `robotstxt_og_external_image_enabled` | `true` | Enable/disable external image resolution |
|
||||
| `robotstxt_og_external_image_timeout` | `5` | HEAD request timeout (seconds) |
|
||||
| `robotstxt_og_taxonomy_image` | `''` | Image URL for taxonomy archive pages |
|
||||
| `robotstxt_og_enable_logging` | `false` | Enable debug logging to `debug.log` |
|
||||
|
||||
Full documentation: [`docs/FILTERS-HOOKS.md`](docs/FILTERS-HOOKS.md)
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0-or-later — see [LICENSE](https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
## Author
|
||||
|
||||
**ROBOTSTXT** — [robotstxt.es](https://www.robotstxt.es/)
|
||||
Contributors: javiercasares, robotstxt
|
||||
|
|
@ -54,6 +54,24 @@ class Robotstxt_OG_Admin_Settings {
|
|||
add_action( 'admin_init', array( $this, 'handle_resolve_all' ) );
|
||||
add_action( 'admin_init', array( $this, 'handle_clear_single' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
|
||||
|
||||
// Allow editors (edit_others_posts) to save settings via the Settings API.
|
||||
// options.php defaults to manage_options; this filter lowers it for our group.
|
||||
add_filter( 'option_page_capability_robotstxt_og_settings', array( $this, 'get_settings_capability' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the capability required to save plugin settings.
|
||||
*
|
||||
* Allows users with edit_others_posts (editors and above) to save plugin
|
||||
* settings through the WordPress Settings API (options.php).
|
||||
*
|
||||
* @since 1.0.3
|
||||
*
|
||||
* @return string Capability slug.
|
||||
*/
|
||||
public function get_settings_capability(): string {
|
||||
return 'edit_others_posts';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -77,7 +95,7 @@ class Robotstxt_OG_Admin_Settings {
|
|||
}
|
||||
|
||||
// Verify nonce.
|
||||
$nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_SPECIAL_CHARS );
|
||||
$nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||
$nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : '';
|
||||
|
||||
if ( ! wp_verify_nonce( $nonce, 'robotstxt_og_clear_single_' . $post_id ) ) {
|
||||
|
|
@ -85,7 +103,7 @@ class Robotstxt_OG_Admin_Settings {
|
|||
}
|
||||
|
||||
// Check permissions.
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
if ( ! current_user_can( 'edit_others_posts' ) ) {
|
||||
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-og' ) );
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +138,7 @@ class Robotstxt_OG_Admin_Settings {
|
|||
add_options_page(
|
||||
__( 'OpenGraph Settings', 'robotstxt-og' ),
|
||||
__( 'OpenGraph', 'robotstxt-og' ),
|
||||
'manage_options',
|
||||
'edit_others_posts',
|
||||
'robotstxt-og-settings',
|
||||
array( $this, 'render_settings_page' )
|
||||
);
|
||||
|
|
@ -344,7 +362,8 @@ class Robotstxt_OG_Admin_Settings {
|
|||
* @return void
|
||||
*/
|
||||
public function render_fallback_image_field(): void {
|
||||
$image_url = (string) get_option( 'robotstxt_og_fallback_image', '' );
|
||||
$option_value = get_option( 'robotstxt_og_fallback_image', '' );
|
||||
$image_url = is_string( $option_value ) ? $option_value : '';
|
||||
?>
|
||||
<input
|
||||
type="url"
|
||||
|
|
@ -368,7 +387,8 @@ class Robotstxt_OG_Admin_Settings {
|
|||
* @return void
|
||||
*/
|
||||
public function render_homepage_image_field(): void {
|
||||
$image_url = (string) get_option( 'robotstxt_og_homepage_image', '' );
|
||||
$option_value = get_option( 'robotstxt_og_homepage_image', '' );
|
||||
$image_url = is_string( $option_value ) ? $option_value : '';
|
||||
?>
|
||||
<input
|
||||
type="url"
|
||||
|
|
@ -492,7 +512,8 @@ class Robotstxt_OG_Admin_Settings {
|
|||
* @return void
|
||||
*/
|
||||
public function render_twitter_site_field(): void {
|
||||
$handle = (string) get_option( 'robotstxt_og_twitter_site', '' );
|
||||
$option_value = get_option( 'robotstxt_og_twitter_site', '' );
|
||||
$handle = is_string( $option_value ) ? $option_value : '';
|
||||
?>
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -517,7 +538,7 @@ class Robotstxt_OG_Admin_Settings {
|
|||
*/
|
||||
public function render_settings_page(): void {
|
||||
// Check user capabilities.
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
if ( ! current_user_can( 'edit_others_posts' ) ) {
|
||||
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-og' ) );
|
||||
}
|
||||
|
||||
|
|
@ -534,13 +555,13 @@ class Robotstxt_OG_Admin_Settings {
|
|||
*/
|
||||
public function handle_clear_cache(): void {
|
||||
// Check if this is a cache clear request.
|
||||
$clear_cache = filter_input( INPUT_GET, 'robotstxt_og_clear_cache', FILTER_SANITIZE_SPECIAL_CHARS );
|
||||
$clear_cache = filter_input( INPUT_GET, 'robotstxt_og_clear_cache', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||
if ( null === $clear_cache ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify nonce.
|
||||
$nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_SPECIAL_CHARS );
|
||||
$nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||
$nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : '';
|
||||
|
||||
if ( ! wp_verify_nonce( $nonce, 'robotstxt_og_clear_cache' ) ) {
|
||||
|
|
@ -548,7 +569,7 @@ class Robotstxt_OG_Admin_Settings {
|
|||
}
|
||||
|
||||
// Check permissions.
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
if ( ! current_user_can( 'edit_others_posts' ) ) {
|
||||
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-og' ) );
|
||||
}
|
||||
|
||||
|
|
@ -581,13 +602,13 @@ class Robotstxt_OG_Admin_Settings {
|
|||
*/
|
||||
public function handle_resolve_all(): void {
|
||||
// Check if this is a resolve all request.
|
||||
$resolve_all = filter_input( INPUT_GET, 'robotstxt_og_resolve_all', FILTER_SANITIZE_SPECIAL_CHARS );
|
||||
$resolve_all = filter_input( INPUT_GET, 'robotstxt_og_resolve_all', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||
if ( null === $resolve_all ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify nonce.
|
||||
$nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_SPECIAL_CHARS );
|
||||
$nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||
$nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : '';
|
||||
|
||||
if ( ! wp_verify_nonce( $nonce, 'robotstxt_og_resolve_all' ) ) {
|
||||
|
|
@ -595,7 +616,7 @@ class Robotstxt_OG_Admin_Settings {
|
|||
}
|
||||
|
||||
// Check permissions.
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
if ( ! current_user_can( 'edit_others_posts' ) ) {
|
||||
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-og' ) );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'settings';
|
||||
$tab_raw = isset( $_GET['tab'] ) && is_string( $_GET['tab'] ) ? $_GET['tab'] : '';
|
||||
$active_tab = $tab_raw ? sanitize_key( wp_unslash( $tab_raw ) ) : 'settings';
|
||||
$valid_tabs = array( 'settings', 'tools', 'diagnostics' );
|
||||
|
||||
if ( ! in_array( $active_tab, $valid_tabs, true ) ) {
|
||||
|
|
@ -174,7 +175,8 @@ $page_url = admin_url( 'options-general.php?page=robotstxt-og-settings' );
|
|||
|
||||
$posts_per_page = 20;
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$current_page = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1;
|
||||
$paged_raw = isset( $_GET['paged'] ) && is_string( $_GET['paged'] ) ? $_GET['paged'] : '';
|
||||
$current_page = $paged_raw ? max( 1, absint( $paged_raw ) ) : 1;
|
||||
$offset = ( $current_page - 1 ) * $posts_per_page;
|
||||
$total_pages = (int) ceil( $total_cached / $posts_per_page );
|
||||
|
||||
|
|
@ -237,7 +239,8 @@ $page_url = admin_url( 'options-general.php?page=robotstxt-og-settings' );
|
|||
<td>
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$test_url_raw = isset( $_GET['test_url'] ) ? esc_url_raw( wp_unslash( $_GET['test_url'] ) ) : '';
|
||||
$test_url_input = isset( $_GET['test_url'] ) && is_string( $_GET['test_url'] ) ? $_GET['test_url'] : '';
|
||||
$test_url_raw = $test_url_input ? esc_url_raw( wp_unslash( $test_url_input ) ) : '';
|
||||
?>
|
||||
<input
|
||||
type="url"
|
||||
|
|
@ -289,7 +292,14 @@ $page_url = admin_url( 'options-general.php?page=robotstxt-og-settings' );
|
|||
echo '<th>' . esc_html__( 'Value', 'robotstxt-og' ) . '</th></tr></thead><tbody>';
|
||||
foreach ( $headers as $header_key => $header_value ) {
|
||||
echo '<tr><td><code>' . esc_html( $header_key ) . '</code></td>';
|
||||
echo '<td>' . esc_html( is_array( $header_value ) ? implode( ', ', $header_value ) : $header_value ) . '</td></tr>';
|
||||
if ( is_array( $header_value ) ) {
|
||||
$display_value = implode( ', ', array_map( static fn( $v ) => is_scalar( $v ) ? (string) $v : '', $header_value ) );
|
||||
} elseif ( is_string( $header_value ) ) {
|
||||
$display_value = $header_value;
|
||||
} else {
|
||||
$display_value = '';
|
||||
}
|
||||
echo '<td>' . esc_html( $display_value ) . '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
|
@ -324,7 +334,7 @@ $page_url = admin_url( 'options-general.php?page=robotstxt-og-settings' );
|
|||
<tr>
|
||||
<td><?php echo absint( $cached_entry->post_id ); ?></td>
|
||||
<td>
|
||||
<a href="<?php echo esc_url( get_edit_post_link( $cached_entry->post_id ) ); ?>">
|
||||
<a href="<?php echo esc_url( get_edit_post_link( $cached_entry->post_id ) ?? '' ); ?>">
|
||||
<?php echo esc_html( $cached_entry->post_title ); ?>
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,39 @@
|
|||
== Changelog ==
|
||||
|
||||
= 1.0.1 =
|
||||
= 1.1.0 =
|
||||
|
||||
_Release date: 2026-03-05_
|
||||
_Release date: 2026-03-28_
|
||||
|
||||
**Compatibility**
|
||||
**Fixed**
|
||||
|
||||
* Confirmed compatibility with WordPress 7.0. Supported range is now WordPress 6.7 – 7.0.
|
||||
* Fatal `TypeError` in `handle_thumbnail_change()`: the `deleted_post_meta` action passes an array of meta IDs as its first argument (unlike `updated_post_meta`, which passes a single `int`). The method signature now accepts `int|array` to handle both actions correctly.
|
||||
|
||||
**Security**
|
||||
|
||||
* Added SSRF protection in `Robotstxt_OG_Image_Resolver::is_safe_url()`: all outbound HTTP HEAD requests now validate that the target host resolves to a public IP address. Private (RFC 1918), loopback, link-local, and reserved ranges are blocked.
|
||||
* Replaced deprecated `FILTER_SANITIZE_SPECIAL_CHARS` with `FILTER_SANITIZE_FULL_SPECIAL_CHARS` in nonce and input handling throughout admin and meta box classes.
|
||||
|
||||
**Added**
|
||||
|
||||
* GDPR Privacy API: registered `wp_privacy_personal_data_exporters` and `wp_privacy_personal_data_erasers` filters. Custom `_og_title` and `_og_description` post meta are now included in WordPress personal data export and erase requests.
|
||||
* PHPUnit 10 test suite using Brain\Monkey for WordPress function mocking. Covers SSRF IP validation, image format detection (`ensure_compatible_format()`), external URL detection, and cache clearing. No WordPress installation required to run tests (`vendor/bin/phpunit`).
|
||||
|
||||
**Changed**
|
||||
|
||||
* Settings page and REST API permission check changed from `manage_options` to `edit_others_posts`. Editors and above can now access OG settings and trigger fallback image resolution. The WordPress Settings API form is unlocked via the `option_page_capability_robotstxt_og_settings` filter.
|
||||
* REST API `check_permission()` now also verifies `edit_post` for the specific post ID requested.
|
||||
* Minimum supported WordPress version raised from 6.7 to 6.8 (two previous major versions back from WordPress 7.0).
|
||||
* Added `Network: true` plugin header, formally documenting Multisite compatibility.
|
||||
|
||||
**Tests**
|
||||
|
||||
* WordPress: 6.8, 6.9, 7.0
|
||||
* PHP: 8.2, 8.5
|
||||
* PHP_CodeSniffer: 3.13.5
|
||||
* WordPress Coding Standards: 3.3.0
|
||||
* PHPCompatibility: 9.3.5
|
||||
* PHPStan: level 9
|
||||
* PHPUnit: 10.5
|
||||
|
||||
= 1.0.0 =
|
||||
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
# Filters & Hooks Reference
|
||||
|
||||
Developer reference for all filters and actions provided by the **OpenGraph (by ROBOTSTXT)** plugin.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Filters](#filters)
|
||||
- [robotstxt_og_external_image_enabled](#robotstxt_og_external_image_enabled)
|
||||
- [robotstxt_og_external_image_timeout](#robotstxt_og_external_image_timeout)
|
||||
- [robotstxt_og_taxonomy_image](#robotstxt_og_taxonomy_image)
|
||||
- [robotstxt_og_enable_logging](#robotstxt_og_enable_logging)
|
||||
- [Actions](#actions)
|
||||
- [SEO Plugin Integrations](#seo-plugin-integrations)
|
||||
|
||||
---
|
||||
|
||||
## Filters
|
||||
|
||||
### `robotstxt_og_external_image_enabled`
|
||||
|
||||
Controls whether the plugin attempts to resolve fallback images for external URLs (images hosted on a different domain than the WordPress site).
|
||||
|
||||
**Default:** `true`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `$enabled` | `bool` | Whether external image resolution is enabled. |
|
||||
| `$image_url` | `string` | The external image URL being evaluated. |
|
||||
|
||||
**Returns:** `bool`
|
||||
|
||||
**Example — disable external image resolution entirely:**
|
||||
|
||||
```php
|
||||
add_filter( 'robotstxt_og_external_image_enabled', '__return_false' );
|
||||
```
|
||||
|
||||
**Example — disable only for a specific CDN domain:**
|
||||
|
||||
```php
|
||||
add_filter( 'robotstxt_og_external_image_enabled', function ( bool $enabled, string $image_url ): bool {
|
||||
if ( str_contains( $image_url, 'cdn.example.com' ) ) {
|
||||
return false;
|
||||
}
|
||||
return $enabled;
|
||||
}, 10, 2 );
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `robotstxt_og_external_image_timeout`
|
||||
|
||||
Sets the HTTP request timeout (in seconds) used when verifying whether a fallback image URL exists via a HEAD request.
|
||||
|
||||
**Default:** `5` (seconds)
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `$timeout` | `int` | Timeout in seconds for the HEAD request. |
|
||||
| `$url` | `string` | The image URL being tested. |
|
||||
|
||||
**Returns:** `int`
|
||||
|
||||
**Example — increase timeout for slow external servers:**
|
||||
|
||||
```php
|
||||
add_filter( 'robotstxt_og_external_image_timeout', function ( int $timeout, string $url ): int {
|
||||
if ( str_contains( $url, 'slow-cdn.example.com' ) ) {
|
||||
return 15;
|
||||
}
|
||||
return $timeout;
|
||||
}, 10, 2 );
|
||||
```
|
||||
|
||||
**Example — set a global lower timeout for performance:**
|
||||
|
||||
```php
|
||||
add_filter( 'robotstxt_og_external_image_timeout', function (): int {
|
||||
return 3;
|
||||
} );
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `robotstxt_og_taxonomy_image`
|
||||
|
||||
Provides a fallback OG image URL for taxonomy archive pages (categories, tags, custom taxonomies). By default, taxonomy archives do not have a featured image, so this filter is the primary way to supply one.
|
||||
|
||||
**Default:** `''` (empty string — no image)
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `$image_url` | `string` | Image URL to use. Empty string by default. |
|
||||
| `$term_id` | `int` | The term ID of the current taxonomy archive. |
|
||||
|
||||
**Returns:** `string` A valid image URL, or empty string to skip.
|
||||
|
||||
**Example — use a custom field set on the term:**
|
||||
|
||||
```php
|
||||
add_filter( 'robotstxt_og_taxonomy_image', function ( string $image_url, int $term_id ): string {
|
||||
$custom_image_id = get_term_meta( $term_id, 'og_image_id', true );
|
||||
|
||||
if ( $custom_image_id ) {
|
||||
$url = wp_get_attachment_url( (int) $custom_image_id );
|
||||
return $url ? $url : $image_url;
|
||||
}
|
||||
|
||||
return $image_url;
|
||||
}, 10, 2 );
|
||||
```
|
||||
|
||||
**Example — use a WooCommerce category thumbnail:**
|
||||
|
||||
```php
|
||||
add_filter( 'robotstxt_og_taxonomy_image', function ( string $image_url, int $term_id ): string {
|
||||
$thumbnail_id = get_term_meta( $term_id, 'thumbnail_id', true );
|
||||
|
||||
if ( $thumbnail_id ) {
|
||||
$url = wp_get_attachment_url( (int) $thumbnail_id );
|
||||
return $url ? $url : $image_url;
|
||||
}
|
||||
|
||||
return $image_url;
|
||||
}, 10, 2 );
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `robotstxt_og_enable_logging`
|
||||
|
||||
Enables or disables debug logging to `wp-content/debug.log`. When enabled, resolution events (cache hits, cache misses, format detection, HEAD request results) are written to the error log.
|
||||
|
||||
**Default:** `false`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `$enabled` | `bool` | Whether debug logging is active. |
|
||||
|
||||
**Returns:** `bool`
|
||||
|
||||
**Note:** Requires `WP_DEBUG` and `WP_DEBUG_LOG` to be enabled in `wp-config.php` for output to appear in `debug.log`.
|
||||
|
||||
**Example — enable logging (e.g. during development, in `wp-config.php`):**
|
||||
|
||||
```php
|
||||
// wp-config.php
|
||||
define( 'WP_DEBUG', true );
|
||||
define( 'WP_DEBUG_LOG', true );
|
||||
```
|
||||
|
||||
```php
|
||||
// functions.php or a mu-plugin
|
||||
add_filter( 'robotstxt_og_enable_logging', '__return_true' );
|
||||
```
|
||||
|
||||
**Example — enable logging only for specific users:**
|
||||
|
||||
```php
|
||||
add_filter( 'robotstxt_og_enable_logging', function ( bool $enabled ): bool {
|
||||
return current_user_can( 'manage_options' ) ? true : $enabled;
|
||||
} );
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Actions
|
||||
|
||||
The plugin does not currently expose custom action hooks. WordPress core hooks used internally include:
|
||||
|
||||
| Hook | Context | Purpose |
|
||||
|------|---------|---------|
|
||||
| `plugins_loaded` | Global | Loads text domain for translations. |
|
||||
| `wp_head` | Frontend | Injects `og:image` meta tags (only when no SEO plugin is active). |
|
||||
| `admin_menu` | Admin | Registers the Settings > OpenGraph settings page. |
|
||||
| `admin_init` | Admin | Registers settings, handles cache clear/resolve actions. |
|
||||
| `admin_enqueue_scripts` | Admin | Enqueues media uploader and admin CSS/JS. |
|
||||
| `rest_api_init` | REST API | Registers the `robotstxt-og/v1` REST endpoints. |
|
||||
| `updated_post_meta` | Global | Auto-clears fallback cache when `_thumbnail_id` changes. |
|
||||
| `deleted_post_meta` | Global | Auto-clears fallback cache when `_thumbnail_id` is removed. |
|
||||
|
||||
---
|
||||
|
||||
## SEO Plugin Integrations
|
||||
|
||||
When a supported SEO plugin is detected, the plugin switches from direct `og:image` tag injection to filtering the SEO plugin's output. This prevents duplicate meta tags.
|
||||
|
||||
### Yoast SEO
|
||||
|
||||
**Filter:** `wpseo_opengraph_image`
|
||||
|
||||
When Yoast SEO is active (`WPSEO_VERSION` is defined), the plugin hooks into this filter to provide the resolved fallback image. The plugin only overrides the value if a valid fallback URL is resolved; otherwise it returns the original Yoast value unchanged.
|
||||
|
||||
### RankMath
|
||||
|
||||
**Filter:** `rank_math/opengraph/facebook/og_image`
|
||||
|
||||
When RankMath is active (`RankMath` class exists), the plugin hooks into this filter with the same logic as the Yoast integration.
|
||||
|
||||
### Adding Support for Other SEO Plugins
|
||||
|
||||
To integrate with another SEO plugin, hook into the plugin's OG image filter and call the resolver manually:
|
||||
|
||||
```php
|
||||
add_filter( 'your_seo_plugin_og_image_filter', function ( string $image ) : string {
|
||||
if ( ! is_singular() ) {
|
||||
return $image;
|
||||
}
|
||||
|
||||
$post_id = get_queried_object_id();
|
||||
$resolver = Robotstxt_OG_Image_Fallback::get_instance()->get_resolver();
|
||||
$fallback = $resolver->get_fallback_image( $post_id );
|
||||
|
||||
return ! empty( $fallback ) ? $fallback : $image;
|
||||
} );
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Postmeta Keys (Internal Cache)
|
||||
|
||||
These postmeta keys are used internally for caching and should not be modified directly:
|
||||
|
||||
| Meta Key | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `_og_image_fallback_url` | `string` | Cached resolved fallback image URL for a post. |
|
||||
|
||||
These transient keys are used for negative caching (failed HEAD request results):
|
||||
|
||||
| Transient Key Pattern | TTL | Description |
|
||||
|-----------------------|-----|-------------|
|
||||
| `robotstxt_og_miss_{md5_of_url}` | 1 hour | Marks a URL as unreachable to prevent repeated requests. |
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
# Open Graph & Twitter Cards — Reference
|
||||
|
||||
This document covers the Open Graph (OG) and Twitter Cards meta tag specifications relevant to this plugin, including which tags are mandatory, recommended, optional, and which this plugin outputs automatically.
|
||||
|
||||
---
|
||||
|
||||
## Open Graph Protocol
|
||||
|
||||
Defined by Facebook/Meta. All OG tags use the `property` attribute.
|
||||
|
||||
### Core Tags (og: namespace)
|
||||
|
||||
| Property | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `og:title` | string | **Required** | Title of the content. Used by all social crawlers. |
|
||||
| `og:type` | string | **Required** | Content type: `website`, `article`, `video.movie`, etc. |
|
||||
| `og:url` | URL | **Required** | Canonical URL of the page. |
|
||||
| `og:description` | string | Recommended | Short description (2–4 sentences). Max ~300 chars. |
|
||||
| `og:site_name` | string | Recommended | Name of the overall site (e.g. "My Blog"). |
|
||||
| `og:locale` | string | Recommended | Locale in `language_TERRITORY` format (e.g. `es_ES`). |
|
||||
| `og:image` | URL | **Required** (for cards) | Must be JPEG or PNG for social crawler compatibility. Min 200×200 px. Recommended 1200×630 px. |
|
||||
| `og:image:secure_url` | URL | Optional | HTTPS version of `og:image`. Same value when the site is HTTPS-only. |
|
||||
| `og:image:type` | MIME type | Recommended | MIME type of the image (`image/jpeg`, `image/png`). |
|
||||
| `og:image:width` | integer | Recommended | Width in pixels. Avoids reflow in crawler previews. |
|
||||
| `og:image:height` | integer | Recommended | Height in pixels. |
|
||||
| `og:image:alt` | string | Recommended | Alt text for the image. Required for accessibility audits. |
|
||||
|
||||
### Article Tags (article: namespace)
|
||||
|
||||
Used when `og:type = article`. All are optional but recommended for news/blog content.
|
||||
|
||||
| Property | Type | Notes |
|
||||
|---|---|---|
|
||||
| `article:published_time` | ISO 8601 datetime | Publication date (`c` format in PHP: `get_the_date('c')`). |
|
||||
| `article:modified_time` | ISO 8601 datetime | Last modification date. |
|
||||
| `article:author` | URL | Profile page of the author (Facebook profile URL). Often omitted. |
|
||||
| `article:section` | string | Primary category or section (e.g. "Technology"). |
|
||||
| `article:tag` | string | Topic tags. Can be repeated once per tag. |
|
||||
| `article:expiration_time` | ISO 8601 datetime | When the article expires (rarely used). |
|
||||
|
||||
### Video Tags (video: namespace)
|
||||
|
||||
For `og:type = video.movie`, `video.episode`, etc. Out of scope for this plugin.
|
||||
|
||||
---
|
||||
|
||||
## Twitter Cards
|
||||
|
||||
Defined by X (formerly Twitter). Tags use the `name` attribute (not `property`).
|
||||
|
||||
Twitter falls back to `og:*` tags if the corresponding `twitter:*` tag is absent — **except** `twitter:card`, which is always required.
|
||||
|
||||
### Card Types
|
||||
|
||||
| Value | Description |
|
||||
|---|---|
|
||||
| `summary` | Small square image (minimum 144×144 px). |
|
||||
| `summary_large_image` | Large rectangular image (minimum 300×157 px, recommended 1200×628 px). Most common for blog/news content. |
|
||||
| `app` | Promotes a mobile app. |
|
||||
| `player` | Embeds a video/audio player. |
|
||||
|
||||
### Twitter Tags
|
||||
|
||||
| Name | Required | Falls back to | Notes |
|
||||
|---|---|---|---|
|
||||
| `twitter:card` | **Required** | — | Must always be present. Without it, no Twitter Card is shown. |
|
||||
| `twitter:site` | Recommended | — | `@username` of the site's Twitter/X account. |
|
||||
| `twitter:creator` | Optional | — | `@username` of the content author. |
|
||||
| `twitter:title` | Recommended | `og:title` | Title of the content. |
|
||||
| `twitter:description` | Recommended | `og:description` | Description. Max 200 chars. |
|
||||
| `twitter:image` | Recommended | `og:image` | Must be JPEG, PNG, WebP, or GIF. Max 5 MB. |
|
||||
| `twitter:image:alt` | Recommended | `og:image:alt` | Alt text for the image. Max 420 chars. |
|
||||
|
||||
---
|
||||
|
||||
## What This Plugin Outputs
|
||||
|
||||
### When no SEO plugin is active (direct injection)
|
||||
|
||||
The plugin outputs a **complete** set of OG and Twitter Card tags via `wp_head` (priority 5).
|
||||
|
||||
#### Open Graph tags
|
||||
|
||||
```html
|
||||
<!-- Core -->
|
||||
<meta property="og:title" content="..." />
|
||||
<meta property="og:type" content="article" /> <!-- or "website" -->
|
||||
<meta property="og:url" content="..." />
|
||||
<meta property="og:description" content="..." /> <!-- when available -->
|
||||
<meta property="og:site_name" content="..." />
|
||||
<meta property="og:locale" content="es_ES" />
|
||||
|
||||
<!-- Image (when a compatible image is resolved) -->
|
||||
<meta property="og:image" content="https://...jpg" />
|
||||
<meta property="og:image:secure_url" content="https://...jpg" /> <!-- HTTPS only -->
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
<meta property="og:image:alt" content="..." /> <!-- when set in media library -->
|
||||
|
||||
<!-- Article-specific (og:type = article only) -->
|
||||
<meta property="article:published_time" content="2026-02-18T00:00:00+00:00" />
|
||||
<meta property="article:modified_time" content="2026-02-18T00:00:00+00:00" />
|
||||
<meta property="article:section" content="Technology" /> <!-- primary category -->
|
||||
<meta property="article:tag" content="WordPress" /> <!-- repeated per tag -->
|
||||
```
|
||||
|
||||
#### Twitter Card tags
|
||||
|
||||
```html
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@example" /> <!-- when configured in Settings -->
|
||||
<meta name="twitter:image" content="https://...jpg" /> <!-- when image available -->
|
||||
```
|
||||
|
||||
> Twitter falls back to `og:title`, `og:description`, and `og:image` automatically, so those tags are not duplicated.
|
||||
|
||||
### When Yoast SEO or RankMath is active
|
||||
|
||||
The plugin acts as a **corrector only**: it filters the image URL via the SEO plugin's filter hook, converting incompatible formats (AVIF/WebP) to JPEG/PNG. All other OG/Twitter tags are managed by the SEO plugin.
|
||||
|
||||
---
|
||||
|
||||
## og:type Values Reference
|
||||
|
||||
| Value | When to use |
|
||||
|---|---|
|
||||
| `website` | Default for homepages and most pages. |
|
||||
| `article` | Blog posts, news articles. This plugin uses this for `is_singular('post')`. |
|
||||
| `profile` | User profile pages. |
|
||||
| `video.movie` | Movie pages. |
|
||||
| `video.episode` | TV episode pages. |
|
||||
| `music.song` | Song pages. |
|
||||
| `music.album` | Album pages. |
|
||||
|
||||
---
|
||||
|
||||
## Per-Post Overrides (Editor Meta Box)
|
||||
|
||||
The plugin adds an **"Open Graph / Social Media"** meta box to all post editors, allowing per-post overrides of:
|
||||
|
||||
| Field | OG Tag | Fallback |
|
||||
|---|---|---|
|
||||
| Custom Title | `og:title` | Post title (`get_the_title()`) |
|
||||
| Custom Description | `og:description` | Post excerpt, or empty |
|
||||
|
||||
These overrides are stored as post meta:
|
||||
- `_og_title` — custom OG title
|
||||
- `_og_description` — custom OG description
|
||||
|
||||
---
|
||||
|
||||
## Context → og:type Mapping (this plugin)
|
||||
|
||||
| WordPress context | `og:type` |
|
||||
|---|---|
|
||||
| `is_singular('post')` | `article` |
|
||||
| `is_singular('page')` | `website` |
|
||||
| `is_singular(other)` | `website` |
|
||||
| `is_front_page()` / `is_home()` | `website` |
|
||||
| `is_tax()` / `is_category()` / `is_tag()` | `website` |
|
||||
|
||||
---
|
||||
|
||||
## Image Compatibility
|
||||
|
||||
Social crawlers (Facebook, X, LinkedIn, WhatsApp, Telegram) generally require:
|
||||
- Format: **JPEG or PNG** (WebP partial support; AVIF not supported)
|
||||
- Minimum size: 200×200 px (Facebook requires 200×200 for `summary`)
|
||||
- Recommended: 1200×630 px for `summary_large_image`
|
||||
- Max file size: 8 MB (Facebook), 5 MB (Twitter)
|
||||
|
||||
This plugin's core function is to detect when a featured image is in an incompatible format (AVIF, WebP) and automatically serve a JPEG/PNG alternative via fallback resolution.
|
||||
|
||||
---
|
||||
|
||||
## Validation Tools
|
||||
|
||||
- **Facebook**: [Sharing Debugger](https://developers.facebook.com/tools/debug/)
|
||||
- **Twitter/X**: [Card Validator](https://cards-dev.twitter.com/validator)
|
||||
- **LinkedIn**: [Post Inspector](https://www.linkedin.com/post-inspector/)
|
||||
- **OpenGraph.xyz**: [OpenGraph preview](https://www.opengraph.xyz/)
|
||||
- **Metatags.io**: [Meta tag preview](https://metatags.io/)
|
||||
103
docs/WP-CLI.md
103
docs/WP-CLI.md
|
|
@ -1,103 +0,0 @@
|
|||
# WP-CLI Command Reference
|
||||
|
||||
The plugin registers WP-CLI commands under the `og-fallback` namespace.
|
||||
|
||||
## Commands
|
||||
|
||||
### `wp og-fallback resolve`
|
||||
|
||||
Resolves (or re-resolves) the OG fallback image for one or all posts with featured images.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
wp og-fallback resolve [<post_id>] [--all] [--dry-run] [--post-type=<type>]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `<post_id>` | (optional) Single post ID to resolve. |
|
||||
| `--all` | Process all posts that have a featured image. |
|
||||
| `--dry-run` | Preview without making any changes. |
|
||||
| `--post-type=<type>` | Limit `--all` to a specific post type (default: `any`). |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Resolve fallback for a single post
|
||||
wp og-fallback resolve 123
|
||||
|
||||
# Preview for a single post without saving
|
||||
wp og-fallback resolve 123 --dry-run
|
||||
|
||||
# Re-resolve all posts (clears cache first)
|
||||
wp og-fallback resolve --all
|
||||
|
||||
# Re-resolve only 'product' post type posts
|
||||
wp og-fallback resolve --all --post-type=product
|
||||
|
||||
# Dry-run all (shows count, no changes)
|
||||
wp og-fallback resolve --all --dry-run
|
||||
```
|
||||
|
||||
**Output examples:**
|
||||
|
||||
```
|
||||
Success: Post 123 resolved to https://example.com/uploads/image.jpg
|
||||
Warning: Post 456: no compatible image found.
|
||||
Success: Resolved 47 posts. Failed: 2.
|
||||
Found 49 posts with featured images. (dry-run, no changes made)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `wp og-fallback clear-cache`
|
||||
|
||||
Deletes cached fallback URLs from postmeta.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
wp og-fallback clear-cache [<post_id>] [--all] [--dry-run]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `<post_id>` | (optional) Single post ID to clear cache for. |
|
||||
| `--all` | Clear all cached fallback URLs. |
|
||||
| `--dry-run` | Preview without making any changes. |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Clear cache for a single post
|
||||
wp og-fallback clear-cache 123
|
||||
|
||||
# Clear all cached fallback URLs
|
||||
wp og-fallback clear-cache --all
|
||||
|
||||
# Preview how many entries would be cleared
|
||||
wp og-fallback clear-cache --all --dry-run
|
||||
```
|
||||
|
||||
**Output examples:**
|
||||
|
||||
```
|
||||
Success: Cleared cached fallback URL for post 123.
|
||||
Warning: Post 456 has no cached fallback URL.
|
||||
Success: Cleared 47 cached fallback URLs.
|
||||
Found 47 cached fallback URLs. (dry-run, no changes made)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Both commands require WP-CLI 2.x.
|
||||
- No capability check is enforced at the CLI level (WP-CLI access implies server-level trust).
|
||||
- The `--all` flag with `resolve` is equivalent to clicking **Re-resolve All Images** in the admin Tools tab.
|
||||
- The `--all` flag with `clear-cache` is equivalent to clicking **Clear All Cached URLs** in the admin Tools tab.
|
||||
|
|
@ -87,14 +87,14 @@ class Robotstxt_OG_CLI extends WP_CLI_Command {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param array $args Positional arguments.
|
||||
* @param array $assoc_args Associative arguments.
|
||||
* @param array<int, string> $args Positional arguments.
|
||||
* @param array<string, string|bool> $assoc_args Associative arguments.
|
||||
* @return void
|
||||
*/
|
||||
public function resolve( array $args, array $assoc_args ): void {
|
||||
$all = isset( $assoc_args['all'] );
|
||||
$dry_run = isset( $assoc_args['dry-run'] );
|
||||
$post_type = isset( $assoc_args['post-type'] ) ? $assoc_args['post-type'] : 'any';
|
||||
$post_type = ( isset( $assoc_args['post-type'] ) && is_string( $assoc_args['post-type'] ) ) ? $assoc_args['post-type'] : 'any';
|
||||
|
||||
if ( $all ) {
|
||||
$this->resolve_all( $dry_run, $post_type );
|
||||
|
|
@ -132,8 +132,8 @@ class Robotstxt_OG_CLI extends WP_CLI_Command {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param array $args Positional arguments.
|
||||
* @param array $assoc_args Associative arguments.
|
||||
* @param array<int, string> $args Positional arguments.
|
||||
* @param array<string, string|bool> $assoc_args Associative arguments.
|
||||
* @return void
|
||||
*/
|
||||
public function clear_cache( array $args, array $assoc_args ): void {
|
||||
|
|
@ -320,7 +320,7 @@ class Robotstxt_OG_CLI extends WP_CLI_Command {
|
|||
|
||||
if ( $dry_run ) {
|
||||
/* translators: 1: post ID, 2: cached URL or 'none' */
|
||||
WP_CLI::line( sprintf( __( 'Post %1$d cached URL: %2$s (dry-run, no changes made)', 'robotstxt-og' ), $post_id, ! empty( $cached ) ? $cached : 'none' ) );
|
||||
WP_CLI::line( sprintf( __( 'Post %1$d cached URL: %2$s (dry-run, no changes made)', 'robotstxt-og' ), $post_id, ( is_string( $cached ) && ! empty( $cached ) ) ? $cached : 'none' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,6 +136,10 @@ class Robotstxt_OG_Image_Fallback {
|
|||
add_action( 'updated_post_meta', array( $this, 'handle_thumbnail_change' ), 10, 4 );
|
||||
add_action( 'deleted_post_meta', array( $this, 'handle_thumbnail_change' ), 10, 4 );
|
||||
|
||||
// Register GDPR privacy data handlers.
|
||||
add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_privacy_exporter' ) );
|
||||
add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_privacy_eraser' ) );
|
||||
|
||||
// Register activation and deactivation hooks.
|
||||
register_activation_hook( ROBOTSTXT_OG_PATH . 'robotstxt-og.php', array( $this, 'activate' ) );
|
||||
register_deactivation_hook( ROBOTSTXT_OG_PATH . 'robotstxt-og.php', array( $this, 'deactivate' ) );
|
||||
|
|
@ -209,13 +213,13 @@ class Robotstxt_OG_Image_Fallback {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param int $meta_id ID of the meta data entry.
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key being updated.
|
||||
* @param mixed $meta_value New meta value (unused).
|
||||
* @param int|int[] $meta_id ID or array of IDs of the meta data entry.
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key being updated.
|
||||
* @param mixed $meta_value New meta value (unused).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_thumbnail_change( int $meta_id, int $post_id, string $meta_key, $meta_value = null ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
public function handle_thumbnail_change( int|array $meta_id, int $post_id, string $meta_key, $meta_value = null ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
if ( '_thumbnail_id' !== $meta_key ) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -223,6 +227,171 @@ class Robotstxt_OG_Image_Fallback {
|
|||
$this->resolver->clear_cache( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the personal data exporter.
|
||||
*
|
||||
* @since 1.0.3
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $exporters List of exporters.
|
||||
* @return array<int, array<string, mixed>> Updated exporters list.
|
||||
*/
|
||||
public function register_privacy_exporter( array $exporters ): array {
|
||||
$exporters[] = array(
|
||||
'exporter_friendly_name' => __( 'OpenGraph Custom Post Data', 'robotstxt-og' ),
|
||||
'callback' => array( $this, 'export_privacy_data' ),
|
||||
);
|
||||
return $exporters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the personal data eraser.
|
||||
*
|
||||
* @since 1.0.3
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $erasers List of erasers.
|
||||
* @return array<int, array<string, mixed>> Updated erasers list.
|
||||
*/
|
||||
public function register_privacy_eraser( array $erasers ): array {
|
||||
$erasers[] = array(
|
||||
'eraser_friendly_name' => __( 'OpenGraph Custom Post Data', 'robotstxt-og' ),
|
||||
'callback' => array( $this, 'erase_privacy_data' ),
|
||||
);
|
||||
return $erasers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export personal data stored by this plugin for a given email address.
|
||||
*
|
||||
* Exports the custom og:title and og:description stored in post meta for
|
||||
* all posts authored by the user with the given email address.
|
||||
*
|
||||
* @since 1.0.3
|
||||
*
|
||||
* @param string $email_address User email address.
|
||||
* @param int $page Pagination page (1-based).
|
||||
* @return array{data: array<int, array<string, mixed>>, done: bool} Export result.
|
||||
*/
|
||||
public function export_privacy_data( string $email_address, int $page = 1 ): array {
|
||||
$user = get_user_by( 'email', $email_address );
|
||||
|
||||
if ( ! $user ) {
|
||||
return array(
|
||||
'data' => array(),
|
||||
'done' => true,
|
||||
);
|
||||
}
|
||||
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'author' => $user->ID,
|
||||
'post_type' => 'any',
|
||||
'post_status' => 'any',
|
||||
'posts_per_page' => 100,
|
||||
'paged' => $page,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
$data = array();
|
||||
|
||||
foreach ( $posts as $post_id ) {
|
||||
$og_title = get_post_meta( $post_id, '_og_title', true );
|
||||
$og_desc = get_post_meta( $post_id, '_og_description', true );
|
||||
|
||||
if ( empty( $og_title ) && empty( $og_desc ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item_data = array();
|
||||
|
||||
if ( ! empty( $og_title ) && is_string( $og_title ) ) {
|
||||
$item_data[] = array(
|
||||
'name' => __( 'Custom OG Title', 'robotstxt-og' ),
|
||||
'value' => $og_title,
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! empty( $og_desc ) && is_string( $og_desc ) ) {
|
||||
$item_data[] = array(
|
||||
'name' => __( 'Custom OG Description', 'robotstxt-og' ),
|
||||
'value' => $og_desc,
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! empty( $item_data ) ) {
|
||||
$data[] = array(
|
||||
'group_id' => 'robotstxt-og-post-meta',
|
||||
'group_label' => __( 'OpenGraph Post Meta', 'robotstxt-og' ),
|
||||
'item_id' => 'post-' . $post_id,
|
||||
'data' => $item_data,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$done = count( $posts ) < 100;
|
||||
|
||||
return array(
|
||||
'data' => $data,
|
||||
'done' => $done,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase personal data stored by this plugin for a given email address.
|
||||
*
|
||||
* Removes custom og:title and og:description from all posts authored by
|
||||
* the user with the given email address.
|
||||
*
|
||||
* @since 1.0.3
|
||||
*
|
||||
* @param string $email_address User email address.
|
||||
* @param int $page Pagination page (1-based).
|
||||
* @return array{items_removed: int, items_retained: int, messages: string[], done: bool} Erase result.
|
||||
*/
|
||||
public function erase_privacy_data( string $email_address, int $page = 1 ): array {
|
||||
$user = get_user_by( 'email', $email_address );
|
||||
|
||||
if ( ! $user ) {
|
||||
return array(
|
||||
'items_removed' => 0,
|
||||
'items_retained' => 0,
|
||||
'messages' => array(),
|
||||
'done' => true,
|
||||
);
|
||||
}
|
||||
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'author' => $user->ID,
|
||||
'post_type' => 'any',
|
||||
'post_status' => 'any',
|
||||
'posts_per_page' => 100,
|
||||
'paged' => $page,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
$items_removed = 0;
|
||||
|
||||
foreach ( $posts as $post_id ) {
|
||||
$deleted_title = delete_post_meta( $post_id, '_og_title' );
|
||||
$deleted_desc = delete_post_meta( $post_id, '_og_description' );
|
||||
|
||||
if ( $deleted_title || $deleted_desc ) {
|
||||
++$items_removed;
|
||||
}
|
||||
}
|
||||
|
||||
$done = count( $posts ) < 100;
|
||||
|
||||
return array(
|
||||
'items_removed' => $items_removed,
|
||||
'items_retained' => 0,
|
||||
'messages' => array(),
|
||||
'done' => $done,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image resolver instance.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class Robotstxt_OG_Image_Resolver {
|
|||
// Check postmeta cache first.
|
||||
$cached_url = get_post_meta( $post_id, '_og_image_fallback_url', true );
|
||||
|
||||
if ( ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) {
|
||||
if ( is_string( $cached_url ) && ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) {
|
||||
$this->log(
|
||||
'cache_hit',
|
||||
array(
|
||||
|
|
@ -103,7 +103,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
}
|
||||
|
||||
// Detect file extension.
|
||||
$path_info = pathinfo( wp_parse_url( $image_url, PHP_URL_PATH ) );
|
||||
$url_path = wp_parse_url( $image_url, PHP_URL_PATH );
|
||||
$path_info = pathinfo( is_string( $url_path ) ? $url_path : '' );
|
||||
$extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : '';
|
||||
|
||||
// If already a compatible format, save and return.
|
||||
|
|
@ -191,9 +192,14 @@ class Robotstxt_OG_Image_Resolver {
|
|||
// Strip query string for URL manipulation.
|
||||
$clean_url = strtok( $image_url, '?' );
|
||||
|
||||
if ( ! is_string( $clean_url ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Parse URL and get base path without extension.
|
||||
$path_info = pathinfo( wp_parse_url( $clean_url, PHP_URL_PATH ) );
|
||||
$filename = $path_info['filename'] ?? '';
|
||||
$url_path = wp_parse_url( $clean_url, PHP_URL_PATH );
|
||||
$path_info = pathinfo( is_string( $url_path ) ? $url_path : '' );
|
||||
$filename = $path_info['filename'];
|
||||
$dir = $path_info['dirname'] ?? '';
|
||||
|
||||
if ( empty( $filename ) ) {
|
||||
|
|
@ -224,10 +230,52 @@ class Robotstxt_OG_Image_Resolver {
|
|||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a URL is safe to make server-side HTTP requests to.
|
||||
*
|
||||
* Rejects non-http(s) schemes and URLs that resolve to private or
|
||||
* reserved IP ranges to prevent Server-Side Request Forgery (SSRF).
|
||||
*
|
||||
* @since 1.0.3
|
||||
*
|
||||
* @param string $url URL to validate.
|
||||
* @return bool True if the URL is safe to request.
|
||||
*/
|
||||
private function is_safe_url( string $url ): bool {
|
||||
$scheme = wp_parse_url( $url, PHP_URL_SCHEME );
|
||||
if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = wp_parse_url( $url, PHP_URL_HOST );
|
||||
if ( ! is_string( $host ) || '' === $host ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Strip IPv6 brackets for validation.
|
||||
$host_bare = trim( $host, '[]' );
|
||||
|
||||
// If the host is already an IP address, validate it directly.
|
||||
if ( filter_var( $host_bare, FILTER_VALIDATE_IP ) !== false ) {
|
||||
return (bool) filter_var( $host_bare, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
|
||||
}
|
||||
|
||||
// Resolve hostname to IP and validate it is not private/reserved.
|
||||
$resolved = gethostbyname( $host_bare );
|
||||
|
||||
// gethostbyname() returns the original string on failure.
|
||||
if ( $resolved === $host_bare ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) filter_var( $resolved, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URL exists using HTTP HEAD request.
|
||||
*
|
||||
* Uses negative caching to avoid repeated requests for non-existent URLs.
|
||||
* URLs resolving to private or reserved IP ranges are rejected (SSRF protection).
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
|
|
@ -235,6 +283,12 @@ class Robotstxt_OG_Image_Resolver {
|
|||
* @return bool True if URL returns 200-299 status code.
|
||||
*/
|
||||
private function url_exists( string $url ): bool {
|
||||
// Reject URLs that resolve to private/reserved IP ranges (SSRF protection).
|
||||
if ( ! $this->is_safe_url( $url ) ) {
|
||||
$this->log( 'ssrf_blocked', array( 'url' => $url ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check negative cache to avoid repeated failed requests.
|
||||
$cache_key = self::NEGATIVE_CACHE_PREFIX . md5( $url );
|
||||
|
||||
|
|
@ -333,7 +387,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
* @return string Compatible image URL or empty string.
|
||||
*/
|
||||
private function get_global_fallback(): string {
|
||||
$fallback_url = (string) get_option( 'robotstxt_og_fallback_image', '' );
|
||||
$option_value = get_option( 'robotstxt_og_fallback_image', '' );
|
||||
$fallback_url = is_string( $option_value ) ? $option_value : '';
|
||||
|
||||
if ( empty( $fallback_url ) || ! $this->is_valid_url( $fallback_url ) ) {
|
||||
return '';
|
||||
|
|
@ -354,7 +409,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
* @return string Compatible image URL or empty string.
|
||||
*/
|
||||
public function get_homepage_image(): string {
|
||||
$image_url = (string) get_option( 'robotstxt_og_homepage_image', '' );
|
||||
$option_value = get_option( 'robotstxt_og_homepage_image', '' );
|
||||
$image_url = is_string( $option_value ) ? $option_value : '';
|
||||
|
||||
if ( ! empty( $image_url ) && $this->is_valid_url( $image_url ) ) {
|
||||
return esc_url_raw( $image_url );
|
||||
|
|
@ -378,7 +434,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
* @return string Compatible image URL or empty string.
|
||||
*/
|
||||
public function ensure_compatible_format( string $image_url ): string {
|
||||
$path_info = pathinfo( wp_parse_url( $image_url, PHP_URL_PATH ) );
|
||||
$url_path = wp_parse_url( $image_url, PHP_URL_PATH );
|
||||
$path_info = pathinfo( is_string( $url_path ) ? $url_path : '' );
|
||||
$extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : '';
|
||||
|
||||
// Already a compatible format.
|
||||
|
|
@ -459,7 +516,7 @@ class Robotstxt_OG_Image_Resolver {
|
|||
// Check term meta cache first.
|
||||
$cached_url = get_term_meta( $term_id, '_og_image_fallback_url', true );
|
||||
|
||||
if ( ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) {
|
||||
if ( is_string( $cached_url ) && ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) {
|
||||
$this->log(
|
||||
'taxonomy_cache_hit',
|
||||
array(
|
||||
|
|
@ -504,7 +561,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
}
|
||||
|
||||
// Detect file extension and resolve if needed.
|
||||
$path_info = pathinfo( wp_parse_url( $image_url, PHP_URL_PATH ) );
|
||||
$url_path = wp_parse_url( $image_url, PHP_URL_PATH );
|
||||
$path_info = pathinfo( is_string( $url_path ) ? $url_path : '' );
|
||||
$extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : '';
|
||||
|
||||
if ( in_array( $extension, array( 'jpg', 'jpeg', 'png' ), true ) ) {
|
||||
|
|
@ -555,8 +613,8 @@ class Robotstxt_OG_Image_Resolver {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $event Event name (e.g., 'cache_hit', 'head_request_error').
|
||||
* @param array $context Additional context data.
|
||||
* @param string $event Event name (e.g., 'cache_hit', 'head_request_error').
|
||||
* @param array<string, mixed> $context Additional context data.
|
||||
* @return void
|
||||
*/
|
||||
private function log( string $event, array $context = array() ): void {
|
||||
|
|
|
|||
|
|
@ -118,8 +118,10 @@ class Robotstxt_OG_Meta_Box {
|
|||
public function render_meta_box( WP_Post $post ): void {
|
||||
wp_nonce_field( 'robotstxt_og_meta_box', 'robotstxt_og_meta_box_nonce' );
|
||||
|
||||
$og_title = (string) get_post_meta( $post->ID, '_og_title', true );
|
||||
$og_description = (string) get_post_meta( $post->ID, '_og_description', true );
|
||||
$raw_title = get_post_meta( $post->ID, '_og_title', true );
|
||||
$og_title = is_string( $raw_title ) ? $raw_title : '';
|
||||
$raw_desc = get_post_meta( $post->ID, '_og_description', true );
|
||||
$og_description = is_string( $raw_desc ) ? $raw_desc : '';
|
||||
|
||||
?>
|
||||
<table class="form-table" role="presentation">
|
||||
|
|
@ -182,7 +184,7 @@ class Robotstxt_OG_Meta_Box {
|
|||
}
|
||||
|
||||
// Verify nonce.
|
||||
$nonce_raw = filter_input( INPUT_POST, 'robotstxt_og_meta_box_nonce', FILTER_SANITIZE_SPECIAL_CHARS );
|
||||
$nonce_raw = filter_input( INPUT_POST, 'robotstxt_og_meta_box_nonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||
$nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : '';
|
||||
|
||||
if ( ! wp_verify_nonce( $nonce, 'robotstxt_og_meta_box' ) ) {
|
||||
|
|
@ -197,7 +199,7 @@ class Robotstxt_OG_Meta_Box {
|
|||
}
|
||||
|
||||
// Save og:title.
|
||||
$og_title_raw = filter_input( INPUT_POST, 'robotstxt_og_title', FILTER_SANITIZE_SPECIAL_CHARS );
|
||||
$og_title_raw = filter_input( INPUT_POST, 'robotstxt_og_title', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||
$og_title = $og_title_raw ? sanitize_text_field( wp_unslash( $og_title_raw ) ) : '';
|
||||
|
||||
if ( empty( $og_title ) ) {
|
||||
|
|
|
|||
|
|
@ -121,7 +121,8 @@ class Robotstxt_OG_REST_API {
|
|||
* @return WP_REST_Response|WP_Error Response object.
|
||||
*/
|
||||
public function handle_resolve( WP_REST_Request $request ) {
|
||||
$post_id = (int) $request->get_param( 'post_id' );
|
||||
$param = $request->get_param( 'post_id' );
|
||||
$post_id = is_numeric( $param ) ? (int) $param : 0;
|
||||
|
||||
// Verify post exists.
|
||||
$post = get_post( $post_id );
|
||||
|
|
@ -159,7 +160,8 @@ class Robotstxt_OG_REST_API {
|
|||
* @return WP_REST_Response|WP_Error Response object.
|
||||
*/
|
||||
public function handle_status( WP_REST_Request $request ) {
|
||||
$post_id = (int) $request->get_param( 'post_id' );
|
||||
$param = $request->get_param( 'post_id' );
|
||||
$post_id = is_numeric( $param ) ? (int) $param : 0;
|
||||
|
||||
// Verify post exists.
|
||||
$post = get_post( $post_id );
|
||||
|
|
@ -189,14 +191,15 @@ class Robotstxt_OG_REST_API {
|
|||
/**
|
||||
* Check REST API permission.
|
||||
*
|
||||
* Requires manage_options capability.
|
||||
* Requires edit_others_posts capability (editors and above).
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param WP_REST_Request $request REST request object.
|
||||
* @return bool|WP_Error True if authorized, WP_Error otherwise.
|
||||
*/
|
||||
public function check_permission() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
public function check_permission( WP_REST_Request $request ) {
|
||||
if ( ! current_user_can( 'edit_others_posts' ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__( 'You do not have permission to access this endpoint.', 'robotstxt-og' ),
|
||||
|
|
@ -204,6 +207,18 @@ class Robotstxt_OG_REST_API {
|
|||
);
|
||||
}
|
||||
|
||||
// For post-specific endpoints, also verify the user can edit that post.
|
||||
$param = $request->get_param( 'post_id' );
|
||||
$post_id = is_numeric( $param ) ? (int) $param : 0;
|
||||
|
||||
if ( $post_id > 0 && ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__( 'You do not have permission to edit this post.', 'robotstxt-og' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,7 +225,8 @@ class Robotstxt_OG_Tags {
|
|||
private function get_og_title(): string {
|
||||
if ( is_singular() ) {
|
||||
$post_id = get_queried_object_id();
|
||||
$custom_title = (string) get_post_meta( $post_id, '_og_title', true );
|
||||
$raw_title = get_post_meta( $post_id, '_og_title', true );
|
||||
$custom_title = is_string( $raw_title ) ? $raw_title : '';
|
||||
|
||||
if ( ! empty( $custom_title ) ) {
|
||||
return $custom_title;
|
||||
|
|
@ -262,13 +263,15 @@ class Robotstxt_OG_Tags {
|
|||
private function get_og_description(): string {
|
||||
if ( is_singular() ) {
|
||||
$post_id = get_queried_object_id();
|
||||
$custom_desc = (string) get_post_meta( $post_id, '_og_description', true );
|
||||
$raw_desc = get_post_meta( $post_id, '_og_description', true );
|
||||
$custom_desc = is_string( $raw_desc ) ? $raw_desc : '';
|
||||
|
||||
if ( ! empty( $custom_desc ) ) {
|
||||
return $custom_desc;
|
||||
}
|
||||
|
||||
$excerpt = (string) get_post_field( 'post_excerpt', $post_id );
|
||||
$raw_excerpt = get_post_field( 'post_excerpt', $post_id );
|
||||
$excerpt = is_string( $raw_excerpt ) ? $raw_excerpt : '';
|
||||
|
||||
if ( ! empty( $excerpt ) ) {
|
||||
return wp_strip_all_tags( $excerpt );
|
||||
|
|
@ -455,7 +458,8 @@ class Robotstxt_OG_Tags {
|
|||
}
|
||||
|
||||
// Alt text from the media library (set when uploading/editing the image).
|
||||
$alt_text = (string) get_post_meta( $image_id, '_wp_attachment_image_alt', true );
|
||||
$raw_alt = get_post_meta( $image_id, '_wp_attachment_image_alt', true );
|
||||
$alt_text = is_string( $raw_alt ) ? $raw_alt : '';
|
||||
|
||||
if ( ! empty( $alt_text ) ) {
|
||||
printf(
|
||||
|
|
@ -489,7 +493,7 @@ class Robotstxt_OG_Tags {
|
|||
|
||||
$published = get_the_date( 'c', $post );
|
||||
|
||||
if ( ! empty( $published ) ) {
|
||||
if ( is_string( $published ) && ! empty( $published ) ) {
|
||||
printf(
|
||||
'<meta property="article:published_time" content="%s" />' . "\n",
|
||||
esc_attr( $published )
|
||||
|
|
@ -498,7 +502,7 @@ class Robotstxt_OG_Tags {
|
|||
|
||||
$modified = get_the_modified_date( 'c', $post );
|
||||
|
||||
if ( ! empty( $modified ) ) {
|
||||
if ( is_string( $modified ) && ! empty( $modified ) ) {
|
||||
printf(
|
||||
'<meta property="article:modified_time" content="%s" />' . "\n",
|
||||
esc_attr( $modified )
|
||||
|
|
@ -541,7 +545,8 @@ class Robotstxt_OG_Tags {
|
|||
* @return void
|
||||
*/
|
||||
private function output_twitter_card_tags( string $image_url ): void {
|
||||
$card_type = (string) get_option( 'robotstxt_og_twitter_card_type', 'summary_large_image' );
|
||||
$card_option = get_option( 'robotstxt_og_twitter_card_type', 'summary_large_image' );
|
||||
$card_type = is_string( $card_option ) ? $card_option : 'summary_large_image';
|
||||
|
||||
if ( ! in_array( $card_type, array( 'summary', 'summary_large_image' ), true ) ) {
|
||||
$card_type = 'summary_large_image';
|
||||
|
|
@ -554,7 +559,8 @@ class Robotstxt_OG_Tags {
|
|||
);
|
||||
|
||||
// Site handle (e.g. @example).
|
||||
$twitter_site = sanitize_text_field( (string) get_option( 'robotstxt_og_twitter_site', '' ) );
|
||||
$twitter_option = get_option( 'robotstxt_og_twitter_site', '' );
|
||||
$twitter_site = sanitize_text_field( is_string( $twitter_option ) ? $twitter_option : '' );
|
||||
|
||||
if ( ! empty( $twitter_site ) ) {
|
||||
// Ensure the handle includes the @ prefix.
|
||||
|
|
|
|||
BIN
languages/robotstxt-og-ca.mo
Normal file
BIN
languages/robotstxt-og-ca.mo
Normal file
Binary file not shown.
447
languages/robotstxt-og-ca.po
Normal file
447
languages/robotstxt-og-ca.po
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
# Translation of OpenGraph (by ROBOTSTXT) in Catalan
|
||||
# This file is distributed under the same license as the OpenGraph (by ROBOTSTXT) package.
|
||||
# Copyright (C) 2026 ROBOTSTXT
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OpenGraph (by ROBOTSTXT) 1.1.0\n"
|
||||
"Report-Msgid-Bugs-To: https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/issues\n"
|
||||
"POT-Creation-Date: 2026-03-28\n"
|
||||
"PO-Revision-Date: 2026-03-28 00:00+0000\n"
|
||||
"Last-Translator: ROBOTSTXT <hola@robotstxt.es>\n"
|
||||
"Language-Team: Catalan\n"
|
||||
"Language: ca\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Manual\n"
|
||||
"X-Domain: robotstxt-og\n"
|
||||
|
||||
#. Plugin Name of the plugin
|
||||
msgid "OpenGraph (by ROBOTSTXT)"
|
||||
msgstr "OpenGraph (by ROBOTSTXT)"
|
||||
|
||||
#. Plugin URI of the plugin
|
||||
msgid "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og"
|
||||
msgstr "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og"
|
||||
|
||||
#. Description of the plugin
|
||||
msgid "Intelligent Open Graph image fallback for social media crawlers. Automatically detects and serves compatible image formats (JPEG/PNG) when modern formats (AVIF/WebP) are used as featured images."
|
||||
msgstr "Imatge de reserva intel·ligent per a Open Graph en rastrejadors de xarxes socials. Detecta i serveix automàticament formats d'imatge compatibles (JPEG/PNG) quan s'utilitzen formats moderns (AVIF/WebP) com a imatges destacades."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "OpenGraph Settings"
|
||||
msgstr "Configuració d'OpenGraph"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "OpenGraph"
|
||||
msgstr "OpenGraph"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "You do not have sufficient permissions to access this page."
|
||||
msgstr "No tens permisos suficients per accedir a aquesta pàgina."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Security check failed"
|
||||
msgstr "Ha fallat la verificació de seguretat"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "General Settings"
|
||||
msgstr "Configuració general"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Configure OpenGraph image fallback behavior."
|
||||
msgstr "Configura el comportament de la imatge de reserva per a OpenGraph."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Global Fallback Image"
|
||||
msgstr "Imatge de reserva global"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Homepage Image"
|
||||
msgstr "Imatge de la pàgina principal"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Data Management"
|
||||
msgstr "Gestió de dades"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Social Media Tags"
|
||||
msgstr "Etiquetes per a xarxes socials"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Choose which social media meta tag groups to output and configure their behavior."
|
||||
msgstr "Tria quins grups de metaetiquetes per a xarxes socials vols generar i configura'n el comportament."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Facebook / OG Tags"
|
||||
msgstr "Etiquetes Facebook / OG"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Twitter Card Tags"
|
||||
msgstr "Etiquetes Twitter Card"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Twitter Card Type"
|
||||
msgstr "Tipus de Twitter Card"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Select Image"
|
||||
msgstr "Selecciona la imatge"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Remove Image"
|
||||
msgstr "Elimina la imatge"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Used as Open Graph image when a post has no featured image or when no compatible format (JPEG/PNG) is found."
|
||||
msgstr "S'utilitza com a imatge d'Open Graph quan una entrada no té imatge destacada o no es troba cap format compatible (JPEG/PNG)."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Dedicated Open Graph image for the site homepage. Falls back to the Global Fallback Image if not set."
|
||||
msgstr "Imatge d'Open Graph dedicada a la pàgina principal del lloc. Si no es configura, s'utilitza la imatge de reserva global."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Delete all plugin data when uninstalling"
|
||||
msgstr "Elimina totes les dades del connector en desinstal·lar"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Warning: This will permanently delete all cached fallback URLs and plugin settings. This action cannot be undone."
|
||||
msgstr "Avís: Això eliminarà permanentment totes les URL de reserva en memòria cau i la configuració del connector. Aquesta acció no es pot desfer."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Enable Facebook / Open Graph image tags"
|
||||
msgstr "Activa les etiquetes d'imatge per a Facebook / Open Graph"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Outputs og:title, og:type, og:url, og:description, og:site_name, og:locale, og:image, og:image:width, og:image:height, og:image:type, and og:image:alt meta tags."
|
||||
msgstr "Genera les metaetiquetes og:title, og:type, og:url, og:description, og:site_name, og:locale, og:image, og:image:width, og:image:height, og:image:type i og:image:alt."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Enable Twitter Card image tags"
|
||||
msgstr "Activa les etiquetes d'imatge per a Twitter Card"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Outputs twitter:card, twitter:site, and twitter:image meta tags."
|
||||
msgstr "Genera les metaetiquetes twitter:card, twitter:site i twitter:image."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Summary with large image"
|
||||
msgstr "Resum amb imatge gran"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Summary (small image)"
|
||||
msgstr "Resum (imatge petita)"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Controls the twitter:card meta tag value. \"Summary with large image\" is recommended for most sites."
|
||||
msgstr "Controla el valor de la metaetiqueta twitter:card. Es recomana «Resum amb imatge gran» per a la majoria de llocs."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Twitter/X Site Handle"
|
||||
msgstr "Compte Twitter/X del lloc"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
msgid "Your site's Twitter/X account handle (e.g. @example). Used for the twitter:site meta tag."
|
||||
msgstr "El compte Twitter/X del teu lloc (p. ex. @exemple). S'utilitza per a la metaetiqueta twitter:site."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
#. translators: %d: number of cleared cache entries
|
||||
msgid "Successfully cleared %d cached fallback URLs."
|
||||
msgstr "S'han eliminat correctament %d URL de reserva en memòria cau."
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
#. translators: 1: number of successful resolutions 2: number of failed resolutions
|
||||
msgid "Batch resolution complete. Success: %1$d, Failed: %2$d"
|
||||
msgstr "Resolució per lots completada. Correctes: %1$d, Fallides: %2$d"
|
||||
|
||||
#: admin/class-robotstxt-og-admin-settings.php
|
||||
#. translators: %d: post ID
|
||||
msgid "Successfully cleared cached fallback URL for post #%d."
|
||||
msgstr "S'ha eliminat correctament la URL de reserva en memòria cau per a l'entrada #%d."
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Settings tabs"
|
||||
msgstr "Pestanyes de configuració"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Settings"
|
||||
msgstr "Configuració"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Tools"
|
||||
msgstr "Eines"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Diagnostics"
|
||||
msgstr "Diagnòstics"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Save Settings"
|
||||
msgstr "Desa la configuració"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Cache Management"
|
||||
msgstr "Gestió de la memòria cau"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Use these tools to manage cached fallback image URLs."
|
||||
msgstr "Utilitza aquestes eines per gestionar les URL d'imatge de reserva emmagatzemades en memòria cau."
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Clear All Caches"
|
||||
msgstr "Buida tota la memòria cau"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Clear All Cached URLs"
|
||||
msgstr "Elimina totes les URL en memòria cau"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Delete all cached fallback URLs. Images will be re-resolved on next page view."
|
||||
msgstr "Elimina totes les URL de reserva en memòria cau. Les imatges es resoldran de nou en la propera visita a la pàgina."
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Re-resolve All Images"
|
||||
msgstr "Torna a resoldre totes les imatges"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Re-resolve All Images Now"
|
||||
msgstr "Torna a resoldre totes les imatges ara"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Clear cache and immediately re-resolve all posts with featured images. May take time on large sites."
|
||||
msgstr "Buida la memòria cau i torna a resoldre immediatament totes les entrades amb imatge destacada. Pot trigar en llocs grans."
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Plugin Information"
|
||||
msgstr "Informació del connector"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Version"
|
||||
msgstr "Versió"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Documentation"
|
||||
msgstr "Documentació"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "View Documentation"
|
||||
msgstr "Visualitza la documentació"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Support"
|
||||
msgstr "Suport"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Report an Issue"
|
||||
msgstr "Informa d'un problema"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Statistics"
|
||||
msgstr "Estadístiques"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Posts with featured images"
|
||||
msgstr "Entrades amb imatge destacada"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Posts with cached fallback URLs"
|
||||
msgstr "Entrades amb URL de reserva en memòria cau"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Coverage"
|
||||
msgstr "Cobertura"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "N/A"
|
||||
msgstr "N/D"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Test URL"
|
||||
msgstr "Prova la URL"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Test whether a URL is reachable via HTTP HEAD request."
|
||||
msgstr "Comprova si una URL és accessible mitjançant una petició HTTP HEAD."
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "URL to test"
|
||||
msgstr "URL a provar"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Test Result"
|
||||
msgstr "Resultat de la prova"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Error:"
|
||||
msgstr "Error:"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "HTTP Status:"
|
||||
msgstr "Estat HTTP:"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Header"
|
||||
msgstr "Capçalera"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Value"
|
||||
msgstr "Valor"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
#. translators: %d: total entries
|
||||
msgid "Cached Fallback URLs (%d)"
|
||||
msgstr "URL de reserva en memòria cau (%d)"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "No cached fallback URLs found."
|
||||
msgstr "No s'han trobat URL de reserva en memòria cau."
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Post ID"
|
||||
msgstr "ID d'entrada"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Title"
|
||||
msgstr "Títol"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Type"
|
||||
msgstr "Tipus"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Cached Fallback URL"
|
||||
msgstr "URL de reserva en memòria cau"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Actions"
|
||||
msgstr "Accions"
|
||||
|
||||
#: admin/views/settings-page.php
|
||||
msgid "Clear"
|
||||
msgstr "Neteja"
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: %d: number of posts found
|
||||
msgid "Found %d posts with featured images. (dry-run, no changes made)"
|
||||
msgstr "S'han trobat %d entrades amb imatge destacada. (simulació, no s'han fet canvis)"
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
msgid "No posts with featured images found."
|
||||
msgstr "No s'han trobat entrades amb imatge destacada."
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: %d: number of posts to process
|
||||
msgid "Resolving %d posts"
|
||||
msgstr "S'estan resolent %d entrades"
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: 1: successful count, 2: failed count
|
||||
msgid "Resolved %1$d posts. Failed: %2$d."
|
||||
msgstr "S'han resolt %1$d entrades. Fallides: %2$d."
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: %d: post ID
|
||||
msgid "Post %d not found."
|
||||
msgstr "L'entrada %d no s'ha trobat."
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: 1: post ID, 2: image URL
|
||||
msgid "Post %1$d has featured image: %2$s (dry-run, no changes made)"
|
||||
msgstr "L'entrada %1$d té imatge destacada: %2$s (simulació, no s'han fet canvis)"
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: 1: post ID, 2: resolved URL
|
||||
msgid "Post %1$d resolved to %2$s"
|
||||
msgstr "L'entrada %1$d s'ha resolt a %2$s"
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: %d: post ID
|
||||
msgid "Post %d: no compatible image found."
|
||||
msgstr "Entrada %d: no s'ha trobat cap imatge compatible."
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: %d: number of cache entries found
|
||||
msgid "Found %d cached fallback URLs. (dry-run, no changes made)"
|
||||
msgstr "S'han trobat %d URL de reserva en memòria cau. (simulació, no s'han fet canvis)"
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: %d: number of cleared entries
|
||||
msgid "Cleared %d cached fallback URLs."
|
||||
msgstr "S'han eliminat %d URL de reserva en memòria cau."
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: 1: post ID, 2: cached URL or 'none'
|
||||
msgid "Post %1$d cached URL: %2$s (dry-run, no changes made)"
|
||||
msgstr "URL en memòria cau de l'entrada %1$d: %2$s (simulació, no s'han fet canvis)"
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: %d: post ID
|
||||
msgid "Post %d has no cached fallback URL."
|
||||
msgstr "L'entrada %d no té URL de reserva en memòria cau."
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
#. translators: %d: post ID
|
||||
msgid "Cleared cached fallback URL for post %d."
|
||||
msgstr "S'ha eliminat la URL de reserva en memòria cau per a l'entrada %d."
|
||||
|
||||
#: includes/class-robotstxt-og-cli.php
|
||||
msgid "Please specify a post ID or use --all flag."
|
||||
msgstr "Especifica un ID d'entrada o utilitza l'opció --all."
|
||||
|
||||
#: includes/class-robotstxt-og-rest-api.php
|
||||
msgid "Post not found."
|
||||
msgstr "L'entrada no s'ha trobat."
|
||||
|
||||
#: includes/class-robotstxt-og-rest-api.php
|
||||
msgid "You do not have permission to access this endpoint."
|
||||
msgstr "No tens permís per accedir a aquest punt d'accés."
|
||||
|
||||
#: includes/class-robotstxt-og-rest-api.php
|
||||
msgid "You do not have permission to edit this post."
|
||||
msgstr "No tens permís per editar aquesta entrada."
|
||||
|
||||
#: includes/class-robotstxt-og-rest-api.php
|
||||
msgid "The post ID to resolve the fallback image for."
|
||||
msgstr "L'ID de l'entrada per a la qual resoldre la imatge de reserva."
|
||||
|
||||
#: includes/class-robotstxt-og-rest-api.php
|
||||
msgid "The post ID to get the fallback image status for."
|
||||
msgstr "L'ID de l'entrada de la qual obtenir l'estat de la imatge de reserva."
|
||||
|
||||
#: includes/class-robotstxt-og-meta-box.php
|
||||
msgid "Open Graph / Social Media"
|
||||
msgstr "Open Graph / Xarxes Socials"
|
||||
|
||||
#: includes/class-robotstxt-og-meta-box.php
|
||||
msgid "Custom Title"
|
||||
msgstr "Títol personalitzat"
|
||||
|
||||
#: includes/class-robotstxt-og-meta-box.php
|
||||
msgid "Overrides the default og:title for this post. Leave blank to use the post title automatically."
|
||||
msgstr "Substitueix l'og:title predeterminat per a aquesta entrada. Deixa-ho en blanc per utilitzar el títol de l'entrada automàticament."
|
||||
|
||||
#: includes/class-robotstxt-og-meta-box.php
|
||||
msgid "Custom Description"
|
||||
msgstr "Descripció personalitzada"
|
||||
|
||||
#: includes/class-robotstxt-og-meta-box.php
|
||||
msgid "Overrides the default og:description for this post. Leave blank to use the excerpt automatically."
|
||||
msgstr "Substitueix l'og:description predeterminat per a aquesta entrada. Deixa-ho en blanc per utilitzar l'extracte automàticament."
|
||||
|
||||
#: includes/class-robotstxt-og-image-fallback.php
|
||||
msgid "OpenGraph Custom Post Data"
|
||||
msgstr "Dades personalitzades d'Open Graph"
|
||||
|
||||
#: includes/class-robotstxt-og-image-fallback.php
|
||||
msgid "OpenGraph Post Meta"
|
||||
msgstr "Metadades Open Graph de l'entrada"
|
||||
|
||||
#: includes/class-robotstxt-og-image-fallback.php
|
||||
msgid "Custom OG Title"
|
||||
msgstr "Títol OG personalitzat"
|
||||
|
||||
#: includes/class-robotstxt-og-image-fallback.php
|
||||
msgid "Custom OG Description"
|
||||
msgstr "Descripció OG personalitzada"
|
||||
Binary file not shown.
|
|
@ -6,8 +6,8 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: OpenGraph (by ROBOTSTXT) 1.1.0\n"
|
||||
"Report-Msgid-Bugs-To: https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/issues\n"
|
||||
"POT-Creation-Date: 2026-02-18\n"
|
||||
"PO-Revision-Date: 2026-02-18 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-03-28\n"
|
||||
"PO-Revision-Date: 2026-03-28 00:00+0000\n"
|
||||
"Last-Translator: ROBOTSTXT <hola@robotstxt.es>\n"
|
||||
"Language-Team: Spanish (Spain)\n"
|
||||
"Language: es_ES\n"
|
||||
|
|
@ -398,6 +398,26 @@ msgstr "Entrada no encontrada."
|
|||
msgid "You do not have permission to access this endpoint."
|
||||
msgstr "No tienes permiso para acceder a este endpoint."
|
||||
|
||||
#: includes/class-robotstxt-og-rest-api.php
|
||||
msgid "You do not have permission to edit this post."
|
||||
msgstr "No tienes permiso para editar esta entrada."
|
||||
|
||||
#: includes/class-robotstxt-og-image-fallback.php
|
||||
msgid "OpenGraph Custom Post Data"
|
||||
msgstr "Datos personalizados de Open Graph"
|
||||
|
||||
#: includes/class-robotstxt-og-image-fallback.php
|
||||
msgid "OpenGraph Post Meta"
|
||||
msgstr "Metadatos Open Graph de la entrada"
|
||||
|
||||
#: includes/class-robotstxt-og-image-fallback.php
|
||||
msgid "Custom OG Title"
|
||||
msgstr "Título OG personalizado"
|
||||
|
||||
#: includes/class-robotstxt-og-image-fallback.php
|
||||
msgid "Custom OG Description"
|
||||
msgstr "Descripción OG personalizada"
|
||||
|
||||
#: includes/class-robotstxt-og-rest-api.php
|
||||
msgid "The post ID to resolve the fallback image for."
|
||||
msgstr "El ID de entrada para el que resolver la imagen de respaldo."
|
||||
|
|
|
|||
25
readme.txt
25
readme.txt
|
|
@ -1,11 +1,11 @@
|
|||
=== OpenGraph (by ROBOTSTXT) ===
|
||||
Contributors: javiercasares, robotstxt
|
||||
Tags: opengraph, open graph, twitter card, social media, seo
|
||||
Requires at least: 6.7
|
||||
Requires at least: 6.8
|
||||
Tested up to: 7.0
|
||||
Stable tag: 1.0.1
|
||||
Stable tag: 1.1.0
|
||||
Requires PHP: 8.2
|
||||
Version: 1.0.1
|
||||
Version: 1.1.0
|
||||
License: GPL-3.0-or-later
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ Social media crawlers — Facebook, Twitter/X, LinkedIn, WhatsApp — do not sup
|
|||
* WP-CLI commands for batch operations
|
||||
* REST API endpoints for programmatic access
|
||||
* Multisite compatible (per-site configuration)
|
||||
* Full internationalization support (es_ES included)
|
||||
* Full internationalization support (es_ES and ca included)
|
||||
|
||||
== Using the plugin ==
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ Clear all cached fallback URLs:
|
|||
|
||||
= REST API =
|
||||
|
||||
The plugin exposes two REST API endpoints under the `robotstxt-og/v1` namespace. Both require the `manage_options` capability (administrator authentication).
|
||||
The plugin exposes two REST API endpoints under the `robotstxt-og/v1` namespace. Both require the `edit_others_posts` capability (editor and above).
|
||||
|
||||
**Force re-resolve a post's fallback image:**
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ Yes. The plugin hooks into `wp_head` for direct tag injection and filters Yoast/
|
|||
|
||||
== Compatibility ==
|
||||
|
||||
* WordPress: 6.7 - 7.0
|
||||
* WordPress: 6.8 - 7.0
|
||||
* PHP: 8.2 - 8.5
|
||||
* WP-CLI: 2.x
|
||||
* MariaDB: 10.6+
|
||||
|
|
@ -175,11 +175,18 @@ Yes. The plugin hooks into `wp_head` for direct tag injection and filters Yoast/
|
|||
|
||||
== Changelog ==
|
||||
|
||||
= 1.0.1 =
|
||||
= 1.1.0 =
|
||||
|
||||
_Release date: 2026-03-05_
|
||||
_Release date: 2026-03-28_
|
||||
|
||||
* Confirmed compatibility with WordPress 7.0.
|
||||
* Fixed: Fatal `TypeError` in `handle_thumbnail_change()` when `deleted_post_meta` passes an array of meta IDs as the first argument.
|
||||
* Security: Added SSRF protection — outbound HEAD requests now block private and reserved IP ranges.
|
||||
* Security: Replaced deprecated `FILTER_SANITIZE_SPECIAL_CHARS` with `FILTER_SANITIZE_FULL_SPECIAL_CHARS`.
|
||||
* Added: GDPR Privacy API — custom OG title and description are included in WordPress personal data export and erase.
|
||||
* Changed: Settings page and REST API now require `edit_others_posts`, allowing editors to manage OG settings.
|
||||
* Changed: Minimum supported WordPress version raised to 6.8.
|
||||
* Changed: Added `Network: true` header confirming Multisite compatibility.
|
||||
* Added: PHPUnit test suite (Brain\Monkey) covering SSRF protection, format detection, and cache clearing.
|
||||
|
||||
= 1.0.0 =
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
* Plugin Name: OpenGraph (by ROBOTSTXT)
|
||||
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-og
|
||||
* Description: Intelligent Open Graph image fallback for social media crawlers. Automatically detects and serves compatible image formats (JPEG/PNG) when modern formats (AVIF/WebP) are used as featured images.
|
||||
* Version: 1.0.1
|
||||
* Requires at least: 6.7
|
||||
* Version: 1.1.0
|
||||
* Requires at least: 6.8
|
||||
* Requires PHP: 8.2
|
||||
* Author: ROBOTSTXT
|
||||
* Author URI: https://www.robotstxt.es/
|
||||
|
|
@ -12,11 +12,12 @@
|
|||
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
* Text Domain: robotstxt-og
|
||||
* Domain Path: /languages
|
||||
* Network: true
|
||||
* Gitea Plugin URI: ROBOTSTXT/robotstxt-og
|
||||
* Contributors: javiercasares, robotstxt
|
||||
*
|
||||
* @package ROBOTSTXT_OG
|
||||
* @version 1.0.1
|
||||
* @version 1.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
|
|
@ -24,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
}
|
||||
|
||||
// Define plugin constants.
|
||||
define( 'ROBOTSTXT_OG_VERSION', '1.0.1' );
|
||||
define( 'ROBOTSTXT_OG_VERSION', '1.1.0' );
|
||||
define( 'ROBOTSTXT_OG_PATH', plugin_dir_path( __FILE__ ) );
|
||||
define( 'ROBOTSTXT_OG_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'ROBOTSTXT_OG_BASENAME', plugin_basename( __FILE__ ) );
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Plugin headers.
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private array $plugin_data;
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Get plugin headers.
|
||||
*
|
||||
* @return array Plugin data.
|
||||
* @return array<string, mixed> Plugin data.
|
||||
*/
|
||||
private function get_plugin_data(): array {
|
||||
if ( ! function_exists( 'get_plugin_data' ) ) {
|
||||
|
|
@ -125,7 +125,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
*/
|
||||
private function build_json_url(): string {
|
||||
// Try Gitea Plugin URI (format: "OWNER/REPO" or full URL).
|
||||
if ( ! empty( $this->plugin_data['Gitea Plugin URI'] ) ) {
|
||||
if ( ! empty( $this->plugin_data['Gitea Plugin URI'] ) && is_string( $this->plugin_data['Gitea Plugin URI'] ) ) {
|
||||
$gitea_uri = $this->plugin_data['Gitea Plugin URI'];
|
||||
|
||||
// If it's already a full URL, use it.
|
||||
|
|
@ -141,7 +141,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
}
|
||||
|
||||
// Fallback: try to extract from Plugin URI.
|
||||
if ( ! empty( $this->plugin_data['PluginURI'] ) ) {
|
||||
if ( ! empty( $this->plugin_data['PluginURI'] ) && is_string( $this->plugin_data['PluginURI'] ) ) {
|
||||
$plugin_uri = $this->plugin_data['PluginURI'];
|
||||
if ( str_contains( $plugin_uri, 'git.robotstxt.es' ) ) {
|
||||
return rtrim( $plugin_uri, '/' ) . '/raw/branch/main/update.json';
|
||||
|
|
@ -160,7 +160,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
* @return object The modified transient.
|
||||
*/
|
||||
public function inject_update_info( $transient ) {
|
||||
if ( ! is_object( $transient ) ) {
|
||||
if ( ! ( $transient instanceof stdClass ) ) {
|
||||
$transient = new stdClass();
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
return $transient;
|
||||
}
|
||||
|
||||
if ( version_compare( $remote['version'], $current_version, '>' ) ) {
|
||||
if ( is_string( $current_version ) && is_string( $remote['version'] ) && version_compare( $remote['version'], $current_version, '>' ) ) {
|
||||
$update = (object) array(
|
||||
'slug' => $remote['slug'] ?? $this->plugin_slug,
|
||||
'plugin' => $this->plugin_basename,
|
||||
|
|
@ -204,11 +204,11 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Provide "View details" modal content.
|
||||
*
|
||||
* @param false|object|array $result The result object or array.
|
||||
* @param string $action The type of information being requested.
|
||||
* @param object $args Plugin API arguments.
|
||||
* @param false|object|array<string, mixed> $result The result object or array.
|
||||
* @param string $action The type of information being requested.
|
||||
* @param object $args Plugin API arguments.
|
||||
*
|
||||
* @return false|object The plugin information object or false.
|
||||
* @return false|object|array<string, mixed> The plugin information object or false.
|
||||
*/
|
||||
public function provide_plugin_details( $result, string $action, object $args ) {
|
||||
if ( 'plugin_information' !== $action ) {
|
||||
|
|
@ -245,7 +245,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Get remote data with caching and HMAC signature verification.
|
||||
*
|
||||
* @return array Remote data.
|
||||
* @return array<string, mixed> Remote data.
|
||||
*/
|
||||
private function get_remote_data(): array {
|
||||
$cached = get_site_transient( $this->cache_key );
|
||||
|
|
@ -255,7 +255,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
if ( is_array( $cached ) && isset( $cached['signature'], $cached['data'] ) ) {
|
||||
$expected_sig = hash_hmac( 'sha256', $this->cache_key . serialize( $cached['data'] ), AUTH_SALT );
|
||||
|
||||
if ( hash_equals( $expected_sig, $cached['signature'] ) ) {
|
||||
if ( is_string( $cached['signature'] ) && hash_equals( $expected_sig, $cached['signature'] ) ) {
|
||||
// Signature valid, return data.
|
||||
return is_array( $cached['data'] ) ? $cached['data'] : array();
|
||||
}
|
||||
|
|
@ -283,7 +283,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
set_site_transient( $this->cache_key, $remote ?: array(), 6 * HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return is_array( $remote ) ? $remote : array();
|
||||
return $remote;
|
||||
}
|
||||
|
||||
// Legacy cache format without signature (backward compatibility).
|
||||
|
|
@ -293,7 +293,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Fetch JSON from remote URL.
|
||||
*
|
||||
* @return array Decoded JSON data.
|
||||
* @return array<string, mixed> Decoded JSON data.
|
||||
*/
|
||||
private function fetch_json(): array {
|
||||
$response = wp_remote_get(
|
||||
|
|
@ -324,18 +324,18 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Check compatibility.
|
||||
*
|
||||
* @param array $remote Remote data.
|
||||
* @param array<string, mixed> $remote Remote data.
|
||||
*
|
||||
* @return bool True if compatible.
|
||||
*/
|
||||
private function is_compatible( array $remote ): bool {
|
||||
if ( ! empty( $remote['requires_php'] ) ) {
|
||||
if ( ! empty( $remote['requires_php'] ) && is_string( $remote['requires_php'] ) ) {
|
||||
if ( version_compare( PHP_VERSION, $remote['requires_php'], '<' ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $remote['requires'] ) ) {
|
||||
if ( ! empty( $remote['requires'] ) && is_string( $remote['requires'] ) ) {
|
||||
if ( version_compare( get_bloginfo( 'version' ), $remote['requires'], '<' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -355,7 +355,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
}
|
||||
|
||||
// This is a cache clear request - now verify nonce.
|
||||
$nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_UNSAFE_RAW );
|
||||
$nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||
$nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : '';
|
||||
|
||||
if ( ! wp_verify_nonce( $nonce, 'robotstxt_clear_update_cache' ) ) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
|||
/**
|
||||
* Delete plugin data if user opted in to data deletion.
|
||||
*/
|
||||
function robotstxt_og_uninstall_cleanup() {
|
||||
function robotstxt_og_uninstall_cleanup(): void {
|
||||
// Check if user opted in to delete data on uninstall.
|
||||
$delete_data = get_option( 'robotstxt_og_delete_data_on_uninstall', false );
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ function robotstxt_og_uninstall_cleanup() {
|
|||
);
|
||||
|
||||
foreach ( $sites as $site ) {
|
||||
switch_to_blog( $site->blog_id );
|
||||
switch_to_blog( (int) $site->blog_id );
|
||||
|
||||
// Delete site-specific options.
|
||||
delete_option( 'robotstxt_og_fallback_image' );
|
||||
|
|
|
|||
12
update.json
12
update.json
|
|
@ -1,20 +1,20 @@
|
|||
{
|
||||
"name": "OpenGraph (by ROBOTSTXT)",
|
||||
"slug": "robotstxt-og",
|
||||
"version": "1.0.1",
|
||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/releases/download/1.0.1/robotstxt-og-1.0.1.zip",
|
||||
"requires": "6.7",
|
||||
"version": "1.1.0",
|
||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/releases/download/1.1.0/robotstxt-og-1.1.0.zip",
|
||||
"requires": "6.8",
|
||||
"requires_php": "8.2",
|
||||
"tested": "7.0",
|
||||
"last_updated": "2026-03-05",
|
||||
"last_updated": "2026-03-28",
|
||||
"author": "ROBOTSTXT",
|
||||
"author_profile": "https://www.robotstxt.es/",
|
||||
"homepage": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og",
|
||||
"description": "Intelligent Open Graph meta tags with automatic image fallback from AVIF/WebP to JPEG/PNG for social media crawlers. Outputs a complete set of og:* and twitter:* meta tags, detects incompatible image formats, and resolves JPEG/PNG alternatives via HTTP HEAD requests.",
|
||||
"changelog": "<h3>1.0.1 — 2026-03-05</h3><ul><li><strong>Compatibility:</strong> Confirmed compatibility with WordPress 7.0. Supported range is now WordPress 6.7 – 7.0.</li></ul><h3>1.0.0 — 2026-02-18</h3><ul><li><strong>Initial Release:</strong> First stable release of OpenGraph (by ROBOTSTXT).</li><li><strong>Added:</strong> Full Open Graph meta tags: og:title, og:type, og:url, og:description, og:site_name, og:locale, og:image, og:image:width, og:image:height, og:image:type, og:image:alt.</li><li><strong>Added:</strong> Article tags for singular posts: article:published_time, article:modified_time, article:section, article:tag.</li><li><strong>Added:</strong> Twitter Card tags: twitter:card, twitter:site, twitter:image.</li><li><strong>Added:</strong> Automatic detection and HTTP HEAD-based resolution of JPEG/PNG alternatives for AVIF, WebP, GIF, BMP, SVG, and TIFF images.</li><li><strong>Added:</strong> Postmeta caching with automatic invalidation on featured image change.</li><li><strong>Added:</strong> Per-post OG title and description overrides via post editor meta box.</li><li><strong>Added:</strong> Global fallback image URL and homepage-specific image URL settings.</li><li><strong>Added:</strong> Yoast SEO and RankMath integration (no duplicate tags).</li><li><strong>Added:</strong> Admin settings page with Settings, Tools, and Diagnostics tabs.</li><li><strong>Added:</strong> WP-CLI commands: wp og-fallback resolve and wp og-fallback clear-cache.</li><li><strong>Added:</strong> REST API: POST /wp-json/robotstxt-og/v1/resolve/{post_id} and GET /wp-json/robotstxt-og/v1/status/{post_id}.</li><li><strong>Added:</strong> Developer filters: robotstxt_og_external_image_enabled, robotstxt_og_external_image_timeout, robotstxt_og_taxonomy_image, robotstxt_og_enable_logging.</li><li><strong>Added:</strong> Multisite compatible, full i18n support, Spanish (es_ES) translation included.</li><li><strong>Security:</strong> Nonce verification, manage_options capability checks, input sanitization, output escaping, prepared SQL statements throughout.</li></ul>",
|
||||
"changelog": "<h3>1.1.0 — 2026-03-28</h3><ul><li><strong>Fixed:</strong> Fatal TypeError in handle_thumbnail_change(): the deleted_post_meta action passes an array of meta IDs as its first argument. The method signature now accepts int|array to handle both updated_post_meta and deleted_post_meta correctly.</li><li><strong>Security:</strong> Added SSRF protection to all outbound HTTP HEAD requests via is_safe_url() — private and reserved IP ranges are now blocked.</li><li><strong>Security:</strong> Replaced deprecated FILTER_SANITIZE_SPECIAL_CHARS with FILTER_SANITIZE_FULL_SPECIAL_CHARS throughout.</li><li><strong>Added:</strong> GDPR Privacy API support — custom OG title and description post meta are now included in WordPress personal data export and erase requests.</li><li><strong>Changed:</strong> Settings page and REST API now require edit_others_posts capability, allowing editors to manage OG settings and refresh fallback images.</li><li><strong>Changed:</strong> Minimum supported WordPress version raised to 6.8. Network: true header added.</li><li><strong>Added:</strong> PHPUnit test suite with Brain\\Monkey; covers SSRF logic, image format detection, and cache clearing.</li></ul><h3>1.0.0 — 2026-02-18</h3><ul><li><strong>Initial Release:</strong> First stable release of OpenGraph (by ROBOTSTXT).</li></ul>",
|
||||
"sections": {
|
||||
"description": "<p>Social media crawlers — Facebook, Twitter/X, LinkedIn, WhatsApp — do not support modern image formats such as AVIF and WebP. When a post’s featured image uses one of these formats, the platform shows a broken or missing image preview.</p><p><strong>OpenGraph (by ROBOTSTXT)</strong> outputs a complete set of Open Graph and Twitter Card meta tags and solves this automatically. It detects whether each post’s featured image is in a supported format and, when needed, resolves and caches a compatible JPEG or PNG alternative.</p><ul><li>Full og:* and twitter:* meta tag output</li><li>Automatic AVIF/WebP/GIF/BMP/SVG/TIFF detection with JPEG/PNG fallback</li><li>Per-post OG title and description overrides via post editor meta box</li><li>Global fallback image URL and homepage-specific image URL</li><li>Yoast SEO and RankMath integration (no duplicate tags)</li><li>Postmeta caching with automatic invalidation</li><li>Admin panel with Settings, Tools, and Diagnostics tabs</li><li>WP-CLI commands and REST API endpoints</li><li>Multisite compatible</li></ul>",
|
||||
"changelog": "<h3>1.0.1 — 2026-03-05</h3><ul><li><strong>Compatibility:</strong> Confirmed compatibility with WordPress 7.0. Supported range is now WordPress 6.7 – 7.0.</li></ul><h3>1.0.0 — 2026-02-18</h3><ul><li><strong>Initial Release:</strong> First stable release of OpenGraph (by ROBOTSTXT).</li><li><strong>Added:</strong> Full Open Graph meta tags: og:title, og:type, og:url, og:description, og:site_name, og:locale, og:image, og:image:width, og:image:height, og:image:type, og:image:alt.</li><li><strong>Added:</strong> Article tags for singular posts: article:published_time, article:modified_time, article:section, article:tag.</li><li><strong>Added:</strong> Twitter Card tags: twitter:card, twitter:site, twitter:image.</li><li><strong>Added:</strong> Automatic detection and HTTP HEAD-based resolution of JPEG/PNG alternatives for AVIF, WebP, GIF, BMP, SVG, and TIFF images.</li><li><strong>Added:</strong> Postmeta caching with automatic invalidation on featured image change.</li><li><strong>Added:</strong> Per-post OG title and description overrides via post editor meta box.</li><li><strong>Added:</strong> Global fallback image URL and homepage-specific image URL settings.</li><li><strong>Added:</strong> Yoast SEO and RankMath integration (no duplicate tags).</li><li><strong>Added:</strong> Admin settings page with Settings, Tools, and Diagnostics tabs.</li><li><strong>Added:</strong> WP-CLI commands: wp og-fallback resolve and wp og-fallback clear-cache.</li><li><strong>Added:</strong> REST API: POST /wp-json/robotstxt-og/v1/resolve/{post_id} and GET /wp-json/robotstxt-og/v1/status/{post_id}.</li><li><strong>Added:</strong> Developer filters: robotstxt_og_external_image_enabled, robotstxt_og_external_image_timeout, robotstxt_og_taxonomy_image, robotstxt_og_enable_logging.</li><li><strong>Added:</strong> Multisite compatible, full i18n support, Spanish (es_ES) translation included.</li><li><strong>Security:</strong> Nonce verification, manage_options capability checks, input sanitization, output escaping, prepared SQL statements throughout.</li></ul>"
|
||||
"changelog": "<h3>1.1.0 — 2026-03-28</h3><ul><li><strong>Fixed:</strong> Fatal TypeError in handle_thumbnail_change(): the deleted_post_meta action passes an array of meta IDs as its first argument. The method signature now accepts int|array to handle both updated_post_meta and deleted_post_meta correctly.</li><li><strong>Security:</strong> Added SSRF protection to all outbound HTTP HEAD requests via is_safe_url() — private and reserved IP ranges are now blocked.</li><li><strong>Security:</strong> Replaced deprecated FILTER_SANITIZE_SPECIAL_CHARS with FILTER_SANITIZE_FULL_SPECIAL_CHARS throughout.</li><li><strong>Added:</strong> GDPR Privacy API support — custom OG title and description post meta are now included in WordPress personal data export and erase requests.</li><li><strong>Changed:</strong> Settings page and REST API now require edit_others_posts capability, allowing editors to manage OG settings and refresh fallback images.</li><li><strong>Changed:</strong> Minimum supported WordPress version raised to 6.8. Network: true header added.</li><li><strong>Added:</strong> PHPUnit test suite with Brain\\Monkey; covers SSRF logic, image format detection, and cache clearing.</li></ul><h3>1.0.0 — 2026-02-18</h3><ul><li><strong>Initial Release:</strong> First stable release of OpenGraph (by ROBOTSTXT).</li></ul>"
|
||||
},
|
||||
"banners": {
|
||||
"low": "",
|
||||
|
|
|
|||
Loading…
Reference in a new issue