v1.2.0
This commit is contained in:
parent
ddaaff71ad
commit
3bb9ed33e1
12 changed files with 1762 additions and 409 deletions
|
|
@ -223,6 +223,126 @@ class Robotstxt_OG_Admin_Settings {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_fb_app_id',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'default' => '',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_fb_admins',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'default' => '',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_fb_pages',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'default' => '',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_pinterest_verify',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'default' => '',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_pinterest_nopin',
|
||||||
|
array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'sanitize_callback' => 'rest_sanitize_boolean',
|
||||||
|
'default' => false,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_telegram_channel',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'default' => '',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_slack_app_id',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'default' => '',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_theme_color',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_hex_color',
|
||||||
|
'default' => '',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_app_name',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'default' => '',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_web_app_capable',
|
||||||
|
array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'sanitize_callback' => 'rest_sanitize_boolean',
|
||||||
|
'default' => false,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_app_status_bar_style',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => array( $this, 'sanitize_status_bar_style' ),
|
||||||
|
'default' => '',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
'robotstxt_og_settings',
|
||||||
|
'robotstxt_og_format_detection',
|
||||||
|
array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'sanitize_callback' => 'rest_sanitize_boolean',
|
||||||
|
'default' => false,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// --- General Settings section ---
|
// --- General Settings section ---
|
||||||
add_settings_section(
|
add_settings_section(
|
||||||
'robotstxt_og_main_section',
|
'robotstxt_og_main_section',
|
||||||
|
|
@ -294,6 +414,118 @@ class Robotstxt_OG_Admin_Settings {
|
||||||
'robotstxt-og-settings',
|
'robotstxt-og-settings',
|
||||||
'robotstxt_og_social_section'
|
'robotstxt_og_social_section'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- Platform Integration section ---
|
||||||
|
add_settings_section(
|
||||||
|
'robotstxt_og_platform_section',
|
||||||
|
__( 'Platform Integration', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_platform_section' ),
|
||||||
|
'robotstxt-og-settings'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_fb_app_id',
|
||||||
|
__( 'Facebook App ID', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_fb_app_id_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_platform_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_fb_admins',
|
||||||
|
__( 'Facebook Admins', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_fb_admins_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_platform_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_fb_pages',
|
||||||
|
__( 'Facebook Pages', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_fb_pages_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_platform_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_pinterest_verify',
|
||||||
|
__( 'Pinterest Domain Verification', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_pinterest_verify_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_platform_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_pinterest_nopin',
|
||||||
|
__( 'Disable Pinterest Pinning', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_pinterest_nopin_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_platform_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_telegram_channel',
|
||||||
|
__( 'Telegram Channel', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_telegram_channel_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_platform_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_slack_app_id',
|
||||||
|
__( 'Slack App ID', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_slack_app_id_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_platform_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Mobile / Web App section ---
|
||||||
|
add_settings_section(
|
||||||
|
'robotstxt_og_mobile_section',
|
||||||
|
__( 'Mobile / Web App', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_mobile_section' ),
|
||||||
|
'robotstxt-og-settings'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_theme_color',
|
||||||
|
__( 'Theme Color', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_theme_color_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_mobile_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_app_name',
|
||||||
|
__( 'Web App Name', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_app_name_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_mobile_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_web_app_capable',
|
||||||
|
__( 'PWA Standalone Mode', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_web_app_capable_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_mobile_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_app_status_bar_style',
|
||||||
|
__( 'iOS Status Bar Style', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_app_status_bar_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_mobile_section'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'robotstxt_og_format_detection',
|
||||||
|
__( 'Disable Auto Phone Detection', 'robotstxt-og' ),
|
||||||
|
array( $this, 'render_format_detection_field' ),
|
||||||
|
'robotstxt-og-settings',
|
||||||
|
'robotstxt_og_mobile_section'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -529,6 +761,262 @@ class Robotstxt_OG_Admin_Settings {
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the Platform Integration section description.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_platform_section(): void {
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<?php esc_html_e( 'Site-wide identity and verification tags for Facebook, Pinterest, Telegram, and Slack. Emitted in addition to the Open Graph / Twitter tags when no SEO plugin is handling them.', 'robotstxt-og' ); ?>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a regular-text settings field for a given option.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param string $option_name Option key.
|
||||||
|
* @param string $placeholder Input placeholder.
|
||||||
|
* @param string $description Field description.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_text_option( string $option_name, string $placeholder, string $description ): void {
|
||||||
|
$raw = get_option( $option_name, '' );
|
||||||
|
$value = is_string( $raw ) ? $raw : '';
|
||||||
|
?>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="<?php echo esc_attr( $option_name ); ?>"
|
||||||
|
name="<?php echo esc_attr( $option_name ); ?>"
|
||||||
|
value="<?php echo esc_attr( $value ); ?>"
|
||||||
|
placeholder="<?php echo esc_attr( $placeholder ); ?>"
|
||||||
|
class="regular-text"
|
||||||
|
/>
|
||||||
|
<p class="description"><?php echo esc_html( $description ); ?></p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render Facebook App ID field.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_fb_app_id_field(): void {
|
||||||
|
$this->render_text_option( 'robotstxt_og_fb_app_id', '123456789012345', __( 'Facebook App ID, used for the fb:app_id meta tag and Insights.', 'robotstxt-og' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render Facebook Admins field.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_fb_admins_field(): void {
|
||||||
|
$this->render_text_option( 'robotstxt_og_fb_admins', '100000000000001,100000000000002', __( 'Comma-separated Facebook user IDs of page admins (fb:admins).', 'robotstxt-og' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render Facebook Pages field.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_fb_pages_field(): void {
|
||||||
|
$this->render_text_option( 'robotstxt_og_fb_pages', '1234567890', __( 'Facebook Page ID or comma-separated IDs (fb:pages).', 'robotstxt-og' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render Pinterest domain verification field.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_pinterest_verify_field(): void {
|
||||||
|
$this->render_text_option( 'robotstxt_og_pinterest_verify', 'abcdef0123456789abcdef', __( 'Pinterest domain verification token (p:domain_verify).', 'robotstxt-og' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render Pinterest "disable pinning" field.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_pinterest_nopin_field(): void {
|
||||||
|
$value = (bool) get_option( 'robotstxt_og_pinterest_nopin', false );
|
||||||
|
?>
|
||||||
|
<label for="robotstxt_og_pinterest_nopin">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="robotstxt_og_pinterest_nopin"
|
||||||
|
name="robotstxt_og_pinterest_nopin"
|
||||||
|
value="1"
|
||||||
|
<?php checked( $value ); ?>
|
||||||
|
/>
|
||||||
|
<?php esc_html_e( 'Emit the Pinterest "nopin" meta tag to discourage pinning images from this site.', 'robotstxt-og' ); ?>
|
||||||
|
</label>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render Telegram channel field.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_telegram_channel_field(): void {
|
||||||
|
$this->render_text_option( 'robotstxt_og_telegram_channel', '@channel', __( 'Telegram channel handle (telegram:channel), including the leading @.', 'robotstxt-og' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render Slack App ID field.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_slack_app_id_field(): void {
|
||||||
|
$this->render_text_option( 'robotstxt_og_slack_app_id', 'A0123456789', __( 'Slack App ID for custom unfurls (slack-app-id).', 'robotstxt-og' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize the iOS status bar style to a safelist value.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param mixed $value Raw value.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function sanitize_status_bar_style( $value ): string {
|
||||||
|
$value = is_string( $value ) ? $value : '';
|
||||||
|
$allowed = array( '', 'default', 'black', 'black-translucent' );
|
||||||
|
|
||||||
|
return in_array( $value, $allowed, true ) ? $value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the Mobile / Web App section description.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_mobile_section(): void {
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<?php esc_html_e( 'Optional mobile and Progressive Web App metadata. Tags are emitted only when configured.', 'robotstxt-og' ); ?>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a checkbox settings field for a boolean option.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param string $option_name Option key.
|
||||||
|
* @param string $label_text Checkbox label.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function render_checkbox_option( string $option_name, string $label_text ): void {
|
||||||
|
$value = (bool) get_option( $option_name, false );
|
||||||
|
?>
|
||||||
|
<label for="<?php echo esc_attr( $option_name ); ?>">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="<?php echo esc_attr( $option_name ); ?>"
|
||||||
|
name="<?php echo esc_attr( $option_name ); ?>"
|
||||||
|
value="1"
|
||||||
|
<?php checked( $value ); ?>
|
||||||
|
/>
|
||||||
|
<?php echo esc_html( $label_text ); ?>
|
||||||
|
</label>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render theme-color field.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_theme_color_field(): void {
|
||||||
|
$this->render_text_option( 'robotstxt_og_theme_color', '#ffffff', __( 'Hex color for the browser UI chrome (theme-color).', 'robotstxt-og' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render web app name field.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_app_name_field(): void {
|
||||||
|
$this->render_text_option( 'robotstxt_og_app_name', 'My Site', __( 'Web app name; emitted as application-name and apple-mobile-web-app-title.', 'robotstxt-og' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the PWA standalone toggle.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_web_app_capable_field(): void {
|
||||||
|
$this->render_checkbox_option( 'robotstxt_og_web_app_capable', __( 'Emit mobile-web-app-capable and apple-mobile-web-app-capable (standalone PWA mode).', 'robotstxt-og' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the iOS status bar style dropdown.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_app_status_bar_field(): void {
|
||||||
|
$raw = get_option( 'robotstxt_og_app_status_bar_style', '' );
|
||||||
|
$value = is_string( $raw ) ? $raw : '';
|
||||||
|
$options = array(
|
||||||
|
'' => __( 'Default (omit tag)', 'robotstxt-og' ),
|
||||||
|
'default' => 'default',
|
||||||
|
'black' => 'black',
|
||||||
|
'black-translucent' => 'black-translucent',
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<select name="robotstxt_og_app_status_bar_style" id="robotstxt_og_app_status_bar_style">
|
||||||
|
<?php foreach ( $options as $val => $label ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $value, $val ); ?>>
|
||||||
|
<?php echo esc_html( $label ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the format-detection toggle.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_format_detection_field(): void {
|
||||||
|
$this->render_checkbox_option( 'robotstxt_og_format_detection', __( 'Emit format-detection to disable automatic phone-number linking.', 'robotstxt-og' ) );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render settings page.
|
* Render settings page.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ $page_url = admin_url( 'options-general.php?page=robotstxt-og-settings' );
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
if ( ! empty( $test_url_raw ) ) {
|
if ( ! empty( $test_url_raw ) ) {
|
||||||
$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 ) ) : '';
|
$nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : '';
|
||||||
|
|
||||||
if ( wp_verify_nonce( $nonce, 'robotstxt_og_test_url' ) ) {
|
if ( wp_verify_nonce( $nonce, 'robotstxt_og_test_url' ) ) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,24 @@
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 1.2.0 =
|
||||||
|
|
||||||
|
_Release date: 2026-08-10_
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
* Per-term Open Graph metadata for Categories and Tags on the term edit page (`term.php`): custom OG title, OG image URL, and OG description, exposed on term archives.
|
||||||
|
* Manual `og:image:alt` override (per-post meta box and per-term), emitted as `og:image:alt` and `twitter:image:alt`.
|
||||||
|
* Per-post `og:type` override (Default / `website` / `article`); `article:*` sub-tags now emit on the resolved article type.
|
||||||
|
* Twitter/X: emit `twitter:url` (canonical) and per-post `twitter:creator` (author handle); handles normalised with a leading `@`.
|
||||||
|
* Platform integration tags (global settings): Facebook `fb:app_id`/`admins`/`pages`, Pinterest `p:domain_verify` + disable-pinning toggle, Telegram `telegram:channel`, Slack `slack-app-id`.
|
||||||
|
* Mobile / Web App tags (global settings): `theme-color`, web-app name, PWA standalone toggle, iOS status bar style, `format-detection`; Open Graph namespace added to `<html>` via `language_attributes`.
|
||||||
|
* Uninstall cleanup extended to remove the per-term OG meta and `_og_image_alt` / `_og_type` / `_twitter_creator` (opt-in only).
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
* Minimum supported WordPress lowered from 6.8 to 4.9.8 (verified by WP-Compat; bound by register_post_meta/register_term_meta at 4.9.8).
|
||||||
|
* Minimum supported PHP lowered from 8.2 to 8.0 (verified by the PHPCompatibility scan; the code uses 8.0 features only).
|
||||||
|
|
||||||
= 1.1.0 =
|
= 1.1.0 =
|
||||||
|
|
||||||
_Release date: 2026-03-28_
|
_Release date: 2026-03-28_
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,14 @@ class Robotstxt_OG_Image_Fallback {
|
||||||
*/
|
*/
|
||||||
private Robotstxt_OG_Meta_Box $meta_box;
|
private Robotstxt_OG_Meta_Box $meta_box;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Term meta instance.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
* @var Robotstxt_OG_Term_Meta
|
||||||
|
*/
|
||||||
|
private Robotstxt_OG_Term_Meta $term_meta;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get singleton instance.
|
* Get singleton instance.
|
||||||
*
|
*
|
||||||
|
|
@ -103,6 +111,9 @@ class Robotstxt_OG_Image_Fallback {
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function init(): void {
|
public function init(): void {
|
||||||
|
// Load bundled translations (required for non-wordpress.org distribution).
|
||||||
|
load_plugin_textdomain( 'robotstxt-og', false, basename( ROBOTSTXT_OG_PATH ) . '/languages' );
|
||||||
|
|
||||||
// Load dependencies.
|
// Load dependencies.
|
||||||
$this->load_dependencies();
|
$this->load_dependencies();
|
||||||
|
|
||||||
|
|
@ -121,6 +132,10 @@ class Robotstxt_OG_Image_Fallback {
|
||||||
$this->meta_box = new Robotstxt_OG_Meta_Box();
|
$this->meta_box = new Robotstxt_OG_Meta_Box();
|
||||||
$this->meta_box->init();
|
$this->meta_box->init();
|
||||||
|
|
||||||
|
// Initialize term meta (category/tag OG fields on term.php).
|
||||||
|
$this->term_meta = new Robotstxt_OG_Term_Meta();
|
||||||
|
$this->term_meta->init();
|
||||||
|
|
||||||
// Initialize admin settings if in admin context.
|
// Initialize admin settings if in admin context.
|
||||||
if ( is_admin() ) {
|
if ( is_admin() ) {
|
||||||
$this->admin_settings = new Robotstxt_OG_Admin_Settings( $this->resolver );
|
$this->admin_settings = new Robotstxt_OG_Admin_Settings( $this->resolver );
|
||||||
|
|
@ -157,6 +172,7 @@ class Robotstxt_OG_Image_Fallback {
|
||||||
require_once ROBOTSTXT_OG_PATH . 'includes/class-robotstxt-og-tags.php';
|
require_once ROBOTSTXT_OG_PATH . 'includes/class-robotstxt-og-tags.php';
|
||||||
require_once ROBOTSTXT_OG_PATH . 'includes/class-robotstxt-og-rest-api.php';
|
require_once ROBOTSTXT_OG_PATH . 'includes/class-robotstxt-og-rest-api.php';
|
||||||
require_once ROBOTSTXT_OG_PATH . 'includes/class-robotstxt-og-meta-box.php';
|
require_once ROBOTSTXT_OG_PATH . 'includes/class-robotstxt-og-meta-box.php';
|
||||||
|
require_once ROBOTSTXT_OG_PATH . 'includes/class-robotstxt-og-term-meta.php';
|
||||||
|
|
||||||
// Load admin class if in admin context.
|
// Load admin class if in admin context.
|
||||||
if ( is_admin() ) {
|
if ( is_admin() ) {
|
||||||
|
|
@ -297,8 +313,10 @@ class Robotstxt_OG_Image_Fallback {
|
||||||
foreach ( $posts as $post_id ) {
|
foreach ( $posts as $post_id ) {
|
||||||
$og_title = get_post_meta( $post_id, '_og_title', true );
|
$og_title = get_post_meta( $post_id, '_og_title', true );
|
||||||
$og_desc = get_post_meta( $post_id, '_og_description', true );
|
$og_desc = get_post_meta( $post_id, '_og_description', true );
|
||||||
|
$creator = get_post_meta( $post_id, '_twitter_creator', true );
|
||||||
|
$og_alt = get_post_meta( $post_id, '_og_image_alt', true );
|
||||||
|
|
||||||
if ( empty( $og_title ) && empty( $og_desc ) ) {
|
if ( empty( $og_title ) && empty( $og_desc ) && empty( $creator ) && empty( $og_alt ) ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,6 +336,20 @@ class Robotstxt_OG_Image_Fallback {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $creator ) && is_string( $creator ) ) {
|
||||||
|
$item_data[] = array(
|
||||||
|
'name' => __( 'Twitter Author Handle', 'robotstxt-og' ),
|
||||||
|
'value' => $creator,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $og_alt ) && is_string( $og_alt ) ) {
|
||||||
|
$item_data[] = array(
|
||||||
|
'name' => __( 'Custom OG Image Alt', 'robotstxt-og' ),
|
||||||
|
'value' => $og_alt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ( ! empty( $item_data ) ) {
|
if ( ! empty( $item_data ) ) {
|
||||||
$data[] = array(
|
$data[] = array(
|
||||||
'group_id' => 'robotstxt-og-post-meta',
|
'group_id' => 'robotstxt-og-post-meta',
|
||||||
|
|
@ -374,10 +406,15 @@ class Robotstxt_OG_Image_Fallback {
|
||||||
$items_removed = 0;
|
$items_removed = 0;
|
||||||
|
|
||||||
foreach ( $posts as $post_id ) {
|
foreach ( $posts as $post_id ) {
|
||||||
$deleted_title = delete_post_meta( $post_id, '_og_title' );
|
$removed = false;
|
||||||
$deleted_desc = delete_post_meta( $post_id, '_og_description' );
|
|
||||||
|
|
||||||
if ( $deleted_title || $deleted_desc ) {
|
foreach ( array( '_og_title', '_og_description', '_twitter_creator', '_og_image_alt' ) as $meta_key ) {
|
||||||
|
if ( delete_post_meta( $post_id, $meta_key ) ) {
|
||||||
|
$removed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $removed ) {
|
||||||
++$items_removed;
|
++$items_removed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,29 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||||
*/
|
*/
|
||||||
class Robotstxt_OG_Meta_Box {
|
class Robotstxt_OG_Meta_Box {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed og:type override values (the auto-derived default is stored as empty).
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
public const ALLOWED_OG_TYPES = array( 'website', 'article' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize an og:type override value to a safe value or empty string.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param mixed $value Raw value.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function sanitize_og_type( $value ): string {
|
||||||
|
$value = is_string( $value ) ? $value : '';
|
||||||
|
|
||||||
|
return in_array( $value, self::ALLOWED_OG_TYPES, true ) ? $value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize hooks.
|
* Initialize hooks.
|
||||||
*
|
*
|
||||||
|
|
@ -82,6 +105,48 @@ class Robotstxt_OG_Meta_Box {
|
||||||
'show_in_rest' => true,
|
'show_in_rest' => true,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
register_post_meta(
|
||||||
|
$post_type,
|
||||||
|
'_og_image_alt',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'single' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'auth_callback' => static function () {
|
||||||
|
return current_user_can( 'edit_posts' );
|
||||||
|
},
|
||||||
|
'show_in_rest' => true,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_post_meta(
|
||||||
|
$post_type,
|
||||||
|
'_og_type',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'single' => true,
|
||||||
|
'sanitize_callback' => array( __CLASS__, 'sanitize_og_type' ),
|
||||||
|
'auth_callback' => static function () {
|
||||||
|
return current_user_can( 'edit_posts' );
|
||||||
|
},
|
||||||
|
'show_in_rest' => true,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_post_meta(
|
||||||
|
$post_type,
|
||||||
|
'_twitter_creator',
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'single' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'auth_callback' => static function () {
|
||||||
|
return current_user_can( 'edit_posts' );
|
||||||
|
},
|
||||||
|
'show_in_rest' => true,
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,10 +187,58 @@ class Robotstxt_OG_Meta_Box {
|
||||||
$og_title = is_string( $raw_title ) ? $raw_title : '';
|
$og_title = is_string( $raw_title ) ? $raw_title : '';
|
||||||
$raw_desc = get_post_meta( $post->ID, '_og_description', true );
|
$raw_desc = get_post_meta( $post->ID, '_og_description', true );
|
||||||
$og_description = is_string( $raw_desc ) ? $raw_desc : '';
|
$og_description = is_string( $raw_desc ) ? $raw_desc : '';
|
||||||
|
$raw_alt = get_post_meta( $post->ID, '_og_image_alt', true );
|
||||||
|
$og_image_alt = is_string( $raw_alt ) ? $raw_alt : '';
|
||||||
|
$raw_type = get_post_meta( $post->ID, '_og_type', true );
|
||||||
|
$og_type = self::sanitize_og_type( $raw_type );
|
||||||
|
$raw_creator = get_post_meta( $post->ID, '_twitter_creator', true );
|
||||||
|
$twitter_creator = is_string( $raw_creator ) ? $raw_creator : '';
|
||||||
?>
|
?>
|
||||||
<table class="form-table" role="presentation">
|
<table class="form-table" role="presentation">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="robotstxt_og_type">
|
||||||
|
<?php esc_html_e( 'Open Graph Type', 'robotstxt-og' ); ?>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select id="robotstxt_og_type" name="robotstxt_og_type">
|
||||||
|
<option value="" <?php selected( $og_type, '' ); ?>>
|
||||||
|
<?php esc_html_e( 'Default (auto: article for posts, website otherwise)', 'robotstxt-og' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="website" <?php selected( $og_type, 'website' ); ?>>
|
||||||
|
<?php esc_html_e( 'website', 'robotstxt-og' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="article" <?php selected( $og_type, 'article' ); ?>>
|
||||||
|
<?php esc_html_e( 'article', 'robotstxt-og' ); ?>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Overrides og:type for this post. Selecting "article" also emits the article:* meta tags (published/modified time, section, tags).', 'robotstxt-og' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="robotstxt_og_twitter_creator">
|
||||||
|
<?php esc_html_e( 'Twitter Author Handle', 'robotstxt-og' ); ?>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="robotstxt_og_twitter_creator"
|
||||||
|
name="robotstxt_og_twitter_creator"
|
||||||
|
value="<?php echo esc_attr( $twitter_creator ); ?>"
|
||||||
|
class="large-text"
|
||||||
|
placeholder="@username"
|
||||||
|
/>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Twitter/X handle of the post author, emitted as twitter:creator. Leave blank to omit.', 'robotstxt-og' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<label for="robotstxt_og_title">
|
<label for="robotstxt_og_title">
|
||||||
|
|
@ -163,6 +276,25 @@ class Robotstxt_OG_Meta_Box {
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="robotstxt_og_image_alt">
|
||||||
|
<?php esc_html_e( 'Custom OG Image Alt', 'robotstxt-og' ); ?>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="robotstxt_og_image_alt"
|
||||||
|
name="robotstxt_og_image_alt"
|
||||||
|
value="<?php echo esc_attr( $og_image_alt ); ?>"
|
||||||
|
class="large-text"
|
||||||
|
/>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Alt text for the og:image of this post. Leave blank to use the featured image alt text from the media library.', 'robotstxt-og' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<?php
|
<?php
|
||||||
|
|
@ -217,5 +349,35 @@ class Robotstxt_OG_Meta_Box {
|
||||||
} else {
|
} else {
|
||||||
update_post_meta( $post_id, '_og_description', $og_desc );
|
update_post_meta( $post_id, '_og_description', $og_desc );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save og:image:alt.
|
||||||
|
$og_alt_raw = filter_input( INPUT_POST, 'robotstxt_og_image_alt', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||||
|
$og_alt = $og_alt_raw ? sanitize_text_field( wp_unslash( $og_alt_raw ) ) : '';
|
||||||
|
|
||||||
|
if ( empty( $og_alt ) ) {
|
||||||
|
delete_post_meta( $post_id, '_og_image_alt' );
|
||||||
|
} else {
|
||||||
|
update_post_meta( $post_id, '_og_image_alt', $og_alt );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save og:type override (whitelisted).
|
||||||
|
$og_type_raw = filter_input( INPUT_POST, 'robotstxt_og_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||||
|
$og_type = self::sanitize_og_type( $og_type_raw ? wp_unslash( $og_type_raw ) : '' );
|
||||||
|
|
||||||
|
if ( '' === $og_type ) {
|
||||||
|
delete_post_meta( $post_id, '_og_type' );
|
||||||
|
} else {
|
||||||
|
update_post_meta( $post_id, '_og_type', $og_type );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save twitter:creator handle.
|
||||||
|
$creator_raw = filter_input( INPUT_POST, 'robotstxt_og_twitter_creator', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||||
|
$creator = $creator_raw ? sanitize_text_field( wp_unslash( $creator_raw ) ) : '';
|
||||||
|
|
||||||
|
if ( '' === $creator ) {
|
||||||
|
delete_post_meta( $post_id, '_twitter_creator' );
|
||||||
|
} else {
|
||||||
|
update_post_meta( $post_id, '_twitter_creator', $creator );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ class Robotstxt_OG_Tags {
|
||||||
if ( $this->inject_directly ) {
|
if ( $this->inject_directly ) {
|
||||||
// No SEO plugin detected, inject all tags directly.
|
// No SEO plugin detected, inject all tags directly.
|
||||||
add_action( 'wp_head', array( $this, 'inject_og_tags' ), 5 );
|
add_action( 'wp_head', array( $this, 'inject_og_tags' ), 5 );
|
||||||
|
add_action( 'wp_head', array( $this, 'output_platform_tags' ), 6 );
|
||||||
|
add_action( 'wp_head', array( $this, 'output_web_app_tags' ), 7 );
|
||||||
|
add_filter( 'language_attributes', array( $this, 'add_og_namespace_prefix' ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,6 +115,7 @@ class Robotstxt_OG_Tags {
|
||||||
}
|
}
|
||||||
|
|
||||||
$image_url = $this->get_contextual_image();
|
$image_url = $this->get_contextual_image();
|
||||||
|
$image_alt = '' !== $image_url ? $this->get_contextual_image_alt( $image_url ) : '';
|
||||||
|
|
||||||
if ( $enable_facebook ) {
|
if ( $enable_facebook ) {
|
||||||
// Output all basic OG tags: title, type, url, description, site_name, locale.
|
// Output all basic OG tags: title, type, url, description, site_name, locale.
|
||||||
|
|
@ -119,7 +123,7 @@ class Robotstxt_OG_Tags {
|
||||||
|
|
||||||
// Output image tags only when a compatible image is available.
|
// Output image tags only when a compatible image is available.
|
||||||
if ( ! empty( $image_url ) ) {
|
if ( ! empty( $image_url ) ) {
|
||||||
$this->output_og_image_tags( $image_url );
|
$this->output_og_image_tags( $image_url, $image_alt );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output article-specific tags (only for singular blog posts).
|
// Output article-specific tags (only for singular blog posts).
|
||||||
|
|
@ -127,7 +131,99 @@ class Robotstxt_OG_Tags {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $enable_twitter ) {
|
if ( $enable_twitter ) {
|
||||||
$this->output_twitter_card_tags( $image_url );
|
$this->output_twitter_card_tags( $image_url, $image_alt );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output platform-specific meta tags (Facebook, Pinterest, Telegram, Slack).
|
||||||
|
*
|
||||||
|
* Emits only the tags whose setting is non-empty. Runs independently of the
|
||||||
|
* enable_facebook/enable_twitter toggles, but only when this plugin is the
|
||||||
|
* OG handler (no SEO plugin detected).
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function output_platform_tags(): void {
|
||||||
|
$this->emit_option_tag( 'property', 'fb:app_id', 'robotstxt_og_fb_app_id' );
|
||||||
|
$this->emit_option_tag( 'property', 'fb:admins', 'robotstxt_og_fb_admins' );
|
||||||
|
$this->emit_option_tag( 'property', 'fb:pages', 'robotstxt_og_fb_pages' );
|
||||||
|
$this->emit_option_tag( 'name', 'p:domain_verify', 'robotstxt_og_pinterest_verify' );
|
||||||
|
$this->emit_option_tag( 'name', 'telegram:channel', 'robotstxt_og_telegram_channel' );
|
||||||
|
$this->emit_option_tag( 'name', 'slack-app-id', 'robotstxt_og_slack_app_id' );
|
||||||
|
|
||||||
|
if ( (bool) get_option( 'robotstxt_og_pinterest_nopin', false ) ) {
|
||||||
|
printf( '<meta name="pinterest" content="nopin" />' . "\n" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output mobile / web-app meta tags.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function output_web_app_tags(): void {
|
||||||
|
$this->emit_option_tag( 'name', 'theme-color', 'robotstxt_og_theme_color' );
|
||||||
|
$this->emit_option_tag( 'name', 'apple-mobile-web-app-status-bar-style', 'robotstxt_og_app_status_bar_style' );
|
||||||
|
|
||||||
|
// Web app name -> two tags.
|
||||||
|
$raw_name = get_option( 'robotstxt_og_app_name', '' );
|
||||||
|
$name = is_string( $raw_name ) ? trim( $raw_name ) : '';
|
||||||
|
|
||||||
|
if ( '' !== $name ) {
|
||||||
|
printf( '<meta name="application-name" content="%s" />' . "\n", esc_attr( $name ) );
|
||||||
|
printf( '<meta name="apple-mobile-web-app-title" content="%s" />' . "\n", esc_attr( $name ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( (bool) get_option( 'robotstxt_og_web_app_capable', false ) ) {
|
||||||
|
printf( '<meta name="mobile-web-app-capable" content="yes" />' . "\n" );
|
||||||
|
printf( '<meta name="apple-mobile-web-app-capable" content="yes" />' . "\n" );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( (bool) get_option( 'robotstxt_og_format_detection', false ) ) {
|
||||||
|
printf( '<meta name="format-detection" content="telephone=no" />' . "\n" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the Open Graph namespace prefix to the <html> tag attributes.
|
||||||
|
*
|
||||||
|
* Hooked to `language_attributes`. Skipped if the OG namespace is already
|
||||||
|
* present (e.g. added by the theme).
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param string $output Existing attribute string.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function add_og_namespace_prefix( string $output ): string {
|
||||||
|
if ( false !== stripos( $output, 'og:' ) ) {
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output . ' prefix="og: http://ogp.me/ns#"';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a single meta tag driven by a string option, when non-empty.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param string $attr Attribute kind: 'property' or 'name'.
|
||||||
|
* @param string $tag Tag name (e.g. 'fb:app_id').
|
||||||
|
* @param string $option Option key.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function emit_option_tag( string $attr, string $tag, string $option ): void {
|
||||||
|
$raw = get_option( $option, '' );
|
||||||
|
$value = is_string( $raw ) ? trim( $raw ) : '';
|
||||||
|
|
||||||
|
if ( '' !== $value ) {
|
||||||
|
printf( '<meta %s="%s" content="%s" />' . "\n", esc_attr( $attr ), esc_attr( $tag ), esc_attr( $value ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,6 +309,70 @@ class Robotstxt_OG_Tags {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the og:image:alt text for the current context.
|
||||||
|
*
|
||||||
|
* A per-post / per-term manual override (`_og_image_alt`) wins; otherwise the
|
||||||
|
* alt text is read from the media library attachment.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param string $image_url Resolved image URL.
|
||||||
|
* @return string Alt text, or empty string.
|
||||||
|
*/
|
||||||
|
private function get_contextual_image_alt( string $image_url ): string {
|
||||||
|
$manual = $this->get_manual_image_alt();
|
||||||
|
|
||||||
|
if ( '' !== $manual ) {
|
||||||
|
return $manual;
|
||||||
|
}
|
||||||
|
|
||||||
|
$image_id = attachment_url_to_postid( $image_url );
|
||||||
|
|
||||||
|
if ( $image_id ) {
|
||||||
|
$raw_alt = get_post_meta( $image_id, '_wp_attachment_image_alt', true );
|
||||||
|
|
||||||
|
return is_string( $raw_alt ) ? $raw_alt : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the per-context manual og:image:alt override.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return string Override alt text, or empty string.
|
||||||
|
*/
|
||||||
|
private function get_manual_image_alt(): string {
|
||||||
|
if ( is_singular() ) {
|
||||||
|
$post_id = get_queried_object_id();
|
||||||
|
|
||||||
|
if ( $post_id ) {
|
||||||
|
$raw = get_post_meta( $post_id, '_og_image_alt', true );
|
||||||
|
|
||||||
|
if ( is_string( $raw ) && '' !== $raw ) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_tax() || is_category() || is_tag() ) {
|
||||||
|
$term = get_queried_object();
|
||||||
|
|
||||||
|
if ( $term instanceof WP_Term ) {
|
||||||
|
$raw = get_term_meta( $term->term_id, '_og_image_alt', true );
|
||||||
|
|
||||||
|
if ( is_string( $raw ) && '' !== $raw ) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the og:title for the current context.
|
* Get the og:title for the current context.
|
||||||
*
|
*
|
||||||
|
|
@ -243,6 +403,12 @@ class Robotstxt_OG_Tags {
|
||||||
$term = get_queried_object();
|
$term = get_queried_object();
|
||||||
|
|
||||||
if ( $term instanceof WP_Term ) {
|
if ( $term instanceof WP_Term ) {
|
||||||
|
$custom_title = get_term_meta( $term->term_id, '_og_title', true );
|
||||||
|
|
||||||
|
if ( is_string( $custom_title ) && '' !== $custom_title ) {
|
||||||
|
return $custom_title;
|
||||||
|
}
|
||||||
|
|
||||||
return (string) $term->name;
|
return (string) $term->name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -287,10 +453,18 @@ class Robotstxt_OG_Tags {
|
||||||
if ( is_tax() || is_category() || is_tag() ) {
|
if ( is_tax() || is_category() || is_tag() ) {
|
||||||
$term = get_queried_object();
|
$term = get_queried_object();
|
||||||
|
|
||||||
if ( $term instanceof WP_Term && ! empty( $term->description ) ) {
|
if ( $term instanceof WP_Term ) {
|
||||||
|
$custom_desc = get_term_meta( $term->term_id, '_og_description', true );
|
||||||
|
|
||||||
|
if ( is_string( $custom_desc ) && '' !== $custom_desc ) {
|
||||||
|
return wp_strip_all_tags( $custom_desc );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $term->description ) ) {
|
||||||
return wp_strip_all_tags( $term->description );
|
return wp_strip_all_tags( $term->description );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
@ -337,6 +511,20 @@ class Robotstxt_OG_Tags {
|
||||||
* @return string 'article' or 'website'.
|
* @return string 'article' or 'website'.
|
||||||
*/
|
*/
|
||||||
private function get_og_type(): string {
|
private function get_og_type(): string {
|
||||||
|
// Per-post override wins.
|
||||||
|
if ( is_singular() ) {
|
||||||
|
$post_id = get_queried_object_id();
|
||||||
|
|
||||||
|
if ( $post_id ) {
|
||||||
|
$custom = get_post_meta( $post_id, '_og_type', true );
|
||||||
|
|
||||||
|
if ( is_string( $custom ) && '' !== $custom ) {
|
||||||
|
return $custom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: "article" for blog posts, "website" otherwise.
|
||||||
if ( is_singular( 'post' ) ) {
|
if ( is_singular( 'post' ) ) {
|
||||||
return 'article';
|
return 'article';
|
||||||
}
|
}
|
||||||
|
|
@ -411,9 +599,10 @@ class Robotstxt_OG_Tags {
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*
|
*
|
||||||
* @param string $image_url Image URL.
|
* @param string $image_url Image URL.
|
||||||
|
* @param string $manual_alt Manual alt override (preferred over media library).
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function output_og_image_tags( string $image_url ): void {
|
private function output_og_image_tags( string $image_url, string $manual_alt = '' ): void {
|
||||||
// Basic OG image tag.
|
// Basic OG image tag.
|
||||||
printf(
|
printf(
|
||||||
'<meta property="og:image" content="%s" />' . "\n",
|
'<meta property="og:image" content="%s" />' . "\n",
|
||||||
|
|
@ -428,7 +617,7 @@ class Robotstxt_OG_Tags {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get dimensions, MIME type, and alt text from the media library.
|
// Try to get dimensions and MIME type from the media library.
|
||||||
$image_id = attachment_url_to_postid( $image_url );
|
$image_id = attachment_url_to_postid( $image_url );
|
||||||
|
|
||||||
if ( $image_id ) {
|
if ( $image_id ) {
|
||||||
|
|
@ -456,19 +645,23 @@ class Robotstxt_OG_Tags {
|
||||||
esc_attr( $mime_type )
|
esc_attr( $mime_type )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Alt text from the media library (set when uploading/editing the image).
|
// Alt text: a manual override wins over the media library value.
|
||||||
|
$alt_text = '' !== $manual_alt ? $manual_alt : '';
|
||||||
|
|
||||||
|
if ( '' === $alt_text && $image_id ) {
|
||||||
$raw_alt = 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 : '';
|
$alt_text = is_string( $raw_alt ) ? $raw_alt : '';
|
||||||
|
}
|
||||||
|
|
||||||
if ( ! empty( $alt_text ) ) {
|
if ( '' !== $alt_text ) {
|
||||||
printf(
|
printf(
|
||||||
'<meta property="og:image:alt" content="%s" />' . "\n",
|
'<meta property="og:image:alt" content="%s" />' . "\n",
|
||||||
esc_attr( $alt_text )
|
esc_attr( $alt_text )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Output article-specific Open Graph meta tags.
|
* Output article-specific Open Graph meta tags.
|
||||||
|
|
@ -481,7 +674,7 @@ class Robotstxt_OG_Tags {
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function output_article_tags(): void {
|
private function output_article_tags(): void {
|
||||||
if ( ! is_singular( 'post' ) ) {
|
if ( 'article' !== $this->get_og_type() ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -532,6 +725,51 @@ class Robotstxt_OG_Tags {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise a Twitter/X handle: trim and ensure a leading "@".
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param string $handle Raw handle.
|
||||||
|
* @return string Normalised handle, or empty string.
|
||||||
|
*/
|
||||||
|
private function format_twitter_handle( string $handle ): string {
|
||||||
|
$handle = trim( $handle );
|
||||||
|
|
||||||
|
if ( '' === $handle ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( '@' !== substr( $handle, 0, 1 ) ) {
|
||||||
|
$handle = '@' . $handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the per-post twitter:creator handle.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return string Handle (without "@"), or empty string.
|
||||||
|
*/
|
||||||
|
private function get_twitter_creator(): string {
|
||||||
|
if ( ! is_singular() ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$post_id = get_queried_object_id();
|
||||||
|
|
||||||
|
if ( ! $post_id ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = get_post_meta( $post_id, '_twitter_creator', true );
|
||||||
|
|
||||||
|
return is_string( $raw ) ? trim( $raw ) : '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Output Twitter Card meta tags.
|
* Output Twitter Card meta tags.
|
||||||
*
|
*
|
||||||
|
|
@ -542,9 +780,10 @@ class Robotstxt_OG_Tags {
|
||||||
* @since 1.1.0
|
* @since 1.1.0
|
||||||
*
|
*
|
||||||
* @param string $image_url Image URL, or empty string if no image available.
|
* @param string $image_url Image URL, or empty string if no image available.
|
||||||
|
* @param string $image_alt Alt text for the image, or empty string.
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function output_twitter_card_tags( string $image_url ): void {
|
private function output_twitter_card_tags( string $image_url, string $image_alt = '' ): void {
|
||||||
$card_option = 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';
|
$card_type = is_string( $card_option ) ? $card_option : 'summary_large_image';
|
||||||
|
|
||||||
|
|
@ -558,28 +797,50 @@ class Robotstxt_OG_Tags {
|
||||||
esc_attr( $card_type )
|
esc_attr( $card_type )
|
||||||
);
|
);
|
||||||
|
|
||||||
// Site handle (e.g. @example).
|
// Canonical URL.
|
||||||
$twitter_option = get_option( 'robotstxt_og_twitter_site', '' );
|
$url = $this->get_og_url();
|
||||||
$twitter_site = sanitize_text_field( is_string( $twitter_option ) ? $twitter_option : '' );
|
|
||||||
|
|
||||||
if ( ! empty( $twitter_site ) ) {
|
if ( '' !== $url ) {
|
||||||
// Ensure the handle includes the @ prefix.
|
printf(
|
||||||
if ( '@' !== substr( $twitter_site, 0, 1 ) ) {
|
'<meta name="twitter:url" content="%s" />' . "\n",
|
||||||
$twitter_site = '@' . $twitter_site;
|
esc_url( $url )
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Site handle (e.g. @example).
|
||||||
|
$twitter_option = get_option( 'robotstxt_og_twitter_site', '' );
|
||||||
|
$twitter_site = $this->format_twitter_handle( is_string( $twitter_option ) ? $twitter_option : '' );
|
||||||
|
|
||||||
|
if ( '' !== $twitter_site ) {
|
||||||
printf(
|
printf(
|
||||||
'<meta name="twitter:site" content="%s" />' . "\n",
|
'<meta name="twitter:site" content="%s" />' . "\n",
|
||||||
esc_attr( $twitter_site )
|
esc_attr( $twitter_site )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Author handle (twitter:creator) — per-post override.
|
||||||
|
$creator = $this->format_twitter_handle( $this->get_twitter_creator() );
|
||||||
|
|
||||||
|
if ( '' !== $creator ) {
|
||||||
|
printf(
|
||||||
|
'<meta name="twitter:creator" content="%s" />' . "\n",
|
||||||
|
esc_attr( $creator )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Image (only when available; Twitter falls back to og:image otherwise).
|
// Image (only when available; Twitter falls back to og:image otherwise).
|
||||||
if ( ! empty( $image_url ) ) {
|
if ( ! empty( $image_url ) ) {
|
||||||
printf(
|
printf(
|
||||||
'<meta name="twitter:image" content="%s" />' . "\n",
|
'<meta name="twitter:image" content="%s" />' . "\n",
|
||||||
esc_url( $image_url )
|
esc_url( $image_url )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ( ! empty( $image_alt ) ) {
|
||||||
|
printf(
|
||||||
|
'<meta name="twitter:image:alt" content="%s" />' . "\n",
|
||||||
|
esc_attr( $image_alt )
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
359
includes/class-robotstxt-og-term-meta.php
Normal file
359
includes/class-robotstxt-og-term-meta.php
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Term Meta Class
|
||||||
|
*
|
||||||
|
* Provides per-term Open Graph fields (title, image, description) on the
|
||||||
|
* category and tag edit pages (term.php), and exposes the stored image to the
|
||||||
|
* taxonomy archive image resolution via the `robotstxt_og_taxonomy_image`
|
||||||
|
* filter.
|
||||||
|
*
|
||||||
|
* @package ROBOTSTXT_OG
|
||||||
|
* @since 1.2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Robotstxt_OG_Term_Meta
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*/
|
||||||
|
class Robotstxt_OG_Term_Meta {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Term meta key for the custom OG title.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const META_TITLE = '_og_title';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Term meta key for the custom OG image URL.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const META_IMAGE = '_og_image';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Term meta key for the custom OG description.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const META_DESCRIPTION = '_og_description';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Term meta key for the custom OG image alt text.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const META_IMAGE_ALT = '_og_image_alt';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nonce action for the term edit form.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const NONCE_ACTION = 'robotstxt_og_term_meta';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nonce field name for the term edit form.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const NONCE_NAME = 'robotstxt_og_term_meta_nonce';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize hooks.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function init(): void {
|
||||||
|
add_action( 'init', array( $this, 'register_term_meta' ) );
|
||||||
|
add_filter( 'robotstxt_og_taxonomy_image', array( $this, 'filter_taxonomy_image' ), 10, 2 );
|
||||||
|
|
||||||
|
if ( ! is_admin() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $this->get_supported_taxonomies() as $taxonomy ) {
|
||||||
|
add_action( "{$taxonomy}_edit_form_fields", array( $this, 'render_term_fields' ) );
|
||||||
|
add_action(
|
||||||
|
"edited_{$taxonomy}",
|
||||||
|
function ( $term_id ) use ( $taxonomy ): void {
|
||||||
|
$this->save_term_meta( (int) $term_id, $taxonomy );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of taxonomies the OG fields are exposed on.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return string[] Taxonomy slugs.
|
||||||
|
*/
|
||||||
|
protected function get_supported_taxonomies(): array {
|
||||||
|
$taxonomies = array( 'category', 'post_tag' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the taxonomies that expose per-term Open Graph fields.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param string[] $taxonomies Taxonomy slugs.
|
||||||
|
*/
|
||||||
|
return apply_filters( 'robotstxt_og_term_meta_taxonomies', $taxonomies );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register term meta for REST API / block editor access.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register_term_meta(): void {
|
||||||
|
$fields = array(
|
||||||
|
self::META_TITLE => 'sanitize_text_field',
|
||||||
|
self::META_IMAGE => 'sanitize_url',
|
||||||
|
self::META_IMAGE_ALT => 'sanitize_text_field',
|
||||||
|
self::META_DESCRIPTION => 'sanitize_textarea_field',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $this->get_supported_taxonomies() as $taxonomy ) {
|
||||||
|
$cap = $this->get_edit_cap( $taxonomy );
|
||||||
|
|
||||||
|
foreach ( $fields as $key => $sanitize ) {
|
||||||
|
register_term_meta(
|
||||||
|
$taxonomy,
|
||||||
|
$key,
|
||||||
|
array(
|
||||||
|
'type' => 'string',
|
||||||
|
'single' => true,
|
||||||
|
'sanitize_callback' => $sanitize,
|
||||||
|
'auth_callback' => static function () use ( $cap ) {
|
||||||
|
return current_user_can( $cap );
|
||||||
|
},
|
||||||
|
'show_in_rest' => true,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the edit capability for a taxonomy.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param string $taxonomy Taxonomy slug.
|
||||||
|
* @return string Capability name.
|
||||||
|
*/
|
||||||
|
protected function get_edit_cap( string $taxonomy ): string {
|
||||||
|
$tax_obj = get_taxonomy( $taxonomy );
|
||||||
|
|
||||||
|
if ( $tax_obj && isset( $tax_obj->cap->edit_terms ) && '' !== $tax_obj->cap->edit_terms ) {
|
||||||
|
return $tax_obj->cap->edit_terms;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'manage_categories';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supply the stored term image URL during taxonomy archive resolution.
|
||||||
|
*
|
||||||
|
* Hooked to `robotstxt_og_taxonomy_image`. The resolver still validates the
|
||||||
|
* URL and resolves a compatible format, so a raw stored value is fine.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param string $url Default URL (empty string).
|
||||||
|
* @param int $term_id Term ID.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function filter_taxonomy_image( string $url, $term_id ): string {
|
||||||
|
$term_id = (int) $term_id;
|
||||||
|
|
||||||
|
if ( $term_id <= 0 ) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stored = get_term_meta( $term_id, self::META_IMAGE, true );
|
||||||
|
$stored = is_string( $stored ) ? $stored : '';
|
||||||
|
|
||||||
|
return '' !== $stored ? $stored : $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the OG fields on the term edit page.
|
||||||
|
*
|
||||||
|
* Fires inside the existing term-edit form-table, so rows are emitted
|
||||||
|
* directly (no wrapping table).
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param WP_Term $term The term being edited.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_term_fields( WP_Term $term ): void {
|
||||||
|
wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME );
|
||||||
|
|
||||||
|
$title = $this->get_meta_string( (int) $term->term_id, self::META_TITLE );
|
||||||
|
$image = $this->get_meta_string( (int) $term->term_id, self::META_IMAGE );
|
||||||
|
$alt = $this->get_meta_string( (int) $term->term_id, self::META_IMAGE_ALT );
|
||||||
|
$desc = $this->get_meta_string( (int) $term->term_id, self::META_DESCRIPTION );
|
||||||
|
?>
|
||||||
|
<tr class="form-field robotstxt-og-term-row">
|
||||||
|
<th scope="row">
|
||||||
|
<label for="robotstxt_og_term_title"><?php esc_html_e( 'Custom OG Title', 'robotstxt-og' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="robotstxt_og_term_title"
|
||||||
|
name="robotstxt_og_term_title"
|
||||||
|
value="<?php echo esc_attr( $title ); ?>"
|
||||||
|
class="large-text"
|
||||||
|
/>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Overrides og:title for this term archive. Leave blank to use the term name.', 'robotstxt-og' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="form-field robotstxt-og-term-row">
|
||||||
|
<th scope="row">
|
||||||
|
<label for="robotstxt_og_term_image"><?php esc_html_e( 'OG Image URL', 'robotstxt-og' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="robotstxt_og_term_image"
|
||||||
|
name="robotstxt_og_term_image"
|
||||||
|
value="<?php echo esc_attr( $image ); ?>"
|
||||||
|
class="large-text"
|
||||||
|
/>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Absolute HTTPS URL for og:image on this term archive. JPEG or PNG recommended.', 'robotstxt-og' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="form-field robotstxt-og-term-row">
|
||||||
|
<th scope="row">
|
||||||
|
<label for="robotstxt_og_term_image_alt"><?php esc_html_e( 'OG Image Alt', 'robotstxt-og' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="robotstxt_og_term_image_alt"
|
||||||
|
name="robotstxt_og_term_image_alt"
|
||||||
|
value="<?php echo esc_attr( $alt ); ?>"
|
||||||
|
class="large-text"
|
||||||
|
/>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Alt text for the term archive og:image. Leave blank to use the media library alt text.', 'robotstxt-og' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="form-field robotstxt-og-term-row">
|
||||||
|
<th scope="row">
|
||||||
|
<label for="robotstxt_og_term_description"><?php esc_html_e( 'Custom OG Description', 'robotstxt-og' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<textarea
|
||||||
|
id="robotstxt_og_term_description"
|
||||||
|
name="robotstxt_og_term_description"
|
||||||
|
class="large-text"
|
||||||
|
rows="3"
|
||||||
|
><?php echo esc_textarea( $desc ); ?></textarea>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Overrides og:description for this term archive. Leave blank to use the term description.', 'robotstxt-og' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the term OG fields.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param int $term_id Term ID.
|
||||||
|
* @param string $taxonomy Taxonomy slug.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function save_term_meta( int $term_id, string $taxonomy ): void {
|
||||||
|
$nonce_raw = filter_input( INPUT_POST, self::NONCE_NAME, FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||||
|
$nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : '';
|
||||||
|
|
||||||
|
if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! current_user_can( $this->get_edit_cap( $taxonomy ) ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title_raw = filter_input( INPUT_POST, 'robotstxt_og_term_title', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||||
|
$title = $title_raw ? sanitize_text_field( wp_unslash( $title_raw ) ) : '';
|
||||||
|
|
||||||
|
$image_raw = filter_input( INPUT_POST, 'robotstxt_og_term_image', FILTER_UNSAFE_RAW );
|
||||||
|
$image = $image_raw ? sanitize_url( wp_unslash( $image_raw ) ) : '';
|
||||||
|
|
||||||
|
$alt_raw = filter_input( INPUT_POST, 'robotstxt_og_term_image_alt', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
||||||
|
$alt = $alt_raw ? sanitize_text_field( wp_unslash( $alt_raw ) ) : '';
|
||||||
|
|
||||||
|
$desc_raw = filter_input( INPUT_POST, 'robotstxt_og_term_description', FILTER_UNSAFE_RAW );
|
||||||
|
$desc = $desc_raw ? sanitize_textarea_field( wp_unslash( $desc_raw ) ) : '';
|
||||||
|
|
||||||
|
$this->store_meta( $term_id, self::META_TITLE, $title );
|
||||||
|
$this->store_meta( $term_id, self::META_IMAGE, $image );
|
||||||
|
$this->store_meta( $term_id, self::META_IMAGE_ALT, $alt );
|
||||||
|
$this->store_meta( $term_id, self::META_DESCRIPTION, $desc );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store (or delete) a single term meta value.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param int $term_id Term ID.
|
||||||
|
* @param string $key Meta key.
|
||||||
|
* @param string $value Sanitized value.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function store_meta( int $term_id, string $key, string $value ): void {
|
||||||
|
if ( '' === trim( $value ) ) {
|
||||||
|
delete_term_meta( $term_id, $key );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_term_meta( $term_id, $key, $value );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a term meta value as a string.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param int $term_id Term ID.
|
||||||
|
* @param string $key Meta key.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function get_meta_string( int $term_id, string $key ): string {
|
||||||
|
$raw = get_term_meta( $term_id, $key, true );
|
||||||
|
|
||||||
|
return is_string( $raw ) ? $raw : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
31
readme.txt
31
readme.txt
|
|
@ -1,11 +1,11 @@
|
||||||
=== OpenGraph (by ROBOTSTXT) ===
|
=== OpenGraph (by ROBOTSTXT) ===
|
||||||
Contributors: javiercasares, robotstxt
|
Contributors: javiercasares, robotstxt
|
||||||
Tags: opengraph, open graph, twitter card, social media, seo
|
Tags: opengraph, open graph, twitter card, social media, seo
|
||||||
Requires at least: 6.8
|
Requires at least: 4.9.8
|
||||||
Tested up to: 7.0
|
Tested up to: 7.0
|
||||||
Stable tag: 1.1.0
|
Stable tag: 1.2.0
|
||||||
Requires PHP: 8.2
|
Requires PHP: 8.0
|
||||||
Version: 1.1.0
|
Version: 1.2.0
|
||||||
License: GPL-3.0-or-later
|
License: GPL-3.0-or-later
|
||||||
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
|
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
|
@ -29,10 +29,11 @@ Social media crawlers — Facebook, Twitter/X, LinkedIn, WhatsApp — do not sup
|
||||||
|
|
||||||
* Full Open Graph meta tag output: `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`
|
* Full Open Graph meta tag output: `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`
|
||||||
* Article-specific tags: `article:published_time`, `article:modified_time`, `article:section`, `article:tag`
|
* Article-specific tags: `article:published_time`, `article:modified_time`, `article:section`, `article:tag`
|
||||||
* Twitter Card tags: `twitter:card`, `twitter:site`, `twitter:image`
|
* Twitter Card tags: `twitter:card`, `twitter:site`, `twitter:creator`, `twitter:url`, `twitter:image`, `twitter:image:alt`
|
||||||
* Automatic format detection — AVIF, WebP, GIF, BMP, SVG, TIFF
|
* Automatic format detection — AVIF, WebP, GIF, BMP, SVG, TIFF
|
||||||
* HTTP HEAD-based image verification (no file downloads)
|
* HTTP HEAD-based image verification (no file downloads)
|
||||||
* Per-post custom OG title and description override (post editor meta box)
|
* Per-post custom OG title and description override (post editor meta box)
|
||||||
|
* Per-term custom OG title, image, and description override for Categories and Tags (term edit page)
|
||||||
* Global fallback image URL and homepage-specific image URL
|
* Global fallback image URL and homepage-specific image URL
|
||||||
* Cached results in postmeta for performance, with automatic invalidation on featured image change
|
* Cached results in postmeta for performance, with automatic invalidation on featured image change
|
||||||
* Negative caching for failed resolutions (1-hour transient)
|
* Negative caching for failed resolutions (1-hour transient)
|
||||||
|
|
@ -163,8 +164,8 @@ Yes. The plugin hooks into `wp_head` for direct tag injection and filters Yoast/
|
||||||
|
|
||||||
== Compatibility ==
|
== Compatibility ==
|
||||||
|
|
||||||
* WordPress: 6.8 - 7.0
|
* WordPress: 4.9.8 - 7.0
|
||||||
* PHP: 8.2 - 8.5
|
* PHP: 8.0 - 8.5
|
||||||
* WP-CLI: 2.x
|
* WP-CLI: 2.x
|
||||||
* MariaDB: 10.6+
|
* MariaDB: 10.6+
|
||||||
|
|
||||||
|
|
@ -175,6 +176,20 @@ Yes. The plugin hooks into `wp_head` for direct tag injection and filters Yoast/
|
||||||
|
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 1.2.0 =
|
||||||
|
|
||||||
|
_Release date: 2026-08-10_
|
||||||
|
|
||||||
|
* Added: Per-term Open Graph metadata for Categories and Tags on the term edit page — custom OG title, OG image URL, and OG description, exposed on term archives.
|
||||||
|
* Added: Manual `og:image:alt` override (per-post meta box and per-term), emitted as `og:image:alt` and `twitter:image:alt`.
|
||||||
|
* Added: Per-post `og:type` override (Default / `website` / `article`); `article:*` sub-tags now emit on the resolved article type.
|
||||||
|
* Added: Twitter/X `twitter:url` (canonical) and per-post `twitter:creator` (author handle); handles normalised with a leading `@`.
|
||||||
|
* Added: Platform integration tags (global settings) — Facebook `fb:app_id`/`admins`/`pages`, Pinterest `p:domain_verify` + disable-pinning toggle, Telegram `telegram:channel`, Slack `slack-app-id`.
|
||||||
|
* Added: Mobile / Web App tags (global settings) — `theme-color`, web-app name, PWA standalone toggle, iOS status bar style, `format-detection`; Open Graph namespace added to `<html>`.
|
||||||
|
* Added: Uninstall cleanup extended to remove the per-term OG meta and `_og_image_alt` / `_og_type` / `_twitter_creator` (opt-in only).
|
||||||
|
* Changed: Minimum supported WordPress lowered from 6.8 to 4.9.8 (verified by WP-Compat).
|
||||||
|
* Changed: Minimum supported PHP lowered from 8.2 to 8.0 (verified by the PHPCompatibility scan).
|
||||||
|
|
||||||
= 1.1.0 =
|
= 1.1.0 =
|
||||||
|
|
||||||
_Release date: 2026-03-28_
|
_Release date: 2026-03-28_
|
||||||
|
|
@ -195,7 +210,7 @@ _Release date: 2026-02-18_
|
||||||
* Initial stable release.
|
* Initial stable release.
|
||||||
* Full Open Graph meta tag output: `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`.
|
* Full Open Graph meta tag output: `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`.
|
||||||
* Article-specific tags for singular posts: `article:published_time`, `article:modified_time`, `article:section`, `article:tag`.
|
* Article-specific tags for singular posts: `article:published_time`, `article:modified_time`, `article:section`, `article:tag`.
|
||||||
* Twitter Card tags: `twitter:card`, `twitter:site`, `twitter:image`.
|
* Twitter Card tags: `twitter:card`, `twitter:site`, `twitter:creator`, `twitter:url`, `twitter:image`, `twitter:image:alt`.
|
||||||
* Automatic format detection (AVIF, WebP, GIF, BMP, SVG, TIFF) with HTTP HEAD-based JPEG/PNG fallback resolution.
|
* Automatic format detection (AVIF, WebP, GIF, BMP, SVG, TIFF) with HTTP HEAD-based JPEG/PNG fallback resolution.
|
||||||
* Postmeta caching with automatic invalidation on featured image change.
|
* Postmeta caching with automatic invalidation on featured image change.
|
||||||
* Per-post OG title and description overrides via post editor meta box.
|
* Per-post OG title and description overrides via post editor meta box.
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
* Plugin Name: OpenGraph (by ROBOTSTXT)
|
* Plugin Name: OpenGraph (by ROBOTSTXT)
|
||||||
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-og
|
* 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.
|
* 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.1.0
|
* Version: 1.2.0
|
||||||
* Requires at least: 6.8
|
* Requires at least: 4.9.8
|
||||||
* Requires PHP: 8.2
|
* Requires PHP: 8.0
|
||||||
* Author: ROBOTSTXT
|
* Author: ROBOTSTXT
|
||||||
* Author URI: https://www.robotstxt.es/
|
* Author URI: https://www.robotstxt.es/
|
||||||
* License: GPL v3 or later
|
* License: GPL v3 or later
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
* Contributors: javiercasares, robotstxt
|
* Contributors: javiercasares, robotstxt
|
||||||
*
|
*
|
||||||
* @package ROBOTSTXT_OG
|
* @package ROBOTSTXT_OG
|
||||||
* @version 1.1.0
|
* @version 1.2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
|
@ -25,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define plugin constants.
|
// Define plugin constants.
|
||||||
define( 'ROBOTSTXT_OG_VERSION', '1.1.0' );
|
define( 'ROBOTSTXT_OG_VERSION', '1.2.0' );
|
||||||
define( 'ROBOTSTXT_OG_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'ROBOTSTXT_OG_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
define( 'ROBOTSTXT_OG_URL', plugin_dir_url( __FILE__ ) );
|
define( 'ROBOTSTXT_OG_URL', plugin_dir_url( __FILE__ ) );
|
||||||
define( 'ROBOTSTXT_OG_BASENAME', plugin_basename( __FILE__ ) );
|
define( 'ROBOTSTXT_OG_BASENAME', plugin_basename( __FILE__ ) );
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
||||||
// Verify HMAC signature if AUTH_SALT is defined and cache has signature.
|
// Verify HMAC signature if AUTH_SALT is defined and cache has signature.
|
||||||
if ( false !== $cached && defined( 'AUTH_SALT' ) && '' !== AUTH_SALT ) {
|
if ( false !== $cached && defined( 'AUTH_SALT' ) && '' !== AUTH_SALT ) {
|
||||||
if ( is_array( $cached ) && isset( $cached['signature'], $cached['data'] ) ) {
|
if ( is_array( $cached ) && isset( $cached['signature'], $cached['data'] ) ) {
|
||||||
|
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- HMAC signature over a trusted local array; not deserialization of untrusted input.
|
||||||
$expected_sig = hash_hmac( 'sha256', $this->cache_key . serialize( $cached['data'] ), AUTH_SALT );
|
$expected_sig = hash_hmac( 'sha256', $this->cache_key . serialize( $cached['data'] ), AUTH_SALT );
|
||||||
|
|
||||||
if ( is_string( $cached['signature'] ) && hash_equals( $expected_sig, $cached['signature'] ) ) {
|
if ( is_string( $cached['signature'] ) && hash_equals( $expected_sig, $cached['signature'] ) ) {
|
||||||
|
|
@ -273,14 +274,15 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
||||||
// Store with HMAC signature if AUTH_SALT is available.
|
// Store with HMAC signature if AUTH_SALT is available.
|
||||||
if ( defined( 'AUTH_SALT' ) && '' !== AUTH_SALT ) {
|
if ( defined( 'AUTH_SALT' ) && '' !== AUTH_SALT ) {
|
||||||
$payload = array(
|
$payload = array(
|
||||||
'data' => $remote ?: array(),
|
'data' => $remote,
|
||||||
'timestamp' => time(),
|
'timestamp' => time(),
|
||||||
'signature' => hash_hmac( 'sha256', $this->cache_key . serialize( $remote ?: array() ), AUTH_SALT ),
|
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- HMAC signature over a trusted local array; not deserialization of untrusted input.
|
||||||
|
'signature' => hash_hmac( 'sha256', $this->cache_key . serialize( $remote ), AUTH_SALT ),
|
||||||
);
|
);
|
||||||
set_site_transient( $this->cache_key, $payload, 6 * HOUR_IN_SECONDS );
|
set_site_transient( $this->cache_key, $payload, 6 * HOUR_IN_SECONDS );
|
||||||
} else {
|
} else {
|
||||||
// Fallback to standard caching.
|
// Fallback to standard caching.
|
||||||
set_site_transient( $this->cache_key, $remote ?: array(), 6 * HOUR_IN_SECONDS );
|
set_site_transient( $this->cache_key, $remote, 6 * HOUR_IN_SECONDS );
|
||||||
}
|
}
|
||||||
|
|
||||||
return $remote;
|
return $remote;
|
||||||
|
|
|
||||||
|
|
@ -24,40 +24,56 @@ function robotstxt_og_uninstall_cleanup(): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all plugin options.
|
// All plugin options (general, social, platform integration, mobile/web-app).
|
||||||
delete_option( 'robotstxt_og_fallback_image' );
|
$options = array(
|
||||||
delete_option( 'robotstxt_og_homepage_image' );
|
'robotstxt_og_fallback_image',
|
||||||
delete_option( 'robotstxt_og_enable_facebook' );
|
'robotstxt_og_homepage_image',
|
||||||
delete_option( 'robotstxt_og_enable_twitter' );
|
'robotstxt_og_enable_facebook',
|
||||||
delete_option( 'robotstxt_og_twitter_card_type' );
|
'robotstxt_og_enable_twitter',
|
||||||
delete_option( 'robotstxt_og_twitter_site' );
|
'robotstxt_og_twitter_card_type',
|
||||||
delete_option( 'robotstxt_og_delete_data_on_uninstall' );
|
'robotstxt_og_twitter_site',
|
||||||
|
'robotstxt_og_fb_app_id',
|
||||||
|
'robotstxt_og_fb_admins',
|
||||||
|
'robotstxt_og_fb_pages',
|
||||||
|
'robotstxt_og_pinterest_verify',
|
||||||
|
'robotstxt_og_pinterest_nopin',
|
||||||
|
'robotstxt_og_telegram_channel',
|
||||||
|
'robotstxt_og_slack_app_id',
|
||||||
|
'robotstxt_og_theme_color',
|
||||||
|
'robotstxt_og_app_name',
|
||||||
|
'robotstxt_og_web_app_capable',
|
||||||
|
'robotstxt_og_app_status_bar_style',
|
||||||
|
'robotstxt_og_format_detection',
|
||||||
|
'robotstxt_og_delete_data_on_uninstall',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $options as $option_name ) {
|
||||||
|
delete_option( $option_name );
|
||||||
|
}
|
||||||
|
|
||||||
// Delete all cached postmeta (fallback URLs and per-post OG overrides).
|
// Delete all cached postmeta (fallback URLs and per-post OG overrides).
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
// Bulk delete during uninstall — no WP API exists to delete all postmeta rows by key at once.
|
// Bulk delete during uninstall — no WP API exists to delete all postmeta rows by key at once.
|
||||||
|
foreach ( array( '_og_image_fallback_url', '_og_title', '_og_description', '_og_image_alt', '_og_type', '_twitter_creator' ) as $post_meta_key ) {
|
||||||
$wpdb->query(
|
$wpdb->query(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
"DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
"DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
||||||
'_og_image_fallback_url'
|
$post_meta_key
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all term-level OG meta (per-term overrides + cached taxonomy fallback).
|
||||||
|
foreach ( array( '_og_title', '_og_image', '_og_description', '_og_image_alt', '_og_image_fallback_url' ) as $term_meta_key ) {
|
||||||
$wpdb->query(
|
$wpdb->query(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
"DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
"DELETE FROM {$wpdb->termmeta} WHERE meta_key = %s",
|
||||||
'_og_title'
|
$term_meta_key
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
$wpdb->query(
|
|
||||||
$wpdb->prepare(
|
|
||||||
"DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
|
||||||
'_og_description'
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
|
|
||||||
// Clear related transients.
|
// Clear related transients.
|
||||||
|
|
@ -75,37 +91,31 @@ function robotstxt_og_uninstall_cleanup(): void {
|
||||||
switch_to_blog( (int) $site->blog_id );
|
switch_to_blog( (int) $site->blog_id );
|
||||||
|
|
||||||
// Delete site-specific options.
|
// Delete site-specific options.
|
||||||
delete_option( 'robotstxt_og_fallback_image' );
|
foreach ( $options as $option_name ) {
|
||||||
delete_option( 'robotstxt_og_homepage_image' );
|
delete_option( $option_name );
|
||||||
delete_option( 'robotstxt_og_enable_facebook' );
|
}
|
||||||
delete_option( 'robotstxt_og_enable_twitter' );
|
|
||||||
delete_option( 'robotstxt_og_twitter_card_type' );
|
|
||||||
delete_option( 'robotstxt_og_twitter_site' );
|
|
||||||
delete_option( 'robotstxt_og_delete_data_on_uninstall' );
|
|
||||||
|
|
||||||
// Delete site-specific postmeta.
|
// Delete site-specific postmeta.
|
||||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
// Bulk delete during uninstall — no WP API exists to delete all postmeta rows by key at once.
|
// Bulk delete during uninstall — no WP API exists to delete all postmeta rows by key at once.
|
||||||
|
foreach ( array( '_og_image_fallback_url', '_og_title', '_og_description', '_og_image_alt', '_og_type', '_twitter_creator' ) as $post_meta_key ) {
|
||||||
$wpdb->query(
|
$wpdb->query(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
"DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
"DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
||||||
'_og_image_fallback_url'
|
$post_meta_key
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all term-level OG meta (per-term overrides + cached taxonomy fallback).
|
||||||
|
foreach ( array( '_og_title', '_og_image', '_og_description', '_og_image_alt', '_og_image_fallback_url' ) as $term_meta_key ) {
|
||||||
$wpdb->query(
|
$wpdb->query(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
"DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
"DELETE FROM {$wpdb->termmeta} WHERE meta_key = %s",
|
||||||
'_og_title'
|
$term_meta_key
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
$wpdb->query(
|
|
||||||
$wpdb->prepare(
|
|
||||||
"DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
|
||||||
'_og_description'
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
|
|
||||||
// Clear site-specific transients.
|
// Clear site-specific transients.
|
||||||
|
|
|
||||||
14
update.json
14
update.json
|
|
@ -1,20 +1,20 @@
|
||||||
{
|
{
|
||||||
"name": "OpenGraph (by ROBOTSTXT)",
|
"name": "OpenGraph (by ROBOTSTXT)",
|
||||||
"slug": "robotstxt-og",
|
"slug": "robotstxt-og",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/releases/download/1.1.0/robotstxt-og-1.1.0.zip",
|
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/releases/download/1.2.0/robotstxt-og-1.2.0.zip",
|
||||||
"requires": "6.8",
|
"requires": "4.9.8",
|
||||||
"requires_php": "8.2",
|
"requires_php": "8.0",
|
||||||
"tested": "7.0",
|
"tested": "7.0",
|
||||||
"last_updated": "2026-03-28",
|
"last_updated": "2026-08-10",
|
||||||
"author": "ROBOTSTXT",
|
"author": "ROBOTSTXT",
|
||||||
"author_profile": "https://www.robotstxt.es/",
|
"author_profile": "https://www.robotstxt.es/",
|
||||||
"homepage": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og",
|
"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.",
|
"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.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>",
|
"changelog": "<h3>1.2.0 — 2026-08-10</h3><ul><li><strong>Added:</strong> Per-term Open Graph metadata for Categories and Tags (term edit page) — custom OG title, image, and description, exposed on term archives.</li><li><strong>Added:</strong> Manual og:image:alt override (per-post/term), emitted as og:image:alt and twitter:image:alt.</li><li><strong>Added:</strong> Per-post og:type override (Default / website / article); article:* sub-tags now emit on the resolved article type.</li><li><strong>Added:</strong> Twitter/X twitter:url (canonical) and per-post twitter:creator (author handle).</li><li><strong>Added:</strong> Platform integration tags (global settings) — Facebook fb:app_id/admins/pages, Pinterest p:domain_verify + disable-pinning toggle, Telegram telegram:channel, Slack slack-app-id.</li><li><strong>Added:</strong> Mobile / Web App tags (global settings) — theme-color, web-app name, PWA standalone toggle, iOS status bar style, format-detection; Open Graph namespace added to <html>.</li></ul><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": {
|
"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>",
|
"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.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>"
|
"changelog": "<h3>1.2.0 — 2026-08-10</h3><ul><li><strong>Added:</strong> Per-term Open Graph metadata for Categories and Tags (term edit page) — custom OG title, image, and description, exposed on term archives.</li><li><strong>Added:</strong> Manual og:image:alt override (per-post/term), emitted as og:image:alt and twitter:image:alt.</li><li><strong>Added:</strong> Per-post og:type override (Default / website / article); article:* sub-tags now emit on the resolved article type.</li><li><strong>Added:</strong> Twitter/X twitter:url (canonical) and per-post twitter:creator (author handle).</li><li><strong>Added:</strong> Platform integration tags (global settings) — Facebook fb:app_id/admins/pages, Pinterest p:domain_verify + disable-pinning toggle, Telegram telegram:channel, Slack slack-app-id.</li><li><strong>Added:</strong> Mobile / Web App tags (global settings) — theme-color, web-app name, PWA standalone toggle, iOS status bar style, format-detection; Open Graph namespace added to <html>.</li></ul><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": {
|
"banners": {
|
||||||
"low": "",
|
"low": "",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue