Changeset 3722856
- Timestamp:
- 10/01/2026 10:02:46 AM (3 weeks ago)
- Location:
- simple-seo
- Files:
-
- 30 edited
- 1 copied
-
assets/screenshot-1.png (modified) ( previous)
-
assets/screenshot-2.png (modified) ( previous)
-
assets/screenshot-3.png (modified) ( previous)
-
assets/screenshot-4.png (modified) ( previous)
-
assets/screenshot-5.png (modified) ( previous)
-
assets/screenshot-6.png (modified) ( previous)
-
tags/1.5.0 (copied) (copied from simple-seo/trunk )
-
tags/1.5.0/build/editor-panel.asset.php (modified) (2 diffs)
-
tags/1.5.0/build/editor-panel.js (modified) (1 diff)
-
tags/1.5.0/js/editor-panel.js (modified) (4 diffs)
-
tags/1.5.0/readme.txt (modified) (6 diffs)
-
tags/1.5.0/simple-seo.php (modified) (2 diffs)
-
tags/1.5.0/src/admin.php (modified) (3 diffs)
-
tags/1.5.0/src/class-simple-history-logger.php (modified) (5 diffs)
-
tags/1.5.0/src/classic-editor.php (modified) (3 diffs)
-
tags/1.5.0/src/link-previews.php (modified) (3 diffs)
-
tags/1.5.0/src/meta.php (modified) (4 diffs)
-
tags/1.5.0/src/settings.php (modified) (2 diffs)
-
tags/1.5.0/src/simple-history.php (modified) (1 diff)
-
trunk/build/editor-panel.asset.php (modified) (2 diffs)
-
trunk/build/editor-panel.js (modified) (1 diff)
-
trunk/js/editor-panel.js (modified) (4 diffs)
-
trunk/readme.txt (modified) (6 diffs)
-
trunk/simple-seo.php (modified) (2 diffs)
-
trunk/src/admin.php (modified) (3 diffs)
-
trunk/src/class-simple-history-logger.php (modified) (5 diffs)
-
trunk/src/classic-editor.php (modified) (3 diffs)
-
trunk/src/link-previews.php (modified) (3 diffs)
-
trunk/src/meta.php (modified) (4 diffs)
-
trunk/src/settings.php (modified) (2 diffs)
-
trunk/src/simple-history.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
simple-seo/tags/1.5.0/build/editor-panel.asset.php
r3720725 r3722856 2 2 'dependencies' => array( 3 3 'react-jsx-runtime', 4 'wp-block-editor', 4 5 'wp-components', 5 6 'wp-core-data', … … 10 11 'wp-plugins' 11 12 ), 12 'version' => ' 15619d0c8ed75730152f'13 'version' => '34f3d3968f2319195d78' 13 14 ); -
simple-seo/tags/1.5.0/build/editor-panel.js
r3720725 r3722856 1 (()=>{"use strict";const e=window.wp.plugins,s=window.wp.editor,i=window.wp. coreData,t=window.wp.data,o=window.wp.element,n=window.wp.components,l=window.wp.i18n,r=window.ReactJSXRuntime,{otherPlugin:a,simpleHistoryUrl:p,frontPageId:_,faqUrl:m}=window.simpleSeoEditor||{};(0,e.registerPlugin)("simple-seo",{render:function(){const e=(0,t.useSelect)(e=>e(s.store).getCurrentPostType(),[]),d=(0,t.useSelect)(e=>e(s.store).getCurrentPostId(),[]),[c,h]=(0,i.useEntityProp)("postType",e,"meta");if(!c)return null;const w=""!==c._simple_seo_title.trim()||""!==c._simple_seo_description.trim()||!!c._simple_seo_noindex,u=e=>s=>h({[e]:s});return(0,r.jsx)(s.PluginDocumentSettingPanel,{name:"simple-seo",title:(0,l.__)("Simple SEO","simple-seo"),children:(0,r.jsxs)(n.Flex,{direction:"column",gap:4,children:[a&&(0,r.jsx)(n.Notice,{status:"warning",isDismissible:!1,children:(0,l.sprintf)(/* translators: %s: name of another SEO plugin, like "Yoast SEO" or "The SEO Framework". */ /* translators: %s: name of another SEO plugin, like "Yoast SEO" or "The SEO Framework". */2 (0, l.__)("%s plugin is active and handles SEO, so these fields aren’t used.","simple-seo"),a)}),(0,r.jsx)(n.TextareaControl,{__nextHasNoMarginBottom:!0,className:"simple-seo-autogrow simple-seo-title",rows:1,label:(0,l.__)("SEO title","simple-seo"),help:d===_?(0,l.__)("The whole title of the front page.","simple-seo"):(0,l.__)("About 50 characters. The site name is added after it.","simple-seo"),value:c._simple_seo_title,onChange:e=>u("_simple_seo_title")(e.replace(/[\r\n]+/g," ")),onKeyDown:e=>{"Enter"===e.key&&e.preventDefault()}}),(0,r.jsx)(n.TextareaControl,{__nextHasNoMarginBottom:!0,className:"simple-seo-autogrow",rows:2,label:(0,l.__)("Meta description","simple-seo"),help:(0,l.__)("Often shown in search results.","simple-seo"),value:c._simple_seo_description,onChange:u("_simple_seo_description")}),(0,r.jsx)(n.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,l.__)("Discourage search engines from indexing this page","simple-seo"),help:(0,l.__)("It’s up to search engines to honor this request.","simple-seo"),checked:!!c._simple_seo_noindex,onChange:e=>h({_simple_seo_noindex:e})}),p&&w&&(0,r.jsx)("p",{className:"simple-seo-tip",children:(0,o.createInterpolateElement)((0,l.__)("Tip: <a>Simple History</a> logs every change to these fields.","simple-seo"),{a:(0,r.jsx)("a",{href:p})})}),(0,r.jsx)("div",{children:(0,r.jsx)(n.ExternalLink,{href:m,children:(0,l.__)("Learn more about these fields","simple-seo")})})]})})}})})();1 (()=>{"use strict";const e=window.wp.plugins,s=window.wp.editor,i=window.wp.blockEditor,t=window.wp.coreData,o=window.wp.data,l=window.wp.element,n=window.wp.components,a=window.wp.i18n,r=window.ReactJSXRuntime,{otherPlugin:_,simpleHistoryUrl:p,frontPageId:m,faqUrl:d}=window.simpleSeoEditor||{};function c({imageId:e,onChange:s}){const l=(0,o.useSelect)(s=>e?s(t.store).getMedia(e):void 0,[e]),_=l?.media_details?.sizes?.medium?.source_url??l?.source_url;return(0,r.jsx)(i.MediaUploadCheck,{children:(0,r.jsxs)(n.BaseControl,{__nextHasNoMarginBottom:!0,id:"simple-seo-share-image",help:(0,a.__)("Used in link previews instead of the featured image.","simple-seo"),children:[(0,r.jsx)(n.BaseControl.VisualLabel,{children:(0,a.__)("Share image","simple-seo")}),(0,r.jsx)(i.MediaUpload,{title:(0,a.__)("Share image","simple-seo"),allowedTypes:["image"],value:e,onSelect:e=>s(e.id),render:({open:i})=>(0,r.jsxs)(n.Flex,{direction:"column",gap:2,align:"flex-start",children:[_&&(0,r.jsx)("img",{src:_,alt:l?.alt_text??"",style:{maxWidth:"100%",height:"auto"}}),(0,r.jsxs)(n.Flex,{justify:"flex-start",gap:2,children:[(0,r.jsx)(n.Button,{__next40pxDefaultSize:!0,id:"simple-seo-share-image","aria-describedby":"simple-seo-share-image__help",variant:"secondary",onClick:i,children:e?(0,a.__)("Replace","simple-seo"):(0,a.__)("Set share image","simple-seo")}),!!e&&(0,r.jsx)(n.Button,{__next40pxDefaultSize:!0,variant:"tertiary",isDestructive:!0,onClick:()=>s(0),children:(0,a.__)("Remove","simple-seo")})]})]})})]})})}(0,e.registerPlugin)("simple-seo",{render:function(){const e=(0,o.useSelect)(e=>e(s.store).getCurrentPostType(),[]),i=(0,o.useSelect)(e=>e(s.store).getCurrentPostId(),[]),[h,g]=(0,t.useEntityProp)("postType",e,"meta");if(!h)return null;const u=""!==h._simple_seo_title.trim()||""!==h._simple_seo_description.trim()||!!h._simple_seo_noindex||!!h._simple_seo_share_image,x=e=>s=>g({[e]:s});return(0,r.jsx)(s.PluginDocumentSettingPanel,{name:"simple-seo",title:(0,a.__)("Simple SEO","simple-seo"),children:(0,r.jsxs)(n.Flex,{direction:"column",gap:4,children:[_&&(0,r.jsx)(n.Notice,{status:"warning",isDismissible:!1,children:(0,a.sprintf)(/* translators: %s: name of another SEO plugin, like "Yoast SEO" or "The SEO Framework". */ /* translators: %s: name of another SEO plugin, like "Yoast SEO" or "The SEO Framework". */ 2 (0,a.__)("%s plugin is active and handles SEO, so these fields aren’t used.","simple-seo"),_)}),(0,r.jsx)(n.TextareaControl,{__nextHasNoMarginBottom:!0,className:"simple-seo-autogrow simple-seo-title",rows:1,label:(0,a.__)("SEO title","simple-seo"),help:i===m?(0,a.__)("The whole title of the front page.","simple-seo"):(0,a.__)("About 50 characters. The site name is added after it.","simple-seo"),value:h._simple_seo_title,onChange:e=>x("_simple_seo_title")(e.replace(/[\r\n]+/g," ")),onKeyDown:e=>{"Enter"===e.key&&e.preventDefault()}}),(0,r.jsx)(n.TextareaControl,{__nextHasNoMarginBottom:!0,className:"simple-seo-autogrow",rows:2,label:(0,a.__)("Meta description","simple-seo"),help:(0,a.__)("Often shown in search results.","simple-seo"),value:h._simple_seo_description,onChange:x("_simple_seo_description")}),(0,r.jsx)(c,{imageId:h._simple_seo_share_image??0,onChange:x("_simple_seo_share_image")}),(0,r.jsx)(n.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,a.__)("Discourage search engines from indexing this page","simple-seo"),help:(0,a.__)("It’s up to search engines to honor this request.","simple-seo"),checked:!!h._simple_seo_noindex,onChange:e=>g({_simple_seo_noindex:e})}),p&&u&&(0,r.jsx)("p",{className:"simple-seo-tip",children:(0,l.createInterpolateElement)((0,a.__)("Tip: <a>Simple History</a> logs every change to these fields.","simple-seo"),{a:(0,r.jsx)("a",{href:p})})}),(0,r.jsx)("div",{children:(0,r.jsx)(n.ExternalLink,{href:d,children:(0,a.__)("Learn more about these fields","simple-seo")})})]})})}})})(); -
simple-seo/tags/1.5.0/js/editor-panel.js
r3720725 r3722856 11 11 store as editorStore, 12 12 } from '@wordpress/editor'; 13 import { useEntityProp } from '@wordpress/core-data'; 13 import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor'; 14 import { store as coreStore, useEntityProp } from '@wordpress/core-data'; 14 15 import { useSelect } from '@wordpress/data'; 15 16 import { createInterpolateElement } from '@wordpress/element'; 16 17 import { 18 BaseControl, 19 Button, 17 20 CheckboxControl, 18 21 ExternalLink, … … 26 29 window.simpleSeoEditor || {}; 27 30 31 /** 32 * The post's share image: a preview and the media library buttons, like core's 33 * Featured image panel. Shows nothing for users who can't upload files. 34 * 35 * @param {Object} props 36 * @param {number} props.imageId Attachment ID, or 0. 37 * @param {(id: number) => void} props.onChange Called with the new attachment ID, 0 to remove it. 38 */ 39 function ShareImageControl( { imageId, onChange } ) { 40 const media = useSelect( 41 ( select ) => 42 imageId ? select( coreStore ).getMedia( imageId ) : undefined, 43 [ imageId ] 44 ); 45 const previewUrl = 46 media?.media_details?.sizes?.medium?.source_url ?? media?.source_url; 47 48 return ( 49 <MediaUploadCheck> 50 { /* A visual label, not a <label>: that would replace the button's own name. */ } 51 <BaseControl 52 __nextHasNoMarginBottom 53 id="simple-seo-share-image" 54 help={ __( 55 'Used in link previews instead of the featured image.', 56 'simple-seo' 57 ) } 58 > 59 <BaseControl.VisualLabel> 60 { __( 'Share image', 'simple-seo' ) } 61 </BaseControl.VisualLabel> 62 <MediaUpload 63 title={ __( 'Share image', 'simple-seo' ) } 64 allowedTypes={ [ 'image' ] } 65 value={ imageId } 66 onSelect={ ( image ) => onChange( image.id ) } 67 render={ ( { open } ) => ( 68 <Flex direction="column" gap={ 2 } align="flex-start"> 69 { previewUrl && ( 70 <img 71 src={ previewUrl } 72 alt={ media?.alt_text ?? '' } 73 style={ { 74 maxWidth: '100%', 75 height: 'auto', 76 } } 77 /> 78 ) } 79 <Flex justify="flex-start" gap={ 2 }> 80 <Button 81 __next40pxDefaultSize 82 id="simple-seo-share-image" 83 aria-describedby="simple-seo-share-image__help" 84 variant="secondary" 85 onClick={ open } 86 > 87 { imageId 88 ? __( 'Replace', 'simple-seo' ) 89 : __( 90 'Set share image', 91 'simple-seo' 92 ) } 93 </Button> 94 { !! imageId && ( 95 <Button 96 __next40pxDefaultSize 97 variant="tertiary" 98 isDestructive 99 onClick={ () => onChange( 0 ) } 100 > 101 { __( 'Remove', 'simple-seo' ) } 102 </Button> 103 ) } 104 </Flex> 105 </Flex> 106 ) } 107 /> 108 </BaseControl> 109 </MediaUploadCheck> 110 ); 111 } 112 28 113 function SimpleSeoPanel() { 29 114 const postType = useSelect( … … 46 131 meta._simple_seo_title.trim() !== '' || 47 132 meta._simple_seo_description.trim() !== '' || 48 !! meta._simple_seo_noindex; 133 !! meta._simple_seo_noindex || 134 !! meta._simple_seo_share_image; 49 135 50 136 // Meta edits are merged, so pass only the changed key. … … 118 204 /> 119 205 206 <ShareImageControl 207 imageId={ meta._simple_seo_share_image ?? 0 } 208 onChange={ update( '_simple_seo_share_image' ) } 209 /> 210 120 211 <CheckboxControl 121 212 __nextHasNoMarginBottom -
simple-seo/tags/1.5.0/readme.txt
r3720763 r3722856 6 6 Tested up to: 7.1 7 7 Requires PHP: 7.4 8 Stable tag: 1. 4.08 Stable tag: 1.5.0 9 9 License: GPLv2 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html 11 11 12 The SEO basics and nothing more: a title, a description and a "don't index" checkbox for every page. No upsells, no settings maze.12 The SEO basics: a title, a description and a "don't index" checkbox for every page. Small, fast, and no upsell banners all over your admin. 13 13 14 14 == Description == … … 24 24 A few things just happen: link previews with the right title, description and image in Slack, Mastodon, Bluesky and friends. Pages you keep out of search are left out of the sitemap too. There's a panel in the block editor, a box in the Classic Editor, and the fields in Quick Edit. Using Yoast SEO or another big SEO plugin? Simple SEO steps aside. 25 25 26 No s ettings page, no scores, no "Go Pro" banners, no extra database queries. <del>No nags.</del> Okay, one small grey tip about [Simple History](https://wordpress.org/plugins/simple-history/), my activity log plugin.26 No scores, no "Go Pro" banners, no extra database queries. <del>No nags.</del> Okay, one small grey tip about [Simple History](https://wordpress.org/plugins/simple-history/), my activity log plugin. 27 27 28 28 No plugin makes a page rank. Good content does. Simple SEO hands search engines and shared links the title, description and image you chose, and asks search engines to skip the pages you don't want listed. That's also all AI search needs from a plugin: no llms.txt, no "AI optimization". … … 32 32 Simple SEO is old. Like really old. It arrived on WordPress.org in August 2010 (seven weeks before Yoast SEO!). Of the SEO plugins still maintained today, only All in One SEO is older. Then it slept from 2012 to 2026. 33 33 34 I woke it up mostly for myself: I wanted the SEO basics on my own sites without a plugin that asks for attention every time I log in. It stays small on purpose, so feature requests often get "that's a bit much for a plugin called Simple".34 I woke it up mostly for myself: I wanted the SEO basics on my own sites without a plugin that asks for attention every time I log in. It grows when I need something on my own sites, but it stays small and fast on purpose. 35 35 36 36 No big company or investors, just one developer: me, the guy behind [Simple History](https://simple-history.com/). With both active, every SEO change is logged: who, when, and what it said before. I also make [CMS Tree Page View](https://wordpress.org/plugins/cms-tree-page-view/), and the two work together too. If Simple SEO saves you time, [a donation](https://eskapism.se/sida/donate/) keeps it going. … … 60 60 A wide picture, 1200 × 630 pixels, not your logo. It's used in link previews when a post has no featured image. 61 61 62 = Can a post have a different image in link previews? = 63 64 Yes, pick a share image in the post's Simple SEO fields. Link previews use it instead of the featured image, and your theme keeps showing the featured image. Handy when the picture that looks good on your site isn't the one that works in a shared link, like a card with the headline on it. 65 62 66 = I use Yoast SEO (or Rank Math, All in One SEO, SEOPress, The SEO Framework). = 63 67 … … 70 74 = Can I use WP-CLI, the REST API or filters? = 71 75 72 Yup. The fields are post meta: `_simple_seo_title`, `_simple_seo_description` and `_simple_seo_noindex`, in the REST API with `?context=edit`. Everything Simple SEO outputs goes through a filter first. See [the hooks](https://github.com/bonny/WordPress-Simple-SEO/blob/main/docs/hooks.md).76 Yup. The fields are post meta: `_simple_seo_title`, `_simple_seo_description`, `_simple_seo_noindex` and `_simple_seo_share_image` (an attachment ID), in the REST API with `?context=edit`. Everything Simple SEO outputs goes through a filter first. See [the hooks](https://github.com/bonny/WordPress-Simple-SEO/blob/main/docs/hooks.md). 73 77 74 78 = What happened to the menu label? = … … 78 82 == Screenshots == 79 83 80 1. A few SEO fields in the page sidebar of the block editor. That's it.84 1. The SEO fields in the page sidebar of the block editor. 81 85 2. See and edit the SEO of every page right from the Pages list, with Quick Edit. 82 86 3. The same column for posts, so you can spot the ones missing a description. 83 87 4. Works in the Classic Editor too. 84 5. One setting: adefault share image for link previews, in Settings → General.88 5. A default share image for link previews, in Settings → General. 85 89 6. With Simple History, every SEO change is logged: who, when, and what it said before. 86 90 87 91 == Changelog == 92 93 = 1.5.0 (October 2026) = 94 - New: a share image for each post, used in link previews instead of the featured image. Your theme keeps showing the featured image. In both editors, and logged in Simple History. 88 95 89 96 = 1.4.0 (September 2026) = -
simple-seo/tags/1.5.0/simple-seo.php
r3720725 r3722856 4 4 * Plugin URI: https://wordpress.org/plugins/simple-seo/ 5 5 * Description: Change the page title and menu label output for any page or post, which can be useful for SEO (Search Engine Optimization) reasons. It may also increase the usability of your website, making it more friendly and understandable for your visitors. 6 * Version: 1. 4.06 * Version: 1.5.0 7 7 * Requires at least: 6.6 8 8 * Requires PHP: 7.4 … … 35 35 } 36 36 37 define( 'SIMPLE_SEO_VERSION', '1. 4.0' );37 define( 'SIMPLE_SEO_VERSION', '1.5.0' ); 38 38 39 39 // This file must stay parseable by PHP 5.6: WordPress before 5.2 ignores "Requires PHP" -
simple-seo/tags/1.5.0/src/admin.php
r3715169 r3722856 46 46 47 47 /** 48 * Help text for a post's share image. 49 */ 50 function share_image_help(): string { 51 return __( 'Used in link previews instead of the featured image.', 'simple-seo' ); 52 } 53 54 /** 48 55 * Whether a post uses any of the fields. The Simple History tip only shows then. 49 56 * … … 54 61 || '' !== get_description( $post_id ) 55 62 || is_noindex( $post_id ) 63 || get_share_image_id( $post_id ) 56 64 || '' !== get_menu_label( $post_id ); 65 } 66 67 /** 68 * An image picker: a preview, the media library buttons and a hidden attachment ID, like core's 69 * Site Icon. Used by the default share image in Settings and the share image in the Classic box. 70 * Needs enqueue_image_field_script(). 71 * 72 * @param string $name Form field name. 73 * @param int $attachment_id The image, or 0. 74 * @param string $preview_url URL of the image to show, or ''. 75 */ 76 function image_field( string $name, int $attachment_id, string $preview_url ): void { 77 $has_image = $attachment_id && '' !== $preview_url; 78 79 printf( 80 '<div class="simple-seo-share-image"><img src="%1$s" alt="" style="max-width:300px;width:100%%;height:auto;display:block;margin-bottom:8px" %2$s />', 81 esc_url( $preview_url ), 82 $has_image ? '' : 'hidden' 83 ); 84 85 printf( 86 '<input type="hidden" name="%1$s" value="%2$s" />', 87 esc_attr( $name ), 88 esc_attr( $has_image ? (string) $attachment_id : '' ) 89 ); 90 91 printf( 92 '<button type="button" class="button simple-seo-share-image-choose" data-choose="%1$s" data-change="%2$s">%3$s</button> ', 93 esc_attr__( 'Choose image', 'simple-seo' ), 94 esc_attr__( 'Change image', 'simple-seo' ), 95 $has_image ? esc_html__( 'Change image', 'simple-seo' ) : esc_html__( 'Choose image', 'simple-seo' ) 96 ); 97 98 printf( 99 '<button type="button" class="button button-secondary simple-seo-share-image-remove" %1$s>%2$s</button></div>', 100 $has_image ? '' : 'hidden', 101 esc_html__( 'Remove image', 'simple-seo' ) 102 ); 103 } 104 105 /** 106 * The media library and the script behind image_field(). Can be called while the page renders: 107 * both print in the footer. 108 */ 109 function enqueue_image_field_script(): void { 110 wp_enqueue_media(); 111 112 $asset = require dirname( __DIR__ ) . '/build/share-image-field.asset.php'; 113 114 wp_enqueue_script( 115 'simple-seo-share-image-field', 116 plugins_url( 'build/share-image-field.js', \SIMPLE_SEO_PLUGIN_FILE ), 117 $asset['dependencies'], 118 $asset['version'], 119 true 120 ); 57 121 } 58 122 … … 91 155 wp_add_inline_style( 92 156 'simple-seo-admin', 93 '#simple-seo label { display: block; margin-bottom: 4px; font-weight: 600; }157 '#simple-seo label, #simple-seo .simple-seo-field-label { display: block; margin-bottom: 4px; font-weight: 600; } 94 158 #simple-seo .description { display: block; margin-top: 4px; } 159 #simple-seo .simple-seo-field { margin: 1em 0; } 95 160 .simple-seo-quick-edit .simple-seo-field, .simple-seo-quick-edit .simple-seo-check { display: block; margin-top: 6px; } 96 161 .simple-seo-quick-edit input[type="text"] { display: block; width: 100%; } -
simple-seo/tags/1.5.0/src/class-simple-history-logger.php
r3715169 r3722856 9 9 namespace SimpleSEO; 10 10 11 use Simple_History\Event_Details\Event_Details_Container; 11 12 use Simple_History\Event_Details\Event_Details_Group; 12 13 use Simple_History\Event_Details\Event_Details_Group_Diff_Table_Formatter; … … 43 44 return [ 44 45 'name' => __( 'Simple SEO', 'simple-seo' ), 45 'description' => __( 'Logs changes to the SEO title, meta description, "Discourage search engines", menu label and default share image.', 'simple-seo' ),46 'description' => __( 'Logs changes to the SEO title, meta description, "Discourage search engines", menu label, share image and default share image.', 'simple-seo' ), 46 47 'name_via' => __( 'Using plugin Simple SEO', 'simple-seo' ), 47 48 'capability' => 'edit_posts', … … 257 258 258 259 /** 259 * A before/after table of the fields that changed .260 * A before/after table of the fields that changed, and the share image side by side. 260 261 * 261 262 * @param object $row Log row. 262 * @return Event_Details_Group 263 * @return Event_Details_Group|Event_Details_Container 263 264 */ 264 265 public function get_log_row_details_output( $row ) { 265 266 if ( 0 === strpos( (string) ( $row->context['_message_key'] ?? '' ), 'share_image_' ) ) { 266 return $this->share_image_details( $row->context );267 return $this->share_image_details( $row->context, __( 'Default share image', 'simple-seo' ) ); 267 268 } 268 269 … … 278 279 ); 279 280 280 return $group; 281 if ( ! array_key_exists( 'share_image_new', $row->context ) ) { 282 return $group; 283 } 284 285 // The image diff is its own group, as Simple History's featured image. Older versions without 286 // the container class get its URL as a row in the table. 287 if ( ! class_exists( Event_Details_Container::class ) ) { 288 $group->add_items( [ new Event_Details_Item( [ 'share_image' ], __( 'Share image', 'simple-seo' ) ) ] ); 289 return $group; 290 } 291 292 return new Event_Details_Container( [ $group, $this->share_image_details( $row->context, __( 'Share image', 'simple-seo' ) ) ], $row->context ); 281 293 } 282 294 … … 285 297 * 286 298 * @param array<string, mixed> $context Event context. 299 * @param string $label Row label. 287 300 * @return Event_Details_Group 288 301 */ 289 private function share_image_details( array $context ) {290 $item = new Event_Details_Item( [ 'share_image' ], __( 'Default share image', 'simple-seo' ));302 private function share_image_details( array $context, string $label ) { 303 $item = new Event_Details_Item( [ 'share_image' ], $label ); 291 304 $group = new Event_Details_Group(); 292 305 -
simple-seo/tags/1.5.0/src/classic-editor.php
r3720725 r3722856 69 69 } 70 70 71 share_image_field_in_box( $post->ID ); 72 71 73 printf( 72 74 '<p><label><input type="checkbox" name="simple_seo[noindex_on]" value="1" aria-describedby="simple-seo-noindex-help" %1$s /> %2$s</label><span class="description" id="simple-seo-noindex-help">%3$s</span></p>', … … 96 98 esc_html__( '(opens in a new tab)', 'simple-seo' ) 97 99 ); 100 } 101 102 /** 103 * The post's share image, for users who can use the media library (as core's Featured image). 104 * Without the field the form posts no share_image, so saving leaves a stored image alone. 105 * 106 * @param int $post_id Post ID. 107 */ 108 function share_image_field_in_box( int $post_id ): void { 109 if ( ! current_user_can( 'upload_files' ) ) { 110 return; 111 } 112 113 $share_image_id = get_share_image_id( $post_id ); 114 115 // A group, not a <label>: that would replace the buttons' own names. 116 printf( 117 '<div class="simple-seo-field" role="group" aria-labelledby="simple-seo-share-image-label" aria-describedby="simple-seo-share-image-help"><span class="simple-seo-field-label" id="simple-seo-share-image-label">%s</span>', 118 esc_html__( 'Share image', 'simple-seo' ) 119 ); 120 image_field( 'simple_seo[share_image]', $share_image_id, (string) wp_get_attachment_image_url( $share_image_id, 'medium' ) ); 121 printf( '<span class="description" id="simple-seo-share-image-help">%s</span></div>', esc_html( share_image_help() ) ); 122 enqueue_image_field_script(); 98 123 } 99 124 … … 154 179 update_post_meta( $post_id, NOINDEX_KEY, ! empty( $fields['noindex_on'] ) ); 155 180 181 // Quick Edit posts the other fields but not this one. 182 if ( isset( $fields['share_image'] ) ) { 183 update_post_meta( $post_id, SHARE_IMAGE_KEY, absint( $fields['share_image'] ) ); 184 } 185 156 186 if ( 'page' === $post->post_type ) { 157 187 save_menu_label( $post_id, $text( 'menu_label' ) ); -
simple-seo/tags/1.5.0/src/link-previews.php
r3720725 r3722856 75 75 $title = '' !== $title ? $title : wp_strip_all_tags( get_the_title( $post_id ) ); 76 76 $url = (string) get_permalink( $post_id ); 77 $image = featured_image( $post_id);77 $image = attachment_image( get_share_image_id( $post_id ) ) ?? attachment_image( (int) get_post_thumbnail_id( $post_id ) ); 78 78 79 79 // No meta description: the hand-written excerpt, else the start of the content, so a … … 98 98 /** 99 99 * The link preview image: [ 'url', 'width', 'height', 'alt' ], or null for none. 100 * By default the featured image, else the default share image from Settings → General.100 * By default the post's share image, else its featured image, else the default share image from Settings → General. 101 101 * 102 * @param array{url: string, width: int, height: int, alt: string}|null $image The featuredimage, if any.102 * @param array{url: string, width: int, height: int, alt: string}|null $image The image, if any. 103 103 * @param int $post_id The post, 0 on a front page with the latest posts. 104 104 */ … … 157 157 158 158 /** 159 * The post's featured image at full size, or null.159 * An image attachment at full size, or null when there is none (0, or deleted). 160 160 * 161 * @param int $ post_id Post ID.161 * @param int $attachment_id Attachment ID. 162 162 * @return array{url: string, width: int, height: int, alt: string}|null 163 163 */ 164 function featured_image( int $post_id ): ?array { 165 $attachment_id = (int) get_post_thumbnail_id( $post_id ); 166 $src = $attachment_id ? wp_get_attachment_image_src( $attachment_id, 'full' ) : false; 164 function attachment_image( int $attachment_id ): ?array { 165 $src = $attachment_id ? wp_get_attachment_image_src( $attachment_id, 'full' ) : false; 167 166 168 167 if ( ! $src ) { -
simple-seo/tags/1.5.0/src/meta.php
r3714576 r3722856 15 15 const DESCRIPTION_KEY = '_simple_seo_description'; 16 16 const NOINDEX_KEY = '_simple_seo_noindex'; 17 18 // Attachment ID of the image for link previews, used instead of the featured image. 19 const SHARE_IMAGE_KEY = '_simple_seo_share_image'; 17 20 18 21 // Menu label, pages only, Classic Editor only. Same keys as before 1.0: the label is used when the flag is 1. … … 29 32 add_action( 'deleted_post_meta', __NAMESPACE__ . '\\forget_legacy_title', 10, 3 ); 30 33 add_filter( 'default_post_metadata', __NAMESPACE__ . '\\legacy_title_default', 20, 4 ); 34 add_action( 'added_post_meta', __NAMESPACE__ . '\\forget_empty_share_image', 10, 4 ); 35 add_action( 'updated_post_meta', __NAMESPACE__ . '\\forget_empty_share_image', 10, 4 ); 36 add_action( 'delete_attachment', __NAMESPACE__ . '\\forget_deleted_share_image' ); 31 37 32 38 /** … … 59 65 ); 60 66 } 67 68 register_post_meta( 69 '', 70 SHARE_IMAGE_KEY, 71 [ 72 'type' => 'integer', 73 'description' => __( 'Attachment ID of the image for link previews, used instead of the featured image. 0 or none uses the featured image.', 'simple-seo' ), 74 'single' => true, 75 'default' => 0, 76 'show_in_rest' => [ 'schema' => [ 'context' => [ 'edit' ] ] ], 77 'sanitize_callback' => __NAMESPACE__ . '\\sanitize_share_image_id', 78 'auth_callback' => fn( $allowed, $meta_key, $post_id ) => current_user_can( 'edit_post', $post_id ), 79 ] 80 ); 81 } 82 83 /** 84 * The share image must be an image attachment; anything else is stored as 0, which is then deleted. 85 * 86 * @param mixed $value Attachment ID. 87 */ 88 function sanitize_share_image_id( $value ): int { 89 $attachment_id = absint( $value ); 90 91 return $attachment_id && wp_attachment_is_image( $attachment_id ) ? $attachment_id : 0; 92 } 93 94 /** 95 * No share image (0, as the editors send on Remove) deletes the key, as an empty text field does. 96 * 97 * @param int $meta_id Meta ID, unused. 98 * @param int $object_id Post ID. 99 * @param string $meta_key Meta key. 100 * @param mixed $meta_value The stored value. 101 */ 102 function forget_empty_share_image( $meta_id, $object_id, $meta_key, $meta_value ): void { 103 if ( SHARE_IMAGE_KEY === $meta_key && ! absint( $meta_value ) ) { 104 delete_post_meta( $object_id, SHARE_IMAGE_KEY ); 105 } 61 106 } 62 107 … … 105 150 function get_description( int $post_id ): string { 106 151 return trim( (string) get_post_meta( $post_id, DESCRIPTION_KEY, true ) ); 152 } 153 154 /** 155 * A deleted image is no longer anyone's share image, so the editors don't show an empty field. 156 * One bulk delete for all posts (core's $delete_all), only when an attachment is deleted: one 157 * query when no post uses the image, three when some do. 158 * 159 * @param int $attachment_id Attachment ID. 160 */ 161 function forget_deleted_share_image( $attachment_id ): void { 162 delete_metadata( 'post', 0, SHARE_IMAGE_KEY, (int) $attachment_id, true ); 163 } 164 165 /** 166 * The post's own share image (attachment ID), or 0 to use the featured image. 167 * 168 * @param int $post_id Post ID. 169 */ 170 function get_share_image_id( int $post_id ): int { 171 return absint( get_post_meta( $post_id, SHARE_IMAGE_KEY, true ) ); 107 172 } 108 173 -
simple-seo/tags/1.5.0/src/settings.php
r3720725 r3722856 74 74 $image = share_image(); 75 75 76 printf( 77 '<div class="simple-seo-share-image"><img src="%1$s" alt="" style="max-width:300px;height:auto;display:block;margin-bottom:8px" %2$s />', 78 esc_url( $image['url'] ?? '' ), 79 $image ? '' : 'hidden' 80 ); 81 82 printf( 83 '<input type="hidden" name="%1$s" value="%2$s" />', 84 esc_attr( SHARE_IMAGE_OPTION ), 85 esc_attr( (string) ( $image['id'] ?? '' ) ) 86 ); 87 88 printf( 89 '<button type="button" class="button simple-seo-share-image-choose" data-choose="%1$s" data-change="%2$s">%3$s</button> ', 90 esc_attr__( 'Choose image', 'simple-seo' ), 91 esc_attr__( 'Change image', 'simple-seo' ), 92 $image ? esc_html__( 'Change image', 'simple-seo' ) : esc_html__( 'Choose image', 'simple-seo' ) 93 ); 94 95 printf( 96 '<button type="button" class="button button-secondary simple-seo-share-image-remove" %1$s>%2$s</button></div>', 97 $image ? '' : 'hidden', 98 esc_html__( 'Remove image', 'simple-seo' ) 99 ); 76 image_field( SHARE_IMAGE_OPTION, (int) ( $image['id'] ?? 0 ), (string) ( $image['url'] ?? '' ) ); 100 77 101 78 printf( … … 115 92 } 116 93 117 wp_enqueue_media(); 118 119 $asset = require dirname( __DIR__ ) . '/build/share-image-field.asset.php'; 120 121 wp_enqueue_script( 122 'simple-seo-share-image-field', 123 plugins_url( 'build/share-image-field.js', \SIMPLE_SEO_PLUGIN_FILE ), 124 $asset['dependencies'], 125 $asset['version'], 126 true 127 ); 94 enqueue_image_field_script(); 128 95 } 129 96 -
simple-seo/tags/1.5.0/src/simple-history.php
r3714576 r3722856 57 57 'noindex' => is_noindex( $post_id ) ? __( 'Yes', 'simple-seo' ) : __( 'No', 'simple-seo' ), 58 58 'menu_label' => get_menu_label( $post_id ), 59 'share_image' => (string) wp_get_attachment_url( get_share_image_id( $post_id ) ), 59 60 ]; 60 61 } -
simple-seo/trunk/build/editor-panel.asset.php
r3720725 r3722856 2 2 'dependencies' => array( 3 3 'react-jsx-runtime', 4 'wp-block-editor', 4 5 'wp-components', 5 6 'wp-core-data', … … 10 11 'wp-plugins' 11 12 ), 12 'version' => ' 15619d0c8ed75730152f'13 'version' => '34f3d3968f2319195d78' 13 14 ); -
simple-seo/trunk/build/editor-panel.js
r3720725 r3722856 1 (()=>{"use strict";const e=window.wp.plugins,s=window.wp.editor,i=window.wp. coreData,t=window.wp.data,o=window.wp.element,n=window.wp.components,l=window.wp.i18n,r=window.ReactJSXRuntime,{otherPlugin:a,simpleHistoryUrl:p,frontPageId:_,faqUrl:m}=window.simpleSeoEditor||{};(0,e.registerPlugin)("simple-seo",{render:function(){const e=(0,t.useSelect)(e=>e(s.store).getCurrentPostType(),[]),d=(0,t.useSelect)(e=>e(s.store).getCurrentPostId(),[]),[c,h]=(0,i.useEntityProp)("postType",e,"meta");if(!c)return null;const w=""!==c._simple_seo_title.trim()||""!==c._simple_seo_description.trim()||!!c._simple_seo_noindex,u=e=>s=>h({[e]:s});return(0,r.jsx)(s.PluginDocumentSettingPanel,{name:"simple-seo",title:(0,l.__)("Simple SEO","simple-seo"),children:(0,r.jsxs)(n.Flex,{direction:"column",gap:4,children:[a&&(0,r.jsx)(n.Notice,{status:"warning",isDismissible:!1,children:(0,l.sprintf)(/* translators: %s: name of another SEO plugin, like "Yoast SEO" or "The SEO Framework". */ /* translators: %s: name of another SEO plugin, like "Yoast SEO" or "The SEO Framework". */2 (0, l.__)("%s plugin is active and handles SEO, so these fields aren’t used.","simple-seo"),a)}),(0,r.jsx)(n.TextareaControl,{__nextHasNoMarginBottom:!0,className:"simple-seo-autogrow simple-seo-title",rows:1,label:(0,l.__)("SEO title","simple-seo"),help:d===_?(0,l.__)("The whole title of the front page.","simple-seo"):(0,l.__)("About 50 characters. The site name is added after it.","simple-seo"),value:c._simple_seo_title,onChange:e=>u("_simple_seo_title")(e.replace(/[\r\n]+/g," ")),onKeyDown:e=>{"Enter"===e.key&&e.preventDefault()}}),(0,r.jsx)(n.TextareaControl,{__nextHasNoMarginBottom:!0,className:"simple-seo-autogrow",rows:2,label:(0,l.__)("Meta description","simple-seo"),help:(0,l.__)("Often shown in search results.","simple-seo"),value:c._simple_seo_description,onChange:u("_simple_seo_description")}),(0,r.jsx)(n.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,l.__)("Discourage search engines from indexing this page","simple-seo"),help:(0,l.__)("It’s up to search engines to honor this request.","simple-seo"),checked:!!c._simple_seo_noindex,onChange:e=>h({_simple_seo_noindex:e})}),p&&w&&(0,r.jsx)("p",{className:"simple-seo-tip",children:(0,o.createInterpolateElement)((0,l.__)("Tip: <a>Simple History</a> logs every change to these fields.","simple-seo"),{a:(0,r.jsx)("a",{href:p})})}),(0,r.jsx)("div",{children:(0,r.jsx)(n.ExternalLink,{href:m,children:(0,l.__)("Learn more about these fields","simple-seo")})})]})})}})})();1 (()=>{"use strict";const e=window.wp.plugins,s=window.wp.editor,i=window.wp.blockEditor,t=window.wp.coreData,o=window.wp.data,l=window.wp.element,n=window.wp.components,a=window.wp.i18n,r=window.ReactJSXRuntime,{otherPlugin:_,simpleHistoryUrl:p,frontPageId:m,faqUrl:d}=window.simpleSeoEditor||{};function c({imageId:e,onChange:s}){const l=(0,o.useSelect)(s=>e?s(t.store).getMedia(e):void 0,[e]),_=l?.media_details?.sizes?.medium?.source_url??l?.source_url;return(0,r.jsx)(i.MediaUploadCheck,{children:(0,r.jsxs)(n.BaseControl,{__nextHasNoMarginBottom:!0,id:"simple-seo-share-image",help:(0,a.__)("Used in link previews instead of the featured image.","simple-seo"),children:[(0,r.jsx)(n.BaseControl.VisualLabel,{children:(0,a.__)("Share image","simple-seo")}),(0,r.jsx)(i.MediaUpload,{title:(0,a.__)("Share image","simple-seo"),allowedTypes:["image"],value:e,onSelect:e=>s(e.id),render:({open:i})=>(0,r.jsxs)(n.Flex,{direction:"column",gap:2,align:"flex-start",children:[_&&(0,r.jsx)("img",{src:_,alt:l?.alt_text??"",style:{maxWidth:"100%",height:"auto"}}),(0,r.jsxs)(n.Flex,{justify:"flex-start",gap:2,children:[(0,r.jsx)(n.Button,{__next40pxDefaultSize:!0,id:"simple-seo-share-image","aria-describedby":"simple-seo-share-image__help",variant:"secondary",onClick:i,children:e?(0,a.__)("Replace","simple-seo"):(0,a.__)("Set share image","simple-seo")}),!!e&&(0,r.jsx)(n.Button,{__next40pxDefaultSize:!0,variant:"tertiary",isDestructive:!0,onClick:()=>s(0),children:(0,a.__)("Remove","simple-seo")})]})]})})]})})}(0,e.registerPlugin)("simple-seo",{render:function(){const e=(0,o.useSelect)(e=>e(s.store).getCurrentPostType(),[]),i=(0,o.useSelect)(e=>e(s.store).getCurrentPostId(),[]),[h,g]=(0,t.useEntityProp)("postType",e,"meta");if(!h)return null;const u=""!==h._simple_seo_title.trim()||""!==h._simple_seo_description.trim()||!!h._simple_seo_noindex||!!h._simple_seo_share_image,x=e=>s=>g({[e]:s});return(0,r.jsx)(s.PluginDocumentSettingPanel,{name:"simple-seo",title:(0,a.__)("Simple SEO","simple-seo"),children:(0,r.jsxs)(n.Flex,{direction:"column",gap:4,children:[_&&(0,r.jsx)(n.Notice,{status:"warning",isDismissible:!1,children:(0,a.sprintf)(/* translators: %s: name of another SEO plugin, like "Yoast SEO" or "The SEO Framework". */ /* translators: %s: name of another SEO plugin, like "Yoast SEO" or "The SEO Framework". */ 2 (0,a.__)("%s plugin is active and handles SEO, so these fields aren’t used.","simple-seo"),_)}),(0,r.jsx)(n.TextareaControl,{__nextHasNoMarginBottom:!0,className:"simple-seo-autogrow simple-seo-title",rows:1,label:(0,a.__)("SEO title","simple-seo"),help:i===m?(0,a.__)("The whole title of the front page.","simple-seo"):(0,a.__)("About 50 characters. The site name is added after it.","simple-seo"),value:h._simple_seo_title,onChange:e=>x("_simple_seo_title")(e.replace(/[\r\n]+/g," ")),onKeyDown:e=>{"Enter"===e.key&&e.preventDefault()}}),(0,r.jsx)(n.TextareaControl,{__nextHasNoMarginBottom:!0,className:"simple-seo-autogrow",rows:2,label:(0,a.__)("Meta description","simple-seo"),help:(0,a.__)("Often shown in search results.","simple-seo"),value:h._simple_seo_description,onChange:x("_simple_seo_description")}),(0,r.jsx)(c,{imageId:h._simple_seo_share_image??0,onChange:x("_simple_seo_share_image")}),(0,r.jsx)(n.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,a.__)("Discourage search engines from indexing this page","simple-seo"),help:(0,a.__)("It’s up to search engines to honor this request.","simple-seo"),checked:!!h._simple_seo_noindex,onChange:e=>g({_simple_seo_noindex:e})}),p&&u&&(0,r.jsx)("p",{className:"simple-seo-tip",children:(0,l.createInterpolateElement)((0,a.__)("Tip: <a>Simple History</a> logs every change to these fields.","simple-seo"),{a:(0,r.jsx)("a",{href:p})})}),(0,r.jsx)("div",{children:(0,r.jsx)(n.ExternalLink,{href:d,children:(0,a.__)("Learn more about these fields","simple-seo")})})]})})}})})(); -
simple-seo/trunk/js/editor-panel.js
r3720725 r3722856 11 11 store as editorStore, 12 12 } from '@wordpress/editor'; 13 import { useEntityProp } from '@wordpress/core-data'; 13 import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor'; 14 import { store as coreStore, useEntityProp } from '@wordpress/core-data'; 14 15 import { useSelect } from '@wordpress/data'; 15 16 import { createInterpolateElement } from '@wordpress/element'; 16 17 import { 18 BaseControl, 19 Button, 17 20 CheckboxControl, 18 21 ExternalLink, … … 26 29 window.simpleSeoEditor || {}; 27 30 31 /** 32 * The post's share image: a preview and the media library buttons, like core's 33 * Featured image panel. Shows nothing for users who can't upload files. 34 * 35 * @param {Object} props 36 * @param {number} props.imageId Attachment ID, or 0. 37 * @param {(id: number) => void} props.onChange Called with the new attachment ID, 0 to remove it. 38 */ 39 function ShareImageControl( { imageId, onChange } ) { 40 const media = useSelect( 41 ( select ) => 42 imageId ? select( coreStore ).getMedia( imageId ) : undefined, 43 [ imageId ] 44 ); 45 const previewUrl = 46 media?.media_details?.sizes?.medium?.source_url ?? media?.source_url; 47 48 return ( 49 <MediaUploadCheck> 50 { /* A visual label, not a <label>: that would replace the button's own name. */ } 51 <BaseControl 52 __nextHasNoMarginBottom 53 id="simple-seo-share-image" 54 help={ __( 55 'Used in link previews instead of the featured image.', 56 'simple-seo' 57 ) } 58 > 59 <BaseControl.VisualLabel> 60 { __( 'Share image', 'simple-seo' ) } 61 </BaseControl.VisualLabel> 62 <MediaUpload 63 title={ __( 'Share image', 'simple-seo' ) } 64 allowedTypes={ [ 'image' ] } 65 value={ imageId } 66 onSelect={ ( image ) => onChange( image.id ) } 67 render={ ( { open } ) => ( 68 <Flex direction="column" gap={ 2 } align="flex-start"> 69 { previewUrl && ( 70 <img 71 src={ previewUrl } 72 alt={ media?.alt_text ?? '' } 73 style={ { 74 maxWidth: '100%', 75 height: 'auto', 76 } } 77 /> 78 ) } 79 <Flex justify="flex-start" gap={ 2 }> 80 <Button 81 __next40pxDefaultSize 82 id="simple-seo-share-image" 83 aria-describedby="simple-seo-share-image__help" 84 variant="secondary" 85 onClick={ open } 86 > 87 { imageId 88 ? __( 'Replace', 'simple-seo' ) 89 : __( 90 'Set share image', 91 'simple-seo' 92 ) } 93 </Button> 94 { !! imageId && ( 95 <Button 96 __next40pxDefaultSize 97 variant="tertiary" 98 isDestructive 99 onClick={ () => onChange( 0 ) } 100 > 101 { __( 'Remove', 'simple-seo' ) } 102 </Button> 103 ) } 104 </Flex> 105 </Flex> 106 ) } 107 /> 108 </BaseControl> 109 </MediaUploadCheck> 110 ); 111 } 112 28 113 function SimpleSeoPanel() { 29 114 const postType = useSelect( … … 46 131 meta._simple_seo_title.trim() !== '' || 47 132 meta._simple_seo_description.trim() !== '' || 48 !! meta._simple_seo_noindex; 133 !! meta._simple_seo_noindex || 134 !! meta._simple_seo_share_image; 49 135 50 136 // Meta edits are merged, so pass only the changed key. … … 118 204 /> 119 205 206 <ShareImageControl 207 imageId={ meta._simple_seo_share_image ?? 0 } 208 onChange={ update( '_simple_seo_share_image' ) } 209 /> 210 120 211 <CheckboxControl 121 212 __nextHasNoMarginBottom -
simple-seo/trunk/readme.txt
r3720763 r3722856 6 6 Tested up to: 7.1 7 7 Requires PHP: 7.4 8 Stable tag: 1. 4.08 Stable tag: 1.5.0 9 9 License: GPLv2 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html 11 11 12 The SEO basics and nothing more: a title, a description and a "don't index" checkbox for every page. No upsells, no settings maze.12 The SEO basics: a title, a description and a "don't index" checkbox for every page. Small, fast, and no upsell banners all over your admin. 13 13 14 14 == Description == … … 24 24 A few things just happen: link previews with the right title, description and image in Slack, Mastodon, Bluesky and friends. Pages you keep out of search are left out of the sitemap too. There's a panel in the block editor, a box in the Classic Editor, and the fields in Quick Edit. Using Yoast SEO or another big SEO plugin? Simple SEO steps aside. 25 25 26 No s ettings page, no scores, no "Go Pro" banners, no extra database queries. <del>No nags.</del> Okay, one small grey tip about [Simple History](https://wordpress.org/plugins/simple-history/), my activity log plugin.26 No scores, no "Go Pro" banners, no extra database queries. <del>No nags.</del> Okay, one small grey tip about [Simple History](https://wordpress.org/plugins/simple-history/), my activity log plugin. 27 27 28 28 No plugin makes a page rank. Good content does. Simple SEO hands search engines and shared links the title, description and image you chose, and asks search engines to skip the pages you don't want listed. That's also all AI search needs from a plugin: no llms.txt, no "AI optimization". … … 32 32 Simple SEO is old. Like really old. It arrived on WordPress.org in August 2010 (seven weeks before Yoast SEO!). Of the SEO plugins still maintained today, only All in One SEO is older. Then it slept from 2012 to 2026. 33 33 34 I woke it up mostly for myself: I wanted the SEO basics on my own sites without a plugin that asks for attention every time I log in. It stays small on purpose, so feature requests often get "that's a bit much for a plugin called Simple".34 I woke it up mostly for myself: I wanted the SEO basics on my own sites without a plugin that asks for attention every time I log in. It grows when I need something on my own sites, but it stays small and fast on purpose. 35 35 36 36 No big company or investors, just one developer: me, the guy behind [Simple History](https://simple-history.com/). With both active, every SEO change is logged: who, when, and what it said before. I also make [CMS Tree Page View](https://wordpress.org/plugins/cms-tree-page-view/), and the two work together too. If Simple SEO saves you time, [a donation](https://eskapism.se/sida/donate/) keeps it going. … … 60 60 A wide picture, 1200 × 630 pixels, not your logo. It's used in link previews when a post has no featured image. 61 61 62 = Can a post have a different image in link previews? = 63 64 Yes, pick a share image in the post's Simple SEO fields. Link previews use it instead of the featured image, and your theme keeps showing the featured image. Handy when the picture that looks good on your site isn't the one that works in a shared link, like a card with the headline on it. 65 62 66 = I use Yoast SEO (or Rank Math, All in One SEO, SEOPress, The SEO Framework). = 63 67 … … 70 74 = Can I use WP-CLI, the REST API or filters? = 71 75 72 Yup. The fields are post meta: `_simple_seo_title`, `_simple_seo_description` and `_simple_seo_noindex`, in the REST API with `?context=edit`. Everything Simple SEO outputs goes through a filter first. See [the hooks](https://github.com/bonny/WordPress-Simple-SEO/blob/main/docs/hooks.md).76 Yup. The fields are post meta: `_simple_seo_title`, `_simple_seo_description`, `_simple_seo_noindex` and `_simple_seo_share_image` (an attachment ID), in the REST API with `?context=edit`. Everything Simple SEO outputs goes through a filter first. See [the hooks](https://github.com/bonny/WordPress-Simple-SEO/blob/main/docs/hooks.md). 73 77 74 78 = What happened to the menu label? = … … 78 82 == Screenshots == 79 83 80 1. A few SEO fields in the page sidebar of the block editor. That's it.84 1. The SEO fields in the page sidebar of the block editor. 81 85 2. See and edit the SEO of every page right from the Pages list, with Quick Edit. 82 86 3. The same column for posts, so you can spot the ones missing a description. 83 87 4. Works in the Classic Editor too. 84 5. One setting: adefault share image for link previews, in Settings → General.88 5. A default share image for link previews, in Settings → General. 85 89 6. With Simple History, every SEO change is logged: who, when, and what it said before. 86 90 87 91 == Changelog == 92 93 = 1.5.0 (October 2026) = 94 - New: a share image for each post, used in link previews instead of the featured image. Your theme keeps showing the featured image. In both editors, and logged in Simple History. 88 95 89 96 = 1.4.0 (September 2026) = -
simple-seo/trunk/simple-seo.php
r3720725 r3722856 4 4 * Plugin URI: https://wordpress.org/plugins/simple-seo/ 5 5 * Description: Change the page title and menu label output for any page or post, which can be useful for SEO (Search Engine Optimization) reasons. It may also increase the usability of your website, making it more friendly and understandable for your visitors. 6 * Version: 1. 4.06 * Version: 1.5.0 7 7 * Requires at least: 6.6 8 8 * Requires PHP: 7.4 … … 35 35 } 36 36 37 define( 'SIMPLE_SEO_VERSION', '1. 4.0' );37 define( 'SIMPLE_SEO_VERSION', '1.5.0' ); 38 38 39 39 // This file must stay parseable by PHP 5.6: WordPress before 5.2 ignores "Requires PHP" -
simple-seo/trunk/src/admin.php
r3715169 r3722856 46 46 47 47 /** 48 * Help text for a post's share image. 49 */ 50 function share_image_help(): string { 51 return __( 'Used in link previews instead of the featured image.', 'simple-seo' ); 52 } 53 54 /** 48 55 * Whether a post uses any of the fields. The Simple History tip only shows then. 49 56 * … … 54 61 || '' !== get_description( $post_id ) 55 62 || is_noindex( $post_id ) 63 || get_share_image_id( $post_id ) 56 64 || '' !== get_menu_label( $post_id ); 65 } 66 67 /** 68 * An image picker: a preview, the media library buttons and a hidden attachment ID, like core's 69 * Site Icon. Used by the default share image in Settings and the share image in the Classic box. 70 * Needs enqueue_image_field_script(). 71 * 72 * @param string $name Form field name. 73 * @param int $attachment_id The image, or 0. 74 * @param string $preview_url URL of the image to show, or ''. 75 */ 76 function image_field( string $name, int $attachment_id, string $preview_url ): void { 77 $has_image = $attachment_id && '' !== $preview_url; 78 79 printf( 80 '<div class="simple-seo-share-image"><img src="%1$s" alt="" style="max-width:300px;width:100%%;height:auto;display:block;margin-bottom:8px" %2$s />', 81 esc_url( $preview_url ), 82 $has_image ? '' : 'hidden' 83 ); 84 85 printf( 86 '<input type="hidden" name="%1$s" value="%2$s" />', 87 esc_attr( $name ), 88 esc_attr( $has_image ? (string) $attachment_id : '' ) 89 ); 90 91 printf( 92 '<button type="button" class="button simple-seo-share-image-choose" data-choose="%1$s" data-change="%2$s">%3$s</button> ', 93 esc_attr__( 'Choose image', 'simple-seo' ), 94 esc_attr__( 'Change image', 'simple-seo' ), 95 $has_image ? esc_html__( 'Change image', 'simple-seo' ) : esc_html__( 'Choose image', 'simple-seo' ) 96 ); 97 98 printf( 99 '<button type="button" class="button button-secondary simple-seo-share-image-remove" %1$s>%2$s</button></div>', 100 $has_image ? '' : 'hidden', 101 esc_html__( 'Remove image', 'simple-seo' ) 102 ); 103 } 104 105 /** 106 * The media library and the script behind image_field(). Can be called while the page renders: 107 * both print in the footer. 108 */ 109 function enqueue_image_field_script(): void { 110 wp_enqueue_media(); 111 112 $asset = require dirname( __DIR__ ) . '/build/share-image-field.asset.php'; 113 114 wp_enqueue_script( 115 'simple-seo-share-image-field', 116 plugins_url( 'build/share-image-field.js', \SIMPLE_SEO_PLUGIN_FILE ), 117 $asset['dependencies'], 118 $asset['version'], 119 true 120 ); 57 121 } 58 122 … … 91 155 wp_add_inline_style( 92 156 'simple-seo-admin', 93 '#simple-seo label { display: block; margin-bottom: 4px; font-weight: 600; }157 '#simple-seo label, #simple-seo .simple-seo-field-label { display: block; margin-bottom: 4px; font-weight: 600; } 94 158 #simple-seo .description { display: block; margin-top: 4px; } 159 #simple-seo .simple-seo-field { margin: 1em 0; } 95 160 .simple-seo-quick-edit .simple-seo-field, .simple-seo-quick-edit .simple-seo-check { display: block; margin-top: 6px; } 96 161 .simple-seo-quick-edit input[type="text"] { display: block; width: 100%; } -
simple-seo/trunk/src/class-simple-history-logger.php
r3715169 r3722856 9 9 namespace SimpleSEO; 10 10 11 use Simple_History\Event_Details\Event_Details_Container; 11 12 use Simple_History\Event_Details\Event_Details_Group; 12 13 use Simple_History\Event_Details\Event_Details_Group_Diff_Table_Formatter; … … 43 44 return [ 44 45 'name' => __( 'Simple SEO', 'simple-seo' ), 45 'description' => __( 'Logs changes to the SEO title, meta description, "Discourage search engines", menu label and default share image.', 'simple-seo' ),46 'description' => __( 'Logs changes to the SEO title, meta description, "Discourage search engines", menu label, share image and default share image.', 'simple-seo' ), 46 47 'name_via' => __( 'Using plugin Simple SEO', 'simple-seo' ), 47 48 'capability' => 'edit_posts', … … 257 258 258 259 /** 259 * A before/after table of the fields that changed .260 * A before/after table of the fields that changed, and the share image side by side. 260 261 * 261 262 * @param object $row Log row. 262 * @return Event_Details_Group 263 * @return Event_Details_Group|Event_Details_Container 263 264 */ 264 265 public function get_log_row_details_output( $row ) { 265 266 if ( 0 === strpos( (string) ( $row->context['_message_key'] ?? '' ), 'share_image_' ) ) { 266 return $this->share_image_details( $row->context );267 return $this->share_image_details( $row->context, __( 'Default share image', 'simple-seo' ) ); 267 268 } 268 269 … … 278 279 ); 279 280 280 return $group; 281 if ( ! array_key_exists( 'share_image_new', $row->context ) ) { 282 return $group; 283 } 284 285 // The image diff is its own group, as Simple History's featured image. Older versions without 286 // the container class get its URL as a row in the table. 287 if ( ! class_exists( Event_Details_Container::class ) ) { 288 $group->add_items( [ new Event_Details_Item( [ 'share_image' ], __( 'Share image', 'simple-seo' ) ) ] ); 289 return $group; 290 } 291 292 return new Event_Details_Container( [ $group, $this->share_image_details( $row->context, __( 'Share image', 'simple-seo' ) ) ], $row->context ); 281 293 } 282 294 … … 285 297 * 286 298 * @param array<string, mixed> $context Event context. 299 * @param string $label Row label. 287 300 * @return Event_Details_Group 288 301 */ 289 private function share_image_details( array $context ) {290 $item = new Event_Details_Item( [ 'share_image' ], __( 'Default share image', 'simple-seo' ));302 private function share_image_details( array $context, string $label ) { 303 $item = new Event_Details_Item( [ 'share_image' ], $label ); 291 304 $group = new Event_Details_Group(); 292 305 -
simple-seo/trunk/src/classic-editor.php
r3720725 r3722856 69 69 } 70 70 71 share_image_field_in_box( $post->ID ); 72 71 73 printf( 72 74 '<p><label><input type="checkbox" name="simple_seo[noindex_on]" value="1" aria-describedby="simple-seo-noindex-help" %1$s /> %2$s</label><span class="description" id="simple-seo-noindex-help">%3$s</span></p>', … … 96 98 esc_html__( '(opens in a new tab)', 'simple-seo' ) 97 99 ); 100 } 101 102 /** 103 * The post's share image, for users who can use the media library (as core's Featured image). 104 * Without the field the form posts no share_image, so saving leaves a stored image alone. 105 * 106 * @param int $post_id Post ID. 107 */ 108 function share_image_field_in_box( int $post_id ): void { 109 if ( ! current_user_can( 'upload_files' ) ) { 110 return; 111 } 112 113 $share_image_id = get_share_image_id( $post_id ); 114 115 // A group, not a <label>: that would replace the buttons' own names. 116 printf( 117 '<div class="simple-seo-field" role="group" aria-labelledby="simple-seo-share-image-label" aria-describedby="simple-seo-share-image-help"><span class="simple-seo-field-label" id="simple-seo-share-image-label">%s</span>', 118 esc_html__( 'Share image', 'simple-seo' ) 119 ); 120 image_field( 'simple_seo[share_image]', $share_image_id, (string) wp_get_attachment_image_url( $share_image_id, 'medium' ) ); 121 printf( '<span class="description" id="simple-seo-share-image-help">%s</span></div>', esc_html( share_image_help() ) ); 122 enqueue_image_field_script(); 98 123 } 99 124 … … 154 179 update_post_meta( $post_id, NOINDEX_KEY, ! empty( $fields['noindex_on'] ) ); 155 180 181 // Quick Edit posts the other fields but not this one. 182 if ( isset( $fields['share_image'] ) ) { 183 update_post_meta( $post_id, SHARE_IMAGE_KEY, absint( $fields['share_image'] ) ); 184 } 185 156 186 if ( 'page' === $post->post_type ) { 157 187 save_menu_label( $post_id, $text( 'menu_label' ) ); -
simple-seo/trunk/src/link-previews.php
r3720725 r3722856 75 75 $title = '' !== $title ? $title : wp_strip_all_tags( get_the_title( $post_id ) ); 76 76 $url = (string) get_permalink( $post_id ); 77 $image = featured_image( $post_id);77 $image = attachment_image( get_share_image_id( $post_id ) ) ?? attachment_image( (int) get_post_thumbnail_id( $post_id ) ); 78 78 79 79 // No meta description: the hand-written excerpt, else the start of the content, so a … … 98 98 /** 99 99 * The link preview image: [ 'url', 'width', 'height', 'alt' ], or null for none. 100 * By default the featured image, else the default share image from Settings → General.100 * By default the post's share image, else its featured image, else the default share image from Settings → General. 101 101 * 102 * @param array{url: string, width: int, height: int, alt: string}|null $image The featuredimage, if any.102 * @param array{url: string, width: int, height: int, alt: string}|null $image The image, if any. 103 103 * @param int $post_id The post, 0 on a front page with the latest posts. 104 104 */ … … 157 157 158 158 /** 159 * The post's featured image at full size, or null.159 * An image attachment at full size, or null when there is none (0, or deleted). 160 160 * 161 * @param int $ post_id Post ID.161 * @param int $attachment_id Attachment ID. 162 162 * @return array{url: string, width: int, height: int, alt: string}|null 163 163 */ 164 function featured_image( int $post_id ): ?array { 165 $attachment_id = (int) get_post_thumbnail_id( $post_id ); 166 $src = $attachment_id ? wp_get_attachment_image_src( $attachment_id, 'full' ) : false; 164 function attachment_image( int $attachment_id ): ?array { 165 $src = $attachment_id ? wp_get_attachment_image_src( $attachment_id, 'full' ) : false; 167 166 168 167 if ( ! $src ) { -
simple-seo/trunk/src/meta.php
r3714576 r3722856 15 15 const DESCRIPTION_KEY = '_simple_seo_description'; 16 16 const NOINDEX_KEY = '_simple_seo_noindex'; 17 18 // Attachment ID of the image for link previews, used instead of the featured image. 19 const SHARE_IMAGE_KEY = '_simple_seo_share_image'; 17 20 18 21 // Menu label, pages only, Classic Editor only. Same keys as before 1.0: the label is used when the flag is 1. … … 29 32 add_action( 'deleted_post_meta', __NAMESPACE__ . '\\forget_legacy_title', 10, 3 ); 30 33 add_filter( 'default_post_metadata', __NAMESPACE__ . '\\legacy_title_default', 20, 4 ); 34 add_action( 'added_post_meta', __NAMESPACE__ . '\\forget_empty_share_image', 10, 4 ); 35 add_action( 'updated_post_meta', __NAMESPACE__ . '\\forget_empty_share_image', 10, 4 ); 36 add_action( 'delete_attachment', __NAMESPACE__ . '\\forget_deleted_share_image' ); 31 37 32 38 /** … … 59 65 ); 60 66 } 67 68 register_post_meta( 69 '', 70 SHARE_IMAGE_KEY, 71 [ 72 'type' => 'integer', 73 'description' => __( 'Attachment ID of the image for link previews, used instead of the featured image. 0 or none uses the featured image.', 'simple-seo' ), 74 'single' => true, 75 'default' => 0, 76 'show_in_rest' => [ 'schema' => [ 'context' => [ 'edit' ] ] ], 77 'sanitize_callback' => __NAMESPACE__ . '\\sanitize_share_image_id', 78 'auth_callback' => fn( $allowed, $meta_key, $post_id ) => current_user_can( 'edit_post', $post_id ), 79 ] 80 ); 81 } 82 83 /** 84 * The share image must be an image attachment; anything else is stored as 0, which is then deleted. 85 * 86 * @param mixed $value Attachment ID. 87 */ 88 function sanitize_share_image_id( $value ): int { 89 $attachment_id = absint( $value ); 90 91 return $attachment_id && wp_attachment_is_image( $attachment_id ) ? $attachment_id : 0; 92 } 93 94 /** 95 * No share image (0, as the editors send on Remove) deletes the key, as an empty text field does. 96 * 97 * @param int $meta_id Meta ID, unused. 98 * @param int $object_id Post ID. 99 * @param string $meta_key Meta key. 100 * @param mixed $meta_value The stored value. 101 */ 102 function forget_empty_share_image( $meta_id, $object_id, $meta_key, $meta_value ): void { 103 if ( SHARE_IMAGE_KEY === $meta_key && ! absint( $meta_value ) ) { 104 delete_post_meta( $object_id, SHARE_IMAGE_KEY ); 105 } 61 106 } 62 107 … … 105 150 function get_description( int $post_id ): string { 106 151 return trim( (string) get_post_meta( $post_id, DESCRIPTION_KEY, true ) ); 152 } 153 154 /** 155 * A deleted image is no longer anyone's share image, so the editors don't show an empty field. 156 * One bulk delete for all posts (core's $delete_all), only when an attachment is deleted: one 157 * query when no post uses the image, three when some do. 158 * 159 * @param int $attachment_id Attachment ID. 160 */ 161 function forget_deleted_share_image( $attachment_id ): void { 162 delete_metadata( 'post', 0, SHARE_IMAGE_KEY, (int) $attachment_id, true ); 163 } 164 165 /** 166 * The post's own share image (attachment ID), or 0 to use the featured image. 167 * 168 * @param int $post_id Post ID. 169 */ 170 function get_share_image_id( int $post_id ): int { 171 return absint( get_post_meta( $post_id, SHARE_IMAGE_KEY, true ) ); 107 172 } 108 173 -
simple-seo/trunk/src/settings.php
r3720725 r3722856 74 74 $image = share_image(); 75 75 76 printf( 77 '<div class="simple-seo-share-image"><img src="%1$s" alt="" style="max-width:300px;height:auto;display:block;margin-bottom:8px" %2$s />', 78 esc_url( $image['url'] ?? '' ), 79 $image ? '' : 'hidden' 80 ); 81 82 printf( 83 '<input type="hidden" name="%1$s" value="%2$s" />', 84 esc_attr( SHARE_IMAGE_OPTION ), 85 esc_attr( (string) ( $image['id'] ?? '' ) ) 86 ); 87 88 printf( 89 '<button type="button" class="button simple-seo-share-image-choose" data-choose="%1$s" data-change="%2$s">%3$s</button> ', 90 esc_attr__( 'Choose image', 'simple-seo' ), 91 esc_attr__( 'Change image', 'simple-seo' ), 92 $image ? esc_html__( 'Change image', 'simple-seo' ) : esc_html__( 'Choose image', 'simple-seo' ) 93 ); 94 95 printf( 96 '<button type="button" class="button button-secondary simple-seo-share-image-remove" %1$s>%2$s</button></div>', 97 $image ? '' : 'hidden', 98 esc_html__( 'Remove image', 'simple-seo' ) 99 ); 76 image_field( SHARE_IMAGE_OPTION, (int) ( $image['id'] ?? 0 ), (string) ( $image['url'] ?? '' ) ); 100 77 101 78 printf( … … 115 92 } 116 93 117 wp_enqueue_media(); 118 119 $asset = require dirname( __DIR__ ) . '/build/share-image-field.asset.php'; 120 121 wp_enqueue_script( 122 'simple-seo-share-image-field', 123 plugins_url( 'build/share-image-field.js', \SIMPLE_SEO_PLUGIN_FILE ), 124 $asset['dependencies'], 125 $asset['version'], 126 true 127 ); 94 enqueue_image_field_script(); 128 95 } 129 96 -
simple-seo/trunk/src/simple-history.php
r3714576 r3722856 57 57 'noindex' => is_noindex( $post_id ) ? __( 'Yes', 'simple-seo' ) : __( 'No', 'simple-seo' ), 58 58 'menu_label' => get_menu_label( $post_id ), 59 'share_image' => (string) wp_get_attachment_url( get_share_image_id( $post_id ) ), 59 60 ]; 60 61 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)