Changeset 3423355
- Timestamp:
- 12/19/2025 05:33:40 AM (2 months ago)
- Location:
- search-appearance-toolkit-seo-44
- Files:
-
- 2 added
- 22 edited
- 1 copied
-
tags/4.3 (copied) (copied from search-appearance-toolkit-seo-44/trunk)
-
tags/4.3/README.md (modified) (9 diffs)
-
tags/4.3/build/index.asset.php (modified) (1 diff)
-
tags/4.3/build/index.js (modified) (1 diff)
-
tags/4.3/changelog.txt (modified) (1 diff)
-
tags/4.3/includes/class-seo44-core.php (modified) (3 diffs)
-
tags/4.3/includes/class-seo44-frontend.php (modified) (32 diffs)
-
tags/4.3/includes/class-seo44-metabox.php (modified) (5 diffs)
-
tags/4.3/includes/class-seo44-settings.php (modified) (11 diffs)
-
tags/4.3/js/admin-script.js (modified) (5 diffs)
-
tags/4.3/js/block-editor-script.js (added)
-
tags/4.3/readme.txt (modified) (10 diffs)
-
tags/4.3/seo-44.php (modified) (2 diffs)
-
trunk/README.md (modified) (9 diffs)
-
trunk/build/index.asset.php (modified) (1 diff)
-
trunk/build/index.js (modified) (1 diff)
-
trunk/changelog.txt (modified) (1 diff)
-
trunk/includes/class-seo44-core.php (modified) (3 diffs)
-
trunk/includes/class-seo44-frontend.php (modified) (32 diffs)
-
trunk/includes/class-seo44-metabox.php (modified) (5 diffs)
-
trunk/includes/class-seo44-settings.php (modified) (11 diffs)
-
trunk/js/admin-script.js (modified) (5 diffs)
-
trunk/js/block-editor-script.js (added)
-
trunk/readme.txt (modified) (10 diffs)
-
trunk/seo-44.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
search-appearance-toolkit-seo-44/tags/4.3/README.md
r3405454 r3423355 8 8 * **Tags:** seo, on-page seo, schema, structured data, xml sitemaps 9 9 * **Requires at least:** 5.5 10 * **Tested up to:** 6. 811 * **Stable tag:** 4. 2.010 * **Tested up to:** 6.9 11 * **Stable tag:** 4.3.0 12 12 * **Requires PHP:** 7.4 13 13 * **License:** GPLv2 or later … … 85 85 * **Knowledge Graph Control:** A dedicated interface to manage your brand's digital identity. Define your Founder, Founding Date, Contact Info, and professional Credentials to improve E-E-A-T signals. 86 86 * **Include Images and Videos:** A built-in tool automatically finds all images and embedded YouTube videos in your content and adds them to the schema, boosting their appearance in search results. 87 * **FAQ and How-To Detection:** Enable a smart scanner to detect patterns in your content for FAQ and How-To sections on your website and incorporate this useful format into the schema. 87 * **FAQ and How-To Detection:** Enable smart scanners to detect patterns in your content for FAQ and How-To sections on your website and incorporate this useful format into the schema. 88 * **Jump Links HowTo Scanner:** The plugin can use a Jump Links Block as a "Map" to generate detailed HowTo schema steps, while simultaneously scanning your content for Prep Time, Yield, Supplies, and Tools. 89 * **Table of Contents Schema:** Automatically generates `hasPart` structured data that mirrors your Jump Links Block, helping search engines understand your article's deep structure. 88 90 * **Modern Output:** All structured data is generated in the modern JSON-LD format preferred by search engines, following the guidelines set by [Schema.org](https://schema.org/). 89 91 * **Granular Control:** Tailor the Schema settings to fit your site's needs through Enable/disable settings, including on Custom Post Types and Taxonomies. … … 128 130 SEO 44's integration with Google Tag Manager pushes valuable events to the `dataLayer` for use in Google Analytics. The plugin provides an import file and detailed instructions for setting up Google Tag Manager and Google Analytics to receive event tracking data. 129 131 130 ## Site Verification Tags132 ### Site Verification Tags 131 133 Verify your site with search engines quickly and easily. Paste your verification codes for **Google Search Console** and **Bing Webmaster Tools** into the corresponding fields in the Integrations tab. The plugin handles the rest, adding site verification meta tags to your website's head. No coding required. 134 135 ### YouTube Data API 136 To ensure that your site's video schema is as accurate as possible, you may add your YouTube Data API Key. The plugin uses this key to fetch the upload date for any YouTube video embedded in your content, replacing less reliable page scraping options and fallbacks to the post publish date. 132 137 133 138 --- … … 138 143 Search Appearance Toolkit (SEO 44) gives you control over the technical, on-page SEO factors that help search engines understand and rank your content. Key benefits include: 139 144 * **Optimized Snippets:** Control how your titles and descriptions appear in search results. 140 * **Rich Results:** The advanced Schema.org data helps you earn rich results like FAQs, How-Tos, and breadcrumbs in Google.145 * **Rich Results:** The advanced Schema.org data improves indexing and helps you earn rich results in Google. 141 146 * **Crawlability:** A clean and comprehensive XML sitemap ensures search engines can find and index all of your important content and images. 142 147 * **User Engagement:** The Jump Links Block improves user experience, which is a positive ranking signal, and can help you earn "Jump to" links in search results. … … 170 175 SEO 44 helps your content look great when shared on social media platforms. You can enable the automatic generation of **Open Graph** (og:) tags, which Facebook, LinkedIn, and Pinterest use, and **Twitter Card** meta tags for when your content appears on X (formerly Twitter). This ensures your posts have the correct title, description, and preview image when shared. 171 176 172 Use the plugin's Google Tag Manager Integration to facilitate additional connections with soc lal media platforms. Use the Organization Schema settings to associate all of your social media profiles with your website.177 Use the plugin's Google Tag Manager Integration to facilitate additional connections with social media platforms. Use the Organization Schema settings to associate all of your social media profiles with your website. 173 178 174 179 ### How are social media images handled? … … 193 198 194 199 ### What are the benefits of using FAQPage and HowTo schema? 195 The plugin intelligently scans your content for patterns that match question-and-answer formats or step-by-step instructions (when this option is enabled). The plugin locates this content and automatically generates FAQPage or HowTo schema that presents this content within the JSON-LD.196 197 The benefit of this is that Google can use this structured data to display your content in special, highly visible formats in the search results. An FAQ page might appear as a rich snippet with expandable questions , while a How-To article can be featured in a step-by-step guide. Rich snippets make your search results stand out, which can significantlyimprove your click-through rate (CTR).200 The plugin intelligently scans your content for patterns that match question-and-answer formats or step-by-step instructions (when this option is enabled). The plugin locates this content and automatically generates FAQPage or HowTo schema that presents this content as structured data. 201 202 The benefit of this is that Google can use this structured data to display your content in special, highly visible formats in the search results. An FAQ page might appear as a rich snippet with expandable questions or feature within a People Also Ask (PAA) result, while a How-To article can be featured in a step-by-step guide in AI Overviews. Search results that stand out can improve your click-through rate (CTR). 198 203 199 204 Read more about the [FAQPage and HowTo Schema](https://seo44plugin.com/search-appearance-toolkit-seo-44/schema-structured-data/#benefits-of-faqpage-and-howto-schema) created by SEO 44. … … 457 462 * **Google:** [Terms of Service](https://policies.google.com/terms), [Privacy Policy](https://policies.google.com/privacy) 458 463 * **Bing (Microsoft):** [Microsoft Services Agreement](https://www.microsoft.com/en-us/servicesagreement/), [Microsoft Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) 464 465 ### YouTube Data API Integration & Video Metadata 466 467 * **Service Description:** This plugin connects to YouTube to fetch the "Upload Date" for videos embedded in your content. This ensures your VideoObject schema is accurate. 468 * **Data Sent and Conditions:** 469 * **Method 1 (API):** If you provide a YouTube API Key in settings, the plugin sends the Video ID to the Google Data API. 470 * **Method 2 (Public Fallback):** If no API Key is present, the plugin acts as a standard browser and fetches the public video page (via `wp_remote_get`) to locate the upload date in the page meta tags. 471 * **Conditions:** This occurs automatically when a post with a YouTube embed is updated, provided that Schema generation is enabled. 472 * **Service Provider Links:** 473 * **YouTube:** [Terms of Service](https://www.youtube.com/t/terms), [Google Privacy Policy](https://policies.google.com/privacy) 459 474 460 475 --- … … 574 589 575 590 ## Changelog 591 592 ### 4.3.0 593 * **FEATURE:** **Table of Contents Schema:** Automatically generates `hasPart` schema synchronized with the Jump Links Block to support "Jump-to" links in search results. 594 * **FEATURE:** **Advanced HowTo Schema:** A new "Block-Aware" scanner uses the Jump Links Block to strictly define steps while intelligently mining the introduction for tools, time, yield, and video data. 595 * **TWEAK:** Added a "Generate HowTo Schema" checkbox to the SEO 44 metabox, giving users explicit control over when the advanced scanner runs. 596 * **FEATURE:** **YouTube Integration:** Added support for the YouTube Data API v3 to fetch accurate video upload dates for VideoObject schema. 597 * **TESTED:** Tested to WordPress Version 6.9 576 598 577 599 ### 4.2.0 … … 585 607 * **TWEAK:** Updated the image uploader JavaScript to support multiple distinct upload buttons on the settings page. 586 608 587 ### 4.1.0588 **Jump Links Block Updates:**589 * **FEATURE:** **Sticky Positioning:** Keep your table of contents visible while users scroll. Includes a "Top Offset" slider to clear sticky headers, a "Jump Offset" slider to ensure that the sticky header does not cover the heading text, and a "Disable on Mobile" toggle to preserve screen space on small devices.590 * **FEATURE:** **Auto-Hide Title:** Implemented a smart "sticky state" detection. When the block sticks, the title gently collapses and fades out to keep the interface clean (this occurs when a block title is used alongside sticky positioning).591 * **FEATURE:** **Smart Indentation:** Added a "Create Visual Hierarchy" toggle. When enabled, H3 and H4 sub-headings are visually indented to create a clear, nested outline structure.592 * **FEATURE:** **Block Background:** You can now set a background color for the entire block container, perfect for creating "card-style" floating navigation.593 * **FEATURE:** **ScrollSpy:** Automatically highlights the active link in the table of contents as the user scrolls through the corresponding section of the post.594 * **FEATURE:** Added support for Border and Spacing controls. You can now add borders, rounded corners, margins, and padding to the Jump Links block directly from the editor settings.595 * **FEATURE:** Added a "Title tag" control. You can now choose the specific HTML tag (H2, H3, H4, H5, Paragraph, or Div) for the "On This Page" heading to better match your document structure.596 * **REFACTOR:** Optimized the block's styling logic to use CSS variables on the parent container instead of inline styles for every link. This reduces the block's HTML size and improves rendering performance.597 * **FIX:** Resolved an accessibility and HTML validation issue where using multiple Jump Links blocks on a single page created duplicate element IDs. Each block now generates a unique instance ID.598 * **PERFORMANCE:** Refactored the front-end JavaScript to use event delegation for smooth scrolling. This reduces memory usage by attaching a single event listener to the block instead of individual listeners for every link.599 * **TWEAK:** Reorganized the sidebar settings for better clarity between Block Title settings and Content Inclusion settings.600 601 ### 4.0.0602 * **FEATURE:** Added a new "Integrations" tab for third-party services like Google Tag Manager and Webmaster Tools.603 * **FEATURE:** Added Google Tag Manager (GTM) integration. The plugin can now automatically inject the GTM container script into the site's <head> and <body> based on your ID.604 * **FEATURE:** Added Webmaster Verification. You can now add your Google Search Console and Bing Webmaster Tools verification codes directly from the plugin settings.605 * **FEATURE:** Added automatic GTM event tracking. When enabled, the plugin can push the following events to the dataLayer:606 * Rich SEO dataLayer: Pushes page type, category, author, and tags on page load for advanced GTM triggers.607 * Scroll Depth Tracking: Pushes 'scroll_depth' events at 25%, 50%, 75%, and 100% of the page.608 * External & Affiliate Clicks:* Pushes 'external_link_click' or 'affiliate_link_click' (for `rel="sponsored"` links).609 * Jump Link Clicks: Pushes a 'jump_link_click' event when a user clicks a link in the Jump Links Block.610 * **FEATURE:** Added a Google Tag Manager recipe import file (`seo44-gtm-recipe-importer.json`) and new FAQ instructions to fully configure GTM and GA4 event tracking.611 * **ENHANCEMENT:** Centralized all GTM event tracking into a new, efficient `global-tracker.js` file that uses event delegation for better performance.612 * **TWEAK:** Improved the "Integrations" settings page UI for clarity, adding clarifying tooltips and a file downloader.613 614 For a complete list of changes, please see the [full changelog](https://github.com/SethSmigelski/search-appearance-toolkit-seo-44/blob/main/changelog.txt) or the `changelog.txt` file included with the plugin.615 616 609 --- 617 610 -
search-appearance-toolkit-seo-44/tags/4.3/build/index.asset.php
r3405454 r3423355 1 <?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => ' 9e72772c3731a4c27337');1 <?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '2c1769c1ec087de88890'); -
search-appearance-toolkit-seo-44/tags/4.3/build/index.js
r3403604 r3423355 1 (()=>{"use strict";var e,t={313 :()=>{const e=window.wp.blocks,t=window.React,a=window.wp.i18n,o=window.wp.blockEditor,l=window.wp.components,n=window.wp.data,r=window.wp.element;function s(e){return(new DOMParser).parseFromString(e,"text/html").body.textContent||""}const i=(0,t.createElement)("svg",{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M3 21v-2h18v2zm8-4v-6.175L9.4 12.4L8 11l4-4l4 4l-1.4 1.4l-1.6-1.575V17zM3 5V3h18v2z"})),c=(0,t.createElement)("svg",{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M3 5V3h18v2zm9 12l-4-4l1.4-1.4l1.6 1.575V7h2v6.175l1.6-1.575L16 13zm-9 4v-2h18v2z"})),h=(0,t.createElement)("svg",{className:"arrow-down",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10l-6 6z"})),p=JSON.parse('{"UU":"seo44/jump-links"}');(0,e.registerBlockType)(p.UU,{edit:function({attributes:e,setAttributes:p}){const{headingLevels:k,headings:u,showHeading:d,headingText:m,headingTag:g,layout:_,listStyle:b,isEditing:v,isCollapsible:f,isSmartIndentation:x,fontSize:w,textColor:C,linkColor:E,blockBackgroundColor:y,linkBackgroundColor:B,linkBackgroundColorHover:S,linkBorderColor:z,linkBorderRadius:T,isSticky:N,stickyOffset:H,jumpOffset:M,stickyStrategy:P}=e,O={color:C,fontSize:w,"--jump-link-font-size":w||"18px","--seo44-link-color":E,"--seo44-link-bg":"horizontal"===_?B:void 0,"--seo44-link-hover-bg":"horizontal"===_?S:void 0,"--seo44-link-border-color":"horizontal"===_?z:void 0,"--seo44-link-radius":"horizontal"===_&&T?`${T}px`:void 0,"--seo44-block-bg":y,"--seo44-sticky-offset":N?`${H}px`:void 0},I="ol"===b?"ol":"ul",{createInfoNotice:L}=(0,n.useDispatch)("core/notices"),j=e.blockInstanceId?`seo44-jump-links-list-${e.blockInstanceId}`:"seo44-jump-links-list",$=(0,o.useBlockProps)({style:O});$.className=`${$.className} ${"horizontal"===_?"is-layout-horizontal":""} ${f&&!v?"is-collapsible":""} ${"none"===b?"list-style-none":""}`.trim();const D=(0,n.useSelect)(e=>e("core/block-editor").getBlocks(),[]),{updateBlockAttributes:V}=(0,n.useDispatch)("core/block-editor");(0,r.useEffect)(()=>{const t={};e.blockInstanceId||(t.blockInstanceId=Math.random().toString(36).substr(2,9)),Object.keys(t).length>0&&p(t);const o=D.filter(e=>"core/heading"===e.name&&k.includes(`h${e.attributes.level}`)),l=new Set;let n=!1;const r=new Map(u.map(e=>[e.anchor,e])),i=[];for(const e of o){const t=s(e.attributes.content);let a=e.attributes.anchor||t.toLowerCase().replace(/[^a-z0-9\s-]/g,"").trim().replace(/\s+/g,"-"),o=a,c=2;for(;l.has(o);)o=`${a}-${c}`,c++,n=!0;l.add(o),e.attributes.anchor!==o&&V(e.clientId,{anchor:o});const h=r.get(e.attributes.anchor)||r.get(o),p=h&&h.linkText!==h.text?h.linkText:t,k=!h||h.isVisible;i.push({anchor:o,text:t,linkText:p,isVisible:k,level:e.attributes.level})}JSON.stringify(i)!==JSON.stringify(u)&&p({headings:i}),n&&L((0,a.__)("Jump Links Block: Duplicate headings were found. Unique IDs have been auto-generated, but this may be a sign of redundancy. Please review your headings for clarity.","search-appearance-toolkit-seo-44"),{type:"snackbar"})},[D,k,u,e.blockInstanceId,p,V,L]),(0,r.useEffect)(()=>{"horizontal"===_&&"none"!==b&&p({listStyle:"none"})},[_,b,p]);const R=(e,t)=>{const a=[...u],o=a.splice(e,1)[0];"up"===t?a.splice(e-1,0,o):a.splice(e+1,0,o),p({headings:a})},J=e=>{const t=k.includes(e)?k.filter(t=>t!==e):[...k,e];p({headingLevels:t.sort()})};return(0,t.createElement)(r.Fragment,null,(0,t.createElement)(o.InspectorControls,null,(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Presentation","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ButtonGroup,null,(0,t.createElement)(l.Button,{isPrimary:!v,isPressed:!v,onClick:()=>p({isEditing:!1})},(0,a.__)("Viewing Mode","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.Button,{isPrimary:v,isPressed:v,onClick:()=>p({isEditing:!0})},(0,a.__)("Editing Mode","search-appearance-toolkit-seo-44"))),(0,t.createElement)("p",{className:"description"},(0,a.__)("Switch to Editing Mode to customize link text, visibility, and order.","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Appearance","search-appearance-toolkit-seo-44")},(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Layout","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.ButtonGroup,null,(0,t.createElement)(l.Button,{isPrimary:"vertical"===_,isPressed:"vertical"===_,onClick:()=>p({layout:"vertical"})},(0,a.__)("Vertical","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.Button,{isPrimary:"horizontal"===_,isPressed:"horizontal"===_,onClick:()=>p({layout:"horizontal"})},(0,a.__)("Horizontal","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Make Jump Links Area Expandable","search-appearance-toolkit-seo-44"),help:(0,a.__)('Conserve screen space by collapsing a long list of jump links, providing users with an elegant "show more" button to see the entire list.',"search-appearance-toolkit-seo-44"),checked:f,onChange:()=>p({isCollapsible:!f}),__nextHasNoMarginBottom:!0}),(0,t.createElement)(l.SelectControl,{label:(0,a.__)("List Style","search-appearance-toolkit-seo-44"),value:b,options:[{label:(0,a.__)("Bulleted","search-appearance-toolkit-seo-44"),value:"ul"},{label:(0,a.__)("Numbered","search-appearance-toolkit-seo-44"),value:"ol"},{label:(0,a.__)("None","search-appearance-toolkit-seo-44"),value:"none"}],onChange:e=>p({listStyle:e}),disabled:"horizontal"===_,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.FontSizePicker,{fontSizes:[{name:(0,a.__)("S","search-appearance-toolkit-seo-44"),slug:"small",size:"14px"},{name:(0,a.__)("M","search-appearance-toolkit-seo-44"),slug:"normal",size:"17px"},{name:(0,a.__)("L","search-appearance-toolkit-seo-44"),slug:"large",size:"20px"},{name:(0,a.__)("XL","search-appearance-toolkit-seo-44"),slug:"extra-large",size:"23px"}],value:w,onChange:e=>p({fontSize:e}),withReset:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(o.PanelColorSettings,{title:(0,a.__)("Colors","search-appearance-toolkit-seo-44"),colorSettings:[{value:y,onChange:e=>p({blockBackgroundColor:e}),label:(0,a.__)("Block Background","search-appearance-toolkit-seo-44")},{value:E,onChange:e=>p({linkColor:e}),label:(0,a.__)("Link Color","search-appearance-toolkit-seo-44")},{value:C,onChange:e=>p({textColor:e}),label:(0,a.__)("Other Text Color","search-appearance-toolkit-seo-44")}]}),"horizontal"===_&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)("hr",null),(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Horizontal Link Styles","search-appearance-toolkit-seo-44"))),(0,t.createElement)(o.PanelColorSettings,{title:(0,a.__)("Link Colors","search-appearance-toolkit-seo-44"),colorSettings:[{value:B,onChange:e=>p({linkBackgroundColor:e}),label:(0,a.__)("Background","search-appearance-toolkit-seo-44")},{value:S,onChange:e=>p({linkBackgroundColorHover:e}),label:(0,a.__)("Background Hover","search-appearance-toolkit-seo-44")},{value:z,onChange:e=>p({linkBorderColor:e}),label:(0,a.__)("Border","search-appearance-toolkit-seo-44")}]}),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Link Border Radius","search-appearance-toolkit-seo-44"),value:T,onChange:e=>p({linkBorderRadius:e}),min:0,max:50,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}))),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Content Settings","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Display Block Title","search-appearance-toolkit-seo-44"),checked:d,onChange:()=>p({showHeading:!d}),__nextHasNoMarginBottom:!0}),d&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)(l.TextControl,{label:(0,a.__)("Title Text","search-appearance-toolkit-seo-44"),value:m,onChange:e=>p({headingText:e}),help:(0,a.__)("The text that appears above your list of links.","search-appearance-toolkit-seo-44")}),(0,t.createElement)(l.SelectControl,{label:(0,a.__)("Title Tag","search-appearance-toolkit-seo-44"),value:g,options:[{label:"H2",value:"h2"},{label:"H3",value:"h3"},{label:"H4",value:"h4"},{label:"H5",value:"h5"},{label:"Paragraph (Bold)",value:"p"},{label:"Div (No Semantic Value)",value:"div"}],onChange:e=>p({headingTag:e}),help:(0,a.__)("Choose a level that fits your page's structure.","search-appearance-toolkit-seo-44"),__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0})),(0,t.createElement)("hr",null),(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Included Headings","search-appearance-toolkit-seo-44"))),(0,t.createElement)("p",{className:"description",style:{marginBottom:"10px"}},(0,a.__)("Select which heading levels from your post content should appear in the jump links list.","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.CheckboxControl,{label:"H2",checked:k.includes("h2"),onChange:()=>J("h2")}),(0,t.createElement)(l.CheckboxControl,{label:"H3",checked:k.includes("h3"),onChange:()=>J("h3")}),(0,t.createElement)(l.CheckboxControl,{label:"H4",checked:k.includes("h4"),onChange:()=>J("h4")}),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Create Visual Hierarchy","search-appearance-toolkit-seo-44"),help:(0,a.__)("Indents sub-headings (H3, H4) to create a nested outline structure.","search-appearance-toolkit-seo-44"),checked:x,onChange:()=>p({isSmartIndentation:!x}),__nextHasNoMarginBottom:!0})),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Position Settings","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Sticky Position","search-appearance-toolkit-seo-44"),help:(0,a.__)("Keep the table of contents visible while scrolling.","search-appearance-toolkit-seo-44"),checked:N,onChange:()=>p({isSticky:!N}),__nextHasNoMarginBottom:!0}),N&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)("p",{className:"description",style:{marginBottom:"15px"}},(0,a.__)("Customize how the block behaves when it sticks to the top of the screen.","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Top Offset (px)","search-appearance-toolkit-seo-44"),help:(0,a.__)("The distance between the top of the screen and the block when stuck (useful for clearing sticky headers).","search-appearance-toolkit-seo-44"),value:H,onChange:e=>p({stickyOffset:e}),min:0,max:200,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Jump Offset (px)","search-appearance-toolkit-seo-44"),help:(0,a.__)("The buffer distance to stop *before* the heading. Increase this if your sticky header covers the text.","search-appearance-toolkit-seo-44"),value:M,onChange:e=>p({jumpOffset:e}),min:0,max:200,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Disable on Mobile","search-appearance-toolkit-seo-44"),help:(0,a.__)("Prevents the block from sticking on small screens to save reading space.","search-appearance-toolkit-seo-44"),checked:"desktop-only"===P,onChange:e=>p({stickyStrategy:e?"desktop-only":"always"}),__nextHasNoMarginBottom:!0})))),(0,t.createElement)("div",{...$},d&&(0,t.createElement)(o.RichText,{tagName:g,className:"wp-block-seo44-jump-links-heading",value:m,onChange:e=>p({headingText:e}),placeholder:(0,a.__)("On This Page","search-appearance-toolkit-seo-44")}),u.length>0?(0,t.createElement)("nav",{"aria-label":(0,a.__)("Table of contents","search-appearance-toolkit-seo-44")},(0,t.createElement)(I,{id:j},u.map((e,o)=>v?(0,t.createElement)("li",{key:e.anchor},(0,t.createElement)(l.TextControl,{value:e.linkText,onChange:e=>((e,t)=>{const a=[...u];a[e].linkText=t,p({headings:a})})(o,e)}),(0,t.createElement)("div",{className:"edit-controls-wrapper"},(0,t.createElement)("div",{className:"reorder-buttons"},(0,t.createElement)(l.Button,{icon:i,label:(0,a.__)("Move Up","search-appearance-toolkit-seo-44"),onClick:()=>R(o,"up"),disabled:0===o}),(0,t.createElement)(l.Button,{icon:c,label:(0,a.__)("Move Down","search-appearance-toolkit-seo-44"),onClick:()=>R(o,"down"),disabled:o===u.length-1})),(0,t.createElement)(l.ToggleControl,{label:!1!==e.isVisible?(0,a.__)("Included","search-appearance-toolkit-seo-44"):(0,a.__)("This Jump Link will not be shown","search-appearance-toolkit-seo-44"),checked:!1!==e.isVisible,onChange:()=>(e=>{const t=[...u];t[e].isVisible=!t[e].isVisible,p({headings:t})})(o),__nextHasNoMarginBottom:!0}))):!1!==e.isVisible&&(0,t.createElement)("li",{key:e.anchor,className:x?`seo44-jump-link-level-${e.level}`:""},(0,t.createElement)("a",{href:`#${e.anchor}`,onClick:e=>e.preventDefault()},e.linkText)))),!v&&f&&u.length>0&&(0,t.createElement)(l.Tooltip,{text:(0,a.__)("This button is functional on the front-end to expand the list.","search-appearance-toolkit-seo-44")},(0,t.createElement)("button",{type:"button",className:"seo-44-show-more","aria-label":(0,a.__)("Show More","search-appearance-toolkit-seo-44"),"aria-expanded":"false","aria-controls":j,onClick:()=>{L((0,a.__)('The "Show More" button is interactive on the published page.',"search-appearance-toolkit-seo-44"),{type:"snackbar"})}},h))):(0,t.createElement)("p",null,(0,a.__)("No headings found. Select a heading level in the block settings to generate links.","search-appearance-toolkit-seo-44"))))},save:function({attributes:e}){const{blockInstanceId:l,layout:n,isCollapsible:r,isSmartIndentation:s,headings:i,showHeading:c,headingText:h,headingTag:p,listStyle:k,fontSize:u,textColor:d,linkColor:m,blockBackgroundColor:g,linkBackgroundColor:_,linkBackgroundColorHover:b,linkBorderColor:v,linkBorderRadius:f,isSticky:x,stickyOffset:w,jumpOffset:C,stickyStrategy:E}=e,y={color:d,fontSize:u,"--jump-link-font-size":u||"18px","--seo44-block-bg":g,"--seo44-link-color":m,"--seo44-link-bg":"horizontal"===n?_:void 0,"--seo44-link-hover-bg":"horizontal"===n?b:void 0,"--seo44-link-border-color":"horizontal"===n?v:void 0,"--seo44-link-radius":"horizontal"===n&&f?`${f}px`:void 0,"--seo44-sticky-offset":x?`${w}px`:void 0},B="ol"===k?"ol":"ul",S=`seo44-jump-links-list-${l}`,z=o.useBlockProps.save({className:`${"horizontal"===n?"is-layout-horizontal":""} ${r?"is-collapsible":""} ${"none"===k?"list-style-none":""} ${x?"is-sticky":""} ${"desktop-only"===E?"sticky-desktop-only":""}`.trim(),style:y,"data-seo44-jump-offset":x?C:30}),T=(0,t.createElement)("svg",{className:"arrow-down",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10l-6 6z"})),N=(0,t.createElement)("svg",{className:"arrow-up",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14l-6-6z"}));return(0,t.createElement)("div",{...z},(0,t.createElement)("div",{className:"seo44-sticky-sentinel","aria-hidden":"true"}),c&&(0,t.createElement)(o.RichText.Content,{tagName:p||"h2",className:"wp-block-seo44-jump-links-heading",value:h}),i&&i.length>0&&(0,t.createElement)("nav",{"aria-label":(0,a.__)("Table of contents","search-appearance-toolkit-seo-44")},(0,t.createElement)(B,{id:S},i.filter(e=>!1!==e.isVisible).map(e=>(0,t.createElement)("li",{key:e.anchor,className:s?`seo44-jump-link-level-${e.level}`:""},(0,t.createElement)("a",{href:`#${e.anchor}`},e.linkText)))),r&&(0,t.createElement)("button",{type:"button",className:"seo-44-show-more","aria-label":(0,a.__)("Show More","search-appearance-toolkit-seo-44"),"aria-expanded":"false","aria-controls":S},T,N)))}})}},a={};function o(e){var l=a[e];if(void 0!==l)return l.exports;var n=a[e]={exports:{}};return t[e](n,n.exports,o),n.exports}o.m=t,e=[],o.O=(t,a,l,n)=>{if(!a){var r=1/0;for(h=0;h<e.length;h++){for(var[a,l,n]=e[h],s=!0,i=0;i<a.length;i++)(!1&n||r>=n)&&Object.keys(o.O).every(e=>o.O[e](a[i]))?a.splice(i--,1):(s=!1,n<r&&(r=n));if(s){e.splice(h--,1);var c=l();void 0!==c&&(t=c)}}return t}n=n||0;for(var h=e.length;h>0&&e[h-1][2]>n;h--)e[h]=e[h-1];e[h]=[a,l,n]},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={57:0,350:0};o.O.j=t=>0===e[t];var t=(t,a)=>{var l,n,[r,s,i]=a,c=0;if(r.some(t=>0!==e[t])){for(l in s)o.o(s,l)&&(o.m[l]=s[l]);if(i)var h=i(o)}for(t&&t(a);c<r.length;c++)n=r[c],o.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return o.O(h)},a=globalThis.webpackChunkseo_44_jump_links_block=globalThis.webpackChunkseo_44_jump_links_block||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var l=o.O(void 0,[350],()=>o(313));l=o.O(l)})();1 (()=>{"use strict";var e,t={313(){const e=window.wp.blocks,t=window.React,a=window.wp.i18n,o=window.wp.blockEditor,l=window.wp.components,n=window.wp.data,r=window.wp.element;function s(e){return(new DOMParser).parseFromString(e,"text/html").body.textContent||""}const i=(0,t.createElement)("svg",{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M3 21v-2h18v2zm8-4v-6.175L9.4 12.4L8 11l4-4l4 4l-1.4 1.4l-1.6-1.575V17zM3 5V3h18v2z"})),c=(0,t.createElement)("svg",{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M3 5V3h18v2zm9 12l-4-4l1.4-1.4l1.6 1.575V7h2v6.175l1.6-1.575L16 13zm-9 4v-2h18v2z"})),h=(0,t.createElement)("svg",{className:"arrow-down",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10l-6 6z"})),p=JSON.parse('{"UU":"seo44/jump-links"}');(0,e.registerBlockType)(p.UU,{edit:function({attributes:e,setAttributes:p}){const{headingLevels:k,headings:u,showHeading:d,headingText:m,headingTag:g,layout:_,listStyle:b,isEditing:v,isCollapsible:f,isSmartIndentation:x,fontSize:w,textColor:C,linkColor:E,blockBackgroundColor:y,linkBackgroundColor:B,linkBackgroundColorHover:S,linkBorderColor:z,linkBorderRadius:T,isSticky:N,stickyOffset:H,jumpOffset:M,stickyStrategy:P}=e,O={color:C,fontSize:w,"--jump-link-font-size":w||"18px","--seo44-link-color":E,"--seo44-link-bg":"horizontal"===_?B:void 0,"--seo44-link-hover-bg":"horizontal"===_?S:void 0,"--seo44-link-border-color":"horizontal"===_?z:void 0,"--seo44-link-radius":"horizontal"===_&&T?`${T}px`:void 0,"--seo44-block-bg":y,"--seo44-sticky-offset":N?`${H}px`:void 0},I="ol"===b?"ol":"ul",{createInfoNotice:L}=(0,n.useDispatch)("core/notices"),j=e.blockInstanceId?`seo44-jump-links-list-${e.blockInstanceId}`:"seo44-jump-links-list",$=(0,o.useBlockProps)({style:O});$.className=`${$.className} ${"horizontal"===_?"is-layout-horizontal":""} ${f&&!v?"is-collapsible":""} ${"none"===b?"list-style-none":""}`.trim();const D=(0,n.useSelect)(e=>e("core/block-editor").getBlocks(),[]),{updateBlockAttributes:V}=(0,n.useDispatch)("core/block-editor");(0,r.useEffect)(()=>{const t={};e.blockInstanceId||(t.blockInstanceId=Math.random().toString(36).substr(2,9)),Object.keys(t).length>0&&p(t);const o=D.filter(e=>"core/heading"===e.name&&k.includes(`h${e.attributes.level}`)),l=new Set;let n=!1;const r=new Map(u.map(e=>[e.anchor,e])),i=[];for(const e of o){const t=s(e.attributes.content);let a=e.attributes.anchor||t.toLowerCase().replace(/[^a-z0-9\s-]/g,"").trim().replace(/\s+/g,"-"),o=a,c=2;for(;l.has(o);)o=`${a}-${c}`,c++,n=!0;l.add(o),e.attributes.anchor!==o&&V(e.clientId,{anchor:o});const h=r.get(e.attributes.anchor)||r.get(o),p=h&&h.linkText!==h.text?h.linkText:t,k=!h||h.isVisible;i.push({anchor:o,text:t,linkText:p,isVisible:k,level:e.attributes.level})}JSON.stringify(i)!==JSON.stringify(u)&&p({headings:i}),n&&L((0,a.__)("Jump Links Block: Duplicate headings were found. Unique IDs have been auto-generated, but this may be a sign of redundancy. Please review your headings for clarity.","search-appearance-toolkit-seo-44"),{type:"snackbar"})},[D,k,u,e.blockInstanceId,p,V,L]),(0,r.useEffect)(()=>{"horizontal"===_&&"none"!==b&&p({listStyle:"none"})},[_,b,p]);const R=(e,t)=>{const a=[...u],o=a.splice(e,1)[0];"up"===t?a.splice(e-1,0,o):a.splice(e+1,0,o),p({headings:a})},J=e=>{const t=k.includes(e)?k.filter(t=>t!==e):[...k,e];p({headingLevels:t.sort()})};return(0,t.createElement)(r.Fragment,null,(0,t.createElement)(o.InspectorControls,null,(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Presentation","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ButtonGroup,null,(0,t.createElement)(l.Button,{isPrimary:!v,isPressed:!v,onClick:()=>p({isEditing:!1})},(0,a.__)("Viewing Mode","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.Button,{isPrimary:v,isPressed:v,onClick:()=>p({isEditing:!0})},(0,a.__)("Editing Mode","search-appearance-toolkit-seo-44"))),(0,t.createElement)("p",{className:"description"},(0,a.__)("Switch to Editing Mode to customize link text, visibility, and order.","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Appearance","search-appearance-toolkit-seo-44")},(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Layout","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.ButtonGroup,null,(0,t.createElement)(l.Button,{isPrimary:"vertical"===_,isPressed:"vertical"===_,onClick:()=>p({layout:"vertical"})},(0,a.__)("Vertical","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.Button,{isPrimary:"horizontal"===_,isPressed:"horizontal"===_,onClick:()=>p({layout:"horizontal"})},(0,a.__)("Horizontal","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Make Jump Links Area Expandable","search-appearance-toolkit-seo-44"),help:(0,a.__)('Conserve screen space by collapsing a long list of jump links, providing users with an elegant "show more" button to see the entire list.',"search-appearance-toolkit-seo-44"),checked:f,onChange:()=>p({isCollapsible:!f}),__nextHasNoMarginBottom:!0}),(0,t.createElement)(l.SelectControl,{label:(0,a.__)("List Style","search-appearance-toolkit-seo-44"),value:b,options:[{label:(0,a.__)("Bulleted","search-appearance-toolkit-seo-44"),value:"ul"},{label:(0,a.__)("Numbered","search-appearance-toolkit-seo-44"),value:"ol"},{label:(0,a.__)("None","search-appearance-toolkit-seo-44"),value:"none"}],onChange:e=>p({listStyle:e}),disabled:"horizontal"===_,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.FontSizePicker,{fontSizes:[{name:(0,a.__)("S","search-appearance-toolkit-seo-44"),slug:"small",size:"14px"},{name:(0,a.__)("M","search-appearance-toolkit-seo-44"),slug:"normal",size:"17px"},{name:(0,a.__)("L","search-appearance-toolkit-seo-44"),slug:"large",size:"20px"},{name:(0,a.__)("XL","search-appearance-toolkit-seo-44"),slug:"extra-large",size:"23px"}],value:w,onChange:e=>p({fontSize:e}),withReset:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(o.PanelColorSettings,{title:(0,a.__)("Colors","search-appearance-toolkit-seo-44"),colorSettings:[{value:y,onChange:e=>p({blockBackgroundColor:e}),label:(0,a.__)("Block Background","search-appearance-toolkit-seo-44")},{value:E,onChange:e=>p({linkColor:e}),label:(0,a.__)("Link Color","search-appearance-toolkit-seo-44")},{value:C,onChange:e=>p({textColor:e}),label:(0,a.__)("Other Text Color","search-appearance-toolkit-seo-44")}]}),"horizontal"===_&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)("hr",null),(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Horizontal Link Styles","search-appearance-toolkit-seo-44"))),(0,t.createElement)(o.PanelColorSettings,{title:(0,a.__)("Link Colors","search-appearance-toolkit-seo-44"),colorSettings:[{value:B,onChange:e=>p({linkBackgroundColor:e}),label:(0,a.__)("Background","search-appearance-toolkit-seo-44")},{value:S,onChange:e=>p({linkBackgroundColorHover:e}),label:(0,a.__)("Background Hover","search-appearance-toolkit-seo-44")},{value:z,onChange:e=>p({linkBorderColor:e}),label:(0,a.__)("Border","search-appearance-toolkit-seo-44")}]}),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Link Border Radius","search-appearance-toolkit-seo-44"),value:T,onChange:e=>p({linkBorderRadius:e}),min:0,max:50,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}))),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Content Settings","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Display Block Title","search-appearance-toolkit-seo-44"),checked:d,onChange:()=>p({showHeading:!d}),__nextHasNoMarginBottom:!0}),d&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)(l.TextControl,{label:(0,a.__)("Title Text","search-appearance-toolkit-seo-44"),value:m,onChange:e=>p({headingText:e}),help:(0,a.__)("The text that appears above your list of links.","search-appearance-toolkit-seo-44")}),(0,t.createElement)(l.SelectControl,{label:(0,a.__)("Title Tag","search-appearance-toolkit-seo-44"),value:g,options:[{label:"H2",value:"h2"},{label:"H3",value:"h3"},{label:"H4",value:"h4"},{label:"H5",value:"h5"},{label:"Paragraph (Bold)",value:"p"},{label:"Div (No Semantic Value)",value:"div"}],onChange:e=>p({headingTag:e}),help:(0,a.__)("Choose a level that fits your page's structure.","search-appearance-toolkit-seo-44"),__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0})),(0,t.createElement)("hr",null),(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Included Headings","search-appearance-toolkit-seo-44"))),(0,t.createElement)("p",{className:"description",style:{marginBottom:"10px"}},(0,a.__)("Select which heading levels from your post content should appear in the jump links list.","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.CheckboxControl,{label:"H2",checked:k.includes("h2"),onChange:()=>J("h2")}),(0,t.createElement)(l.CheckboxControl,{label:"H3",checked:k.includes("h3"),onChange:()=>J("h3")}),(0,t.createElement)(l.CheckboxControl,{label:"H4",checked:k.includes("h4"),onChange:()=>J("h4")}),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Create Visual Hierarchy","search-appearance-toolkit-seo-44"),help:(0,a.__)("Indents sub-headings (H3, H4) to create a nested outline structure.","search-appearance-toolkit-seo-44"),checked:x,onChange:()=>p({isSmartIndentation:!x}),__nextHasNoMarginBottom:!0})),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Position Settings","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Sticky Position","search-appearance-toolkit-seo-44"),help:(0,a.__)("Keep the table of contents visible while scrolling.","search-appearance-toolkit-seo-44"),checked:N,onChange:()=>p({isSticky:!N}),__nextHasNoMarginBottom:!0}),N&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)("p",{className:"description",style:{marginBottom:"15px"}},(0,a.__)("Customize how the block behaves when it sticks to the top of the screen.","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Top Offset (px)","search-appearance-toolkit-seo-44"),help:(0,a.__)("The distance between the top of the screen and the block when stuck (useful for clearing sticky headers).","search-appearance-toolkit-seo-44"),value:H,onChange:e=>p({stickyOffset:e}),min:0,max:200,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Jump Offset (px)","search-appearance-toolkit-seo-44"),help:(0,a.__)("The buffer distance to stop *before* the heading. Increase this if your sticky header covers the text.","search-appearance-toolkit-seo-44"),value:M,onChange:e=>p({jumpOffset:e}),min:0,max:200,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Disable on Mobile","search-appearance-toolkit-seo-44"),help:(0,a.__)("Prevents the block from sticking on small screens to save reading space.","search-appearance-toolkit-seo-44"),checked:"desktop-only"===P,onChange:e=>p({stickyStrategy:e?"desktop-only":"always"}),__nextHasNoMarginBottom:!0})))),(0,t.createElement)("div",{...$},d&&(0,t.createElement)(o.RichText,{tagName:g,className:"wp-block-seo44-jump-links-heading",value:m,onChange:e=>p({headingText:e}),placeholder:(0,a.__)("On This Page","search-appearance-toolkit-seo-44")}),u.length>0?(0,t.createElement)("nav",{"aria-label":(0,a.__)("Table of contents","search-appearance-toolkit-seo-44")},(0,t.createElement)(I,{id:j},u.map((e,o)=>v?(0,t.createElement)("li",{key:e.anchor},(0,t.createElement)(l.TextControl,{value:e.linkText,onChange:e=>((e,t)=>{const a=[...u];a[e].linkText=t,p({headings:a})})(o,e)}),(0,t.createElement)("div",{className:"edit-controls-wrapper"},(0,t.createElement)("div",{className:"reorder-buttons"},(0,t.createElement)(l.Button,{icon:i,label:(0,a.__)("Move Up","search-appearance-toolkit-seo-44"),onClick:()=>R(o,"up"),disabled:0===o}),(0,t.createElement)(l.Button,{icon:c,label:(0,a.__)("Move Down","search-appearance-toolkit-seo-44"),onClick:()=>R(o,"down"),disabled:o===u.length-1})),(0,t.createElement)(l.ToggleControl,{label:!1!==e.isVisible?(0,a.__)("Included","search-appearance-toolkit-seo-44"):(0,a.__)("This Jump Link will not be shown","search-appearance-toolkit-seo-44"),checked:!1!==e.isVisible,onChange:()=>(e=>{const t=[...u];t[e].isVisible=!t[e].isVisible,p({headings:t})})(o),__nextHasNoMarginBottom:!0}))):!1!==e.isVisible&&(0,t.createElement)("li",{key:e.anchor,className:x?`seo44-jump-link-level-${e.level}`:""},(0,t.createElement)("a",{href:`#${e.anchor}`,onClick:e=>e.preventDefault()},e.linkText)))),!v&&f&&u.length>0&&(0,t.createElement)(l.Tooltip,{text:(0,a.__)("This button is functional on the front-end to expand the list.","search-appearance-toolkit-seo-44")},(0,t.createElement)("button",{type:"button",className:"seo-44-show-more","aria-label":(0,a.__)("Show More","search-appearance-toolkit-seo-44"),"aria-expanded":"false","aria-controls":j,onClick:()=>{L((0,a.__)('The "Show More" button is interactive on the published page.',"search-appearance-toolkit-seo-44"),{type:"snackbar"})}},h))):(0,t.createElement)("p",null,(0,a.__)("No headings found. Select a heading level in the block settings to generate links.","search-appearance-toolkit-seo-44"))))},save:function({attributes:e}){const{blockInstanceId:l,layout:n,isCollapsible:r,isSmartIndentation:s,headings:i,showHeading:c,headingText:h,headingTag:p,listStyle:k,fontSize:u,textColor:d,linkColor:m,blockBackgroundColor:g,linkBackgroundColor:_,linkBackgroundColorHover:b,linkBorderColor:v,linkBorderRadius:f,isSticky:x,stickyOffset:w,jumpOffset:C,stickyStrategy:E}=e,y={color:d,fontSize:u,"--jump-link-font-size":u||"18px","--seo44-block-bg":g,"--seo44-link-color":m,"--seo44-link-bg":"horizontal"===n?_:void 0,"--seo44-link-hover-bg":"horizontal"===n?b:void 0,"--seo44-link-border-color":"horizontal"===n?v:void 0,"--seo44-link-radius":"horizontal"===n&&f?`${f}px`:void 0,"--seo44-sticky-offset":x?`${w}px`:void 0},B="ol"===k?"ol":"ul",S=`seo44-jump-links-list-${l}`,z=o.useBlockProps.save({className:`${"horizontal"===n?"is-layout-horizontal":""} ${r?"is-collapsible":""} ${"none"===k?"list-style-none":""} ${x?"is-sticky":""} ${"desktop-only"===E?"sticky-desktop-only":""}`.trim(),style:y,"data-seo44-jump-offset":x?C:30}),T=(0,t.createElement)("svg",{className:"arrow-down",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10l-6 6z"})),N=(0,t.createElement)("svg",{className:"arrow-up",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14l-6-6z"}));return(0,t.createElement)("div",{...z},(0,t.createElement)("div",{className:"seo44-sticky-sentinel","aria-hidden":"true"}),c&&(0,t.createElement)(o.RichText.Content,{tagName:p||"h2",className:"wp-block-seo44-jump-links-heading",value:h}),i&&i.length>0&&(0,t.createElement)("nav",{"aria-label":(0,a.__)("Table of contents","search-appearance-toolkit-seo-44")},(0,t.createElement)(B,{id:S},i.filter(e=>!1!==e.isVisible).map(e=>(0,t.createElement)("li",{key:e.anchor,className:s?`seo44-jump-link-level-${e.level}`:""},(0,t.createElement)("a",{href:`#${e.anchor}`},e.linkText)))),r&&(0,t.createElement)("button",{type:"button",className:"seo-44-show-more","aria-label":(0,a.__)("Show More","search-appearance-toolkit-seo-44"),"aria-expanded":"false","aria-controls":S},T,N)))}})}},a={};function o(e){var l=a[e];if(void 0!==l)return l.exports;var n=a[e]={exports:{}};return t[e](n,n.exports,o),n.exports}o.m=t,e=[],o.O=(t,a,l,n)=>{if(!a){var r=1/0;for(h=0;h<e.length;h++){for(var[a,l,n]=e[h],s=!0,i=0;i<a.length;i++)(!1&n||r>=n)&&Object.keys(o.O).every(e=>o.O[e](a[i]))?a.splice(i--,1):(s=!1,n<r&&(r=n));if(s){e.splice(h--,1);var c=l();void 0!==c&&(t=c)}}return t}n=n||0;for(var h=e.length;h>0&&e[h-1][2]>n;h--)e[h]=e[h-1];e[h]=[a,l,n]},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={57:0,350:0};o.O.j=t=>0===e[t];var t=(t,a)=>{var l,n,[r,s,i]=a,c=0;if(r.some(t=>0!==e[t])){for(l in s)o.o(s,l)&&(o.m[l]=s[l]);if(i)var h=i(o)}for(t&&t(a);c<r.length;c++)n=r[c],o.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return o.O(h)},a=globalThis.webpackChunkseo_44_jump_links_block=globalThis.webpackChunkseo_44_jump_links_block||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var l=o.O(void 0,[350],()=>o(313));l=o.O(l)})(); -
search-appearance-toolkit-seo-44/tags/4.3/changelog.txt
r3405454 r3423355 1 1 == Changelog == 2 3 = 4.3.0 = 4 * FEATURE: **Table of Contents Schema:** Automatically generates `hasPart` schema synchronized with the Jump Links Block to support "Jump-to" links in search results. 5 * FEATURE: **Advanced HowTo Schema:** A new "Block-Aware" scanner uses the Jump Links Block to strictly define steps while intelligently mining the introduction for tools, time, yield, and video data. 6 * TWEAK: Added a "Generate HowTo Schema" checkbox to the SEO 44 metabox, giving users explicit control over when the advanced scanner runs. 7 * FEATURE: **YouTube Integration:** Added support for the YouTube Data API v3 to fetch accurate video upload dates for VideoObject schema. 8 * TESTED: Tested to WordPress Version 6.9 2 9 3 10 = 4.2.0 = -
search-appearance-toolkit-seo-44/tags/4.3/includes/class-seo44-core.php
r3403604 r3423355 1 1 <?php 2 //4.3 - Added dependencies for block-editor-script.js to access Jump Links Block anchor ids for hasPart and HowTo schema. 2 3 class SEO44_Core { 3 4 … … 62 63 public function admin_enqueue_assets($hook) { 63 64 if ('post.php' == $hook || 'post-new.php' == $hook) { 65 // CSS 64 66 wp_enqueue_style('seo44-admin-styles', plugins_url('../css/admin-styles.css', __FILE__), [], SEO44_VERSION); 65 wp_enqueue_script('seo44-admin-script', plugins_url('../js/admin-script.js', __FILE__), ['jquery'], SEO44_VERSION, true); 67 // General Admin Script (Classic + Block) 68 wp_enqueue_script('seo44-admin-script', plugins_url('../js/admin-script.js', __FILE__), 69 ['jquery'], 70 SEO44_VERSION, 71 true); 72 73 // Localize the General Script (needed for Snippet Preview) 66 74 wp_localize_script('seo44-admin-script', 'seo44_data', [ 67 75 'post_title' => get_the_title(get_the_ID()), … … 69 77 'permalink' => get_permalink(get_the_ID()) 70 78 ]); 79 // Block Editor Specific Script (Passthrough Logic For Jump Links) 80 // We enqueue this separately with the Block Editor dependencies. 81 // WordPress will simply NOT load this file if these handles don't exist (e.g. Classic Editor). 82 wp_enqueue_script('seo44-block-editor-script', plugins_url('../js/block-editor-script.js', __FILE__), 83 ['wp-data', 'wp-editor', 'wp-blocks'], // Dependencies are important for Jump Links Block data 84 SEO44_VERSION, 85 true 86 ); 71 87 } 72 88 if ('settings_page_search-appearance-toolkit-seo-44' == $hook) { -
search-appearance-toolkit-seo-44/tags/4.3/includes/class-seo44-frontend.php
r3405454 r3423355 1 1 <?php 2 // Version 4.3 3 // Create hasPart and HowTo schema from Jump Links Block using metafield passthrough 4 // Added YouTube Data API support for more accurate video upload date 5 2 6 class SEO44_Frontend { 3 7 public function __construct() { … … 5 9 add_action('wp_head', [$this, 'output_header_tags']); 6 10 add_action('wp_head', [$this, 'output_schema_json_ld'], 99); 11 add_action('init', [$this, 'register_schema_meta_fields']); // Retrieve Jump Links Block data for schema via a hidden metabox field 7 12 8 13 $taxonomies = get_taxonomies(['public' => true]); … … 12 17 } 13 18 } 14 19 // Get meta field from metabox for Jump Links Block + HowTo Schema 20 public function register_schema_meta_fields() { 21 register_post_meta('post', '_seo44_howto_step_ids', [ 22 'single' => true, 23 'type' => 'array', 24 'show_in_rest' => [ 25 'schema' => [ 26 'type' => 'array', 27 'items' => [ 28 'type' => 'string', 29 ], 30 ], 31 ], 32 'auth_callback' => function() { return current_user_can('edit_posts'); } 33 ]); 34 } 35 36 //Title 15 37 public function filter_document_title($title_parts) { 16 if (!seo44_get_option('enable_tags')) return $title_parts;17 38 if (!seo44_get_option('enable_tags')) return $title_parts; 39 18 40 $custom_title = ''; 19 41 $fallback_title = ''; … … 23 45 $custom_title = seo44_get_option('homepage_title'); 24 46 } elseif (is_singular()) { 25 $custom_title = get_post_meta(get_the_ID(), seo44_get_option('title_key'), true);47 $custom_title = get_post_meta(get_the_ID(), seo44_get_option('title_key'), true); 26 48 $fallback_title = get_the_title(get_the_ID()); 27 } elseif (is_category() || is_tag() || is_tax()) {49 } elseif (is_category() || is_tag() || is_tax()) { 28 50 $term_id = get_queried_object_id(); 29 51 $custom_title = get_term_meta($term_id, 'seo44_title', true); … … 52 74 unset($title_parts['site'], $title_parts['tagline']); 53 75 } 54 return $title_parts; 55 } 56 57 public function add_term_meta_fields($term, $taxonomy) { 76 return $title_parts; 77 } 78 79 // Meta Tags 80 public function add_term_meta_fields($term, $taxonomy) { 58 81 $title = get_term_meta($term->term_id, 'seo44_title', true); 59 82 $description = get_term_meta($term->term_id, 'seo44_description', true); … … 95 118 } 96 119 97 public function output_header_tags() {98 if (!seo44_get_option('enable_tags')) { return; }99 $description = '';100 $social_title = '';101 $current_url = '';102 103 if (is_front_page()) {104 $description = seo44_get_option('homepage_description');105 $social_title = seo44_get_option('homepage_title') ?: get_bloginfo('name');106 $current_url = home_url('/');107 } elseif (is_singular()) {108 $post_id = get_the_ID();109 $description = get_post_meta($post_id, seo44_get_option('description_key'), true);110 $custom_title = get_post_meta($post_id, seo44_get_option('title_key'), true);111 $social_title = $custom_title ?: get_the_title($post_id);112 $current_url = get_permalink($post_id);120 public function output_header_tags() { 121 if (!seo44_get_option('enable_tags')) { return; } 122 $description = ''; 123 $social_title = ''; 124 $current_url = ''; 125 126 if (is_front_page()) { 127 $description = seo44_get_option('homepage_description'); 128 $social_title = seo44_get_option('homepage_title') ?: get_bloginfo('name'); 129 $current_url = home_url('/'); 130 } elseif (is_singular()) { 131 $post_id = get_the_ID(); 132 $description = get_post_meta($post_id, seo44_get_option('description_key'), true); 133 $custom_title = get_post_meta($post_id, seo44_get_option('title_key'), true); 134 $social_title = $custom_title ?: get_the_title($post_id); 135 $current_url = get_permalink($post_id); 113 136 if (empty($description)) { 114 $post_content = get_the_content(null, false, get_the_ID());137 $post_content = get_the_content(null, false, get_the_ID()); 115 138 $description = wp_trim_words(strip_shortcodes(wp_strip_all_tags($post_content)), 25, '...'); 116 }117 118 } elseif (is_category() || is_tag() || is_tax()) {119 $term = get_queried_object();120 $description = get_term_meta($term->term_id, 'seo44_description', true);121 $custom_title = get_term_meta($term->term_id, 'seo44_title', true);122 $social_title = $custom_title ?: single_term_title('', false) . ' - ' . get_bloginfo('name');123 $current_url = get_term_link($term);139 } 140 141 } elseif (is_category() || is_tag() || is_tax()) { 142 $term = get_queried_object(); 143 $description = get_term_meta($term->term_id, 'seo44_description', true); 144 $custom_title = get_term_meta($term->term_id, 'seo44_title', true); 145 $social_title = $custom_title ?: single_term_title('', false) . ' - ' . get_bloginfo('name'); 146 $current_url = get_term_link($term); 124 147 if (empty($description)) { 125 148 $term_description = trim(wp_strip_all_tags($term->description)); … … 130 153 } 131 154 } 132 } 133 134 if (!empty($description)) { 135 printf('<meta name="description" content="%s">' . "\n", esc_attr(wp_strip_all_tags(html_entity_decode($description, ENT_QUOTES, 'UTF-8')))); 136 } 137 138 if (is_singular()) { 139 $post_id = get_the_ID(); 140 $keywords = get_post_meta($post_id, seo44_get_option('keywords_key'), true); 141 if (seo44_get_option('include_keywords') && !empty($keywords)) { 142 printf('<meta name="keywords" content="%s">' . "\n", esc_attr(wp_strip_all_tags(html_entity_decode($keywords, ENT_QUOTES, 'UTF-8')))); 143 } 144 if (is_singular() && seo44_get_option('include_author')) { 145 $author_id = get_post_field('post_author', $post_id); 146 if ($author_id) { 147 printf('<meta name="author" content="%s">' . "\n", esc_attr($this->get_author_name($author_id))); 148 } 149 } 150 } 151 152 if (seo44_get_option('enable_og_tags') || seo44_get_option('enable_twitter_tags')) { 153 $clean_social_title = esc_attr(wp_strip_all_tags(html_entity_decode($social_title, ENT_QUOTES, 'UTF-8'))); 154 $clean_description = !empty($description) ? esc_attr(wp_strip_all_tags(html_entity_decode($description, ENT_QUOTES, 'UTF-8'))) : ''; 155 156 if (empty($clean_description) && is_singular()) { 157 $clean_description = esc_attr(wp_strip_all_tags(get_the_excerpt(get_the_ID()))); 158 } 159 160 $image_url = ''; $image_width = ''; $image_height = ''; 161 $image_id = 0; 162 if (is_singular() && has_post_thumbnail()) { 163 $image_id = get_post_thumbnail_id(); 164 } 165 if (!$image_id) { 166 $image_id = seo44_get_option('default_social_image_id', 0); 167 } 168 if ($image_id) { 169 $image_data = wp_get_attachment_image_src($image_id, 'full'); 170 if ($image_data) { 171 list($image_url, $image_width, $image_height) = $image_data; 172 } 173 } 174 175 if (seo44_get_option('enable_og_tags')) { 176 printf('<meta property="og:title" content="%s">' . "\n", esc_attr($clean_social_title)); 177 printf('<meta property="og:url" content="%s">' . "\n", esc_url($current_url)); 178 printf('<meta property="og:site_name" content="%s">' . "\n", esc_attr(get_bloginfo('name'))); 179 if (!empty($clean_description)) { printf('<meta property="og:description" content="%s">' . "\n", esc_attr($clean_description)); } 180 if (is_singular('post')) { 181 echo '<meta property="og:type" content="article">' . "\n"; 182 } else { 183 echo '<meta property="og:type" content="website">' . "\n"; 184 } 185 if (!empty($image_url)) { 186 printf('<meta property="og:image" content="%s">' . "\n", esc_url($image_url)); 187 if (!empty($image_width)) { printf('<meta property="og:image:width" content="%s">' . "\n", esc_attr($image_width)); } 188 if (!empty($image_height)) { printf('<meta property="og:image:height" content="%s">' . "\n", esc_attr($image_height)); } 189 } 190 $fb_app_id = seo44_get_option('fb_app_id'); 191 if (!empty($fb_app_id)) { printf('<meta property="fb:app_id" content="%s">' . "\n", esc_attr($fb_app_id)); } 192 } 193 194 if (seo44_get_option('enable_twitter_tags')) { 195 echo '<meta name="twitter:card" content="summary_large_image">' . "\n"; 196 $twitter_handle = seo44_get_option('twitter_handle'); 197 if ( !empty($twitter_handle) && is_string($twitter_handle) ) { 198 $handle = esc_attr(str_replace('@', '', $twitter_handle)); 199 printf('<meta name="twitter:site" content="@%s">' . "\n", esc_attr($handle)); 200 printf('<meta name="twitter:creator" content="@%s">' . "\n", esc_attr($handle)); 201 } 202 printf('<meta name="twitter:title" content="%s">' . "\n", esc_attr($clean_social_title)); 203 if (!empty($clean_description)) { printf('<meta name="twitter:description" content="%s">' . "\n", esc_attr($clean_description)); } 204 if (!empty($image_url)) { printf('<meta name="twitter:image" content="%s">' . "\n", esc_url($image_url)); } 205 } 206 } 207 } 208 155 } 156 157 if (!empty($description)) { 158 printf('<meta name="description" content="%s">' . "\n", esc_attr(wp_strip_all_tags(html_entity_decode($description, ENT_QUOTES, 'UTF-8')))); 159 } 160 161 if (is_singular()) { 162 $post_id = get_the_ID(); 163 $keywords = get_post_meta($post_id, seo44_get_option('keywords_key'), true); 164 if (seo44_get_option('include_keywords') && !empty($keywords)) { 165 printf('<meta name="keywords" content="%s">' . "\n", esc_attr(wp_strip_all_tags(html_entity_decode($keywords, ENT_QUOTES, 'UTF-8')))); 166 } 167 if (is_singular() && seo44_get_option('include_author')) { 168 $author_id = get_post_field('post_author', $post_id); 169 if ($author_id) { 170 printf('<meta name="author" content="%s">' . "\n", esc_attr($this->get_author_name($author_id))); 171 } 172 } 173 } 174 175 if (seo44_get_option('enable_og_tags') || seo44_get_option('enable_twitter_tags')) { 176 $clean_social_title = esc_attr(wp_strip_all_tags(html_entity_decode($social_title, ENT_QUOTES, 'UTF-8'))); 177 $clean_description = !empty($description) ? esc_attr(wp_strip_all_tags(html_entity_decode($description, ENT_QUOTES, 'UTF-8'))) : ''; 178 179 if (empty($clean_description) && is_singular()) { 180 $clean_description = esc_attr(wp_strip_all_tags(get_the_excerpt(get_the_ID()))); 181 } 182 183 $image_url = ''; $image_width = ''; $image_height = ''; 184 $image_id = 0; 185 if (is_singular() && has_post_thumbnail()) { 186 $image_id = get_post_thumbnail_id(); 187 } 188 if (!$image_id) { 189 $image_id = seo44_get_option('default_social_image_id', 0); 190 } 191 if ($image_id) { 192 $image_data = wp_get_attachment_image_src($image_id, 'full'); 193 if ($image_data) { 194 list($image_url, $image_width, $image_height) = $image_data; 195 } 196 } 197 198 if (seo44_get_option('enable_og_tags')) { 199 printf('<meta property="og:title" content="%s">' . "\n", esc_attr($clean_social_title)); 200 printf('<meta property="og:url" content="%s">' . "\n", esc_url($current_url)); 201 printf('<meta property="og:site_name" content="%s">' . "\n", esc_attr(get_bloginfo('name'))); 202 if (!empty($clean_description)) { printf('<meta property="og:description" content="%s">' . "\n", esc_attr($clean_description)); } 203 if (is_singular('post')) { 204 echo '<meta property="og:type" content="article">' . "\n"; 205 } else { 206 echo '<meta property="og:type" content="website">' . "\n"; 207 } 208 if (!empty($image_url)) { 209 printf('<meta property="og:image" content="%s">' . "\n", esc_url($image_url)); 210 if (!empty($image_width)) { printf('<meta property="og:image:width" content="%s">' . "\n", esc_attr($image_width)); } 211 if (!empty($image_height)) { printf('<meta property="og:image:height" content="%s">' . "\n", esc_attr($image_height)); } 212 } 213 $fb_app_id = seo44_get_option('fb_app_id'); 214 if (!empty($fb_app_id)) { printf('<meta property="fb:app_id" content="%s">' . "\n", esc_attr($fb_app_id)); } 215 } 216 217 if (seo44_get_option('enable_twitter_tags')) { 218 echo '<meta name="twitter:card" content="summary_large_image">' . "\n"; 219 $twitter_handle = seo44_get_option('twitter_handle'); 220 if ( !empty($twitter_handle) && is_string($twitter_handle) ) { 221 $handle = esc_attr(str_replace('@', '', $twitter_handle)); 222 printf('<meta name="twitter:site" content="@%s">' . "\n", esc_attr($handle)); 223 printf('<meta name="twitter:creator" content="@%s">' . "\n", esc_attr($handle)); 224 } 225 printf('<meta name="twitter:title" content="%s">' . "\n", esc_attr($clean_social_title)); 226 if (!empty($clean_description)) { printf('<meta name="twitter:description" content="%s">' . "\n", esc_attr($clean_description)); } 227 if (!empty($image_url)) { printf('<meta name="twitter:image" content="%s">' . "\n", esc_url($image_url)); } 228 } 229 } 230 } 231 232 // Now For The Schema Structured Data 209 233 public function output_schema_json_ld() { 210 234 // Note: The following line is intentionally not nonce-checked. 211 // This is a safe, read-only check used by the admin-side scanner to get a clean view of the page212 // without our own schema being output. It does not process or save any data.213 235 $scan_param = isset($_GET['seo44_scan']) ? sanitize_key(wp_unslash($_GET['seo44_scan'])) : ''; 214 236 if ($scan_param === 'true') { return; } … … 218 240 $base_schema = []; 219 241 $special_schemas = []; 220 $breadcrumb_schema = []; // New variable for breadcrumbs242 $breadcrumb_schema = []; // New variable for breadcrumbs 221 243 $post_id = get_the_ID(); 222 244 … … 225 247 $base_schema = $this->get_schema_for_website(); 226 248 227 // NEW:Add Organization Schema to homepage228 if (seo44_get_option('enable_organization_schema')) {229 $org_schema = $this->get_schema_for_organization();230 if (!empty($org_schema)) {231 $special_schemas[] = $org_schema;232 }233 }234 249 // Add Organization Schema to homepage 250 if (seo44_get_option('enable_organization_schema')) { 251 $org_schema = $this->get_schema_for_organization(); 252 if (!empty($org_schema)) { 253 $special_schemas[] = $org_schema; 254 } 255 } 256 235 257 } elseif ( (is_category() || is_tag() || is_tax()) && seo44_get_option('enable_schema_on_taxonomies') ) { 236 258 $base_schema = $this->get_schema_for_taxonomy(); … … 246 268 } 247 269 248 // 3. Attempt to generate special schema 249 if (is_singular() && seo44_get_option('enable_advanced_schema')) { 250 $detected_schemas = $this->detect_and_generate_special_schema($post_id); 251 // Merge instead of overwrite - important for organization schema 252 if (!empty($detected_schemas)) { 253 $special_schemas = array_merge($special_schemas, $detected_schemas); 254 } 255 } 256 257 258 // 4. Hook for Add-ons --- 259 // This filter allows other plugins to add their own custom schema parts to the graph. 260 $addon_schema_parts = apply_filters( 'seo44_add_schema_parts', [], $post_id ); 261 // --- End Hook --- 262 263 // 5. Combine all schemas for output 264 $final_schema_parts = []; 270 // 3. Attempt to generate special schema (FAQ / HowTo) 271 if (is_singular() && seo44_get_option('enable_advanced_schema')) { 272 $detected_schemas = $this->detect_and_generate_special_schema($post_id); 273 // Merge instead of overwrite (Prevents erasing Organization schema on static pages) 274 if (!empty($detected_schemas)) { 275 $special_schemas = array_merge($special_schemas, $detected_schemas); 276 } 277 } 278 279 // 4. Hook for Add-ons --- 280 // This filter allows other plugins to add their own custom schema parts to the graph. 281 $addon_schema_parts = apply_filters( 'seo44_add_schema_parts', [], $post_id ); 282 // --- End Hook --- 283 284 // 5. Combine all schemas for output 285 $final_schema_parts = []; 265 286 if (!empty($base_schema)) { 266 287 $final_schema_parts[] = $base_schema; … … 272 293 $final_schema_parts = array_merge($final_schema_parts, $special_schemas); 273 294 } 274 275 // include any add-on schemas that pass is_array sanity check276 if ( ! empty( $addon_schema_parts ) && is_array( $addon_schema_parts ) ) {277 $final_schema_parts = array_merge( $final_schema_parts, $addon_schema_parts );278 }295 296 // include any add-on schemas that pass is_array sanity check 297 if ( ! empty( $addon_schema_parts ) && is_array( $addon_schema_parts ) ) { 298 $final_schema_parts = array_merge( $final_schema_parts, $addon_schema_parts ); 299 } 279 300 280 301 $final_schema = []; … … 301 322 } 302 323 } 303 304 // --- Helper Functions ---324 325 // --- Helper Functions --- 305 326 306 327 // --- Generate BreadcrumbList Schema for Singular Content --- … … 363 384 ]; 364 385 } 365 386 366 387 // --- Helper Function For Types --- 367 388 public static function get_schema_for_post($post_id) { 368 global $post; 369 $post = get_post($post_id); 370 setup_postdata($post); 371 372 // Get the author's ID and website URL to refer authorship 373 $author_id = $post->post_author; 374 $author_url = get_the_author_meta('user_url', $author_id); 375 376 // Build the author schema array 377 $author_schema = [ 378 '@type' => 'Person', 379 'name' => self::get_author_name_static( $author_id ), // Pass the author ID 380 ]; 381 382 // If the author has a website, add the @id and url properties 383 if ( ! empty( $author_url ) ) { 384 // Ensure the URL has a trailing slash before adding the fragment. 385 $canonical_author_url = trailingslashit( $author_url ); 386 387 $author_schema['@id'] = $canonical_author_url . '#person'; 388 $author_schema['url'] = $canonical_author_url; 389 } 390 391 $schema = [ 392 '@context' => 'https://schema.org', 393 '@type' => 'Article', 394 'mainEntityOfPage' => ['@type' => 'WebPage', '@id' => get_permalink($post_id)], 395 'headline' => get_the_title($post_id), 396 'datePublished' => get_the_date('c', $post_id), 397 'dateModified' => get_the_modified_date('c', $post_id), 398 'author' => $author_schema, // Use the author schema array 399 'publisher' => ['@type' => 'Organization', 'name' => get_bloginfo('name')] 400 ]; 401 if (get_site_icon_url()) { $schema['publisher']['logo'] = ['@type' => 'ImageObject', 'url' => get_site_icon_url()]; } 402 if (has_post_thumbnail($post_id)) { 403 $image_id = get_post_thumbnail_id($post_id); 404 $image_data = wp_get_attachment_image_src($image_id, 'full'); 405 if ($image_data) { 406 $schema['image'] = [ 407 '@type' => 'ImageObject', 408 'url' => $image_data[0], 409 'width' => $image_data[1], 410 'height' => $image_data[2] 411 ]; 412 } 413 } 414 // NEW: Parse content for additional media if the setting is enabled 389 global $post; 390 $post = get_post($post_id); 391 setup_postdata($post); 392 393 // Get the author's ID and website URL to refer authorship 394 $author_id = $post->post_author; 395 $author_url = get_the_author_meta('user_url', $author_id); 396 397 // Build the author schema array 398 $author_schema = [ 399 '@type' => 'Person', 400 'name' => self::get_author_name_static( $author_id ), // Pass the author ID 401 ]; 402 403 // If the author has a website, add the @id and url properties 404 if ( ! empty( $author_url ) ) { 405 // Ensure the URL has a trailing slash before adding the fragment. 406 $canonical_author_url = trailingslashit( $author_url ); 407 408 $author_schema['@id'] = $canonical_author_url . '#person'; 409 $author_schema['url'] = $canonical_author_url; 410 } 411 412 $schema = [ 413 '@context' => 'https://schema.org', 414 '@type' => 'Article', 415 'mainEntityOfPage' => ['@type' => 'WebPage', '@id' => get_permalink($post_id)], 416 'headline' => get_the_title($post_id), 417 'datePublished' => get_the_date('c', $post_id), 418 'dateModified' => get_the_modified_date('c', $post_id), 419 'author' => $author_schema, // Use the author schema array 420 'publisher' => ['@type' => 'Organization', 'name' => get_bloginfo('name')] 421 ]; 422 if (get_site_icon_url()) { $schema['publisher']['logo'] = ['@type' => 'ImageObject', 'url' => get_site_icon_url()]; } 423 424 // FIX: Initialize image key safely here to prevent "Undefined array key" warning 425 $schema['image'] = []; // Default to empty array 426 if (has_post_thumbnail($post_id)) { 427 $image_id = get_post_thumbnail_id($post_id); 428 $image_data = wp_get_attachment_image_src($image_id, 'full'); 429 if ($image_data) { 430 $schema['image'][] = [ 431 '@type' => 'ImageObject', 432 'url' => $image_data[0], 433 'width' => $image_data[1], 434 'height' => $image_data[2] 435 ]; 436 } 437 } 438 // NEW: Parse content for additional media if the setting is enabled 415 439 if (seo44_get_option('scan_content_for_schema')) { 416 440 $media_schema = self::parse_content_for_media_schema($post_id); 417 if (!empty($media_schema['images'])) { 418 // Check if a featured image (or other image) already exists 419 $existing_images = isset($schema['image']) ? (array)$schema['image'] : []; 420 421 // Merge existing images with content images 422 $schema['image'] = array_merge($existing_images, $media_schema['images']); 423 } 441 if (!empty($media_schema['images'])) { 442 // 1. Re-index to prevent "0": {...} keys 443 $schema['image'] = array_merge($schema['image'], $media_schema['images']); 444 } 424 445 if (!empty($media_schema['videos'])) { 425 446 $schema['video'] = $media_schema['videos']; 426 447 } 427 448 } 428 429 $description = get_post_meta($post_id, seo44_get_option('description_key'), true); 430 if (!empty($description)) { $schema['description'] = esc_html(wp_strip_all_tags($description)); } 431 $excerpt = get_the_excerpt($post_id); 432 if (!empty($excerpt)) { $schema['abstract'] = esc_html(wp_strip_all_tags($excerpt)); } 433 $content = get_the_content(null, false, $post_id); 434 $schema['wordCount'] = str_word_count(wp_strip_all_tags($content)); 435 436 $category = get_the_category($post_id); 437 if (!empty($category) && $category[0]->name !== 'Uncategorized') { 438 $schema['articleSection'] = esc_html($category[0]->name); 439 } 440 441 $tags = get_the_tags($post_id); 442 if ($tags) { 443 $keywords = []; 444 foreach ($tags as $tag) { $keywords[] = $tag->name; } 445 $schema['keywords'] = esc_html(implode(', ', $keywords)); 446 } 447 448 wp_reset_postdata(); 449 return $schema; 449 450 // 2. Ensure clean array (re-index) 451 if (!empty($schema['image'])) { 452 $schema['image'] = array_values($schema['image']); 453 // If only one image, Google prefers object over array, but array is valid. 454 // To match your request, we keep it as array_values to remove numeric keys. 455 } else { 456 unset($schema['image']); 457 } 458 459 $description = get_post_meta($post_id, seo44_get_option('description_key'), true); 460 if (!empty($description)) { $schema['description'] = esc_html(wp_strip_all_tags($description)); } 461 $excerpt = get_the_excerpt($post_id); 462 if (!empty($excerpt)) { $schema['abstract'] = esc_html(wp_strip_all_tags($excerpt)); } 463 $content = get_the_content(null, false, $post_id); 464 $schema['wordCount'] = str_word_count(wp_strip_all_tags($content)); 465 466 $category = get_the_category($post_id); 467 if (!empty($category) && $category[0]->name !== 'Uncategorized') { 468 $schema['articleSection'] = esc_html($category[0]->name); 469 } 470 471 $tags = get_the_tags($post_id); 472 if ($tags) { 473 $keywords = []; 474 foreach ($tags as $tag) { $keywords[] = $tag->name; } 475 $schema['keywords'] = esc_html(implode(', ', $keywords)); 476 } 477 478 // NEW: Dynamic Table of Contents (hasPart) 479 // Only run this if we are scanning content, to save performance 480 if (seo44_get_option('enable_jumplinks_schema')) { 481 $toc_parts = self::generate_has_part_schema($content, $post->ID); 482 if (!empty($toc_parts)) { 483 $schema['hasPart'] = $toc_parts; 484 } 485 } 486 wp_reset_postdata(); 487 return $schema; 450 488 } 451 489 452 490 public function get_schema_for_page($post_id) { 453 $schema = [ 454 '@context' => 'https://schema.org', 455 '@type' => 'WebPage', 456 'url' => get_permalink($post_id), 457 'headline' => get_the_title($post_id), 458 'datePublished' => get_the_date('c', $post_id), 459 'dateModified' => get_the_modified_date('c', $post_id), 460 ]; 461 462 // Add the meta description 463 $description = get_post_meta($post_id, seo44_get_option('description_key'), true); 464 if (!empty($description)) { 465 $schema['description'] = esc_html(wp_strip_all_tags($description)); 466 } 467 468 // Add the featured image as a detailed ImageObject 469 if (has_post_thumbnail($post_id)) { 470 $image_id = get_post_thumbnail_id($post_id); 471 $image_data = wp_get_attachment_image_src($image_id, 'full'); 472 if ($image_data) { 473 $schema['primaryImageOfPage'] = [ 474 '@type' => 'ImageObject', 475 'url' => $image_data[0], 476 'width' => $image_data[1], 477 'height' => $image_data[2] 478 ]; 479 } 480 } 481 // Parse content for additional media if the setting is enabled 482 // NEW CODE for get_schema_for_page 483 if (seo44_get_option('scan_content_for_schema')) { 484 $media_schema = self::parse_content_for_media_schema($post_id); 485 486 if (!empty($media_schema['images'])) { 487 // 1. Start with the content images found 488 $all_images = $media_schema['images']; 489 490 // 2. If a Featured Image (primaryImageOfPage) exists, add it to the start of the list 491 if (isset($schema['primaryImageOfPage'])) { 492 array_unshift($all_images, $schema['primaryImageOfPage']); 493 } 494 495 // 3. Set the 'image' property to the complete list 496 $schema['image'] = $all_images; 497 498 // Optional: You can choose to keep or unset primaryImageOfPage. 499 // Keeping it is usually better for SEO so Google knows which one is "Main". 500 // This would be the code to cleanup the unset: 501 // unset($schema['primaryImageOfPage']); 502 } 503 504 if (!empty($media_schema['videos'])) { 505 $schema['video'] = $media_schema['videos']; 506 } 507 } 508 509 return $schema; 491 $schema = [ 492 '@context' => 'https://schema.org', 493 '@type' => 'WebPage', 494 'url' => get_permalink($post_id), 495 'headline' => get_the_title($post_id), 496 'datePublished' => get_the_date('c', $post_id), 497 'dateModified' => get_the_modified_date('c', $post_id), 498 ]; 499 500 // Add the meta description 501 $description = get_post_meta($post_id, seo44_get_option('description_key'), true); 502 if (!empty($description)) { 503 $schema['description'] = esc_html(wp_strip_all_tags($description)); 504 } 505 506 // Add the featured image as a detailed ImageObject 507 $schema['image'] = []; 508 if (has_post_thumbnail($post_id)) { 509 $image_id = get_post_thumbnail_id($post_id); 510 $image_data = wp_get_attachment_image_src($image_id, 'full'); 511 if ($image_data) { 512 $schema['primaryImageOfPage'] = [ 513 '@type' => 'ImageObject', 514 'url' => $image_data[0], 515 'width' => $image_data[1], 516 'height' => $image_data[2] 517 ]; 518 // Initialize main image list with featured image 519 $schema['image'][] = $schema['primaryImageOfPage']; 520 } 521 } 522 523 // Parse content for additional media if the setting is enabled 524 // NEW CODE for get_schema_for_page 525 if (seo44_get_option('scan_content_for_schema')) { 526 $media_schema = self::parse_content_for_media_schema($post_id); 527 528 if (!empty($media_schema['images'])) { 529 // 1. Re-index to prevent "0": {...} keys 530 $schema['image'] = array_merge($schema['image'], $media_schema['images']); 531 } 532 533 if (!empty($media_schema['videos'])) { 534 $schema['video'] = $media_schema['videos']; 535 } 536 } 537 538 // 2. Ensure clean array (re-index) 539 if (!empty($schema['image'])) { 540 $schema['image'] = array_values($schema['image']); 541 } else { 542 unset($schema['image']); 543 } 544 545 // NEW: Dynamic Table of Contents (hasPart) 546 // Only run this if we are scanning content, to save performance 547 if (seo44_get_option('enable_jumplinks_schema')) { 548 // We need to fetch content first since get_schema_for_page doesn't always have it ready 549 $post_content = get_the_content(null, false, $post_id); 550 $toc_parts = self::generate_has_part_schema($post_content, $post_id); 551 552 if (!empty($toc_parts)) { 553 $schema['hasPart'] = $toc_parts; 554 } 555 } 556 557 return $schema; 510 558 } 511 559 public function get_schema_for_website() { 512 $schema = [513 '@context' => 'https://schema.org',514 '@type' => 'WebSite',515 'url' => home_url('/'),516 'name' => get_bloginfo('name'),517 'description' => get_bloginfo('description'),518 'potentialAction' => [519 '@type' => 'SearchAction',520 'target' => home_url('/?s={search_term_string}'),521 'query-input' => 'required name=search_term_string',522 ],523 ];524 return $schema;525 } 526 527 // --- Assemble Organization Schema ---528 public function get_schema_for_organization() {560 $schema = [ 561 '@context' => 'https://schema.org', 562 '@type' => 'WebSite', 563 'url' => home_url('/'), 564 'name' => get_bloginfo('name'), 565 'description' => get_bloginfo('description'), 566 'potentialAction' => [ 567 '@type' => 'SearchAction', 568 'target' => home_url('/?s={search_term_string}'), 569 'query-input' => 'required name=search_term_string', 570 ], 571 ]; 572 return $schema; 573 } 574 575 // --- Assemble Organization Schema --- 576 public function get_schema_for_organization() { 529 577 // 1. Name & URL 530 578 $name = seo44_get_option('org_name') ?: get_bloginfo('name'); … … 553 601 // For sameAs, a URL is desired. Add URLs from fields and construct Twitter / X and Facebook URL 554 602 555 // Twitter/X: Handle logic556 $twitter_handle = seo44_get_option('twitter_handle');557 if ( !empty($twitter_handle) && is_string($twitter_handle) ) {558 // Clean handle just in case they added @559 $clean_handle = str_replace('@', '', $twitter_handle);560 $same_as[] = 'https://x.com/' . esc_attr($clean_handle);561 }603 // Twitter/X: Handle logic 604 $twitter_handle = seo44_get_option('twitter_handle'); 605 if ( !empty($twitter_handle) && is_string($twitter_handle) ) { 606 // Clean handle just in case they added @ 607 $clean_handle = str_replace('@', '', $twitter_handle); 608 $same_as[] = 'https://x.com/' . esc_attr($clean_handle); 609 } 562 610 563 611 $extras = ['social_facebook', 'social_instagram', 'social_linkedin', 'social_youtube', 'social_tiktok']; … … 566 614 if ($val) $same_as[] = esc_url($val); 567 615 } 568 // NEW: Process Additional URLs (One per line)569 $additional_urls = seo44_get_option('social_additional');570 if ( !empty($additional_urls) && is_string($additional_urls) ) {571 // Split by newline, trim whitespace, and filter empty lines572 $urls = array_filter(array_map('trim', explode("\n", $additional_urls)));573 574 foreach ($urls as $raw_url) {575 // Validate it is a real URL before adding576 $clean_url = esc_url_raw($raw_url);577 if (!empty($clean_url)) {578 $same_as[] = $clean_url;579 }580 }581 }616 // NEW: Process Additional URLs (One per line) 617 $additional_urls = seo44_get_option('social_additional'); 618 if ( !empty($additional_urls) && is_string($additional_urls) ) { 619 // Split by newline, trim whitespace, and filter empty lines 620 $urls = array_filter(array_map('trim', explode("\n", $additional_urls))); 621 622 foreach ($urls as $raw_url) { 623 // Validate it is a real URL before adding 624 $clean_url = esc_url_raw($raw_url); 625 if (!empty($clean_url)) { 626 $same_as[] = $clean_url; 627 } 628 } 629 } 582 630 583 631 // 4. Build the Schema … … 589 637 ]; 590 638 591 // Add Alternate Name592 $alt_name = seo44_get_option('org_alternate_name');593 if ($alt_name) {594 $schema['alternateName'] = $alt_name;595 }639 // Add Alternate Name 640 $alt_name = seo44_get_option('org_alternate_name'); 641 if ($alt_name) { 642 $schema['alternateName'] = $alt_name; 643 } 596 644 597 645 // Add Tagline (Slogan) … … 613 661 614 662 // Contact Point (Updated to include Email) 615 $phone = seo44_get_option('org_phone');616 $email = seo44_get_option('org_email');617 618 if ($phone || $email) {619 $contact_point = ['@type' => 'ContactPoint'];620 if ($phone) {621 $contact_point['telephone'] = $phone;622 $contact_point['contactType'] = 'customer service';623 }624 if ($email) {625 $contact_point['email'] = $email;626 }627 $schema['contactPoint'] = $contact_point;628 }629 630 // Email at the top level is also good practice631 if ($email) {632 $schema['email'] = $email;633 }634 635 // 5. Address636 $street = seo44_get_option('org_address_street');637 $city = seo44_get_option('org_address_city');638 if ($street && $city) {639 $schema['address'] = [640 '@type' => 'PostalAddress',641 'streetAddress' => $street,642 'addressLocality' => $city,643 'addressRegion' => seo44_get_option('org_address_state'),644 'postalCode' => seo44_get_option('org_address_zip'),645 'addressCountry' => seo44_get_option('org_address_country')646 ];647 }648 649 // Service Area (New)650 $area_served = seo44_get_option('org_area_served');651 if ($area_served) {652 $schema['areaServed'] = [653 '@type' => 'Place',654 'name' => $area_served655 ];656 }657 // 6. Founder658 $founder = seo44_get_option('org_founder');659 if ($founder) {660 $schema['founder'] = [661 '@type' => 'Person',662 'name' => $founder663 ];664 }665 666 // 7. Founding Date667 $founding_date = seo44_get_option('org_founding_date');668 if ( !empty($founding_date) && is_string($founding_date) ) {669 // Basic validation: ensure it looks somewhat like a year or date670 // You can leave it as raw string, Google parses ISO 8601 (YYYY-MM-DD) well.671 $schema['foundingDate'] = strip_tags($founding_date);672 }673 // 8. Professional License (New)674 $license = seo44_get_option('org_license');675 if ($license) {676 // "hasCredential" is the modern schema property for this677 $schema['hasCredential'] = [678 '@type' => 'EducationalOccupationalCredential',679 'credentialCategory' => 'license',680 'name' => $license, // e.g., "Contractor License #123456"681 'recognizedBy' => [682 '@type' => 'Organization',683 'name' => 'State Licensing Board' // Generic fallback since we don't ask for the issuer684 ]685 ];686 687 // Also add it as a simple identifier for wider compatibility688 $schema['identifier'] = $license;689 }690 // FINAL STEP: Apply Filters for Extensibility using 'seo44_organization_schema'663 $phone = seo44_get_option('org_phone'); 664 $email = seo44_get_option('org_email'); 665 666 if ($phone || $email) { 667 $contact_point = ['@type' => 'ContactPoint']; 668 if ($phone) { 669 $contact_point['telephone'] = $phone; 670 $contact_point['contactType'] = 'customer service'; 671 } 672 if ($email) { 673 $contact_point['email'] = $email; 674 } 675 $schema['contactPoint'] = $contact_point; 676 } 677 678 // Email at the top level is also good practice 679 if ($email) { 680 $schema['email'] = $email; 681 } 682 683 // 5. Address 684 $street = seo44_get_option('org_address_street'); 685 $city = seo44_get_option('org_address_city'); 686 if ($street && $city) { 687 $schema['address'] = [ 688 '@type' => 'PostalAddress', 689 'streetAddress' => $street, 690 'addressLocality' => $city, 691 'addressRegion' => seo44_get_option('org_address_state'), 692 'postalCode' => seo44_get_option('org_address_zip'), 693 'addressCountry' => seo44_get_option('org_address_country') 694 ]; 695 } 696 697 // Service Area (New) 698 $area_served = seo44_get_option('org_area_served'); 699 if ($area_served) { 700 $schema['areaServed'] = [ 701 '@type' => 'Place', 702 'name' => $area_served 703 ]; 704 } 705 // 6. Founder 706 $founder = seo44_get_option('org_founder'); 707 if ($founder) { 708 $schema['founder'] = [ 709 '@type' => 'Person', 710 'name' => $founder 711 ]; 712 } 713 714 // 7. Founding Date 715 $founding_date = seo44_get_option('org_founding_date'); 716 if ( !empty($founding_date) && is_string($founding_date) ) { 717 // Basic validation: ensure it looks somewhat like a year or date 718 // You can leave it as raw string, Google parses ISO 8601 (YYYY-MM-DD) well. 719 $schema['foundingDate'] = wp_strip_all_tags($founding_date); 720 } 721 // 8. Professional License (New) 722 $license = seo44_get_option('org_license'); 723 if ($license) { 724 // "hasCredential" is the modern schema property for this 725 $schema['hasCredential'] = [ 726 '@type' => 'EducationalOccupationalCredential', 727 'credentialCategory' => 'license', 728 'name' => $license, // e.g., "Contractor License #123456" 729 'recognizedBy' => [ 730 '@type' => 'Organization', 731 'name' => 'State Licensing Board' // Generic fallback since we don't ask for the issuer 732 ] 733 ]; 734 735 // Also add it as a simple identifier for wider compatibility 736 $schema['identifier'] = $license; 737 } 738 // FINAL STEP: Apply Filters for Extensibility using 'seo44_organization_schema' 691 739 // Allows developers to add properties like 'duns', 'naics', 'awards', etc. 692 return apply_filters('seo44_organization_schema', $schema);693 } 694 695 // --- Intelligent Schema Detection ---740 return apply_filters('seo44_organization_schema', $schema); 741 } 742 743 // --- Intelligent Schema Detection --- 696 744 697 745 /** … … 703 751 $post = get_post($post_id); 704 752 if (!$post) return []; 705 753 754 // 1. Try Advanced HowTo (Jump Links Content Miner) 755 // This runs FIRST to see if we have a robust HowTo available 756 $advanced_howto = $this->generate_advanced_howto_schema($post); 757 758 $final_schemas = []; 759 if (!empty($advanced_howto)) { 760 $final_schemas[] = $advanced_howto; 761 } 762 763 // 2. Run standard detection for FAQ and HowTo (if not already found) 764 // We still run this for FAQPage, but we might skip HowTo if advanced found one 706 765 if (has_blocks($post->post_content)) { 707 // Use the precise block-based parser for modern content 708 return $this->parse_blocks_for_special_schema($post_id, parse_blocks($post->post_content)); 766 $standard_schemas = $this->parse_blocks_for_special_schema($post_id, parse_blocks($post->post_content), !empty($advanced_howto)); // Pass "skip_howto" flag 709 767 } else { 710 768 // Use the HTML fallback for classic editor or page builder content 711 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound769 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 712 770 $rendered_content = apply_filters('the_content', $post->post_content); 713 return $this->parse_html_for_special_schema($post_id, $rendered_content); 714 } 715 } 716 717 771 $standard_schemas = $this->parse_html_for_special_schema($post_id, $rendered_content, !empty($advanced_howto)); 772 } 773 774 if (!empty($standard_schemas)) { 775 $final_schemas = array_merge($final_schemas, $standard_schemas); 776 } 777 778 return $final_schemas; 779 } 780 781 718 782 /** 719 783 * NEW: Parses HTML content for FAQ and How-To patterns (the fallback method). 784 * Advanced HowTo Scanner (The "Content Miner") 785 * Uses Jump Links block as a map to mine content between headings. 720 786 */ 721 private function parse_html_for_special_schema($post_id, $html) { 787 private function generate_advanced_howto_schema($post) { 788 // 0. Check Cache 789 $cache_key = 'seo44_howto_' . $post->ID . '_' . $post->post_modified; 790 $cached_schema = get_transient($cache_key); 791 if ($cached_schema !== false) { 792 return $cached_schema; 793 } 794 // 1. Strict Check: Is the HowTo Checkbox enabled? 795 $enable_howto_meta = get_post_meta($post->ID, '_seo44_enable_howto', true); 796 797 if ($enable_howto_meta !== 'yes') { 798 return null; 799 } 800 801 // 2. Parse Blocks and Find Jump Links Block 802 $blocks = parse_blocks($post->post_content); 803 $flat_blocks = $this->flatten_blocks($blocks); 804 $jump_links_block = $this->find_jump_links_block($flat_blocks); 805 806 if (!$jump_links_block) return null; 807 808 // 3. Setup Variables 809 $desc_key = seo44_get_option('description_key') ?: 'seo44_description'; 810 $desc = get_post_meta($post->ID, $desc_key, true); 811 if (empty($desc)) { 812 $desc = get_post_meta($post->ID, '_yoast_wpseo_metadesc', true) ?: get_post_meta($post->ID, '_aioseop_description', true); 813 if (empty($desc)) { 814 $desc = get_the_excerpt($post->ID); 815 if (empty($desc)) { 816 $desc = wp_trim_words(strip_shortcodes($post->post_content), 25); 817 } 818 } 819 } 820 821 $steps = []; 822 $current_step = null; 823 $tools = []; 824 $supplies = []; 825 $total_time = ''; 826 $prep_time = ''; 827 $perform_time = ''; 828 $found_first_step = false; 829 $potential_list_type = ''; 830 $video_schema = []; 831 $yield = ''; 832 833 // 4. Iterate Blocks 834 foreach ($flat_blocks as $block) { 835 836 // --- PHASE A: PRE-STEP SCANNING (Intro) --- 837 if (!$found_first_step) { 838 839 if ($block['blockName'] === 'core/paragraph') { 840 // FIX: Decode entities to ensure non-breaking spaces don't break the regex 841 $text = html_entity_decode(wp_strip_all_tags($block['innerHTML'])); 842 843 // FIX: Updated Total Time Regex to prevent matching "Prep Time" as "Time" 844 // Logic: Match "Total Time" OR "Time" that is NOT preceded by Prep/Active/Cook 845 if (empty($total_time)) { 846 $total_time = $this->parse_duration_string($text, '(?:Total\s+|(?<!Prep\s|ation\s|Active\s|Cook\s))Time:\s*'); 847 } 848 if (empty($prep_time)) $prep_time = $this->parse_duration_string($text, 'Prep(?:aration)?\s*Time:\s*'); 849 if (empty($perform_time)) $perform_time = $this->parse_duration_string($text, '(?:Perform|Active|Cook)\s*Time:\s*'); 850 851 if (empty($yield) && preg_match('/(?:Yields|Makes):\s*(.+)/i', $text, $matches)) { 852 $yield = trim($matches[1]); 853 } 854 855 // FIX: Scan for Comma-Separated Tools/Supplies in Paragraphs 856 // e.g. "Tools: Hammer, Nails, Wood" 857 if (preg_match('/(?:Tools?|Equipment|Ingredients?|Supplies|Materials?):\s*(.+)/i', $text, $matches)) { 858 $type = (stripos($matches[0], 'tool') !== false || stripos($matches[0], 'equipment') !== false) ? 'tool' : 'supply'; 859 $items = explode(',', $matches[1]); 860 foreach ($items as $item) { 861 $clean_item = trim($item); 862 $clean_item = rtrim($clean_item, '.'); // Remove trailing period 863 if (!empty($clean_item)) { 864 if ($type === 'tool') { 865 $tools[] = ['@type' => 'HowToTool', 'name' => $clean_item]; 866 } else { 867 $supplies[] = ['@type' => 'HowToSupply', 'name' => $clean_item]; 868 } 869 } 870 } 871 } 872 } 873 874 // (Bullet list logic remains the same) 875 if ($block['blockName'] === 'core/heading') { 876 $text = strtolower(wp_strip_all_tags($block['innerHTML'])); 877 if (strpos($text, 'tool') !== false || strpos($text, 'equipment') !== false) { 878 $potential_list_type = 'tool'; 879 } elseif (strpos($text, 'material') !== false || strpos($text, 'suppl') !== false || strpos($text, 'ingredient') !== false) { 880 $potential_list_type = 'supply'; 881 } else { 882 $potential_list_type = ''; 883 } 884 } 885 if ($block['blockName'] === 'core/list' && $potential_list_type) { 886 if (preg_match_all('/<li[^>]*>(.*?)<\/li>/s', $block['innerHTML'], $list_matches)) { 887 foreach ($list_matches[1] as $li_text) { 888 $clean_li = trim(wp_strip_all_tags($li_text)); 889 if ($potential_list_type === 'tool') { 890 $tools[] = ['@type' => 'HowToTool', 'name' => $clean_li]; 891 } else { 892 $supplies[] = ['@type' => 'HowToSupply', 'name' => $clean_li]; 893 } 894 } 895 } 896 $potential_list_type = ''; 897 } 898 899 // (Video logic remains the same) 900 if ($block['blockName'] === 'core/embed' && isset($block['attrs']['providerNameSlug']) && $block['attrs']['providerNameSlug'] === 'youtube') { 901 $video_url = $block['attrs']['url']; 902 $video_id = self::extract_youtube_video_id($video_url); 903 $upload_date = self::get_youtube_upload_date($video_id, $post->ID); 904 905 $oembed_url = 'https://www.youtube.com/oembed?format=json&url=' . urlencode($video_url); 906 $response = wp_remote_get($oembed_url); 907 908 $vid_title = ''; $vid_thumb = ''; $vid_author = ''; 909 if (!is_wp_error($response)) { 910 $data = json_decode(wp_remote_retrieve_body($response), true); 911 if ($data) { 912 $vid_title = $data['title']; 913 $vid_thumb = $data['thumbnail_url']; 914 $vid_author = $data['author_name']; 915 } 916 } 917 if (empty($vid_title)) $vid_title = get_the_title($post->ID) . ' Video Tutorial'; 918 if (empty($vid_thumb)) $vid_thumb = 'https://i.ytimg.com/vi/' . $video_id . '/hqdefault.jpg'; 919 920 $video_schema = [ 921 '@type' => 'VideoObject', 922 'name' => $vid_title, 923 'description' => get_the_title($post->ID) . ' - Video Guide', 924 'thumbnailUrl' => $vid_thumb, 925 'uploadDate' => $upload_date, 926 'embedUrl' => $video_url, 927 'contentUrl' => $video_url 928 ]; 929 if ($vid_author) { 930 $video_schema['author'] = ['@type' => 'Person', 'name' => $vid_author]; 931 } 932 } 933 } 934 935 // --- PHASE B: STEP MINING --- 936 if ($block['blockName'] === 'core/heading') { 937 if (preg_match('/id="([^"]+)"/', $block['innerHTML'], $id_match)) { 938 $anchor_id = $id_match[1]; 939 940 // --- EXCLUSION LOGIC START --- 941 $whitelist_ids = get_post_meta($post->ID, '_seo44_howto_step_ids', true); 942 $should_include = true; 943 944 // STRATEGY A: Whitelist (New System) 945 if ( ! empty($whitelist_ids) && is_array($whitelist_ids) ) { 946 if ( ! in_array($anchor_id, $whitelist_ids) ) { 947 $should_include = false; 948 } 949 // FIX: Global Guard for Intro/Description 950 // Even if whitelisted, ignore Intro headings so they don't consume metadata 951 // May not need this... 952 if (stripos($anchor_id, 'intro') !== false || stripos($anchor_id, 'description') !== false) { 953 $should_include = false; 954 } 955 } 956 // STRATEGY B: Fallback 957 else { 958 if (stripos($anchor_id, 'intro') !== false) { $should_include = false; } 959 if (substr($anchor_id, -7) === '-nostep') { $should_include = false; } 960 } 961 962 if ( ! $should_include ) { 963 if ($current_step) { $steps[] = $current_step; $current_step = null; } 964 continue; 965 } 966 // --- EXCLUSION LOGIC END --- 967 968 $found_first_step = true; 969 if ($current_step) { $steps[] = $current_step; } 970 971 $current_step = [ 972 '@type' => 'HowToStep', 973 'url' => get_permalink($post->ID) . '#' . $anchor_id, 974 'name' => trim(wp_strip_all_tags($block['innerHTML'])), 975 'text' => '', 976 'image' => [] 977 ]; 978 continue; 979 } 980 } 981 982 if ($current_step) { 983 if ($block['blockName'] === 'core/paragraph') { 984 $text = trim(wp_strip_all_tags($block['innerHTML'])); 985 if (!empty($text)) { $current_step['text'] .= $text . ' '; } 986 } 987 if ($block['blockName'] === 'core/image') { 988 if (preg_match('/src="([^"]+)"/', $block['innerHTML'], $match)) { 989 if (!isset($current_step['image'])) { $current_step['image'] = []; } 990 $current_step['image'][] = $match[1]; 991 } 992 } 993 if ($block['blockName'] === 'core/list') { 994 $current_step['text'] .= trim(wp_strip_all_tags($block['innerHTML'])) . ' '; 995 } 996 } 997 } 998 999 if ($current_step) { $steps[] = $current_step; } 1000 1001 if (!empty($steps)) { 1002 foreach ($steps as $k => $step) { 1003 if (empty($step['image'])) { 1004 $steps[$k]['image'] = ''; 1005 } elseif (count($step['image']) === 1) { 1006 $steps[$k]['image'] = $step['image'][0]; 1007 } 1008 $steps[$k]['text'] = trim($step['text']); 1009 } 1010 1011 $howto = [ 1012 '@context' => 'https://schema.org', 1013 '@type' => 'HowTo', 1014 'name' => get_the_title($post->ID), 1015 'description' => $desc, 1016 'step' => $steps 1017 ]; 1018 1019 if ($total_time) { $howto['totalTime'] = $total_time; } 1020 if ($prep_time) { $howto['prepTime'] = $prep_time; } 1021 if ($perform_time) { $howto['performTime'] = $perform_time; } 1022 if (!empty($tools)) { $howto['tool'] = $tools; } 1023 if (!empty($supplies)) { $howto['supply'] = $supplies; } 1024 if (!empty($video_schema)) { $howto['video'] = $video_schema; } 1025 if ($yield) { $howto['yield'] = $yield; } 1026 $howto['disambiguatingDescription'] = $desc; 1027 1028 if (has_post_thumbnail($post->ID)) { 1029 $howto['image'] = get_the_post_thumbnail_url($post->ID, 'full'); 1030 } 1031 $categories = get_the_category($post->ID); 1032 if (!empty($categories)) { 1033 $howto['about'] = ['@type' => 'Thing', 'name' => $categories[0]->name]; 1034 } 1035 $tags = get_the_tags($post->ID); 1036 if (!empty($tags)) { 1037 $howto['teaches'] = ['@type' => 'DefinedTerm', 'name' => $tags[0]->name]; 1038 } 1039 1040 set_transient($cache_key, $howto, DAY_IN_SECONDS); 1041 return $howto; 1042 } 1043 return null; 1044 } 1045 1046 /** 1047 * Helper: Parse Duration Strings 1048 * FIX: Re-wrote to accept raw patterns without delimiters to prevent syntax errors. 1049 */ 1050 private function parse_duration_string($text, $prefix_pattern) { 1051 // We construct the full regex here to ensure delimiters wrap the WHOLE pattern. 1052 // Matches: Label -> (1) Hours -> (2) Minutes 1053 $regex = '/' . $prefix_pattern . '(?:(\d+)\s*(?:hour|hr)s?\s*)?(?:and\s*)?(?:(\d+)\s*(?:minute|min)s?)?/i'; 1054 1055 if (preg_match($regex, $text, $matches)) { 1056 $hours = !empty($matches[1]) ? intval($matches[1]) : 0; 1057 $minutes = !empty($matches[2]) ? intval($matches[2]) : 0; 1058 1059 if ($hours > 0 || $minutes > 0) { 1060 $duration = 'PT'; 1061 if ($hours > 0) $duration .= $hours . 'H'; 1062 if ($minutes > 0) $duration .= $minutes . 'M'; 1063 return $duration; 1064 } 1065 } 1066 return ''; 1067 } 1068 1069 // private function get_youtube_id($url) { 1070 // preg_match('/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i', $url, $matches); 1071 // return isset($matches[1]) ? $matches[1] : ''; 1072 // } 1073 1074 /** 1075 * Helper: Flattens nested block structures into a single linear array. 1076 */ 1077 private function flatten_blocks($blocks) { 1078 $flat = []; 1079 foreach ($blocks as $block) { 1080 $flat[] = $block; 1081 if (!empty($block['innerBlocks'])) { 1082 $flat = array_merge($flat, $this->flatten_blocks($block['innerBlocks'])); 1083 } 1084 } 1085 return $flat; 1086 } 1087 1088 /** 1089 * Helper to find the Jump Links block recursively 1090 * Updated to be broader and find any block with 'jump-links' in the name 1091 * FIX: Added check to ensure blockName is a string before strpos 1092 */ 1093 private function find_jump_links_block($blocks) { 1094 foreach ($blocks as $block) { 1095 // Broad check for any jump links block 1096 if (isset($block['blockName']) && is_string($block['blockName']) && strpos($block['blockName'], 'jump-links') !== false) { 1097 return $block; 1098 } 1099 // Recursive check for inner blocks (e.g. inside Groups/Columns) 1100 if (!empty($block['innerBlocks'])) { 1101 $found = $this->find_jump_links_block($block['innerBlocks']); 1102 if ($found) return $found; 1103 } 1104 } 1105 return null; 1106 } 1107 1108 private function parse_html_for_special_schema($post_id, $html, $skip_howto = false) { 722 1109 $final_schemas = []; 723 1110 … … 749 1136 } 750 1137 751 // HowTo Detection in HTML752 if ( preg_match('/<h[2-4][^>]*>(installation|directions|instructions)<\/h[2-4]>/i', $html, $howto_heading_match)) {1138 // HowTo Detection (Skipped if Advanced scanner found one) 1139 if (!$skip_howto && preg_match('/<h[2-4][^>]*>(installation|directions|instructions)<\/h[2-4]>/i', $html, $howto_heading_match)) { 753 1140 preg_match('/' . preg_quote($howto_heading_match[0], '/') . '.*?<ol.*?>(.*?)<\/ol>/is', $html, $ol_match); 754 1141 if(isset($ol_match[1])) { … … 777 1164 * The original block-based parser, now in its own function. 778 1165 */ 779 private function parse_blocks_for_special_schema($post_id, $blocks ) {1166 private function parse_blocks_for_special_schema($post_id, $blocks, $skip_howto = false) { 780 1167 $faq_questions = []; 781 1168 $howto_steps = []; … … 786 1173 $final_schemas = []; 787 1174 788 foreach ($blocks as $block) {1175 foreach ($blocks as $block) { 789 1176 $heading_text = strtolower(wp_strip_all_tags($block['innerHTML'])); 790 1177 … … 796 1183 } 797 1184 $is_faq_section = false; 798 $is_howto_section = false; // Stop looking for HowTo steps as well1185 $is_howto_section = false; 799 1186 $current_answer_blocks = []; 800 1187 continue; … … 819 1206 } 820 1207 821 // --- HowTo Detection --- 822 if (!$is_faq_section && $block['blockName'] === 'core/heading' && preg_match('/(installation|directions|instructions)/i', $heading_text)) { 823 $is_howto_section = true; 824 // Use trim() to remove leading/trailing whitespace and newlines 825 $howto_heading_text = trim(wp_strip_all_tags($block['innerHTML'])); 826 continue; // Find the heading, then look for the list in subsequent blocks 827 } 828 // If we are in a how-to section and find the first ordered list, process it. 829 if ($is_howto_section && $block['blockName'] === 'core/list' && isset($block['attrs']['ordered']) && $block['attrs']['ordered']) { 830 831 if (!empty($block['innerBlocks'])) { 832 foreach($block['innerBlocks'] as $list_item_block) { 833 if ($list_item_block['blockName'] === 'core/list-item') { 834 $howto_steps[] = ['@type' => 'HowToStep', 'text' => esc_html(wp_strip_all_tags($list_item_block['innerHTML']))]; 1208 // HowTo Logic (Skipped if Advanced scanner found one) 1209 if (!$skip_howto) { 1210 if (!$is_faq_section && $block['blockName'] === 'core/heading' && preg_match('/(installation|directions|instructions)/i', $heading_text)) { 1211 $is_howto_section = true; 1212 // Use trim() to remove leading/trailing whitespace and newlines 1213 $howto_heading_text = trim(wp_strip_all_tags($block['innerHTML'])); 1214 continue; // Find the heading, then look for the list in subsequent blocks 1215 } 1216 // If we are in a how-to section and find the first ordered list, process it. 1217 if ($is_howto_section && $block['blockName'] === 'core/list' && isset($block['attrs']['ordered']) && $block['attrs']['ordered']) { 1218 if (!empty($block['innerBlocks'])) { 1219 foreach($block['innerBlocks'] as $list_item_block) { 1220 if ($list_item_block['blockName'] === 'core/list-item') { 1221 $howto_steps[] = ['@type' => 'HowToStep', 'text' => esc_html(wp_strip_all_tags($list_item_block['innerHTML']))]; 1222 } 835 1223 } 836 1224 } 837 }838 $is_howto_section = false; // Stop after finding the first ordered list1225 $is_howto_section = false; // Stop after finding the first ordered list 1226 } 839 1227 } 840 1228 } … … 876 1264 877 1265 /** 878 * HELPER: Cleans and formats block content for an answer.1266 * HELPER: Cleans and formats block content for an answer. 879 1267 */ 880 1268 private function clean_and_format_answer($blocks) { … … 909 1297 // Clean up remaining HTML, extra whitespace, and decode entities 910 1298 $clean_text = trim(wp_strip_all_tags(html_entity_decode($answer_html))); 911 return preg_replace('/\n\s*\n/', "\n", $clean_text); // Collapse multiple newlines912 } 913 1299 return preg_replace('/\n\s*\n/', "\n", $clean_text); 1300 } 1301 914 1302 // --- MEDIA PARSING FUNCTION --- 915 1303 … … 927 1315 $content = $post->post_content; 928 1316 $found_block_image_urls = []; 929 $found_video_urls = []; // Fixed: Added missing variable declaration 1317 $found_video_urls = []; 1318 1319 $default_description = get_the_title($post_id) . ' Video'; 930 1320 931 1321 if (has_blocks($content)) { 932 1322 $blocks = parse_blocks($content); 933 1323 foreach ($blocks as $block) { 1324 934 1325 // Handle Image Blocks 935 1326 if ($block['blockName'] === 'core/image' && !empty($block['attrs']['id'])) { … … 947 1338 } 948 1339 $images[] = $image_object; 949 $found_block_image_urls[] = $image_data[0]; // Keep track of URLs we've already added1340 $found_block_image_urls[] = $image_data[0]; 950 1341 } 951 1342 } 952 1343 953 // Handle YouTube Embed Blocks using the oEmbed API 1344 // Handle YouTube Embed Blocks using the oEmbed API + API/Scraping 954 1345 if ($block['blockName'] === 'core/embed' && isset($block['attrs']['providerNameSlug']) && $block['attrs']['providerNameSlug'] === 'youtube') { 955 $oembed_url = 'https://www.youtube.com/oembed?format=json&url=' . urlencode($block['attrs']['url']); 1346 $video_url = $block['attrs']['url']; 1347 1348 // 1. Get Accurate Date 1349 $video_id = self::extract_youtube_video_id($video_url); 1350 $upload_date = self::get_youtube_upload_date($video_id, $post_id); 1351 1352 // 2. Get Metadata 1353 $oembed_url = 'https://www.youtube.com/oembed?format=json&url=' . urlencode($video_url); 956 1354 $response = wp_remote_get($oembed_url); 1355 957 1356 if (!is_wp_error($response)) { 958 1357 $data = json_decode(wp_remote_retrieve_body($response), true); … … 962 1361 'name' => $data['title'], 963 1362 'thumbnailUrl' => $data['thumbnail_url'], 964 'embedUrl' => $block['attrs']['url'], 965 'author' => ['@type' => 'Person', 'name' => $data['author_name']] 1363 'embedUrl' => $video_url, 1364 'contentUrl' => $video_url, 1365 'author' => ['@type' => 'Person', 'name' => $data['author_name']], 1366 'uploadDate' => $upload_date, // Uses API/Scraped date 1367 'description' => $default_description 966 1368 ]; 967 $found_video_urls[] = $ block['attrs']['url']; // Track processed videos1369 $found_video_urls[] = $video_url; 968 1370 } 969 1371 } … … 972 1374 } 973 1375 974 // --- Universal Fallback for <img> and <iframe> tags in Classic Editor or HTML Blocks---975 976 // Find Images (logic is unchanged)1376 // --- Universal Fallback for <img> and <iframe> tags --- 1377 1378 // Find Images 977 1379 if (preg_match_all('/<img[^>]+>/i', $content, $img_matches)) { 978 1380 foreach ($img_matches[0] as $img_tag) { … … 982 1384 if (preg_match('/src\s*=\s*[\'"]([^\'"]+)[\'"]/i', $img_tag, $src_matches)) { 983 1385 $url = $src_matches[1]; 984 // Add the image ONLY if we haven't already added it from a core/image block985 1386 if (!in_array($url, $found_block_image_urls)) { 986 1387 $images[] = ['@type' => 'ImageObject', 'url' => $url]; 987 1388 } 988 1389 } 989 }990 } 991 992 // RESTORED: Find iframes1390 } 1391 } 1392 1393 // Find iframes for YouTube 993 1394 if (preg_match_all('/<iframe[^>]+src="([^"]+)"[^>]*>/i', $content, $iframe_matches)) { 994 1395 foreach($iframe_matches[1] as $iframe_src) { 995 // Skip if we already processed this video from a block 996 if (in_array($iframe_src, $found_video_urls)) { 997 continue; 998 } 999 1000 if (strpos($iframe_src, 'youtube.com/embed') !== false) { 1001 preg_match('/embed\/([a-zA-Z0-9_-]+)/i', $iframe_src, $id_matches); 1002 $video_id = $id_matches[1] ?? null; 1003 if ($video_id) { 1004 $videos[] = [ 1005 '@type' => 'VideoObject', 1006 'name' => 'Embedded YouTube Video', // Title isn't available from a raw iframe 1007 'thumbnailUrl' => 'https://i.ytimg.com/vi/' . $video_id . '/hqdefault.jpg', 1008 'embedUrl' => 'https://www.youtube.com/watch?v=' . $video_id, 1009 ]; 1010 } 1396 if (in_array($iframe_src, $found_video_urls)) { continue; } 1397 1398 // Use the static helper to check ID 1399 $video_id = self::extract_youtube_video_id($iframe_src); 1400 1401 if ($video_id) { 1402 $upload_date = self::get_youtube_upload_date($video_id, $post_id); 1403 1404 $videos[] = [ 1405 '@type' => 'VideoObject', 1406 'name' => 'Embedded YouTube Video', 1407 'description' => $default_description, 1408 'uploadDate' => $upload_date, 1409 'thumbnailUrl' => 'https://i.ytimg.com/vi/' . $video_id . '/hqdefault.jpg', 1410 'embedUrl' => 'https://www.youtube.com/watch?v=' . $video_id, 1411 'contentUrl' => 'https://www.youtube.com/watch?v=' . $video_id 1412 ]; 1011 1413 } 1012 1414 } … … 1015 1417 return ['images' => $images, 'videos' => $videos]; 1016 1418 } 1017 1018 // Function to generate BreadcrumbList schema for Taxonomy pages (not posts & Pages)1419 1420 // Function to generate BreadcrumbList schema for Taxonomy pages (not posts & Pages) 1019 1421 public function get_schema_for_taxonomy() { 1020 1422 $term = get_queried_object(); … … 1063 1465 return $schema; 1064 1466 } 1065 1066 // Function to create author name 1067 1068 public function get_author_name( $author_id ) { 1069 $format = seo44_get_option('author_format', 'display_name'); 1070 switch ($format) { 1071 case 'first_last': return get_the_author_meta('first_name', $author_id) . ' ' . get_the_author_meta('last_name', $author_id); 1072 case 'last_first': return get_the_author_meta('last_name', $author_id) . ', ' . get_the_author_meta('first_name', $author_id); 1073 default: return get_the_author_meta('display_name', $author_id); 1074 } 1075 } 1076 public static function get_author_name_static( $author_id ) { 1077 $frontend = new self(); 1078 return $frontend->get_author_name( $author_id ); 1079 } 1467 1468 // Function to create author name 1469 // With more graceful logic for missing name fields 1470 public function get_author_name( $author_id ) { 1471 $format = seo44_get_option('author_format', 'display_name'); 1472 1473 // 1. Get the raw values first 1474 $first_name = get_the_author_meta( 'first_name', $author_id ); 1475 $last_name = get_the_author_meta( 'last_name', $author_id ); 1476 1477 $formatted_name = ''; 1478 1479 switch ( $format ) { 1480 case 'first_last': 1481 // 2. Concatenate and trim. 1482 // If both are empty, trim() ensures we get "", not " ". 1483 // If one is missing, it removes the leading/trailing space. 1484 $formatted_name = trim( $first_name . ' ' . $last_name ); 1485 break; 1486 1487 case 'last_first': 1488 // 3. Smart comma handling. 1489 // Only add the comma if BOTH fields have data. 1490 if ( ! empty( $last_name ) && ! empty( $first_name ) ) { 1491 $formatted_name = $last_name . ', ' . $first_name; 1492 } else { 1493 // If one is missing, fall back to simple concatenation/trim without the comma 1494 $formatted_name = trim( $last_name . ' ' . $first_name ); 1495 } 1496 break; 1497 1498 // Default case is handled by the fallback check below 1499 } 1500 1501 // 4. The Fallback Check 1502 // If formatted_name is empty (because fields were empty) OR format was 'display_name' 1503 if ( empty( $formatted_name ) ) { 1504 return get_the_author_meta( 'display_name', $author_id ); 1505 } 1506 1507 return $formatted_name; 1508 } 1509 1510 public static function get_author_name_static( $author_id ) { 1511 $frontend = new self(); 1512 return $frontend->get_author_name( $author_id ); 1513 } 1514 1515 /** 1516 * Parses content for headings with IDs to create a "Table of Contents" schema. 1517 * @param string $content The post content. 1518 * @return array Array of WebPageElement objects. 1519 */ 1520 1521 // This could be updated to use IDS passed through to field from jump links block to be more efficient than scan and remove need for checkbox. 1522 // UPDATED: Now respects whitelist to match "included" jump links 1523 private static function generate_has_part_schema($content, $post_id) { 1524 $has_part = []; 1525 1526 // Retrieve the whitelist of IDs (generated by JS when saving the Jump Links block) 1527 $whitelist_ids = get_post_meta($post_id, '_seo44_howto_step_ids', true); 1528 1529 // Regex to find H2-H4 tags that have an ID attribute 1530 if (preg_match_all('/<h[2-4][^>]*id="([^"]+)"[^>]*>(.*?)<\/h[2-4]>/i', $content, $matches, PREG_SET_ORDER)) { 1531 1532 foreach ($matches as $match) { 1533 $anchor_id = $match[1]; 1534 $heading_text = wp_strip_all_tags($match[2]); 1535 1536 // --- FILTER LOGIC --- 1537 // Default to include everything if no whitelist exists (e.g. old post, no jump links block) 1538 // But if a whitelist exists (array), enforce it strictly. 1539 $should_include = true; 1540 1541 if ( ! empty($whitelist_ids) && is_array($whitelist_ids) ) { 1542 if ( ! in_array($anchor_id, $whitelist_ids) ) { 1543 $should_include = false; 1544 } 1545 } 1546 1547 if ($should_include && !empty($anchor_id) && !empty($heading_text)) { 1548 $has_part[] = [ 1549 '@type' => 'WebPageElement', 1550 'name' => $heading_text, 1551 'url' => get_permalink($post_id) . '#' . $anchor_id, 1552 'cssSelector' => '#' . $anchor_id 1553 ]; 1554 } 1555 } 1556 } 1557 return $has_part; 1558 } 1559 1560 1561 /** 1562 * Extracts YouTube video ID from various URL formats 1563 */ 1564 private static function extract_youtube_video_id($url) { 1565 $patterns = [ 1566 '/youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)/', // Standard 1567 '/youtube\.com\/embed\/([a-zA-Z0-9_-]+)/', // Embed 1568 '/youtu\.be\/([a-zA-Z0-9_-]+)/', // Short URL 1569 '/youtube\.com\/v\/([a-zA-Z0-9_-]+)/', // Old format 1570 ]; 1571 1572 foreach ($patterns as $pattern) { 1573 if (preg_match($pattern, $url, $matches)) { 1574 return $matches[1]; 1575 } 1576 } 1577 1578 return null; 1579 } 1580 1581 /** 1582 * Gets YouTube video upload date with multiple fallback strategies 1583 * @param string $video_id YouTube video ID 1584 * @param int $post_id WordPress post ID for fallback 1585 * @return string ISO 8601 formatted date 1586 */ 1587 private static function get_youtube_upload_date($video_id, $post_id) { 1588 if (empty($video_id)) { 1589 return get_the_date('c', $post_id); 1590 } 1591 1592 // Strategy 1: Check transient cache (24 hour cache to avoid API limits) 1593 $cache_key = 'seo44_yt_date_' . $video_id; 1594 $cached_date = get_transient($cache_key); 1595 1596 if ($cached_date !== false) { 1597 return $cached_date; 1598 } 1599 1600 // Strategy 2: Try YouTube Data API (if configured) 1601 $api_key = seo44_get_option('youtube_api_key'); 1602 1603 if (!empty($api_key)) { 1604 $api_date = self::fetch_youtube_date_from_api($video_id, $api_key); 1605 1606 if ($api_date) { 1607 // Cache for 24 hours 1608 set_transient($cache_key, $api_date, DAY_IN_SECONDS); 1609 return $api_date; 1610 } 1611 } 1612 1613 // Strategy 3: Try scraping YouTube page (no API key needed, but less reliable) 1614 $scraped_date = self::scrape_youtube_upload_date($video_id); 1615 1616 if ($scraped_date) { 1617 // Cache for 24 hours 1618 set_transient($cache_key, $scraped_date, DAY_IN_SECONDS); 1619 return $scraped_date; 1620 } 1621 1622 // Strategy 4: Fallback to post publication date 1623 return get_the_date('c', $post_id); 1624 } 1625 1626 /** 1627 * Fetches upload date from YouTube Data API v3 1628 */ 1629 private static function fetch_youtube_date_from_api($video_id, $api_key) { 1630 $api_url = sprintf( 1631 'https://www.googleapis.com/youtube/v3/videos?id=%s&key=%s&part=snippet', 1632 urlencode($video_id), 1633 urlencode($api_key) 1634 ); 1635 1636 $response = wp_remote_get($api_url, [ 1637 'timeout' => 5, 1638 'sslverify' => true 1639 ]); 1640 1641 if (is_wp_error($response)) { 1642 return null; 1643 } 1644 1645 $body = wp_remote_retrieve_body($response); 1646 $data = json_decode($body, true); 1647 1648 if (isset($data['items'][0]['snippet']['publishedAt'])) { 1649 return $data['items'][0]['snippet']['publishedAt']; 1650 } 1651 1652 return null; 1653 } 1654 1655 /** 1656 * Scrapes upload date from YouTube page HTML (fallback method, no API key needed) 1657 * IMPPROVED: Scans Meta tags first, then JSON-LD, then internal player data. 1658 */ 1659 private static function scrape_youtube_upload_date($video_id) { 1660 $video_page_url = 'https://www.youtube.com/watch?v=' . $video_id; 1661 1662 $response = wp_remote_get($video_page_url, [ 1663 'timeout' => 10, 1664 // Use a generic browser agent to avoid being served a "bare" bot page 1665 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 1666 'sslverify' => true 1667 ]); 1668 1669 if (is_wp_error($response)) { 1670 return null; 1671 } 1672 1673 $html = wp_remote_retrieve_body($response); 1674 1675 // Strategy A: Look for Meta Tags (Most Reliable) 1676 // YouTube usually includes: <meta itemprop="datePublished" content="YYYY-MM-DD"> 1677 if (preg_match('/<meta\s+itemprop="datePublished"\s+content="([^"]+)"/i', $html, $matches)) { 1678 return $matches[1]; 1679 } 1680 if (preg_match('/<meta\s+itemprop="uploadDate"\s+content="([^"]+)"/i', $html, $matches)) { 1681 return $matches[1]; 1682 } 1683 1684 // Strategy B: JSON-LD Schema (Your original method) 1685 if (preg_match('/"uploadDate":"([^"]+)"/', $html, $matches)) { 1686 return $matches[1]; 1687 } 1688 1689 // Strategy C: Look for microformatDataRenderer (Internal YouTube Data) 1690 // This is often found inside the large ytInitialPlayerResponse JSON object 1691 if (preg_match('/"publishDate":"([^"]+)"/', $html, $matches)) { 1692 $date_string = $matches[1]; // Usually YYYY-MM-DD 1693 if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_string)) { 1694 return $date_string; 1695 } 1696 // Fallback for compact format YYYYMMDD 1697 if (preg_match('/^(\d{4})(\d{2})(\d{2})$/', $date_string, $date_parts)) { 1698 return sprintf('%s-%s-%s', $date_parts[1], $date_parts[2], $date_parts[3]); 1699 } 1700 } 1701 1702 return null; 1703 } 1080 1704 } -
search-appearance-toolkit-seo-44/tags/4.3/includes/class-seo44-metabox.php
r3389330 r3423355 1 1 <?php 2 // Version 4.3 - Added a Generate HowTo Schema checkbox that appears via activation word 2 3 class SEO44_Metabox { 3 4 … … 23 24 $description = get_post_meta($post->ID, seo44_get_option('description_key'), true); 24 25 $keywords = get_post_meta($post->ID, seo44_get_option('keywords_key'), true); 25 $jump_link_headings = get_post_meta($post->ID, '_seo44_jump_link_headings', true); 26 wp_nonce_field('seo44_jump_links_nonce', 'seo44_jump_links_nonce_field'); 26 27 // NEW: Retrieve the HowTo toggle state 28 // We use 'yes' for checked, empty for unchecked 29 $enable_howto = get_post_meta($post->ID, '_seo44_enable_howto', true); 30 31 // Logic to determine initial visibility: Show if already checked 32 $howto_wrapper_style = ($enable_howto === 'yes') ? '' : 'display:none;'; 33 27 34 ?> 28 <input type="hidden" id="seo44_jump_link_headings_field" name="seo44_jump_link_headings" value="<?php echo esc_attr($jump_link_headings); ?>">35 29 36 <p> 30 37 <label for="seo44_title"><strong><?php esc_html_e('SEO Title', 'search-appearance-toolkit-seo-44'); ?></strong></label><br> … … 54 61 </p> 55 62 <?php endif; ?> 63 <div id="seo44-howto-trigger-wrapper" style="<?php echo esc_attr($howto_wrapper_style); ?>"> 64 <p> 65 <label for="seo44_enable_howto"> 66 <input type="checkbox" id="seo44_enable_howto" name="seo44_enable_howto" value="yes" <?php checked($enable_howto, 'yes'); ?>> 67 <strong><?php esc_html_e('Generate HowTo Schema', 'search-appearance-toolkit-seo-44'); ?></strong> 68 </label> 69 <br> 70 <span class="description"> 71 <?php esc_html_e('It looks like you are publishing a "How-To" guide. When this box is checked, SEO 44 will generate HowTo structured data.', 'search-appearance-toolkit-seo-44'); ?> 72 </span> 73 </p> 74 </div> 56 75 <hr> 57 76 <div id="seo44-snippet-preview"> … … 80 99 public function save_meta_box_data($post_id) { 81 100 if (!isset($_POST['seo44_meta_box_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['seo44_meta_box_nonce'])), 'seo44_save_meta_box_data')) return; 82 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;83 $post_type = get_post_type($post_id);84 // Robust capability checking85 if (!current_user_can("edit_{$post_type}s")) {101 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; 102 $post_type = get_post_type($post_id); 103 // Robust capability checking 104 if (!current_user_can("edit_{$post_type}s")) { 86 105 return; 87 }88 if (isset($_POST['seo44_title'])) {106 } 107 if (isset($_POST['seo44_title'])) { 89 108 update_post_meta($post_id, seo44_get_option('title_key'), sanitize_text_field(wp_unslash($_POST['seo44_title']))); 90 109 } … … 95 114 update_post_meta($post_id, seo44_get_option('keywords_key'), sanitize_text_field(wp_unslash($_POST['seo44_keywords']))); 96 115 } 97 // Save Logic for Hidden Box for Jump Links Block data 98 if (isset($_POST['seo44_jump_links_nonce_field']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['seo44_jump_links_nonce_field'])), 'seo44_jump_links_nonce')) { 99 if (isset($_POST['seo44_jump_link_headings'])) { 100 update_post_meta($post_id, '_seo44_jump_link_headings', sanitize_text_field(wp_unslash($_POST['seo44_jump_link_headings']))); 101 } 116 // NEW: Save HowTo Checkbox 117 if (isset($_POST['seo44_enable_howto'])) { 118 update_post_meta($post_id, '_seo44_enable_howto', 'yes'); 119 } else { 120 // Checkboxes don't send data if unchecked, so we must explicitly delete or update to 'no' 121 // Only do this if our nonce is verified (which implies the form was actually submitted) 122 update_post_meta($post_id, '_seo44_enable_howto', 'no'); 102 123 } 124 103 125 } 104 126 } -
search-appearance-toolkit-seo-44/tags/4.3/includes/class-seo44-settings.php
r3405454 r3423355 1 1 <?php 2 // Version 4.3 3 // Added hasPart Table of Contents Schema, advanced HowTo Schema 4 // Added YouTube Data API to Integrations 2 5 class SEO44_Settings { 3 6 … … 59 62 add_settings_field('seo44_schema_tools', __('Schema Scanner', 'search-appearance-toolkit-seo-44'), [$this, 'render_schema_tools'], 'seo-44_schema', 'seo44_schema_settings_section'); 60 63 add_settings_field('seo44_enable_schema', __('Enable Schema', 'search-appearance-toolkit-seo-44'), [$this, 'render_checkbox_field'], 'seo-44_schema', 'seo44_schema_settings_section', ['id' => 'enable_schema', 'label' => __('Output JSON-LD to your webpages.', 'search-appearance-toolkit-seo-44')]); 61 // NEW: Add checkbox for scanning content62 64 add_settings_field('seo44_scan_content_for_schema', __('Scan Content for Media', 'search-appearance-toolkit-seo-44'), [$this, 'render_checkbox_field'], 'seo-44_schema', 'seo44_schema_settings_section', [ 63 65 'id' => 'scan_content_for_schema', … … 65 67 'tooltip' => 'This provides more detail to search engines but can be slightly more resource-intensive.' 66 68 ]); 67 // NEW: Add checkbox for advanced schema detection 68 add_settings_field('seo44_enable_advanced_schema', __('Enable Advanced Schema Detection', 'search-appearance-toolkit-seo-44'), [$this, 'render_checkbox_field'], 'seo-44_schema', 'seo44_schema_settings_section', [ 69 'id' => 'enable_advanced_schema', 70 'label' => __('Scan content for patterns to generate FAQ and How-To schema.', 'search-appearance-toolkit-seo-44'), 71 'tooltip' => 'If special formats are detected, they will be added to your page\'s schema structured data.' 72 ]); 69 // UPDATED: Using custom callback to include the Jump Links Block recommendation 70 add_settings_field('seo44_enable_advanced_schema', __('Enable Advanced Schema Detection', 'search-appearance-toolkit-seo-44'), [$this, 'render_advanced_schema_checkbox'], 'seo-44_schema', 'seo44_schema_settings_section'); 71 72 // NEW: Jump Links Integration Field 73 add_settings_field('seo44_enable_jumplinks_schema', 74 __('Generate "Table of Contents" Schema', 'search-appearance-toolkit-seo-44'), 75 [$this, 'render_checkbox_field'], 76 'seo-44_schema', 77 'seo44_schema_settings_section', 78 [ 79 'id' => 'enable_jumplinks_schema', 80 'label' => __('Automatically create "hasPart" schema for headings highlighted in Jump Links Blocks, aligning structured data with Jump Links to reinforce support for Jump-to links in search results.', 'search-appearance-toolkit-seo-44'), 81 'tooltip' => 'This helps Google understand the deep structure of your article by mapping your headings (H2-H4) as distinct sections, complementing the visual Jump Links Block on your page.' 82 ] 83 ); 73 84 add_settings_field('seo44_enable_schema_on_taxonomies', __('Enable Schema on Taxonomies', 'search-appearance-toolkit-seo-44'), [$this, 'render_checkbox_field'], 'seo-44_schema', 'seo44_schema_settings_section', [ 74 85 'id' => 'enable_schema_on_taxonomies', … … 145 156 ] 146 157 ); 147 148 149 150 158 // 3. Add Address Fields (Crucial for Local SEO & Disambiguation) 151 159 add_settings_field( … … 190 198 ); 191 199 192 193 200 add_settings_field( 194 201 'org_phone', … … 391 398 ] 392 399 ); 400 // --- Site APIs Header --- 401 add_settings_field( 402 'seo44_site_apis_header', 403 '', // Empty label for full-width header 404 [$this, 'render_site_apis_header_field'], 405 'seo-44_integrations', 406 'seo44_integrations_section' 407 ); 408 409 // --- YouTube API Key --- 410 add_settings_field( 411 'youtube_api_key', 412 __('YouTube API Key', 'search-appearance-toolkit-seo-44'), 413 [$this, 'render_youtube_api_key_field'], 414 'seo-44_integrations', 415 'seo44_integrations_section', 416 [ 417 'label_for' => 'youtube_api_key', 418 'tooltip' => __('This will be used to improve the Video Object schema created for embedded YouTube Videos.', 'search-appearance-toolkit-seo-44') 419 ] 420 ); 393 421 // --- end of settings_init --- 394 422 } … … 400 428 'enable_tags', 'include_keywords', 'include_author', 401 429 'enable_og_tags', 'enable_twitter_tags', 402 'enable_schema', 'scan_content_for_schema', 'enable_ advanced_schema', 'enable_schema_on_cpts', 'enable_schema_on_taxonomies','enable_organization_schema',430 'enable_schema', 'scan_content_for_schema', 'enable_jumplinks_schema', 'enable_advanced_schema', 'enable_schema_on_cpts', 'enable_schema_on_taxonomies','enable_organization_schema', 403 431 'enable_sitemaps', 'enable_sitemap_ping', 404 432 'sitemap_include_images', 'sitemap_include_content_images', … … 423 451 'org_address_street', 'org_address_city', 'org_address_state', 'org_address_zip', 'org_address_country', 424 452 'org_phone', 'org_email', 'org_area_served', 453 'youtube_api_key', 425 454 ]; 426 455 foreach ($text_fields as $tf) { … … 594 623 <?php 595 624 } 625 // UPDATED: Custom renderer for the Advanced Schema Checkbox 626 public function render_advanced_schema_checkbox() { 627 $id = 'enable_advanced_schema'; 628 $checked = seo44_get_option($id); 629 630 // Output the standard checkbox 631 printf( 632 '<label for="%s"><input type="checkbox" id="%s" name="seo44_settings[%s]" value="1" %s /> %s</label>', 633 esc_attr($id), 634 esc_attr($id), 635 esc_attr($id), 636 checked($checked, 1, false), 637 esc_html__('Scan content for patterns to generate FAQ and How-To schema.', 'search-appearance-toolkit-seo-44') 638 ); 639 seo44_render_tooltip('If special formats are detected, they will be added to your page\'s schema structured data.'); 640 641 // Add the new recommendation text with the link 642 echo '<p class="description">'; 643 echo esc_html__('For the most robust HowTo Schema, use a Jump Links Blocks for your How To steps. ', 'search-appearance-toolkit-seo-44'); 644 echo '<a href="https://seo44plugin.com/search-appearance-toolkit-seo-44/schema-structured-data/" target="_blank">' . esc_html__('(Learn more)', 'search-appearance-toolkit-seo-44') . '</a>'; 645 echo '</p>'; 646 647 } 596 648 597 649 public function render_textarea_field($args) { … … 775 827 echo '<p class="description">' . esc_html__('These tags are used to prove you own your site to search engines. Paste in your verification codes here and they will be added to your site\'s <head>.', 'search-appearance-toolkit-seo-44') . '</p>'; 776 828 } 829 public function render_site_apis_header_field() { 830 echo '<h3>' . esc_html__('Site APIs', 'search-appearance-toolkit-seo-44') . '</h3>'; 831 echo '<p class="description">' . esc_html__('Adding a YouTube API key fetches accurate video upload dates for schema structured data. Without this, video upload dates may fall back to your post publication date.', 'search-appearance-toolkit-seo-44') . '</p>'; 832 } 833 public function render_youtube_api_key_field($args) { 834 $value = seo44_get_option('youtube_api_key'); 835 $tooltip = isset($args['tooltip']) ? $args['tooltip'] : ''; 836 ?> 837 <input type="text" 838 id="youtube_api_key" 839 name="seo44_settings[youtube_api_key]" 840 value="<?php echo esc_attr($value); ?>" 841 class="regular-text code"> 842 843 <?php if ($tooltip) { seo44_render_tooltip($tooltip); } ?> 844 845 <p class="description"> 846 <?php esc_html_e('Enter your YouTube Data API v3 key.', 'search-appearance-toolkit-seo-44'); ?> 847 <a href="https://developers.google.com/youtube/v3/getting-started" target="_blank"> 848 <?php esc_html_e('Get API Key', 'search-appearance-toolkit-seo-44'); ?> 849 </a> 850 </p> 851 <?php 852 } 777 853 778 854 public function settings_page_html() { … … 780 856 return; 781 857 } 782 858 783 859 // --- START: SECURE TAB SWITCHING LOGIC --- 784 860 -
search-appearance-toolkit-seo-44/tags/4.3/js/admin-script.js
r3389330 r3423355 1 1 jQuery(document).ready(function($) { 2 2 3 // --- NEW:Add custom class to metabox heading ---3 // --- Add custom class to metabox heading --- 4 4 // Find the metabox by its ID and then find the title element inside it. 5 5 var metabox = $('#seo44_meta_box'); … … 7 7 metabox.find('h2.hndle').addClass('seo44-metabox-heading'); 8 8 } 9 10 // --- "Use Example" Title Button --- 11 $('#seo44-use-example-title').on('click', function() { 12 var exampleText = $('#seo44-title-example').text(); 13 var titleInput = $('#seo44_title'); 14 titleInput.val(exampleText); 15 16 // Trigger the keyup event to update the snippet preview and character counter 17 titleInput.trigger('keyup'); 18 }); 9 19 10 20 // --- Character Counter Functionality (from v1.4) --- … … 36 46 createCounter('seo44_description', 'seo44_description_char_count', 160); 37 47 38 // --- NEW: "Use Example" Title Button --- 39 $('#seo44-use-example-title').on('click', function() { 40 var exampleText = $('#seo44-title-example').text(); 41 var titleInput = $('#seo44_title'); 42 titleInput.val(exampleText); 48 49 // --- HowTo Schema Trigger Logic --- 50 const howToWrapper = $('#seo44-howto-trigger-wrapper'); 51 const howToCheckbox = $('#seo44_enable_howto'); 52 const descriptionField = $('#seo44_description'); 53 54 // List of triggers (lowercase) 55 const triggers = ['step guide', 'step-by-step', 'how to', 'how-to', 'guide', 'walkthrough', 'directions', 'instructions', 'tutorial']; 56 57 // Flag to track if user has manually overridden the automation 58 let userHasInteracted = false; 59 60 // Listen for manual clicks 61 howToCheckbox.on('change', function() { 62 userHasInteracted = true; 63 }); 64 65 function checkHowToTriggers() { 66 // 1. If user manually touched the box, do nothing. 67 if (userHasInteracted) return; 68 69 // 2. Get description text 70 const text = descriptionField.val().toLowerCase(); 43 71 44 // Trigger the keyup event to update the snippet preview and character counter 45 titleInput.trigger('keyup'); 46 }); 72 // 3. Check for triggers 73 const hasTrigger = triggers.some(trigger => text.includes(trigger)); 74 75 if (hasTrigger) { 76 // Found a trigger! 77 howToWrapper.show(); // Make visible 78 79 // Only check it if it wasn't already checked (to avoid redundant events) 80 if (!howToCheckbox.is(':checked')) { 81 howToCheckbox.prop('checked', true); 82 } 83 } else { 84 // No trigger found. 85 // Behavior: If we are purely automated (user hasn't clicked), 86 // we hide it and uncheck it. 87 howToWrapper.hide(); 88 howToCheckbox.prop('checked', false); 89 } 90 } 91 92 // Run on keyup in description 93 descriptionField.on('keyup change paste', checkHowToTriggers); 94 95 // Run once on load to handle pre-filled content (if user hasn't saved yet) 96 // We delay slightly to ensure values are populated 97 setTimeout(function() { 98 // Only run auto-check if the box is currently hidden/unchecked. 99 // If it's already visible/checked from DB, we respect that state. 100 if (!howToCheckbox.is(':checked')) { 101 checkHowToTriggers(); 102 } else { 103 // If it IS checked from DB, mark as "interacted" so we don't auto-uncheck it 104 // just because the user edits the description and removes a keyword. 105 userHasInteracted = true; 106 } 107 }, 500); 108 47 109 48 110 // --- Snippet Preview Functionality --- 49 111 const titleInput = $('#seo44_title'); 50 112 const descriptionInput = $('#seo44_description'); 51 113 52 114 const previewHeaderUrl = $('.seo44-preview-breadcrumb-url'); 53 115 const previewTitle = $('.seo44-preview-title'); … … 58 120 const siteName = seo44_data.site_name; 59 121 const permalink = seo44_data.permalink; 60 122 61 123 function updatePreview() { 62 124 // Update Title 63 125 let titleVal = titleInput.val(); 64 previewTitle.text(titleVal ? titleVal : defaultTitle); 65 66 67 // if (titleVal) { 68 // previewTitle.text(titleVal); 69 // } else { 70 // If the input is empty, show "Post Title - Site Name" 71 // previewTitle.text(defaultTitle + ' - ' + siteName); 72 //} 126 previewTitle.text(titleVal ? titleVal : defaultTitle); 73 127 74 128 // Update Description … … 80 134 previewDescription.text('Enter a meta description to see how it will appear in search results...'); 81 135 } 82 83 // Update Breadcrumb URL136 137 // Update Breadcrumb URL 84 138 let breadcrumb = permalink.replace(/^https?:\/\//, '').replace(/\/$/, ''); 85 139 breadcrumb = breadcrumb.replace(/\//g, ' › '); // › is the > symbol -
search-appearance-toolkit-seo-44/tags/4.3/readme.txt
r3405454 r3423355 3 3 Tags: seo, on-page seo, schema, structured data, xml sitemaps 4 4 Requires at least: 5.5 5 Tested up to: 6. 86 Stable tag: 4. 25 Tested up to: 6.9 6 Stable tag: 4.3 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 72 72 * **Knowledge Graph Control:** A dedicated interface to manage your brand's digital identity. Define your Founder, Founding Date, Contact Info, and professional Credentials to improve E-E-A-T signals. 73 73 * **Include Images and Videos:** A built-in tool automatically finds all images and embedded YouTube videos in your content and adds them to the schema, boosting their appearance in search results. 74 * **FAQ and How-To Detection:** Enable a smart scanner to detect patterns in your content for FAQ and How-To sections on your website and incorporate this useful format into the schema. 74 * **FAQ and How-To Detection:** Enable smart scanners to detect patterns in your content for FAQ and How-To sections on your website and incorporate this useful format into the schema. 75 * **Jump Links HowTo Scanner:** The plugin can use a Jump Links Block as a "Map" to generate detailed HowTo schema steps, while simultaneously scanning your content for Prep Time, Yield, Supplies, and Tools. 76 * **Table of Contents Schema:** Automatically generates `hasPart` structured data that mirrors your Jump Links Block, helping search engines understand your article's deep structure. 75 77 * **Modern Output:** All structured data is generated in the modern JSON-LD format preferred by search engines, following the guidelines set by [Schema.org](https://schema.org/). 76 78 * **Granular Control:** Tailor the Schema settings to fit your site's needs through Enable/disable settings, including on Custom Post Types and Taxonomies. … … 101 103 The Search Appearance Toolkit serves as a hub for connecting your site to essential third-party services, helping you to create valuable analytics data and site insights. 102 104 103 = Site Verification =104 Verify your site with search engines quickly and easily. Paste your verification codes for **Google Search Console** and **Bing Webmaster Tools** into the corresponding fields in the Integrations tab. The plugin handles the rest, adding site verification meta tags to your website's head. No coding required.105 106 105 = Google Tag Manager (GTM) Integration = 107 106 Easily integrate with Google Tag Manager by pasting your `GTM-XXXXXXX` ID into the settings field. The plugin will correctly and safely inject the GTM scripts into your site's `<head>` and `<body>` on every page. No coding required. … … 115 114 * **Jump Link Click Tracking:** Tracks engagement with your Jump Links Block by pushing a `jump_link_click` event, letting you see which sections your users are most interested in. 116 115 117 SEO 44's integration with Google Tag Manager pushes valuable events to the `dataLayer` for use in Google Analytics. The plugin includes an import file and detailed [Instructions for setting up Google Tag Manager and Google Analytics to receive event-tracking data](https://seo44plugin.com/search-appearance-toolkit-seo-44/integrations-setup-guide/). 116 SEO 44's integration with Google Tag Manager pushes valuable events to the `dataLayer` for use in Google Analytics. The plugin includes an import file and detailed [Instructions for setting up Google Tag Manager and Google Analytics to receive event-tracking data](https://seo44plugin.com/search-appearance-toolkit-seo-44/integrations-setup-guide/). 117 118 = Site Verification = 119 Verify your site with search engines quickly and easily. Paste your verification codes for **Google Search Console** and **Bing Webmaster Tools** into the corresponding fields in the Integrations tab. The plugin handles the rest, adding site verification meta tags to your website's head. No coding required. 120 121 = YouTube Data API = 122 To ensure that your site's video schema is as accurate as possible, you may add your YouTube Data API Key. The plugin uses this key to fetch the upload date for any YouTube video embedded in your content, replacing less reliable page scraping options and fallbacks to the post publish date. 118 123 119 124 == Frequently Asked Questions == … … 122 127 Search Appearance Toolkit (SEO 44) gives you control over the technical, on-page SEO factors that help search engines understand and rank your content. Key benefits include: 123 128 * **Optimized Snippets:** Control how your titles and descriptions appear in search results. 124 * **Rich Results:** The advanced Schema.org data helps you earn rich results like FAQs, How-Tos, and breadcrumbs in Google.129 * **Rich Results:** The advanced Schema.org data improves indexing and helps you earn rich results in Google. 125 130 * **Crawlability:** A clean and comprehensive XML sitemap ensures search engines can find and index all of your important content and images. 126 131 * **User Engagement:** The Jump Links Block improves user experience, which is a positive ranking signal, and can help you earn "Jump to" links in search results. … … 154 159 SEO 44 helps your content look great when shared on social media platforms. You can enable the automatic generation of **Open Graph** (og:) tags, which Facebook, LinkedIn, and Pinterest use, and **Twitter Card** meta tags for when your content appears on X (formerly Twitter). This ensures your posts have the correct title, description, and preview image when shared. 155 160 156 Use the plugin's Google Tag Manager Integration to facilitate additional connections with soc lal media platforms. Use the Organization Schema settings to associate all of your social media profiles with your website.161 Use the plugin's Google Tag Manager Integration to facilitate additional connections with social media platforms. Use the Organization Schema settings to associate all of your social media profiles with your website. 157 162 158 163 = How are social media images handled? = … … 177 182 178 183 = What are the benefits of using FAQPage and HowTo schema? = 179 The plugin intelligently scans your content for patterns that match question-and-answer formats or step-by-step instructions (when this option is enabled). The plugin locates this content and automatically generates FAQPage or HowTo schema that presents this content within the JSON-LD.180 181 The benefit of this is that Google can use this structured data to display your content in special, highly visible formats in the search results. An FAQ page might appear as a rich snippet with expandable questions , while a How-To article can be featured in a step-by-step guide. Rich snippets make your search results stand out, which can significantlyimprove your click-through rate (CTR).184 The plugin intelligently scans your content for patterns that match question-and-answer formats or step-by-step instructions (when this option is enabled). The plugin locates this content and automatically generates FAQPage or HowTo schema that presents this content as structured data. 185 186 The benefit of this is that Google can use this structured data to display your content in special, highly visible formats in the search results. An FAQ page might appear as a rich snippet with expandable questions or feature within a People Also Ask (PAA) result, while a How-To article can be featured in a step-by-step guide in AI Overviews. Search results that stand out can improve your click-through rate (CTR). 182 187 183 188 Read more about the [FAQPage and HowTo Schema](https://seo44plugin.com/search-appearance-toolkit-seo-44/schema-structured-data/#benefits-of-faqpage-and-howto-schema) created by SEO 44. … … 285 290 * **Google:** [Terms of Service](https://policies.google.com/terms), [Privacy Policy](https://policies.google.com/privacy) 286 291 * **Bing (Microsoft):** [Microsoft Services Agreement](https://www.microsoft.com/en-us/servicesagreement/), [Microsoft Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) 292 293 = YouTube Data API Integration & Video Metadata = 294 295 * **Service Description:** This plugin connects to YouTube to fetch the "Upload Date" for videos embedded in your content. This ensures your VideoObject schema is accurate. 296 * **Data Sent and Conditions:** 297 * **Method 1 (API):** If you provide a YouTube API Key in settings, the plugin sends the Video ID to the Google Data API. 298 * **Method 2 (Public Fallback):** If no API Key is present, the plugin acts as a standard browser and fetches the public video page (via `wp_remote_get`) to locate the upload date in the page meta tags. 299 * **Conditions:** This occurs automatically when a post with a YouTube embed is updated, provided that Schema generation is enabled. 300 * **Service Provider Links:** 301 * **YouTube:** [Terms of Service](https://www.youtube.com/t/terms), [Google Privacy Policy](https://policies.google.com/privacy) 287 302 288 303 == Screenshots == … … 315 330 == Changelog == 316 331 332 = 4.3.0 = 333 * FEATURE: **Table of Contents Schema:** Automatically generates `hasPart` schema synchronized with the Jump Links Block to support "Jump-to" links in search results. 334 * FEATURE: **Advanced HowTo Schema:** A new "Block-Aware" scanner uses the Jump Links Block to strictly define steps while intelligently mining the introduction for tools, time, yield, and video data. 335 * TWEAK: Added a "Generate HowTo Schema" checkbox to the SEO 44 metabox, giving users explicit control over when the advanced scanner runs. 336 * FEATURE: **YouTube Integration:** Added support for the YouTube Data API v3 to fetch accurate video upload dates for VideoObject schema. 337 * TESTED: Tested to WordPress Version 6.9 338 317 339 = 4.2.0 = 318 340 * FEATURE: **Rich Organization Schema:** Added a comprehensive settings section to generate Organization structured data for the Knowledge Graph. … … 325 347 * TWEAK: Updated the image uploader JavaScript to support multiple distinct upload buttons on the settings page. 326 348 327 = 4.1.0 =328 **Jump Links Block Updates:**329 * FEATURE: **Sticky Positioning:** Keep your table of contents visible while users scroll. Includes a "Top Offset" slider to clear sticky headers, a "Jump Offset" slider to ensure that the sticky header does not cover the heading text, and a "Disable on Mobile" toggle to preserve screen space on small devices.330 * FEATURE: **Auto-Hide Title:** Implemented a smart "sticky state" detection. When the block sticks, the title gently collapses and fades out to keep the interface clean (this occurs when a block title is used alongside sticky positioning).331 * FEATURE: **Smart Indentation:** Added a "Create Visual Hierarchy" toggle. When enabled, H3 and H4 sub-headings are visually indented to create a clear, nested outline structure.332 * FEATURE: **Block Background:** You can now set a background color for the entire block container, perfect for creating "card-style" floating navigation.333 * FEATURE: **ScrollSpy:** Automatically highlights the active link in the table of contents as the user scrolls through the corresponding section of the post.334 * FEATURE: Added support for Border and Spacing controls. You can now add borders, rounded corners, margins, and padding to the Jump Links block directly from the editor settings.335 * FEATURE: Added a "Title tag" control. You can now choose the specific HTML tag (H2, H3, H4, H5, Paragraph, or Div) for the "On This Page" heading to better match your document structure.336 * REFACTOR: Optimized the block's styling logic to use CSS variables on the parent container instead of inline styles for every link. This reduces the block's HTML size and improves rendering performance.337 * FIX: Resolved an accessibility and HTML validation issue where using multiple Jump Links blocks on a single page created duplicate element IDs. Each block now generates a unique instance ID.338 * PERFORMANCE: Refactored the front-end JavaScript to use event delegation for smooth scrolling. This reduces memory usage by attaching a single event listener to the block instead of individual listeners for every link.339 * TWEAK: Reorganized the sidebar settings for better clarity between Block Title settings and Content Inclusion settings.340 341 = 4.0.0 =342 * FEATURE: Added a new "Integrations" tab for third-party services like Google Tag Manager and Webmaster Tools.343 * FEATURE: Added Google Tag Manager (GTM) integration. The plugin can now automatically inject the GTM container script into the site's <head> and <body> based on your ID.344 * FEATURE: Added Webmaster Verification. You can now add your Google Search Console and Bing Webmaster Tools verification codes directly from the plugin settings.345 * FEATURE: Added automatic GTM event tracking. When enabled, the plugin can push the following events to the dataLayer:346 * Rich SEO dataLayer: Pushes page type, category, author, and tags on page load for advanced GTM triggers.347 * Scroll Depth Tracking: Pushes 'scroll_depth' events at 25%, 50%, 75%, and 100% of the page.348 * External & Affiliate Clicks:* Pushes 'external_link_click' or 'affiliate_link_click' (for `rel="sponsored"` links).349 * Jump Link Clicks: Pushes a 'jump_link_click' event when a user clicks a link in the Jump Links Block.350 * FEATURE: Added a Google Tag Manager recipe import file (`seo44-gtm-recipe-importer.json`) and new FAQ instructions to fully configure GTM and GA4 event tracking.351 * ENHANCEMENT: Centralized all GTM event tracking into a new, efficient `global-tracker.js` file that uses event delegation for better performance.352 * TWEAK: Improved the "Integrations" settings page UI for clarity, adding clarifying tooltips and a file downloader.353 354 349 For a complete list of changes, please see the [full changelog](https://seo44plugin.com/search-appearance-toolkit-seo-44/changelog/) or the `changelog.txt` file included with the plugin. 355 350 -
search-appearance-toolkit-seo-44/tags/4.3/seo-44.php
r3405454 r3423355 4 4 * Plugin URI: https://www.sethcreates.com/plugins-for-wordpress/search-appearance-toolkit-seo-44/ 5 5 * Description: A lightweight, powerful SEO plugin for essential meta tags, advanced schema, XML sitemaps, jump links, and easy migration from other plugins. 6 * Version: 4. 2.06 * Version: 4.3 7 7 * Author: Seth Smigelski 8 8 * Author URI: https://www.sethcreates.com/plugins-for-wordpress/ … … 14 14 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly 15 15 16 define( 'SEO44_VERSION', '4. 2.0' );16 define( 'SEO44_VERSION', '4.3' ); 17 17 define( 'SEO44_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); 18 18 -
search-appearance-toolkit-seo-44/trunk/README.md
r3405454 r3423355 8 8 * **Tags:** seo, on-page seo, schema, structured data, xml sitemaps 9 9 * **Requires at least:** 5.5 10 * **Tested up to:** 6. 811 * **Stable tag:** 4. 2.010 * **Tested up to:** 6.9 11 * **Stable tag:** 4.3.0 12 12 * **Requires PHP:** 7.4 13 13 * **License:** GPLv2 or later … … 85 85 * **Knowledge Graph Control:** A dedicated interface to manage your brand's digital identity. Define your Founder, Founding Date, Contact Info, and professional Credentials to improve E-E-A-T signals. 86 86 * **Include Images and Videos:** A built-in tool automatically finds all images and embedded YouTube videos in your content and adds them to the schema, boosting their appearance in search results. 87 * **FAQ and How-To Detection:** Enable a smart scanner to detect patterns in your content for FAQ and How-To sections on your website and incorporate this useful format into the schema. 87 * **FAQ and How-To Detection:** Enable smart scanners to detect patterns in your content for FAQ and How-To sections on your website and incorporate this useful format into the schema. 88 * **Jump Links HowTo Scanner:** The plugin can use a Jump Links Block as a "Map" to generate detailed HowTo schema steps, while simultaneously scanning your content for Prep Time, Yield, Supplies, and Tools. 89 * **Table of Contents Schema:** Automatically generates `hasPart` structured data that mirrors your Jump Links Block, helping search engines understand your article's deep structure. 88 90 * **Modern Output:** All structured data is generated in the modern JSON-LD format preferred by search engines, following the guidelines set by [Schema.org](https://schema.org/). 89 91 * **Granular Control:** Tailor the Schema settings to fit your site's needs through Enable/disable settings, including on Custom Post Types and Taxonomies. … … 128 130 SEO 44's integration with Google Tag Manager pushes valuable events to the `dataLayer` for use in Google Analytics. The plugin provides an import file and detailed instructions for setting up Google Tag Manager and Google Analytics to receive event tracking data. 129 131 130 ## Site Verification Tags132 ### Site Verification Tags 131 133 Verify your site with search engines quickly and easily. Paste your verification codes for **Google Search Console** and **Bing Webmaster Tools** into the corresponding fields in the Integrations tab. The plugin handles the rest, adding site verification meta tags to your website's head. No coding required. 134 135 ### YouTube Data API 136 To ensure that your site's video schema is as accurate as possible, you may add your YouTube Data API Key. The plugin uses this key to fetch the upload date for any YouTube video embedded in your content, replacing less reliable page scraping options and fallbacks to the post publish date. 132 137 133 138 --- … … 138 143 Search Appearance Toolkit (SEO 44) gives you control over the technical, on-page SEO factors that help search engines understand and rank your content. Key benefits include: 139 144 * **Optimized Snippets:** Control how your titles and descriptions appear in search results. 140 * **Rich Results:** The advanced Schema.org data helps you earn rich results like FAQs, How-Tos, and breadcrumbs in Google.145 * **Rich Results:** The advanced Schema.org data improves indexing and helps you earn rich results in Google. 141 146 * **Crawlability:** A clean and comprehensive XML sitemap ensures search engines can find and index all of your important content and images. 142 147 * **User Engagement:** The Jump Links Block improves user experience, which is a positive ranking signal, and can help you earn "Jump to" links in search results. … … 170 175 SEO 44 helps your content look great when shared on social media platforms. You can enable the automatic generation of **Open Graph** (og:) tags, which Facebook, LinkedIn, and Pinterest use, and **Twitter Card** meta tags for when your content appears on X (formerly Twitter). This ensures your posts have the correct title, description, and preview image when shared. 171 176 172 Use the plugin's Google Tag Manager Integration to facilitate additional connections with soc lal media platforms. Use the Organization Schema settings to associate all of your social media profiles with your website.177 Use the plugin's Google Tag Manager Integration to facilitate additional connections with social media platforms. Use the Organization Schema settings to associate all of your social media profiles with your website. 173 178 174 179 ### How are social media images handled? … … 193 198 194 199 ### What are the benefits of using FAQPage and HowTo schema? 195 The plugin intelligently scans your content for patterns that match question-and-answer formats or step-by-step instructions (when this option is enabled). The plugin locates this content and automatically generates FAQPage or HowTo schema that presents this content within the JSON-LD.196 197 The benefit of this is that Google can use this structured data to display your content in special, highly visible formats in the search results. An FAQ page might appear as a rich snippet with expandable questions , while a How-To article can be featured in a step-by-step guide. Rich snippets make your search results stand out, which can significantlyimprove your click-through rate (CTR).200 The plugin intelligently scans your content for patterns that match question-and-answer formats or step-by-step instructions (when this option is enabled). The plugin locates this content and automatically generates FAQPage or HowTo schema that presents this content as structured data. 201 202 The benefit of this is that Google can use this structured data to display your content in special, highly visible formats in the search results. An FAQ page might appear as a rich snippet with expandable questions or feature within a People Also Ask (PAA) result, while a How-To article can be featured in a step-by-step guide in AI Overviews. Search results that stand out can improve your click-through rate (CTR). 198 203 199 204 Read more about the [FAQPage and HowTo Schema](https://seo44plugin.com/search-appearance-toolkit-seo-44/schema-structured-data/#benefits-of-faqpage-and-howto-schema) created by SEO 44. … … 457 462 * **Google:** [Terms of Service](https://policies.google.com/terms), [Privacy Policy](https://policies.google.com/privacy) 458 463 * **Bing (Microsoft):** [Microsoft Services Agreement](https://www.microsoft.com/en-us/servicesagreement/), [Microsoft Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) 464 465 ### YouTube Data API Integration & Video Metadata 466 467 * **Service Description:** This plugin connects to YouTube to fetch the "Upload Date" for videos embedded in your content. This ensures your VideoObject schema is accurate. 468 * **Data Sent and Conditions:** 469 * **Method 1 (API):** If you provide a YouTube API Key in settings, the plugin sends the Video ID to the Google Data API. 470 * **Method 2 (Public Fallback):** If no API Key is present, the plugin acts as a standard browser and fetches the public video page (via `wp_remote_get`) to locate the upload date in the page meta tags. 471 * **Conditions:** This occurs automatically when a post with a YouTube embed is updated, provided that Schema generation is enabled. 472 * **Service Provider Links:** 473 * **YouTube:** [Terms of Service](https://www.youtube.com/t/terms), [Google Privacy Policy](https://policies.google.com/privacy) 459 474 460 475 --- … … 574 589 575 590 ## Changelog 591 592 ### 4.3.0 593 * **FEATURE:** **Table of Contents Schema:** Automatically generates `hasPart` schema synchronized with the Jump Links Block to support "Jump-to" links in search results. 594 * **FEATURE:** **Advanced HowTo Schema:** A new "Block-Aware" scanner uses the Jump Links Block to strictly define steps while intelligently mining the introduction for tools, time, yield, and video data. 595 * **TWEAK:** Added a "Generate HowTo Schema" checkbox to the SEO 44 metabox, giving users explicit control over when the advanced scanner runs. 596 * **FEATURE:** **YouTube Integration:** Added support for the YouTube Data API v3 to fetch accurate video upload dates for VideoObject schema. 597 * **TESTED:** Tested to WordPress Version 6.9 576 598 577 599 ### 4.2.0 … … 585 607 * **TWEAK:** Updated the image uploader JavaScript to support multiple distinct upload buttons on the settings page. 586 608 587 ### 4.1.0588 **Jump Links Block Updates:**589 * **FEATURE:** **Sticky Positioning:** Keep your table of contents visible while users scroll. Includes a "Top Offset" slider to clear sticky headers, a "Jump Offset" slider to ensure that the sticky header does not cover the heading text, and a "Disable on Mobile" toggle to preserve screen space on small devices.590 * **FEATURE:** **Auto-Hide Title:** Implemented a smart "sticky state" detection. When the block sticks, the title gently collapses and fades out to keep the interface clean (this occurs when a block title is used alongside sticky positioning).591 * **FEATURE:** **Smart Indentation:** Added a "Create Visual Hierarchy" toggle. When enabled, H3 and H4 sub-headings are visually indented to create a clear, nested outline structure.592 * **FEATURE:** **Block Background:** You can now set a background color for the entire block container, perfect for creating "card-style" floating navigation.593 * **FEATURE:** **ScrollSpy:** Automatically highlights the active link in the table of contents as the user scrolls through the corresponding section of the post.594 * **FEATURE:** Added support for Border and Spacing controls. You can now add borders, rounded corners, margins, and padding to the Jump Links block directly from the editor settings.595 * **FEATURE:** Added a "Title tag" control. You can now choose the specific HTML tag (H2, H3, H4, H5, Paragraph, or Div) for the "On This Page" heading to better match your document structure.596 * **REFACTOR:** Optimized the block's styling logic to use CSS variables on the parent container instead of inline styles for every link. This reduces the block's HTML size and improves rendering performance.597 * **FIX:** Resolved an accessibility and HTML validation issue where using multiple Jump Links blocks on a single page created duplicate element IDs. Each block now generates a unique instance ID.598 * **PERFORMANCE:** Refactored the front-end JavaScript to use event delegation for smooth scrolling. This reduces memory usage by attaching a single event listener to the block instead of individual listeners for every link.599 * **TWEAK:** Reorganized the sidebar settings for better clarity between Block Title settings and Content Inclusion settings.600 601 ### 4.0.0602 * **FEATURE:** Added a new "Integrations" tab for third-party services like Google Tag Manager and Webmaster Tools.603 * **FEATURE:** Added Google Tag Manager (GTM) integration. The plugin can now automatically inject the GTM container script into the site's <head> and <body> based on your ID.604 * **FEATURE:** Added Webmaster Verification. You can now add your Google Search Console and Bing Webmaster Tools verification codes directly from the plugin settings.605 * **FEATURE:** Added automatic GTM event tracking. When enabled, the plugin can push the following events to the dataLayer:606 * Rich SEO dataLayer: Pushes page type, category, author, and tags on page load for advanced GTM triggers.607 * Scroll Depth Tracking: Pushes 'scroll_depth' events at 25%, 50%, 75%, and 100% of the page.608 * External & Affiliate Clicks:* Pushes 'external_link_click' or 'affiliate_link_click' (for `rel="sponsored"` links).609 * Jump Link Clicks: Pushes a 'jump_link_click' event when a user clicks a link in the Jump Links Block.610 * **FEATURE:** Added a Google Tag Manager recipe import file (`seo44-gtm-recipe-importer.json`) and new FAQ instructions to fully configure GTM and GA4 event tracking.611 * **ENHANCEMENT:** Centralized all GTM event tracking into a new, efficient `global-tracker.js` file that uses event delegation for better performance.612 * **TWEAK:** Improved the "Integrations" settings page UI for clarity, adding clarifying tooltips and a file downloader.613 614 For a complete list of changes, please see the [full changelog](https://github.com/SethSmigelski/search-appearance-toolkit-seo-44/blob/main/changelog.txt) or the `changelog.txt` file included with the plugin.615 616 609 --- 617 610 -
search-appearance-toolkit-seo-44/trunk/build/index.asset.php
r3405454 r3423355 1 <?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => ' 9e72772c3731a4c27337');1 <?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '2c1769c1ec087de88890'); -
search-appearance-toolkit-seo-44/trunk/build/index.js
r3403604 r3423355 1 (()=>{"use strict";var e,t={313 :()=>{const e=window.wp.blocks,t=window.React,a=window.wp.i18n,o=window.wp.blockEditor,l=window.wp.components,n=window.wp.data,r=window.wp.element;function s(e){return(new DOMParser).parseFromString(e,"text/html").body.textContent||""}const i=(0,t.createElement)("svg",{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M3 21v-2h18v2zm8-4v-6.175L9.4 12.4L8 11l4-4l4 4l-1.4 1.4l-1.6-1.575V17zM3 5V3h18v2z"})),c=(0,t.createElement)("svg",{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M3 5V3h18v2zm9 12l-4-4l1.4-1.4l1.6 1.575V7h2v6.175l1.6-1.575L16 13zm-9 4v-2h18v2z"})),h=(0,t.createElement)("svg",{className:"arrow-down",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10l-6 6z"})),p=JSON.parse('{"UU":"seo44/jump-links"}');(0,e.registerBlockType)(p.UU,{edit:function({attributes:e,setAttributes:p}){const{headingLevels:k,headings:u,showHeading:d,headingText:m,headingTag:g,layout:_,listStyle:b,isEditing:v,isCollapsible:f,isSmartIndentation:x,fontSize:w,textColor:C,linkColor:E,blockBackgroundColor:y,linkBackgroundColor:B,linkBackgroundColorHover:S,linkBorderColor:z,linkBorderRadius:T,isSticky:N,stickyOffset:H,jumpOffset:M,stickyStrategy:P}=e,O={color:C,fontSize:w,"--jump-link-font-size":w||"18px","--seo44-link-color":E,"--seo44-link-bg":"horizontal"===_?B:void 0,"--seo44-link-hover-bg":"horizontal"===_?S:void 0,"--seo44-link-border-color":"horizontal"===_?z:void 0,"--seo44-link-radius":"horizontal"===_&&T?`${T}px`:void 0,"--seo44-block-bg":y,"--seo44-sticky-offset":N?`${H}px`:void 0},I="ol"===b?"ol":"ul",{createInfoNotice:L}=(0,n.useDispatch)("core/notices"),j=e.blockInstanceId?`seo44-jump-links-list-${e.blockInstanceId}`:"seo44-jump-links-list",$=(0,o.useBlockProps)({style:O});$.className=`${$.className} ${"horizontal"===_?"is-layout-horizontal":""} ${f&&!v?"is-collapsible":""} ${"none"===b?"list-style-none":""}`.trim();const D=(0,n.useSelect)(e=>e("core/block-editor").getBlocks(),[]),{updateBlockAttributes:V}=(0,n.useDispatch)("core/block-editor");(0,r.useEffect)(()=>{const t={};e.blockInstanceId||(t.blockInstanceId=Math.random().toString(36).substr(2,9)),Object.keys(t).length>0&&p(t);const o=D.filter(e=>"core/heading"===e.name&&k.includes(`h${e.attributes.level}`)),l=new Set;let n=!1;const r=new Map(u.map(e=>[e.anchor,e])),i=[];for(const e of o){const t=s(e.attributes.content);let a=e.attributes.anchor||t.toLowerCase().replace(/[^a-z0-9\s-]/g,"").trim().replace(/\s+/g,"-"),o=a,c=2;for(;l.has(o);)o=`${a}-${c}`,c++,n=!0;l.add(o),e.attributes.anchor!==o&&V(e.clientId,{anchor:o});const h=r.get(e.attributes.anchor)||r.get(o),p=h&&h.linkText!==h.text?h.linkText:t,k=!h||h.isVisible;i.push({anchor:o,text:t,linkText:p,isVisible:k,level:e.attributes.level})}JSON.stringify(i)!==JSON.stringify(u)&&p({headings:i}),n&&L((0,a.__)("Jump Links Block: Duplicate headings were found. Unique IDs have been auto-generated, but this may be a sign of redundancy. Please review your headings for clarity.","search-appearance-toolkit-seo-44"),{type:"snackbar"})},[D,k,u,e.blockInstanceId,p,V,L]),(0,r.useEffect)(()=>{"horizontal"===_&&"none"!==b&&p({listStyle:"none"})},[_,b,p]);const R=(e,t)=>{const a=[...u],o=a.splice(e,1)[0];"up"===t?a.splice(e-1,0,o):a.splice(e+1,0,o),p({headings:a})},J=e=>{const t=k.includes(e)?k.filter(t=>t!==e):[...k,e];p({headingLevels:t.sort()})};return(0,t.createElement)(r.Fragment,null,(0,t.createElement)(o.InspectorControls,null,(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Presentation","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ButtonGroup,null,(0,t.createElement)(l.Button,{isPrimary:!v,isPressed:!v,onClick:()=>p({isEditing:!1})},(0,a.__)("Viewing Mode","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.Button,{isPrimary:v,isPressed:v,onClick:()=>p({isEditing:!0})},(0,a.__)("Editing Mode","search-appearance-toolkit-seo-44"))),(0,t.createElement)("p",{className:"description"},(0,a.__)("Switch to Editing Mode to customize link text, visibility, and order.","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Appearance","search-appearance-toolkit-seo-44")},(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Layout","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.ButtonGroup,null,(0,t.createElement)(l.Button,{isPrimary:"vertical"===_,isPressed:"vertical"===_,onClick:()=>p({layout:"vertical"})},(0,a.__)("Vertical","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.Button,{isPrimary:"horizontal"===_,isPressed:"horizontal"===_,onClick:()=>p({layout:"horizontal"})},(0,a.__)("Horizontal","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Make Jump Links Area Expandable","search-appearance-toolkit-seo-44"),help:(0,a.__)('Conserve screen space by collapsing a long list of jump links, providing users with an elegant "show more" button to see the entire list.',"search-appearance-toolkit-seo-44"),checked:f,onChange:()=>p({isCollapsible:!f}),__nextHasNoMarginBottom:!0}),(0,t.createElement)(l.SelectControl,{label:(0,a.__)("List Style","search-appearance-toolkit-seo-44"),value:b,options:[{label:(0,a.__)("Bulleted","search-appearance-toolkit-seo-44"),value:"ul"},{label:(0,a.__)("Numbered","search-appearance-toolkit-seo-44"),value:"ol"},{label:(0,a.__)("None","search-appearance-toolkit-seo-44"),value:"none"}],onChange:e=>p({listStyle:e}),disabled:"horizontal"===_,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.FontSizePicker,{fontSizes:[{name:(0,a.__)("S","search-appearance-toolkit-seo-44"),slug:"small",size:"14px"},{name:(0,a.__)("M","search-appearance-toolkit-seo-44"),slug:"normal",size:"17px"},{name:(0,a.__)("L","search-appearance-toolkit-seo-44"),slug:"large",size:"20px"},{name:(0,a.__)("XL","search-appearance-toolkit-seo-44"),slug:"extra-large",size:"23px"}],value:w,onChange:e=>p({fontSize:e}),withReset:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(o.PanelColorSettings,{title:(0,a.__)("Colors","search-appearance-toolkit-seo-44"),colorSettings:[{value:y,onChange:e=>p({blockBackgroundColor:e}),label:(0,a.__)("Block Background","search-appearance-toolkit-seo-44")},{value:E,onChange:e=>p({linkColor:e}),label:(0,a.__)("Link Color","search-appearance-toolkit-seo-44")},{value:C,onChange:e=>p({textColor:e}),label:(0,a.__)("Other Text Color","search-appearance-toolkit-seo-44")}]}),"horizontal"===_&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)("hr",null),(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Horizontal Link Styles","search-appearance-toolkit-seo-44"))),(0,t.createElement)(o.PanelColorSettings,{title:(0,a.__)("Link Colors","search-appearance-toolkit-seo-44"),colorSettings:[{value:B,onChange:e=>p({linkBackgroundColor:e}),label:(0,a.__)("Background","search-appearance-toolkit-seo-44")},{value:S,onChange:e=>p({linkBackgroundColorHover:e}),label:(0,a.__)("Background Hover","search-appearance-toolkit-seo-44")},{value:z,onChange:e=>p({linkBorderColor:e}),label:(0,a.__)("Border","search-appearance-toolkit-seo-44")}]}),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Link Border Radius","search-appearance-toolkit-seo-44"),value:T,onChange:e=>p({linkBorderRadius:e}),min:0,max:50,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}))),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Content Settings","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Display Block Title","search-appearance-toolkit-seo-44"),checked:d,onChange:()=>p({showHeading:!d}),__nextHasNoMarginBottom:!0}),d&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)(l.TextControl,{label:(0,a.__)("Title Text","search-appearance-toolkit-seo-44"),value:m,onChange:e=>p({headingText:e}),help:(0,a.__)("The text that appears above your list of links.","search-appearance-toolkit-seo-44")}),(0,t.createElement)(l.SelectControl,{label:(0,a.__)("Title Tag","search-appearance-toolkit-seo-44"),value:g,options:[{label:"H2",value:"h2"},{label:"H3",value:"h3"},{label:"H4",value:"h4"},{label:"H5",value:"h5"},{label:"Paragraph (Bold)",value:"p"},{label:"Div (No Semantic Value)",value:"div"}],onChange:e=>p({headingTag:e}),help:(0,a.__)("Choose a level that fits your page's structure.","search-appearance-toolkit-seo-44"),__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0})),(0,t.createElement)("hr",null),(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Included Headings","search-appearance-toolkit-seo-44"))),(0,t.createElement)("p",{className:"description",style:{marginBottom:"10px"}},(0,a.__)("Select which heading levels from your post content should appear in the jump links list.","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.CheckboxControl,{label:"H2",checked:k.includes("h2"),onChange:()=>J("h2")}),(0,t.createElement)(l.CheckboxControl,{label:"H3",checked:k.includes("h3"),onChange:()=>J("h3")}),(0,t.createElement)(l.CheckboxControl,{label:"H4",checked:k.includes("h4"),onChange:()=>J("h4")}),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Create Visual Hierarchy","search-appearance-toolkit-seo-44"),help:(0,a.__)("Indents sub-headings (H3, H4) to create a nested outline structure.","search-appearance-toolkit-seo-44"),checked:x,onChange:()=>p({isSmartIndentation:!x}),__nextHasNoMarginBottom:!0})),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Position Settings","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Sticky Position","search-appearance-toolkit-seo-44"),help:(0,a.__)("Keep the table of contents visible while scrolling.","search-appearance-toolkit-seo-44"),checked:N,onChange:()=>p({isSticky:!N}),__nextHasNoMarginBottom:!0}),N&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)("p",{className:"description",style:{marginBottom:"15px"}},(0,a.__)("Customize how the block behaves when it sticks to the top of the screen.","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Top Offset (px)","search-appearance-toolkit-seo-44"),help:(0,a.__)("The distance between the top of the screen and the block when stuck (useful for clearing sticky headers).","search-appearance-toolkit-seo-44"),value:H,onChange:e=>p({stickyOffset:e}),min:0,max:200,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Jump Offset (px)","search-appearance-toolkit-seo-44"),help:(0,a.__)("The buffer distance to stop *before* the heading. Increase this if your sticky header covers the text.","search-appearance-toolkit-seo-44"),value:M,onChange:e=>p({jumpOffset:e}),min:0,max:200,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Disable on Mobile","search-appearance-toolkit-seo-44"),help:(0,a.__)("Prevents the block from sticking on small screens to save reading space.","search-appearance-toolkit-seo-44"),checked:"desktop-only"===P,onChange:e=>p({stickyStrategy:e?"desktop-only":"always"}),__nextHasNoMarginBottom:!0})))),(0,t.createElement)("div",{...$},d&&(0,t.createElement)(o.RichText,{tagName:g,className:"wp-block-seo44-jump-links-heading",value:m,onChange:e=>p({headingText:e}),placeholder:(0,a.__)("On This Page","search-appearance-toolkit-seo-44")}),u.length>0?(0,t.createElement)("nav",{"aria-label":(0,a.__)("Table of contents","search-appearance-toolkit-seo-44")},(0,t.createElement)(I,{id:j},u.map((e,o)=>v?(0,t.createElement)("li",{key:e.anchor},(0,t.createElement)(l.TextControl,{value:e.linkText,onChange:e=>((e,t)=>{const a=[...u];a[e].linkText=t,p({headings:a})})(o,e)}),(0,t.createElement)("div",{className:"edit-controls-wrapper"},(0,t.createElement)("div",{className:"reorder-buttons"},(0,t.createElement)(l.Button,{icon:i,label:(0,a.__)("Move Up","search-appearance-toolkit-seo-44"),onClick:()=>R(o,"up"),disabled:0===o}),(0,t.createElement)(l.Button,{icon:c,label:(0,a.__)("Move Down","search-appearance-toolkit-seo-44"),onClick:()=>R(o,"down"),disabled:o===u.length-1})),(0,t.createElement)(l.ToggleControl,{label:!1!==e.isVisible?(0,a.__)("Included","search-appearance-toolkit-seo-44"):(0,a.__)("This Jump Link will not be shown","search-appearance-toolkit-seo-44"),checked:!1!==e.isVisible,onChange:()=>(e=>{const t=[...u];t[e].isVisible=!t[e].isVisible,p({headings:t})})(o),__nextHasNoMarginBottom:!0}))):!1!==e.isVisible&&(0,t.createElement)("li",{key:e.anchor,className:x?`seo44-jump-link-level-${e.level}`:""},(0,t.createElement)("a",{href:`#${e.anchor}`,onClick:e=>e.preventDefault()},e.linkText)))),!v&&f&&u.length>0&&(0,t.createElement)(l.Tooltip,{text:(0,a.__)("This button is functional on the front-end to expand the list.","search-appearance-toolkit-seo-44")},(0,t.createElement)("button",{type:"button",className:"seo-44-show-more","aria-label":(0,a.__)("Show More","search-appearance-toolkit-seo-44"),"aria-expanded":"false","aria-controls":j,onClick:()=>{L((0,a.__)('The "Show More" button is interactive on the published page.',"search-appearance-toolkit-seo-44"),{type:"snackbar"})}},h))):(0,t.createElement)("p",null,(0,a.__)("No headings found. Select a heading level in the block settings to generate links.","search-appearance-toolkit-seo-44"))))},save:function({attributes:e}){const{blockInstanceId:l,layout:n,isCollapsible:r,isSmartIndentation:s,headings:i,showHeading:c,headingText:h,headingTag:p,listStyle:k,fontSize:u,textColor:d,linkColor:m,blockBackgroundColor:g,linkBackgroundColor:_,linkBackgroundColorHover:b,linkBorderColor:v,linkBorderRadius:f,isSticky:x,stickyOffset:w,jumpOffset:C,stickyStrategy:E}=e,y={color:d,fontSize:u,"--jump-link-font-size":u||"18px","--seo44-block-bg":g,"--seo44-link-color":m,"--seo44-link-bg":"horizontal"===n?_:void 0,"--seo44-link-hover-bg":"horizontal"===n?b:void 0,"--seo44-link-border-color":"horizontal"===n?v:void 0,"--seo44-link-radius":"horizontal"===n&&f?`${f}px`:void 0,"--seo44-sticky-offset":x?`${w}px`:void 0},B="ol"===k?"ol":"ul",S=`seo44-jump-links-list-${l}`,z=o.useBlockProps.save({className:`${"horizontal"===n?"is-layout-horizontal":""} ${r?"is-collapsible":""} ${"none"===k?"list-style-none":""} ${x?"is-sticky":""} ${"desktop-only"===E?"sticky-desktop-only":""}`.trim(),style:y,"data-seo44-jump-offset":x?C:30}),T=(0,t.createElement)("svg",{className:"arrow-down",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10l-6 6z"})),N=(0,t.createElement)("svg",{className:"arrow-up",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14l-6-6z"}));return(0,t.createElement)("div",{...z},(0,t.createElement)("div",{className:"seo44-sticky-sentinel","aria-hidden":"true"}),c&&(0,t.createElement)(o.RichText.Content,{tagName:p||"h2",className:"wp-block-seo44-jump-links-heading",value:h}),i&&i.length>0&&(0,t.createElement)("nav",{"aria-label":(0,a.__)("Table of contents","search-appearance-toolkit-seo-44")},(0,t.createElement)(B,{id:S},i.filter(e=>!1!==e.isVisible).map(e=>(0,t.createElement)("li",{key:e.anchor,className:s?`seo44-jump-link-level-${e.level}`:""},(0,t.createElement)("a",{href:`#${e.anchor}`},e.linkText)))),r&&(0,t.createElement)("button",{type:"button",className:"seo-44-show-more","aria-label":(0,a.__)("Show More","search-appearance-toolkit-seo-44"),"aria-expanded":"false","aria-controls":S},T,N)))}})}},a={};function o(e){var l=a[e];if(void 0!==l)return l.exports;var n=a[e]={exports:{}};return t[e](n,n.exports,o),n.exports}o.m=t,e=[],o.O=(t,a,l,n)=>{if(!a){var r=1/0;for(h=0;h<e.length;h++){for(var[a,l,n]=e[h],s=!0,i=0;i<a.length;i++)(!1&n||r>=n)&&Object.keys(o.O).every(e=>o.O[e](a[i]))?a.splice(i--,1):(s=!1,n<r&&(r=n));if(s){e.splice(h--,1);var c=l();void 0!==c&&(t=c)}}return t}n=n||0;for(var h=e.length;h>0&&e[h-1][2]>n;h--)e[h]=e[h-1];e[h]=[a,l,n]},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={57:0,350:0};o.O.j=t=>0===e[t];var t=(t,a)=>{var l,n,[r,s,i]=a,c=0;if(r.some(t=>0!==e[t])){for(l in s)o.o(s,l)&&(o.m[l]=s[l]);if(i)var h=i(o)}for(t&&t(a);c<r.length;c++)n=r[c],o.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return o.O(h)},a=globalThis.webpackChunkseo_44_jump_links_block=globalThis.webpackChunkseo_44_jump_links_block||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var l=o.O(void 0,[350],()=>o(313));l=o.O(l)})();1 (()=>{"use strict";var e,t={313(){const e=window.wp.blocks,t=window.React,a=window.wp.i18n,o=window.wp.blockEditor,l=window.wp.components,n=window.wp.data,r=window.wp.element;function s(e){return(new DOMParser).parseFromString(e,"text/html").body.textContent||""}const i=(0,t.createElement)("svg",{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M3 21v-2h18v2zm8-4v-6.175L9.4 12.4L8 11l4-4l4 4l-1.4 1.4l-1.6-1.575V17zM3 5V3h18v2z"})),c=(0,t.createElement)("svg",{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M3 5V3h18v2zm9 12l-4-4l1.4-1.4l1.6 1.575V7h2v6.175l1.6-1.575L16 13zm-9 4v-2h18v2z"})),h=(0,t.createElement)("svg",{className:"arrow-down",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10l-6 6z"})),p=JSON.parse('{"UU":"seo44/jump-links"}');(0,e.registerBlockType)(p.UU,{edit:function({attributes:e,setAttributes:p}){const{headingLevels:k,headings:u,showHeading:d,headingText:m,headingTag:g,layout:_,listStyle:b,isEditing:v,isCollapsible:f,isSmartIndentation:x,fontSize:w,textColor:C,linkColor:E,blockBackgroundColor:y,linkBackgroundColor:B,linkBackgroundColorHover:S,linkBorderColor:z,linkBorderRadius:T,isSticky:N,stickyOffset:H,jumpOffset:M,stickyStrategy:P}=e,O={color:C,fontSize:w,"--jump-link-font-size":w||"18px","--seo44-link-color":E,"--seo44-link-bg":"horizontal"===_?B:void 0,"--seo44-link-hover-bg":"horizontal"===_?S:void 0,"--seo44-link-border-color":"horizontal"===_?z:void 0,"--seo44-link-radius":"horizontal"===_&&T?`${T}px`:void 0,"--seo44-block-bg":y,"--seo44-sticky-offset":N?`${H}px`:void 0},I="ol"===b?"ol":"ul",{createInfoNotice:L}=(0,n.useDispatch)("core/notices"),j=e.blockInstanceId?`seo44-jump-links-list-${e.blockInstanceId}`:"seo44-jump-links-list",$=(0,o.useBlockProps)({style:O});$.className=`${$.className} ${"horizontal"===_?"is-layout-horizontal":""} ${f&&!v?"is-collapsible":""} ${"none"===b?"list-style-none":""}`.trim();const D=(0,n.useSelect)(e=>e("core/block-editor").getBlocks(),[]),{updateBlockAttributes:V}=(0,n.useDispatch)("core/block-editor");(0,r.useEffect)(()=>{const t={};e.blockInstanceId||(t.blockInstanceId=Math.random().toString(36).substr(2,9)),Object.keys(t).length>0&&p(t);const o=D.filter(e=>"core/heading"===e.name&&k.includes(`h${e.attributes.level}`)),l=new Set;let n=!1;const r=new Map(u.map(e=>[e.anchor,e])),i=[];for(const e of o){const t=s(e.attributes.content);let a=e.attributes.anchor||t.toLowerCase().replace(/[^a-z0-9\s-]/g,"").trim().replace(/\s+/g,"-"),o=a,c=2;for(;l.has(o);)o=`${a}-${c}`,c++,n=!0;l.add(o),e.attributes.anchor!==o&&V(e.clientId,{anchor:o});const h=r.get(e.attributes.anchor)||r.get(o),p=h&&h.linkText!==h.text?h.linkText:t,k=!h||h.isVisible;i.push({anchor:o,text:t,linkText:p,isVisible:k,level:e.attributes.level})}JSON.stringify(i)!==JSON.stringify(u)&&p({headings:i}),n&&L((0,a.__)("Jump Links Block: Duplicate headings were found. Unique IDs have been auto-generated, but this may be a sign of redundancy. Please review your headings for clarity.","search-appearance-toolkit-seo-44"),{type:"snackbar"})},[D,k,u,e.blockInstanceId,p,V,L]),(0,r.useEffect)(()=>{"horizontal"===_&&"none"!==b&&p({listStyle:"none"})},[_,b,p]);const R=(e,t)=>{const a=[...u],o=a.splice(e,1)[0];"up"===t?a.splice(e-1,0,o):a.splice(e+1,0,o),p({headings:a})},J=e=>{const t=k.includes(e)?k.filter(t=>t!==e):[...k,e];p({headingLevels:t.sort()})};return(0,t.createElement)(r.Fragment,null,(0,t.createElement)(o.InspectorControls,null,(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Presentation","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ButtonGroup,null,(0,t.createElement)(l.Button,{isPrimary:!v,isPressed:!v,onClick:()=>p({isEditing:!1})},(0,a.__)("Viewing Mode","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.Button,{isPrimary:v,isPressed:v,onClick:()=>p({isEditing:!0})},(0,a.__)("Editing Mode","search-appearance-toolkit-seo-44"))),(0,t.createElement)("p",{className:"description"},(0,a.__)("Switch to Editing Mode to customize link text, visibility, and order.","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Appearance","search-appearance-toolkit-seo-44")},(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Layout","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.ButtonGroup,null,(0,t.createElement)(l.Button,{isPrimary:"vertical"===_,isPressed:"vertical"===_,onClick:()=>p({layout:"vertical"})},(0,a.__)("Vertical","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.Button,{isPrimary:"horizontal"===_,isPressed:"horizontal"===_,onClick:()=>p({layout:"horizontal"})},(0,a.__)("Horizontal","search-appearance-toolkit-seo-44"))),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Make Jump Links Area Expandable","search-appearance-toolkit-seo-44"),help:(0,a.__)('Conserve screen space by collapsing a long list of jump links, providing users with an elegant "show more" button to see the entire list.',"search-appearance-toolkit-seo-44"),checked:f,onChange:()=>p({isCollapsible:!f}),__nextHasNoMarginBottom:!0}),(0,t.createElement)(l.SelectControl,{label:(0,a.__)("List Style","search-appearance-toolkit-seo-44"),value:b,options:[{label:(0,a.__)("Bulleted","search-appearance-toolkit-seo-44"),value:"ul"},{label:(0,a.__)("Numbered","search-appearance-toolkit-seo-44"),value:"ol"},{label:(0,a.__)("None","search-appearance-toolkit-seo-44"),value:"none"}],onChange:e=>p({listStyle:e}),disabled:"horizontal"===_,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.FontSizePicker,{fontSizes:[{name:(0,a.__)("S","search-appearance-toolkit-seo-44"),slug:"small",size:"14px"},{name:(0,a.__)("M","search-appearance-toolkit-seo-44"),slug:"normal",size:"17px"},{name:(0,a.__)("L","search-appearance-toolkit-seo-44"),slug:"large",size:"20px"},{name:(0,a.__)("XL","search-appearance-toolkit-seo-44"),slug:"extra-large",size:"23px"}],value:w,onChange:e=>p({fontSize:e}),withReset:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(o.PanelColorSettings,{title:(0,a.__)("Colors","search-appearance-toolkit-seo-44"),colorSettings:[{value:y,onChange:e=>p({blockBackgroundColor:e}),label:(0,a.__)("Block Background","search-appearance-toolkit-seo-44")},{value:E,onChange:e=>p({linkColor:e}),label:(0,a.__)("Link Color","search-appearance-toolkit-seo-44")},{value:C,onChange:e=>p({textColor:e}),label:(0,a.__)("Other Text Color","search-appearance-toolkit-seo-44")}]}),"horizontal"===_&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)("hr",null),(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Horizontal Link Styles","search-appearance-toolkit-seo-44"))),(0,t.createElement)(o.PanelColorSettings,{title:(0,a.__)("Link Colors","search-appearance-toolkit-seo-44"),colorSettings:[{value:B,onChange:e=>p({linkBackgroundColor:e}),label:(0,a.__)("Background","search-appearance-toolkit-seo-44")},{value:S,onChange:e=>p({linkBackgroundColorHover:e}),label:(0,a.__)("Background Hover","search-appearance-toolkit-seo-44")},{value:z,onChange:e=>p({linkBorderColor:e}),label:(0,a.__)("Border","search-appearance-toolkit-seo-44")}]}),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Link Border Radius","search-appearance-toolkit-seo-44"),value:T,onChange:e=>p({linkBorderRadius:e}),min:0,max:50,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}))),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Content Settings","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Display Block Title","search-appearance-toolkit-seo-44"),checked:d,onChange:()=>p({showHeading:!d}),__nextHasNoMarginBottom:!0}),d&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)(l.TextControl,{label:(0,a.__)("Title Text","search-appearance-toolkit-seo-44"),value:m,onChange:e=>p({headingText:e}),help:(0,a.__)("The text that appears above your list of links.","search-appearance-toolkit-seo-44")}),(0,t.createElement)(l.SelectControl,{label:(0,a.__)("Title Tag","search-appearance-toolkit-seo-44"),value:g,options:[{label:"H2",value:"h2"},{label:"H3",value:"h3"},{label:"H4",value:"h4"},{label:"H5",value:"h5"},{label:"Paragraph (Bold)",value:"p"},{label:"Div (No Semantic Value)",value:"div"}],onChange:e=>p({headingTag:e}),help:(0,a.__)("Choose a level that fits your page's structure.","search-appearance-toolkit-seo-44"),__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0})),(0,t.createElement)("hr",null),(0,t.createElement)("p",null,(0,t.createElement)("strong",null,(0,a.__)("Included Headings","search-appearance-toolkit-seo-44"))),(0,t.createElement)("p",{className:"description",style:{marginBottom:"10px"}},(0,a.__)("Select which heading levels from your post content should appear in the jump links list.","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.CheckboxControl,{label:"H2",checked:k.includes("h2"),onChange:()=>J("h2")}),(0,t.createElement)(l.CheckboxControl,{label:"H3",checked:k.includes("h3"),onChange:()=>J("h3")}),(0,t.createElement)(l.CheckboxControl,{label:"H4",checked:k.includes("h4"),onChange:()=>J("h4")}),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Create Visual Hierarchy","search-appearance-toolkit-seo-44"),help:(0,a.__)("Indents sub-headings (H3, H4) to create a nested outline structure.","search-appearance-toolkit-seo-44"),checked:x,onChange:()=>p({isSmartIndentation:!x}),__nextHasNoMarginBottom:!0})),(0,t.createElement)(l.PanelBody,{title:(0,a.__)("Position Settings","search-appearance-toolkit-seo-44")},(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Sticky Position","search-appearance-toolkit-seo-44"),help:(0,a.__)("Keep the table of contents visible while scrolling.","search-appearance-toolkit-seo-44"),checked:N,onChange:()=>p({isSticky:!N}),__nextHasNoMarginBottom:!0}),N&&(0,t.createElement)(r.Fragment,null,(0,t.createElement)("p",{className:"description",style:{marginBottom:"15px"}},(0,a.__)("Customize how the block behaves when it sticks to the top of the screen.","search-appearance-toolkit-seo-44")),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Top Offset (px)","search-appearance-toolkit-seo-44"),help:(0,a.__)("The distance between the top of the screen and the block when stuck (useful for clearing sticky headers).","search-appearance-toolkit-seo-44"),value:H,onChange:e=>p({stickyOffset:e}),min:0,max:200,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.RangeControl,{label:(0,a.__)("Jump Offset (px)","search-appearance-toolkit-seo-44"),help:(0,a.__)("The buffer distance to stop *before* the heading. Increase this if your sticky header covers the text.","search-appearance-toolkit-seo-44"),value:M,onChange:e=>p({jumpOffset:e}),min:0,max:200,__nextHasNoMarginBottom:!0,__next40pxDefaultSize:!0}),(0,t.createElement)(l.ToggleControl,{label:(0,a.__)("Disable on Mobile","search-appearance-toolkit-seo-44"),help:(0,a.__)("Prevents the block from sticking on small screens to save reading space.","search-appearance-toolkit-seo-44"),checked:"desktop-only"===P,onChange:e=>p({stickyStrategy:e?"desktop-only":"always"}),__nextHasNoMarginBottom:!0})))),(0,t.createElement)("div",{...$},d&&(0,t.createElement)(o.RichText,{tagName:g,className:"wp-block-seo44-jump-links-heading",value:m,onChange:e=>p({headingText:e}),placeholder:(0,a.__)("On This Page","search-appearance-toolkit-seo-44")}),u.length>0?(0,t.createElement)("nav",{"aria-label":(0,a.__)("Table of contents","search-appearance-toolkit-seo-44")},(0,t.createElement)(I,{id:j},u.map((e,o)=>v?(0,t.createElement)("li",{key:e.anchor},(0,t.createElement)(l.TextControl,{value:e.linkText,onChange:e=>((e,t)=>{const a=[...u];a[e].linkText=t,p({headings:a})})(o,e)}),(0,t.createElement)("div",{className:"edit-controls-wrapper"},(0,t.createElement)("div",{className:"reorder-buttons"},(0,t.createElement)(l.Button,{icon:i,label:(0,a.__)("Move Up","search-appearance-toolkit-seo-44"),onClick:()=>R(o,"up"),disabled:0===o}),(0,t.createElement)(l.Button,{icon:c,label:(0,a.__)("Move Down","search-appearance-toolkit-seo-44"),onClick:()=>R(o,"down"),disabled:o===u.length-1})),(0,t.createElement)(l.ToggleControl,{label:!1!==e.isVisible?(0,a.__)("Included","search-appearance-toolkit-seo-44"):(0,a.__)("This Jump Link will not be shown","search-appearance-toolkit-seo-44"),checked:!1!==e.isVisible,onChange:()=>(e=>{const t=[...u];t[e].isVisible=!t[e].isVisible,p({headings:t})})(o),__nextHasNoMarginBottom:!0}))):!1!==e.isVisible&&(0,t.createElement)("li",{key:e.anchor,className:x?`seo44-jump-link-level-${e.level}`:""},(0,t.createElement)("a",{href:`#${e.anchor}`,onClick:e=>e.preventDefault()},e.linkText)))),!v&&f&&u.length>0&&(0,t.createElement)(l.Tooltip,{text:(0,a.__)("This button is functional on the front-end to expand the list.","search-appearance-toolkit-seo-44")},(0,t.createElement)("button",{type:"button",className:"seo-44-show-more","aria-label":(0,a.__)("Show More","search-appearance-toolkit-seo-44"),"aria-expanded":"false","aria-controls":j,onClick:()=>{L((0,a.__)('The "Show More" button is interactive on the published page.',"search-appearance-toolkit-seo-44"),{type:"snackbar"})}},h))):(0,t.createElement)("p",null,(0,a.__)("No headings found. Select a heading level in the block settings to generate links.","search-appearance-toolkit-seo-44"))))},save:function({attributes:e}){const{blockInstanceId:l,layout:n,isCollapsible:r,isSmartIndentation:s,headings:i,showHeading:c,headingText:h,headingTag:p,listStyle:k,fontSize:u,textColor:d,linkColor:m,blockBackgroundColor:g,linkBackgroundColor:_,linkBackgroundColorHover:b,linkBorderColor:v,linkBorderRadius:f,isSticky:x,stickyOffset:w,jumpOffset:C,stickyStrategy:E}=e,y={color:d,fontSize:u,"--jump-link-font-size":u||"18px","--seo44-block-bg":g,"--seo44-link-color":m,"--seo44-link-bg":"horizontal"===n?_:void 0,"--seo44-link-hover-bg":"horizontal"===n?b:void 0,"--seo44-link-border-color":"horizontal"===n?v:void 0,"--seo44-link-radius":"horizontal"===n&&f?`${f}px`:void 0,"--seo44-sticky-offset":x?`${w}px`:void 0},B="ol"===k?"ol":"ul",S=`seo44-jump-links-list-${l}`,z=o.useBlockProps.save({className:`${"horizontal"===n?"is-layout-horizontal":""} ${r?"is-collapsible":""} ${"none"===k?"list-style-none":""} ${x?"is-sticky":""} ${"desktop-only"===E?"sticky-desktop-only":""}`.trim(),style:y,"data-seo44-jump-offset":x?C:30}),T=(0,t.createElement)("svg",{className:"arrow-down",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10l-6 6z"})),N=(0,t.createElement)("svg",{className:"arrow-up",width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false"},(0,t.createElement)("path",{d:"M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14l-6-6z"}));return(0,t.createElement)("div",{...z},(0,t.createElement)("div",{className:"seo44-sticky-sentinel","aria-hidden":"true"}),c&&(0,t.createElement)(o.RichText.Content,{tagName:p||"h2",className:"wp-block-seo44-jump-links-heading",value:h}),i&&i.length>0&&(0,t.createElement)("nav",{"aria-label":(0,a.__)("Table of contents","search-appearance-toolkit-seo-44")},(0,t.createElement)(B,{id:S},i.filter(e=>!1!==e.isVisible).map(e=>(0,t.createElement)("li",{key:e.anchor,className:s?`seo44-jump-link-level-${e.level}`:""},(0,t.createElement)("a",{href:`#${e.anchor}`},e.linkText)))),r&&(0,t.createElement)("button",{type:"button",className:"seo-44-show-more","aria-label":(0,a.__)("Show More","search-appearance-toolkit-seo-44"),"aria-expanded":"false","aria-controls":S},T,N)))}})}},a={};function o(e){var l=a[e];if(void 0!==l)return l.exports;var n=a[e]={exports:{}};return t[e](n,n.exports,o),n.exports}o.m=t,e=[],o.O=(t,a,l,n)=>{if(!a){var r=1/0;for(h=0;h<e.length;h++){for(var[a,l,n]=e[h],s=!0,i=0;i<a.length;i++)(!1&n||r>=n)&&Object.keys(o.O).every(e=>o.O[e](a[i]))?a.splice(i--,1):(s=!1,n<r&&(r=n));if(s){e.splice(h--,1);var c=l();void 0!==c&&(t=c)}}return t}n=n||0;for(var h=e.length;h>0&&e[h-1][2]>n;h--)e[h]=e[h-1];e[h]=[a,l,n]},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={57:0,350:0};o.O.j=t=>0===e[t];var t=(t,a)=>{var l,n,[r,s,i]=a,c=0;if(r.some(t=>0!==e[t])){for(l in s)o.o(s,l)&&(o.m[l]=s[l]);if(i)var h=i(o)}for(t&&t(a);c<r.length;c++)n=r[c],o.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return o.O(h)},a=globalThis.webpackChunkseo_44_jump_links_block=globalThis.webpackChunkseo_44_jump_links_block||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var l=o.O(void 0,[350],()=>o(313));l=o.O(l)})(); -
search-appearance-toolkit-seo-44/trunk/changelog.txt
r3405454 r3423355 1 1 == Changelog == 2 3 = 4.3.0 = 4 * FEATURE: **Table of Contents Schema:** Automatically generates `hasPart` schema synchronized with the Jump Links Block to support "Jump-to" links in search results. 5 * FEATURE: **Advanced HowTo Schema:** A new "Block-Aware" scanner uses the Jump Links Block to strictly define steps while intelligently mining the introduction for tools, time, yield, and video data. 6 * TWEAK: Added a "Generate HowTo Schema" checkbox to the SEO 44 metabox, giving users explicit control over when the advanced scanner runs. 7 * FEATURE: **YouTube Integration:** Added support for the YouTube Data API v3 to fetch accurate video upload dates for VideoObject schema. 8 * TESTED: Tested to WordPress Version 6.9 2 9 3 10 = 4.2.0 = -
search-appearance-toolkit-seo-44/trunk/includes/class-seo44-core.php
r3403604 r3423355 1 1 <?php 2 //4.3 - Added dependencies for block-editor-script.js to access Jump Links Block anchor ids for hasPart and HowTo schema. 2 3 class SEO44_Core { 3 4 … … 62 63 public function admin_enqueue_assets($hook) { 63 64 if ('post.php' == $hook || 'post-new.php' == $hook) { 65 // CSS 64 66 wp_enqueue_style('seo44-admin-styles', plugins_url('../css/admin-styles.css', __FILE__), [], SEO44_VERSION); 65 wp_enqueue_script('seo44-admin-script', plugins_url('../js/admin-script.js', __FILE__), ['jquery'], SEO44_VERSION, true); 67 // General Admin Script (Classic + Block) 68 wp_enqueue_script('seo44-admin-script', plugins_url('../js/admin-script.js', __FILE__), 69 ['jquery'], 70 SEO44_VERSION, 71 true); 72 73 // Localize the General Script (needed for Snippet Preview) 66 74 wp_localize_script('seo44-admin-script', 'seo44_data', [ 67 75 'post_title' => get_the_title(get_the_ID()), … … 69 77 'permalink' => get_permalink(get_the_ID()) 70 78 ]); 79 // Block Editor Specific Script (Passthrough Logic For Jump Links) 80 // We enqueue this separately with the Block Editor dependencies. 81 // WordPress will simply NOT load this file if these handles don't exist (e.g. Classic Editor). 82 wp_enqueue_script('seo44-block-editor-script', plugins_url('../js/block-editor-script.js', __FILE__), 83 ['wp-data', 'wp-editor', 'wp-blocks'], // Dependencies are important for Jump Links Block data 84 SEO44_VERSION, 85 true 86 ); 71 87 } 72 88 if ('settings_page_search-appearance-toolkit-seo-44' == $hook) { -
search-appearance-toolkit-seo-44/trunk/includes/class-seo44-frontend.php
r3405454 r3423355 1 1 <?php 2 // Version 4.3 3 // Create hasPart and HowTo schema from Jump Links Block using metafield passthrough 4 // Added YouTube Data API support for more accurate video upload date 5 2 6 class SEO44_Frontend { 3 7 public function __construct() { … … 5 9 add_action('wp_head', [$this, 'output_header_tags']); 6 10 add_action('wp_head', [$this, 'output_schema_json_ld'], 99); 11 add_action('init', [$this, 'register_schema_meta_fields']); // Retrieve Jump Links Block data for schema via a hidden metabox field 7 12 8 13 $taxonomies = get_taxonomies(['public' => true]); … … 12 17 } 13 18 } 14 19 // Get meta field from metabox for Jump Links Block + HowTo Schema 20 public function register_schema_meta_fields() { 21 register_post_meta('post', '_seo44_howto_step_ids', [ 22 'single' => true, 23 'type' => 'array', 24 'show_in_rest' => [ 25 'schema' => [ 26 'type' => 'array', 27 'items' => [ 28 'type' => 'string', 29 ], 30 ], 31 ], 32 'auth_callback' => function() { return current_user_can('edit_posts'); } 33 ]); 34 } 35 36 //Title 15 37 public function filter_document_title($title_parts) { 16 if (!seo44_get_option('enable_tags')) return $title_parts;17 38 if (!seo44_get_option('enable_tags')) return $title_parts; 39 18 40 $custom_title = ''; 19 41 $fallback_title = ''; … … 23 45 $custom_title = seo44_get_option('homepage_title'); 24 46 } elseif (is_singular()) { 25 $custom_title = get_post_meta(get_the_ID(), seo44_get_option('title_key'), true);47 $custom_title = get_post_meta(get_the_ID(), seo44_get_option('title_key'), true); 26 48 $fallback_title = get_the_title(get_the_ID()); 27 } elseif (is_category() || is_tag() || is_tax()) {49 } elseif (is_category() || is_tag() || is_tax()) { 28 50 $term_id = get_queried_object_id(); 29 51 $custom_title = get_term_meta($term_id, 'seo44_title', true); … … 52 74 unset($title_parts['site'], $title_parts['tagline']); 53 75 } 54 return $title_parts; 55 } 56 57 public function add_term_meta_fields($term, $taxonomy) { 76 return $title_parts; 77 } 78 79 // Meta Tags 80 public function add_term_meta_fields($term, $taxonomy) { 58 81 $title = get_term_meta($term->term_id, 'seo44_title', true); 59 82 $description = get_term_meta($term->term_id, 'seo44_description', true); … … 95 118 } 96 119 97 public function output_header_tags() {98 if (!seo44_get_option('enable_tags')) { return; }99 $description = '';100 $social_title = '';101 $current_url = '';102 103 if (is_front_page()) {104 $description = seo44_get_option('homepage_description');105 $social_title = seo44_get_option('homepage_title') ?: get_bloginfo('name');106 $current_url = home_url('/');107 } elseif (is_singular()) {108 $post_id = get_the_ID();109 $description = get_post_meta($post_id, seo44_get_option('description_key'), true);110 $custom_title = get_post_meta($post_id, seo44_get_option('title_key'), true);111 $social_title = $custom_title ?: get_the_title($post_id);112 $current_url = get_permalink($post_id);120 public function output_header_tags() { 121 if (!seo44_get_option('enable_tags')) { return; } 122 $description = ''; 123 $social_title = ''; 124 $current_url = ''; 125 126 if (is_front_page()) { 127 $description = seo44_get_option('homepage_description'); 128 $social_title = seo44_get_option('homepage_title') ?: get_bloginfo('name'); 129 $current_url = home_url('/'); 130 } elseif (is_singular()) { 131 $post_id = get_the_ID(); 132 $description = get_post_meta($post_id, seo44_get_option('description_key'), true); 133 $custom_title = get_post_meta($post_id, seo44_get_option('title_key'), true); 134 $social_title = $custom_title ?: get_the_title($post_id); 135 $current_url = get_permalink($post_id); 113 136 if (empty($description)) { 114 $post_content = get_the_content(null, false, get_the_ID());137 $post_content = get_the_content(null, false, get_the_ID()); 115 138 $description = wp_trim_words(strip_shortcodes(wp_strip_all_tags($post_content)), 25, '...'); 116 }117 118 } elseif (is_category() || is_tag() || is_tax()) {119 $term = get_queried_object();120 $description = get_term_meta($term->term_id, 'seo44_description', true);121 $custom_title = get_term_meta($term->term_id, 'seo44_title', true);122 $social_title = $custom_title ?: single_term_title('', false) . ' - ' . get_bloginfo('name');123 $current_url = get_term_link($term);139 } 140 141 } elseif (is_category() || is_tag() || is_tax()) { 142 $term = get_queried_object(); 143 $description = get_term_meta($term->term_id, 'seo44_description', true); 144 $custom_title = get_term_meta($term->term_id, 'seo44_title', true); 145 $social_title = $custom_title ?: single_term_title('', false) . ' - ' . get_bloginfo('name'); 146 $current_url = get_term_link($term); 124 147 if (empty($description)) { 125 148 $term_description = trim(wp_strip_all_tags($term->description)); … … 130 153 } 131 154 } 132 } 133 134 if (!empty($description)) { 135 printf('<meta name="description" content="%s">' . "\n", esc_attr(wp_strip_all_tags(html_entity_decode($description, ENT_QUOTES, 'UTF-8')))); 136 } 137 138 if (is_singular()) { 139 $post_id = get_the_ID(); 140 $keywords = get_post_meta($post_id, seo44_get_option('keywords_key'), true); 141 if (seo44_get_option('include_keywords') && !empty($keywords)) { 142 printf('<meta name="keywords" content="%s">' . "\n", esc_attr(wp_strip_all_tags(html_entity_decode($keywords, ENT_QUOTES, 'UTF-8')))); 143 } 144 if (is_singular() && seo44_get_option('include_author')) { 145 $author_id = get_post_field('post_author', $post_id); 146 if ($author_id) { 147 printf('<meta name="author" content="%s">' . "\n", esc_attr($this->get_author_name($author_id))); 148 } 149 } 150 } 151 152 if (seo44_get_option('enable_og_tags') || seo44_get_option('enable_twitter_tags')) { 153 $clean_social_title = esc_attr(wp_strip_all_tags(html_entity_decode($social_title, ENT_QUOTES, 'UTF-8'))); 154 $clean_description = !empty($description) ? esc_attr(wp_strip_all_tags(html_entity_decode($description, ENT_QUOTES, 'UTF-8'))) : ''; 155 156 if (empty($clean_description) && is_singular()) { 157 $clean_description = esc_attr(wp_strip_all_tags(get_the_excerpt(get_the_ID()))); 158 } 159 160 $image_url = ''; $image_width = ''; $image_height = ''; 161 $image_id = 0; 162 if (is_singular() && has_post_thumbnail()) { 163 $image_id = get_post_thumbnail_id(); 164 } 165 if (!$image_id) { 166 $image_id = seo44_get_option('default_social_image_id', 0); 167 } 168 if ($image_id) { 169 $image_data = wp_get_attachment_image_src($image_id, 'full'); 170 if ($image_data) { 171 list($image_url, $image_width, $image_height) = $image_data; 172 } 173 } 174 175 if (seo44_get_option('enable_og_tags')) { 176 printf('<meta property="og:title" content="%s">' . "\n", esc_attr($clean_social_title)); 177 printf('<meta property="og:url" content="%s">' . "\n", esc_url($current_url)); 178 printf('<meta property="og:site_name" content="%s">' . "\n", esc_attr(get_bloginfo('name'))); 179 if (!empty($clean_description)) { printf('<meta property="og:description" content="%s">' . "\n", esc_attr($clean_description)); } 180 if (is_singular('post')) { 181 echo '<meta property="og:type" content="article">' . "\n"; 182 } else { 183 echo '<meta property="og:type" content="website">' . "\n"; 184 } 185 if (!empty($image_url)) { 186 printf('<meta property="og:image" content="%s">' . "\n", esc_url($image_url)); 187 if (!empty($image_width)) { printf('<meta property="og:image:width" content="%s">' . "\n", esc_attr($image_width)); } 188 if (!empty($image_height)) { printf('<meta property="og:image:height" content="%s">' . "\n", esc_attr($image_height)); } 189 } 190 $fb_app_id = seo44_get_option('fb_app_id'); 191 if (!empty($fb_app_id)) { printf('<meta property="fb:app_id" content="%s">' . "\n", esc_attr($fb_app_id)); } 192 } 193 194 if (seo44_get_option('enable_twitter_tags')) { 195 echo '<meta name="twitter:card" content="summary_large_image">' . "\n"; 196 $twitter_handle = seo44_get_option('twitter_handle'); 197 if ( !empty($twitter_handle) && is_string($twitter_handle) ) { 198 $handle = esc_attr(str_replace('@', '', $twitter_handle)); 199 printf('<meta name="twitter:site" content="@%s">' . "\n", esc_attr($handle)); 200 printf('<meta name="twitter:creator" content="@%s">' . "\n", esc_attr($handle)); 201 } 202 printf('<meta name="twitter:title" content="%s">' . "\n", esc_attr($clean_social_title)); 203 if (!empty($clean_description)) { printf('<meta name="twitter:description" content="%s">' . "\n", esc_attr($clean_description)); } 204 if (!empty($image_url)) { printf('<meta name="twitter:image" content="%s">' . "\n", esc_url($image_url)); } 205 } 206 } 207 } 208 155 } 156 157 if (!empty($description)) { 158 printf('<meta name="description" content="%s">' . "\n", esc_attr(wp_strip_all_tags(html_entity_decode($description, ENT_QUOTES, 'UTF-8')))); 159 } 160 161 if (is_singular()) { 162 $post_id = get_the_ID(); 163 $keywords = get_post_meta($post_id, seo44_get_option('keywords_key'), true); 164 if (seo44_get_option('include_keywords') && !empty($keywords)) { 165 printf('<meta name="keywords" content="%s">' . "\n", esc_attr(wp_strip_all_tags(html_entity_decode($keywords, ENT_QUOTES, 'UTF-8')))); 166 } 167 if (is_singular() && seo44_get_option('include_author')) { 168 $author_id = get_post_field('post_author', $post_id); 169 if ($author_id) { 170 printf('<meta name="author" content="%s">' . "\n", esc_attr($this->get_author_name($author_id))); 171 } 172 } 173 } 174 175 if (seo44_get_option('enable_og_tags') || seo44_get_option('enable_twitter_tags')) { 176 $clean_social_title = esc_attr(wp_strip_all_tags(html_entity_decode($social_title, ENT_QUOTES, 'UTF-8'))); 177 $clean_description = !empty($description) ? esc_attr(wp_strip_all_tags(html_entity_decode($description, ENT_QUOTES, 'UTF-8'))) : ''; 178 179 if (empty($clean_description) && is_singular()) { 180 $clean_description = esc_attr(wp_strip_all_tags(get_the_excerpt(get_the_ID()))); 181 } 182 183 $image_url = ''; $image_width = ''; $image_height = ''; 184 $image_id = 0; 185 if (is_singular() && has_post_thumbnail()) { 186 $image_id = get_post_thumbnail_id(); 187 } 188 if (!$image_id) { 189 $image_id = seo44_get_option('default_social_image_id', 0); 190 } 191 if ($image_id) { 192 $image_data = wp_get_attachment_image_src($image_id, 'full'); 193 if ($image_data) { 194 list($image_url, $image_width, $image_height) = $image_data; 195 } 196 } 197 198 if (seo44_get_option('enable_og_tags')) { 199 printf('<meta property="og:title" content="%s">' . "\n", esc_attr($clean_social_title)); 200 printf('<meta property="og:url" content="%s">' . "\n", esc_url($current_url)); 201 printf('<meta property="og:site_name" content="%s">' . "\n", esc_attr(get_bloginfo('name'))); 202 if (!empty($clean_description)) { printf('<meta property="og:description" content="%s">' . "\n", esc_attr($clean_description)); } 203 if (is_singular('post')) { 204 echo '<meta property="og:type" content="article">' . "\n"; 205 } else { 206 echo '<meta property="og:type" content="website">' . "\n"; 207 } 208 if (!empty($image_url)) { 209 printf('<meta property="og:image" content="%s">' . "\n", esc_url($image_url)); 210 if (!empty($image_width)) { printf('<meta property="og:image:width" content="%s">' . "\n", esc_attr($image_width)); } 211 if (!empty($image_height)) { printf('<meta property="og:image:height" content="%s">' . "\n", esc_attr($image_height)); } 212 } 213 $fb_app_id = seo44_get_option('fb_app_id'); 214 if (!empty($fb_app_id)) { printf('<meta property="fb:app_id" content="%s">' . "\n", esc_attr($fb_app_id)); } 215 } 216 217 if (seo44_get_option('enable_twitter_tags')) { 218 echo '<meta name="twitter:card" content="summary_large_image">' . "\n"; 219 $twitter_handle = seo44_get_option('twitter_handle'); 220 if ( !empty($twitter_handle) && is_string($twitter_handle) ) { 221 $handle = esc_attr(str_replace('@', '', $twitter_handle)); 222 printf('<meta name="twitter:site" content="@%s">' . "\n", esc_attr($handle)); 223 printf('<meta name="twitter:creator" content="@%s">' . "\n", esc_attr($handle)); 224 } 225 printf('<meta name="twitter:title" content="%s">' . "\n", esc_attr($clean_social_title)); 226 if (!empty($clean_description)) { printf('<meta name="twitter:description" content="%s">' . "\n", esc_attr($clean_description)); } 227 if (!empty($image_url)) { printf('<meta name="twitter:image" content="%s">' . "\n", esc_url($image_url)); } 228 } 229 } 230 } 231 232 // Now For The Schema Structured Data 209 233 public function output_schema_json_ld() { 210 234 // Note: The following line is intentionally not nonce-checked. 211 // This is a safe, read-only check used by the admin-side scanner to get a clean view of the page212 // without our own schema being output. It does not process or save any data.213 235 $scan_param = isset($_GET['seo44_scan']) ? sanitize_key(wp_unslash($_GET['seo44_scan'])) : ''; 214 236 if ($scan_param === 'true') { return; } … … 218 240 $base_schema = []; 219 241 $special_schemas = []; 220 $breadcrumb_schema = []; // New variable for breadcrumbs242 $breadcrumb_schema = []; // New variable for breadcrumbs 221 243 $post_id = get_the_ID(); 222 244 … … 225 247 $base_schema = $this->get_schema_for_website(); 226 248 227 // NEW:Add Organization Schema to homepage228 if (seo44_get_option('enable_organization_schema')) {229 $org_schema = $this->get_schema_for_organization();230 if (!empty($org_schema)) {231 $special_schemas[] = $org_schema;232 }233 }234 249 // Add Organization Schema to homepage 250 if (seo44_get_option('enable_organization_schema')) { 251 $org_schema = $this->get_schema_for_organization(); 252 if (!empty($org_schema)) { 253 $special_schemas[] = $org_schema; 254 } 255 } 256 235 257 } elseif ( (is_category() || is_tag() || is_tax()) && seo44_get_option('enable_schema_on_taxonomies') ) { 236 258 $base_schema = $this->get_schema_for_taxonomy(); … … 246 268 } 247 269 248 // 3. Attempt to generate special schema 249 if (is_singular() && seo44_get_option('enable_advanced_schema')) { 250 $detected_schemas = $this->detect_and_generate_special_schema($post_id); 251 // Merge instead of overwrite - important for organization schema 252 if (!empty($detected_schemas)) { 253 $special_schemas = array_merge($special_schemas, $detected_schemas); 254 } 255 } 256 257 258 // 4. Hook for Add-ons --- 259 // This filter allows other plugins to add their own custom schema parts to the graph. 260 $addon_schema_parts = apply_filters( 'seo44_add_schema_parts', [], $post_id ); 261 // --- End Hook --- 262 263 // 5. Combine all schemas for output 264 $final_schema_parts = []; 270 // 3. Attempt to generate special schema (FAQ / HowTo) 271 if (is_singular() && seo44_get_option('enable_advanced_schema')) { 272 $detected_schemas = $this->detect_and_generate_special_schema($post_id); 273 // Merge instead of overwrite (Prevents erasing Organization schema on static pages) 274 if (!empty($detected_schemas)) { 275 $special_schemas = array_merge($special_schemas, $detected_schemas); 276 } 277 } 278 279 // 4. Hook for Add-ons --- 280 // This filter allows other plugins to add their own custom schema parts to the graph. 281 $addon_schema_parts = apply_filters( 'seo44_add_schema_parts', [], $post_id ); 282 // --- End Hook --- 283 284 // 5. Combine all schemas for output 285 $final_schema_parts = []; 265 286 if (!empty($base_schema)) { 266 287 $final_schema_parts[] = $base_schema; … … 272 293 $final_schema_parts = array_merge($final_schema_parts, $special_schemas); 273 294 } 274 275 // include any add-on schemas that pass is_array sanity check276 if ( ! empty( $addon_schema_parts ) && is_array( $addon_schema_parts ) ) {277 $final_schema_parts = array_merge( $final_schema_parts, $addon_schema_parts );278 }295 296 // include any add-on schemas that pass is_array sanity check 297 if ( ! empty( $addon_schema_parts ) && is_array( $addon_schema_parts ) ) { 298 $final_schema_parts = array_merge( $final_schema_parts, $addon_schema_parts ); 299 } 279 300 280 301 $final_schema = []; … … 301 322 } 302 323 } 303 304 // --- Helper Functions ---324 325 // --- Helper Functions --- 305 326 306 327 // --- Generate BreadcrumbList Schema for Singular Content --- … … 363 384 ]; 364 385 } 365 386 366 387 // --- Helper Function For Types --- 367 388 public static function get_schema_for_post($post_id) { 368 global $post; 369 $post = get_post($post_id); 370 setup_postdata($post); 371 372 // Get the author's ID and website URL to refer authorship 373 $author_id = $post->post_author; 374 $author_url = get_the_author_meta('user_url', $author_id); 375 376 // Build the author schema array 377 $author_schema = [ 378 '@type' => 'Person', 379 'name' => self::get_author_name_static( $author_id ), // Pass the author ID 380 ]; 381 382 // If the author has a website, add the @id and url properties 383 if ( ! empty( $author_url ) ) { 384 // Ensure the URL has a trailing slash before adding the fragment. 385 $canonical_author_url = trailingslashit( $author_url ); 386 387 $author_schema['@id'] = $canonical_author_url . '#person'; 388 $author_schema['url'] = $canonical_author_url; 389 } 390 391 $schema = [ 392 '@context' => 'https://schema.org', 393 '@type' => 'Article', 394 'mainEntityOfPage' => ['@type' => 'WebPage', '@id' => get_permalink($post_id)], 395 'headline' => get_the_title($post_id), 396 'datePublished' => get_the_date('c', $post_id), 397 'dateModified' => get_the_modified_date('c', $post_id), 398 'author' => $author_schema, // Use the author schema array 399 'publisher' => ['@type' => 'Organization', 'name' => get_bloginfo('name')] 400 ]; 401 if (get_site_icon_url()) { $schema['publisher']['logo'] = ['@type' => 'ImageObject', 'url' => get_site_icon_url()]; } 402 if (has_post_thumbnail($post_id)) { 403 $image_id = get_post_thumbnail_id($post_id); 404 $image_data = wp_get_attachment_image_src($image_id, 'full'); 405 if ($image_data) { 406 $schema['image'] = [ 407 '@type' => 'ImageObject', 408 'url' => $image_data[0], 409 'width' => $image_data[1], 410 'height' => $image_data[2] 411 ]; 412 } 413 } 414 // NEW: Parse content for additional media if the setting is enabled 389 global $post; 390 $post = get_post($post_id); 391 setup_postdata($post); 392 393 // Get the author's ID and website URL to refer authorship 394 $author_id = $post->post_author; 395 $author_url = get_the_author_meta('user_url', $author_id); 396 397 // Build the author schema array 398 $author_schema = [ 399 '@type' => 'Person', 400 'name' => self::get_author_name_static( $author_id ), // Pass the author ID 401 ]; 402 403 // If the author has a website, add the @id and url properties 404 if ( ! empty( $author_url ) ) { 405 // Ensure the URL has a trailing slash before adding the fragment. 406 $canonical_author_url = trailingslashit( $author_url ); 407 408 $author_schema['@id'] = $canonical_author_url . '#person'; 409 $author_schema['url'] = $canonical_author_url; 410 } 411 412 $schema = [ 413 '@context' => 'https://schema.org', 414 '@type' => 'Article', 415 'mainEntityOfPage' => ['@type' => 'WebPage', '@id' => get_permalink($post_id)], 416 'headline' => get_the_title($post_id), 417 'datePublished' => get_the_date('c', $post_id), 418 'dateModified' => get_the_modified_date('c', $post_id), 419 'author' => $author_schema, // Use the author schema array 420 'publisher' => ['@type' => 'Organization', 'name' => get_bloginfo('name')] 421 ]; 422 if (get_site_icon_url()) { $schema['publisher']['logo'] = ['@type' => 'ImageObject', 'url' => get_site_icon_url()]; } 423 424 // FIX: Initialize image key safely here to prevent "Undefined array key" warning 425 $schema['image'] = []; // Default to empty array 426 if (has_post_thumbnail($post_id)) { 427 $image_id = get_post_thumbnail_id($post_id); 428 $image_data = wp_get_attachment_image_src($image_id, 'full'); 429 if ($image_data) { 430 $schema['image'][] = [ 431 '@type' => 'ImageObject', 432 'url' => $image_data[0], 433 'width' => $image_data[1], 434 'height' => $image_data[2] 435 ]; 436 } 437 } 438 // NEW: Parse content for additional media if the setting is enabled 415 439 if (seo44_get_option('scan_content_for_schema')) { 416 440 $media_schema = self::parse_content_for_media_schema($post_id); 417 if (!empty($media_schema['images'])) { 418 // Check if a featured image (or other image) already exists 419 $existing_images = isset($schema['image']) ? (array)$schema['image'] : []; 420 421 // Merge existing images with content images 422 $schema['image'] = array_merge($existing_images, $media_schema['images']); 423 } 441 if (!empty($media_schema['images'])) { 442 // 1. Re-index to prevent "0": {...} keys 443 $schema['image'] = array_merge($schema['image'], $media_schema['images']); 444 } 424 445 if (!empty($media_schema['videos'])) { 425 446 $schema['video'] = $media_schema['videos']; 426 447 } 427 448 } 428 429 $description = get_post_meta($post_id, seo44_get_option('description_key'), true); 430 if (!empty($description)) { $schema['description'] = esc_html(wp_strip_all_tags($description)); } 431 $excerpt = get_the_excerpt($post_id); 432 if (!empty($excerpt)) { $schema['abstract'] = esc_html(wp_strip_all_tags($excerpt)); } 433 $content = get_the_content(null, false, $post_id); 434 $schema['wordCount'] = str_word_count(wp_strip_all_tags($content)); 435 436 $category = get_the_category($post_id); 437 if (!empty($category) && $category[0]->name !== 'Uncategorized') { 438 $schema['articleSection'] = esc_html($category[0]->name); 439 } 440 441 $tags = get_the_tags($post_id); 442 if ($tags) { 443 $keywords = []; 444 foreach ($tags as $tag) { $keywords[] = $tag->name; } 445 $schema['keywords'] = esc_html(implode(', ', $keywords)); 446 } 447 448 wp_reset_postdata(); 449 return $schema; 449 450 // 2. Ensure clean array (re-index) 451 if (!empty($schema['image'])) { 452 $schema['image'] = array_values($schema['image']); 453 // If only one image, Google prefers object over array, but array is valid. 454 // To match your request, we keep it as array_values to remove numeric keys. 455 } else { 456 unset($schema['image']); 457 } 458 459 $description = get_post_meta($post_id, seo44_get_option('description_key'), true); 460 if (!empty($description)) { $schema['description'] = esc_html(wp_strip_all_tags($description)); } 461 $excerpt = get_the_excerpt($post_id); 462 if (!empty($excerpt)) { $schema['abstract'] = esc_html(wp_strip_all_tags($excerpt)); } 463 $content = get_the_content(null, false, $post_id); 464 $schema['wordCount'] = str_word_count(wp_strip_all_tags($content)); 465 466 $category = get_the_category($post_id); 467 if (!empty($category) && $category[0]->name !== 'Uncategorized') { 468 $schema['articleSection'] = esc_html($category[0]->name); 469 } 470 471 $tags = get_the_tags($post_id); 472 if ($tags) { 473 $keywords = []; 474 foreach ($tags as $tag) { $keywords[] = $tag->name; } 475 $schema['keywords'] = esc_html(implode(', ', $keywords)); 476 } 477 478 // NEW: Dynamic Table of Contents (hasPart) 479 // Only run this if we are scanning content, to save performance 480 if (seo44_get_option('enable_jumplinks_schema')) { 481 $toc_parts = self::generate_has_part_schema($content, $post->ID); 482 if (!empty($toc_parts)) { 483 $schema['hasPart'] = $toc_parts; 484 } 485 } 486 wp_reset_postdata(); 487 return $schema; 450 488 } 451 489 452 490 public function get_schema_for_page($post_id) { 453 $schema = [ 454 '@context' => 'https://schema.org', 455 '@type' => 'WebPage', 456 'url' => get_permalink($post_id), 457 'headline' => get_the_title($post_id), 458 'datePublished' => get_the_date('c', $post_id), 459 'dateModified' => get_the_modified_date('c', $post_id), 460 ]; 461 462 // Add the meta description 463 $description = get_post_meta($post_id, seo44_get_option('description_key'), true); 464 if (!empty($description)) { 465 $schema['description'] = esc_html(wp_strip_all_tags($description)); 466 } 467 468 // Add the featured image as a detailed ImageObject 469 if (has_post_thumbnail($post_id)) { 470 $image_id = get_post_thumbnail_id($post_id); 471 $image_data = wp_get_attachment_image_src($image_id, 'full'); 472 if ($image_data) { 473 $schema['primaryImageOfPage'] = [ 474 '@type' => 'ImageObject', 475 'url' => $image_data[0], 476 'width' => $image_data[1], 477 'height' => $image_data[2] 478 ]; 479 } 480 } 481 // Parse content for additional media if the setting is enabled 482 // NEW CODE for get_schema_for_page 483 if (seo44_get_option('scan_content_for_schema')) { 484 $media_schema = self::parse_content_for_media_schema($post_id); 485 486 if (!empty($media_schema['images'])) { 487 // 1. Start with the content images found 488 $all_images = $media_schema['images']; 489 490 // 2. If a Featured Image (primaryImageOfPage) exists, add it to the start of the list 491 if (isset($schema['primaryImageOfPage'])) { 492 array_unshift($all_images, $schema['primaryImageOfPage']); 493 } 494 495 // 3. Set the 'image' property to the complete list 496 $schema['image'] = $all_images; 497 498 // Optional: You can choose to keep or unset primaryImageOfPage. 499 // Keeping it is usually better for SEO so Google knows which one is "Main". 500 // This would be the code to cleanup the unset: 501 // unset($schema['primaryImageOfPage']); 502 } 503 504 if (!empty($media_schema['videos'])) { 505 $schema['video'] = $media_schema['videos']; 506 } 507 } 508 509 return $schema; 491 $schema = [ 492 '@context' => 'https://schema.org', 493 '@type' => 'WebPage', 494 'url' => get_permalink($post_id), 495 'headline' => get_the_title($post_id), 496 'datePublished' => get_the_date('c', $post_id), 497 'dateModified' => get_the_modified_date('c', $post_id), 498 ]; 499 500 // Add the meta description 501 $description = get_post_meta($post_id, seo44_get_option('description_key'), true); 502 if (!empty($description)) { 503 $schema['description'] = esc_html(wp_strip_all_tags($description)); 504 } 505 506 // Add the featured image as a detailed ImageObject 507 $schema['image'] = []; 508 if (has_post_thumbnail($post_id)) { 509 $image_id = get_post_thumbnail_id($post_id); 510 $image_data = wp_get_attachment_image_src($image_id, 'full'); 511 if ($image_data) { 512 $schema['primaryImageOfPage'] = [ 513 '@type' => 'ImageObject', 514 'url' => $image_data[0], 515 'width' => $image_data[1], 516 'height' => $image_data[2] 517 ]; 518 // Initialize main image list with featured image 519 $schema['image'][] = $schema['primaryImageOfPage']; 520 } 521 } 522 523 // Parse content for additional media if the setting is enabled 524 // NEW CODE for get_schema_for_page 525 if (seo44_get_option('scan_content_for_schema')) { 526 $media_schema = self::parse_content_for_media_schema($post_id); 527 528 if (!empty($media_schema['images'])) { 529 // 1. Re-index to prevent "0": {...} keys 530 $schema['image'] = array_merge($schema['image'], $media_schema['images']); 531 } 532 533 if (!empty($media_schema['videos'])) { 534 $schema['video'] = $media_schema['videos']; 535 } 536 } 537 538 // 2. Ensure clean array (re-index) 539 if (!empty($schema['image'])) { 540 $schema['image'] = array_values($schema['image']); 541 } else { 542 unset($schema['image']); 543 } 544 545 // NEW: Dynamic Table of Contents (hasPart) 546 // Only run this if we are scanning content, to save performance 547 if (seo44_get_option('enable_jumplinks_schema')) { 548 // We need to fetch content first since get_schema_for_page doesn't always have it ready 549 $post_content = get_the_content(null, false, $post_id); 550 $toc_parts = self::generate_has_part_schema($post_content, $post_id); 551 552 if (!empty($toc_parts)) { 553 $schema['hasPart'] = $toc_parts; 554 } 555 } 556 557 return $schema; 510 558 } 511 559 public function get_schema_for_website() { 512 $schema = [513 '@context' => 'https://schema.org',514 '@type' => 'WebSite',515 'url' => home_url('/'),516 'name' => get_bloginfo('name'),517 'description' => get_bloginfo('description'),518 'potentialAction' => [519 '@type' => 'SearchAction',520 'target' => home_url('/?s={search_term_string}'),521 'query-input' => 'required name=search_term_string',522 ],523 ];524 return $schema;525 } 526 527 // --- Assemble Organization Schema ---528 public function get_schema_for_organization() {560 $schema = [ 561 '@context' => 'https://schema.org', 562 '@type' => 'WebSite', 563 'url' => home_url('/'), 564 'name' => get_bloginfo('name'), 565 'description' => get_bloginfo('description'), 566 'potentialAction' => [ 567 '@type' => 'SearchAction', 568 'target' => home_url('/?s={search_term_string}'), 569 'query-input' => 'required name=search_term_string', 570 ], 571 ]; 572 return $schema; 573 } 574 575 // --- Assemble Organization Schema --- 576 public function get_schema_for_organization() { 529 577 // 1. Name & URL 530 578 $name = seo44_get_option('org_name') ?: get_bloginfo('name'); … … 553 601 // For sameAs, a URL is desired. Add URLs from fields and construct Twitter / X and Facebook URL 554 602 555 // Twitter/X: Handle logic556 $twitter_handle = seo44_get_option('twitter_handle');557 if ( !empty($twitter_handle) && is_string($twitter_handle) ) {558 // Clean handle just in case they added @559 $clean_handle = str_replace('@', '', $twitter_handle);560 $same_as[] = 'https://x.com/' . esc_attr($clean_handle);561 }603 // Twitter/X: Handle logic 604 $twitter_handle = seo44_get_option('twitter_handle'); 605 if ( !empty($twitter_handle) && is_string($twitter_handle) ) { 606 // Clean handle just in case they added @ 607 $clean_handle = str_replace('@', '', $twitter_handle); 608 $same_as[] = 'https://x.com/' . esc_attr($clean_handle); 609 } 562 610 563 611 $extras = ['social_facebook', 'social_instagram', 'social_linkedin', 'social_youtube', 'social_tiktok']; … … 566 614 if ($val) $same_as[] = esc_url($val); 567 615 } 568 // NEW: Process Additional URLs (One per line)569 $additional_urls = seo44_get_option('social_additional');570 if ( !empty($additional_urls) && is_string($additional_urls) ) {571 // Split by newline, trim whitespace, and filter empty lines572 $urls = array_filter(array_map('trim', explode("\n", $additional_urls)));573 574 foreach ($urls as $raw_url) {575 // Validate it is a real URL before adding576 $clean_url = esc_url_raw($raw_url);577 if (!empty($clean_url)) {578 $same_as[] = $clean_url;579 }580 }581 }616 // NEW: Process Additional URLs (One per line) 617 $additional_urls = seo44_get_option('social_additional'); 618 if ( !empty($additional_urls) && is_string($additional_urls) ) { 619 // Split by newline, trim whitespace, and filter empty lines 620 $urls = array_filter(array_map('trim', explode("\n", $additional_urls))); 621 622 foreach ($urls as $raw_url) { 623 // Validate it is a real URL before adding 624 $clean_url = esc_url_raw($raw_url); 625 if (!empty($clean_url)) { 626 $same_as[] = $clean_url; 627 } 628 } 629 } 582 630 583 631 // 4. Build the Schema … … 589 637 ]; 590 638 591 // Add Alternate Name592 $alt_name = seo44_get_option('org_alternate_name');593 if ($alt_name) {594 $schema['alternateName'] = $alt_name;595 }639 // Add Alternate Name 640 $alt_name = seo44_get_option('org_alternate_name'); 641 if ($alt_name) { 642 $schema['alternateName'] = $alt_name; 643 } 596 644 597 645 // Add Tagline (Slogan) … … 613 661 614 662 // Contact Point (Updated to include Email) 615 $phone = seo44_get_option('org_phone');616 $email = seo44_get_option('org_email');617 618 if ($phone || $email) {619 $contact_point = ['@type' => 'ContactPoint'];620 if ($phone) {621 $contact_point['telephone'] = $phone;622 $contact_point['contactType'] = 'customer service';623 }624 if ($email) {625 $contact_point['email'] = $email;626 }627 $schema['contactPoint'] = $contact_point;628 }629 630 // Email at the top level is also good practice631 if ($email) {632 $schema['email'] = $email;633 }634 635 // 5. Address636 $street = seo44_get_option('org_address_street');637 $city = seo44_get_option('org_address_city');638 if ($street && $city) {639 $schema['address'] = [640 '@type' => 'PostalAddress',641 'streetAddress' => $street,642 'addressLocality' => $city,643 'addressRegion' => seo44_get_option('org_address_state'),644 'postalCode' => seo44_get_option('org_address_zip'),645 'addressCountry' => seo44_get_option('org_address_country')646 ];647 }648 649 // Service Area (New)650 $area_served = seo44_get_option('org_area_served');651 if ($area_served) {652 $schema['areaServed'] = [653 '@type' => 'Place',654 'name' => $area_served655 ];656 }657 // 6. Founder658 $founder = seo44_get_option('org_founder');659 if ($founder) {660 $schema['founder'] = [661 '@type' => 'Person',662 'name' => $founder663 ];664 }665 666 // 7. Founding Date667 $founding_date = seo44_get_option('org_founding_date');668 if ( !empty($founding_date) && is_string($founding_date) ) {669 // Basic validation: ensure it looks somewhat like a year or date670 // You can leave it as raw string, Google parses ISO 8601 (YYYY-MM-DD) well.671 $schema['foundingDate'] = strip_tags($founding_date);672 }673 // 8. Professional License (New)674 $license = seo44_get_option('org_license');675 if ($license) {676 // "hasCredential" is the modern schema property for this677 $schema['hasCredential'] = [678 '@type' => 'EducationalOccupationalCredential',679 'credentialCategory' => 'license',680 'name' => $license, // e.g., "Contractor License #123456"681 'recognizedBy' => [682 '@type' => 'Organization',683 'name' => 'State Licensing Board' // Generic fallback since we don't ask for the issuer684 ]685 ];686 687 // Also add it as a simple identifier for wider compatibility688 $schema['identifier'] = $license;689 }690 // FINAL STEP: Apply Filters for Extensibility using 'seo44_organization_schema'663 $phone = seo44_get_option('org_phone'); 664 $email = seo44_get_option('org_email'); 665 666 if ($phone || $email) { 667 $contact_point = ['@type' => 'ContactPoint']; 668 if ($phone) { 669 $contact_point['telephone'] = $phone; 670 $contact_point['contactType'] = 'customer service'; 671 } 672 if ($email) { 673 $contact_point['email'] = $email; 674 } 675 $schema['contactPoint'] = $contact_point; 676 } 677 678 // Email at the top level is also good practice 679 if ($email) { 680 $schema['email'] = $email; 681 } 682 683 // 5. Address 684 $street = seo44_get_option('org_address_street'); 685 $city = seo44_get_option('org_address_city'); 686 if ($street && $city) { 687 $schema['address'] = [ 688 '@type' => 'PostalAddress', 689 'streetAddress' => $street, 690 'addressLocality' => $city, 691 'addressRegion' => seo44_get_option('org_address_state'), 692 'postalCode' => seo44_get_option('org_address_zip'), 693 'addressCountry' => seo44_get_option('org_address_country') 694 ]; 695 } 696 697 // Service Area (New) 698 $area_served = seo44_get_option('org_area_served'); 699 if ($area_served) { 700 $schema['areaServed'] = [ 701 '@type' => 'Place', 702 'name' => $area_served 703 ]; 704 } 705 // 6. Founder 706 $founder = seo44_get_option('org_founder'); 707 if ($founder) { 708 $schema['founder'] = [ 709 '@type' => 'Person', 710 'name' => $founder 711 ]; 712 } 713 714 // 7. Founding Date 715 $founding_date = seo44_get_option('org_founding_date'); 716 if ( !empty($founding_date) && is_string($founding_date) ) { 717 // Basic validation: ensure it looks somewhat like a year or date 718 // You can leave it as raw string, Google parses ISO 8601 (YYYY-MM-DD) well. 719 $schema['foundingDate'] = wp_strip_all_tags($founding_date); 720 } 721 // 8. Professional License (New) 722 $license = seo44_get_option('org_license'); 723 if ($license) { 724 // "hasCredential" is the modern schema property for this 725 $schema['hasCredential'] = [ 726 '@type' => 'EducationalOccupationalCredential', 727 'credentialCategory' => 'license', 728 'name' => $license, // e.g., "Contractor License #123456" 729 'recognizedBy' => [ 730 '@type' => 'Organization', 731 'name' => 'State Licensing Board' // Generic fallback since we don't ask for the issuer 732 ] 733 ]; 734 735 // Also add it as a simple identifier for wider compatibility 736 $schema['identifier'] = $license; 737 } 738 // FINAL STEP: Apply Filters for Extensibility using 'seo44_organization_schema' 691 739 // Allows developers to add properties like 'duns', 'naics', 'awards', etc. 692 return apply_filters('seo44_organization_schema', $schema);693 } 694 695 // --- Intelligent Schema Detection ---740 return apply_filters('seo44_organization_schema', $schema); 741 } 742 743 // --- Intelligent Schema Detection --- 696 744 697 745 /** … … 703 751 $post = get_post($post_id); 704 752 if (!$post) return []; 705 753 754 // 1. Try Advanced HowTo (Jump Links Content Miner) 755 // This runs FIRST to see if we have a robust HowTo available 756 $advanced_howto = $this->generate_advanced_howto_schema($post); 757 758 $final_schemas = []; 759 if (!empty($advanced_howto)) { 760 $final_schemas[] = $advanced_howto; 761 } 762 763 // 2. Run standard detection for FAQ and HowTo (if not already found) 764 // We still run this for FAQPage, but we might skip HowTo if advanced found one 706 765 if (has_blocks($post->post_content)) { 707 // Use the precise block-based parser for modern content 708 return $this->parse_blocks_for_special_schema($post_id, parse_blocks($post->post_content)); 766 $standard_schemas = $this->parse_blocks_for_special_schema($post_id, parse_blocks($post->post_content), !empty($advanced_howto)); // Pass "skip_howto" flag 709 767 } else { 710 768 // Use the HTML fallback for classic editor or page builder content 711 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound769 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 712 770 $rendered_content = apply_filters('the_content', $post->post_content); 713 return $this->parse_html_for_special_schema($post_id, $rendered_content); 714 } 715 } 716 717 771 $standard_schemas = $this->parse_html_for_special_schema($post_id, $rendered_content, !empty($advanced_howto)); 772 } 773 774 if (!empty($standard_schemas)) { 775 $final_schemas = array_merge($final_schemas, $standard_schemas); 776 } 777 778 return $final_schemas; 779 } 780 781 718 782 /** 719 783 * NEW: Parses HTML content for FAQ and How-To patterns (the fallback method). 784 * Advanced HowTo Scanner (The "Content Miner") 785 * Uses Jump Links block as a map to mine content between headings. 720 786 */ 721 private function parse_html_for_special_schema($post_id, $html) { 787 private function generate_advanced_howto_schema($post) { 788 // 0. Check Cache 789 $cache_key = 'seo44_howto_' . $post->ID . '_' . $post->post_modified; 790 $cached_schema = get_transient($cache_key); 791 if ($cached_schema !== false) { 792 return $cached_schema; 793 } 794 // 1. Strict Check: Is the HowTo Checkbox enabled? 795 $enable_howto_meta = get_post_meta($post->ID, '_seo44_enable_howto', true); 796 797 if ($enable_howto_meta !== 'yes') { 798 return null; 799 } 800 801 // 2. Parse Blocks and Find Jump Links Block 802 $blocks = parse_blocks($post->post_content); 803 $flat_blocks = $this->flatten_blocks($blocks); 804 $jump_links_block = $this->find_jump_links_block($flat_blocks); 805 806 if (!$jump_links_block) return null; 807 808 // 3. Setup Variables 809 $desc_key = seo44_get_option('description_key') ?: 'seo44_description'; 810 $desc = get_post_meta($post->ID, $desc_key, true); 811 if (empty($desc)) { 812 $desc = get_post_meta($post->ID, '_yoast_wpseo_metadesc', true) ?: get_post_meta($post->ID, '_aioseop_description', true); 813 if (empty($desc)) { 814 $desc = get_the_excerpt($post->ID); 815 if (empty($desc)) { 816 $desc = wp_trim_words(strip_shortcodes($post->post_content), 25); 817 } 818 } 819 } 820 821 $steps = []; 822 $current_step = null; 823 $tools = []; 824 $supplies = []; 825 $total_time = ''; 826 $prep_time = ''; 827 $perform_time = ''; 828 $found_first_step = false; 829 $potential_list_type = ''; 830 $video_schema = []; 831 $yield = ''; 832 833 // 4. Iterate Blocks 834 foreach ($flat_blocks as $block) { 835 836 // --- PHASE A: PRE-STEP SCANNING (Intro) --- 837 if (!$found_first_step) { 838 839 if ($block['blockName'] === 'core/paragraph') { 840 // FIX: Decode entities to ensure non-breaking spaces don't break the regex 841 $text = html_entity_decode(wp_strip_all_tags($block['innerHTML'])); 842 843 // FIX: Updated Total Time Regex to prevent matching "Prep Time" as "Time" 844 // Logic: Match "Total Time" OR "Time" that is NOT preceded by Prep/Active/Cook 845 if (empty($total_time)) { 846 $total_time = $this->parse_duration_string($text, '(?:Total\s+|(?<!Prep\s|ation\s|Active\s|Cook\s))Time:\s*'); 847 } 848 if (empty($prep_time)) $prep_time = $this->parse_duration_string($text, 'Prep(?:aration)?\s*Time:\s*'); 849 if (empty($perform_time)) $perform_time = $this->parse_duration_string($text, '(?:Perform|Active|Cook)\s*Time:\s*'); 850 851 if (empty($yield) && preg_match('/(?:Yields|Makes):\s*(.+)/i', $text, $matches)) { 852 $yield = trim($matches[1]); 853 } 854 855 // FIX: Scan for Comma-Separated Tools/Supplies in Paragraphs 856 // e.g. "Tools: Hammer, Nails, Wood" 857 if (preg_match('/(?:Tools?|Equipment|Ingredients?|Supplies|Materials?):\s*(.+)/i', $text, $matches)) { 858 $type = (stripos($matches[0], 'tool') !== false || stripos($matches[0], 'equipment') !== false) ? 'tool' : 'supply'; 859 $items = explode(',', $matches[1]); 860 foreach ($items as $item) { 861 $clean_item = trim($item); 862 $clean_item = rtrim($clean_item, '.'); // Remove trailing period 863 if (!empty($clean_item)) { 864 if ($type === 'tool') { 865 $tools[] = ['@type' => 'HowToTool', 'name' => $clean_item]; 866 } else { 867 $supplies[] = ['@type' => 'HowToSupply', 'name' => $clean_item]; 868 } 869 } 870 } 871 } 872 } 873 874 // (Bullet list logic remains the same) 875 if ($block['blockName'] === 'core/heading') { 876 $text = strtolower(wp_strip_all_tags($block['innerHTML'])); 877 if (strpos($text, 'tool') !== false || strpos($text, 'equipment') !== false) { 878 $potential_list_type = 'tool'; 879 } elseif (strpos($text, 'material') !== false || strpos($text, 'suppl') !== false || strpos($text, 'ingredient') !== false) { 880 $potential_list_type = 'supply'; 881 } else { 882 $potential_list_type = ''; 883 } 884 } 885 if ($block['blockName'] === 'core/list' && $potential_list_type) { 886 if (preg_match_all('/<li[^>]*>(.*?)<\/li>/s', $block['innerHTML'], $list_matches)) { 887 foreach ($list_matches[1] as $li_text) { 888 $clean_li = trim(wp_strip_all_tags($li_text)); 889 if ($potential_list_type === 'tool') { 890 $tools[] = ['@type' => 'HowToTool', 'name' => $clean_li]; 891 } else { 892 $supplies[] = ['@type' => 'HowToSupply', 'name' => $clean_li]; 893 } 894 } 895 } 896 $potential_list_type = ''; 897 } 898 899 // (Video logic remains the same) 900 if ($block['blockName'] === 'core/embed' && isset($block['attrs']['providerNameSlug']) && $block['attrs']['providerNameSlug'] === 'youtube') { 901 $video_url = $block['attrs']['url']; 902 $video_id = self::extract_youtube_video_id($video_url); 903 $upload_date = self::get_youtube_upload_date($video_id, $post->ID); 904 905 $oembed_url = 'https://www.youtube.com/oembed?format=json&url=' . urlencode($video_url); 906 $response = wp_remote_get($oembed_url); 907 908 $vid_title = ''; $vid_thumb = ''; $vid_author = ''; 909 if (!is_wp_error($response)) { 910 $data = json_decode(wp_remote_retrieve_body($response), true); 911 if ($data) { 912 $vid_title = $data['title']; 913 $vid_thumb = $data['thumbnail_url']; 914 $vid_author = $data['author_name']; 915 } 916 } 917 if (empty($vid_title)) $vid_title = get_the_title($post->ID) . ' Video Tutorial'; 918 if (empty($vid_thumb)) $vid_thumb = 'https://i.ytimg.com/vi/' . $video_id . '/hqdefault.jpg'; 919 920 $video_schema = [ 921 '@type' => 'VideoObject', 922 'name' => $vid_title, 923 'description' => get_the_title($post->ID) . ' - Video Guide', 924 'thumbnailUrl' => $vid_thumb, 925 'uploadDate' => $upload_date, 926 'embedUrl' => $video_url, 927 'contentUrl' => $video_url 928 ]; 929 if ($vid_author) { 930 $video_schema['author'] = ['@type' => 'Person', 'name' => $vid_author]; 931 } 932 } 933 } 934 935 // --- PHASE B: STEP MINING --- 936 if ($block['blockName'] === 'core/heading') { 937 if (preg_match('/id="([^"]+)"/', $block['innerHTML'], $id_match)) { 938 $anchor_id = $id_match[1]; 939 940 // --- EXCLUSION LOGIC START --- 941 $whitelist_ids = get_post_meta($post->ID, '_seo44_howto_step_ids', true); 942 $should_include = true; 943 944 // STRATEGY A: Whitelist (New System) 945 if ( ! empty($whitelist_ids) && is_array($whitelist_ids) ) { 946 if ( ! in_array($anchor_id, $whitelist_ids) ) { 947 $should_include = false; 948 } 949 // FIX: Global Guard for Intro/Description 950 // Even if whitelisted, ignore Intro headings so they don't consume metadata 951 // May not need this... 952 if (stripos($anchor_id, 'intro') !== false || stripos($anchor_id, 'description') !== false) { 953 $should_include = false; 954 } 955 } 956 // STRATEGY B: Fallback 957 else { 958 if (stripos($anchor_id, 'intro') !== false) { $should_include = false; } 959 if (substr($anchor_id, -7) === '-nostep') { $should_include = false; } 960 } 961 962 if ( ! $should_include ) { 963 if ($current_step) { $steps[] = $current_step; $current_step = null; } 964 continue; 965 } 966 // --- EXCLUSION LOGIC END --- 967 968 $found_first_step = true; 969 if ($current_step) { $steps[] = $current_step; } 970 971 $current_step = [ 972 '@type' => 'HowToStep', 973 'url' => get_permalink($post->ID) . '#' . $anchor_id, 974 'name' => trim(wp_strip_all_tags($block['innerHTML'])), 975 'text' => '', 976 'image' => [] 977 ]; 978 continue; 979 } 980 } 981 982 if ($current_step) { 983 if ($block['blockName'] === 'core/paragraph') { 984 $text = trim(wp_strip_all_tags($block['innerHTML'])); 985 if (!empty($text)) { $current_step['text'] .= $text . ' '; } 986 } 987 if ($block['blockName'] === 'core/image') { 988 if (preg_match('/src="([^"]+)"/', $block['innerHTML'], $match)) { 989 if (!isset($current_step['image'])) { $current_step['image'] = []; } 990 $current_step['image'][] = $match[1]; 991 } 992 } 993 if ($block['blockName'] === 'core/list') { 994 $current_step['text'] .= trim(wp_strip_all_tags($block['innerHTML'])) . ' '; 995 } 996 } 997 } 998 999 if ($current_step) { $steps[] = $current_step; } 1000 1001 if (!empty($steps)) { 1002 foreach ($steps as $k => $step) { 1003 if (empty($step['image'])) { 1004 $steps[$k]['image'] = ''; 1005 } elseif (count($step['image']) === 1) { 1006 $steps[$k]['image'] = $step['image'][0]; 1007 } 1008 $steps[$k]['text'] = trim($step['text']); 1009 } 1010 1011 $howto = [ 1012 '@context' => 'https://schema.org', 1013 '@type' => 'HowTo', 1014 'name' => get_the_title($post->ID), 1015 'description' => $desc, 1016 'step' => $steps 1017 ]; 1018 1019 if ($total_time) { $howto['totalTime'] = $total_time; } 1020 if ($prep_time) { $howto['prepTime'] = $prep_time; } 1021 if ($perform_time) { $howto['performTime'] = $perform_time; } 1022 if (!empty($tools)) { $howto['tool'] = $tools; } 1023 if (!empty($supplies)) { $howto['supply'] = $supplies; } 1024 if (!empty($video_schema)) { $howto['video'] = $video_schema; } 1025 if ($yield) { $howto['yield'] = $yield; } 1026 $howto['disambiguatingDescription'] = $desc; 1027 1028 if (has_post_thumbnail($post->ID)) { 1029 $howto['image'] = get_the_post_thumbnail_url($post->ID, 'full'); 1030 } 1031 $categories = get_the_category($post->ID); 1032 if (!empty($categories)) { 1033 $howto['about'] = ['@type' => 'Thing', 'name' => $categories[0]->name]; 1034 } 1035 $tags = get_the_tags($post->ID); 1036 if (!empty($tags)) { 1037 $howto['teaches'] = ['@type' => 'DefinedTerm', 'name' => $tags[0]->name]; 1038 } 1039 1040 set_transient($cache_key, $howto, DAY_IN_SECONDS); 1041 return $howto; 1042 } 1043 return null; 1044 } 1045 1046 /** 1047 * Helper: Parse Duration Strings 1048 * FIX: Re-wrote to accept raw patterns without delimiters to prevent syntax errors. 1049 */ 1050 private function parse_duration_string($text, $prefix_pattern) { 1051 // We construct the full regex here to ensure delimiters wrap the WHOLE pattern. 1052 // Matches: Label -> (1) Hours -> (2) Minutes 1053 $regex = '/' . $prefix_pattern . '(?:(\d+)\s*(?:hour|hr)s?\s*)?(?:and\s*)?(?:(\d+)\s*(?:minute|min)s?)?/i'; 1054 1055 if (preg_match($regex, $text, $matches)) { 1056 $hours = !empty($matches[1]) ? intval($matches[1]) : 0; 1057 $minutes = !empty($matches[2]) ? intval($matches[2]) : 0; 1058 1059 if ($hours > 0 || $minutes > 0) { 1060 $duration = 'PT'; 1061 if ($hours > 0) $duration .= $hours . 'H'; 1062 if ($minutes > 0) $duration .= $minutes . 'M'; 1063 return $duration; 1064 } 1065 } 1066 return ''; 1067 } 1068 1069 // private function get_youtube_id($url) { 1070 // preg_match('/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i', $url, $matches); 1071 // return isset($matches[1]) ? $matches[1] : ''; 1072 // } 1073 1074 /** 1075 * Helper: Flattens nested block structures into a single linear array. 1076 */ 1077 private function flatten_blocks($blocks) { 1078 $flat = []; 1079 foreach ($blocks as $block) { 1080 $flat[] = $block; 1081 if (!empty($block['innerBlocks'])) { 1082 $flat = array_merge($flat, $this->flatten_blocks($block['innerBlocks'])); 1083 } 1084 } 1085 return $flat; 1086 } 1087 1088 /** 1089 * Helper to find the Jump Links block recursively 1090 * Updated to be broader and find any block with 'jump-links' in the name 1091 * FIX: Added check to ensure blockName is a string before strpos 1092 */ 1093 private function find_jump_links_block($blocks) { 1094 foreach ($blocks as $block) { 1095 // Broad check for any jump links block 1096 if (isset($block['blockName']) && is_string($block['blockName']) && strpos($block['blockName'], 'jump-links') !== false) { 1097 return $block; 1098 } 1099 // Recursive check for inner blocks (e.g. inside Groups/Columns) 1100 if (!empty($block['innerBlocks'])) { 1101 $found = $this->find_jump_links_block($block['innerBlocks']); 1102 if ($found) return $found; 1103 } 1104 } 1105 return null; 1106 } 1107 1108 private function parse_html_for_special_schema($post_id, $html, $skip_howto = false) { 722 1109 $final_schemas = []; 723 1110 … … 749 1136 } 750 1137 751 // HowTo Detection in HTML752 if ( preg_match('/<h[2-4][^>]*>(installation|directions|instructions)<\/h[2-4]>/i', $html, $howto_heading_match)) {1138 // HowTo Detection (Skipped if Advanced scanner found one) 1139 if (!$skip_howto && preg_match('/<h[2-4][^>]*>(installation|directions|instructions)<\/h[2-4]>/i', $html, $howto_heading_match)) { 753 1140 preg_match('/' . preg_quote($howto_heading_match[0], '/') . '.*?<ol.*?>(.*?)<\/ol>/is', $html, $ol_match); 754 1141 if(isset($ol_match[1])) { … … 777 1164 * The original block-based parser, now in its own function. 778 1165 */ 779 private function parse_blocks_for_special_schema($post_id, $blocks ) {1166 private function parse_blocks_for_special_schema($post_id, $blocks, $skip_howto = false) { 780 1167 $faq_questions = []; 781 1168 $howto_steps = []; … … 786 1173 $final_schemas = []; 787 1174 788 foreach ($blocks as $block) {1175 foreach ($blocks as $block) { 789 1176 $heading_text = strtolower(wp_strip_all_tags($block['innerHTML'])); 790 1177 … … 796 1183 } 797 1184 $is_faq_section = false; 798 $is_howto_section = false; // Stop looking for HowTo steps as well1185 $is_howto_section = false; 799 1186 $current_answer_blocks = []; 800 1187 continue; … … 819 1206 } 820 1207 821 // --- HowTo Detection --- 822 if (!$is_faq_section && $block['blockName'] === 'core/heading' && preg_match('/(installation|directions|instructions)/i', $heading_text)) { 823 $is_howto_section = true; 824 // Use trim() to remove leading/trailing whitespace and newlines 825 $howto_heading_text = trim(wp_strip_all_tags($block['innerHTML'])); 826 continue; // Find the heading, then look for the list in subsequent blocks 827 } 828 // If we are in a how-to section and find the first ordered list, process it. 829 if ($is_howto_section && $block['blockName'] === 'core/list' && isset($block['attrs']['ordered']) && $block['attrs']['ordered']) { 830 831 if (!empty($block['innerBlocks'])) { 832 foreach($block['innerBlocks'] as $list_item_block) { 833 if ($list_item_block['blockName'] === 'core/list-item') { 834 $howto_steps[] = ['@type' => 'HowToStep', 'text' => esc_html(wp_strip_all_tags($list_item_block['innerHTML']))]; 1208 // HowTo Logic (Skipped if Advanced scanner found one) 1209 if (!$skip_howto) { 1210 if (!$is_faq_section && $block['blockName'] === 'core/heading' && preg_match('/(installation|directions|instructions)/i', $heading_text)) { 1211 $is_howto_section = true; 1212 // Use trim() to remove leading/trailing whitespace and newlines 1213 $howto_heading_text = trim(wp_strip_all_tags($block['innerHTML'])); 1214 continue; // Find the heading, then look for the list in subsequent blocks 1215 } 1216 // If we are in a how-to section and find the first ordered list, process it. 1217 if ($is_howto_section && $block['blockName'] === 'core/list' && isset($block['attrs']['ordered']) && $block['attrs']['ordered']) { 1218 if (!empty($block['innerBlocks'])) { 1219 foreach($block['innerBlocks'] as $list_item_block) { 1220 if ($list_item_block['blockName'] === 'core/list-item') { 1221 $howto_steps[] = ['@type' => 'HowToStep', 'text' => esc_html(wp_strip_all_tags($list_item_block['innerHTML']))]; 1222 } 835 1223 } 836 1224 } 837 }838 $is_howto_section = false; // Stop after finding the first ordered list1225 $is_howto_section = false; // Stop after finding the first ordered list 1226 } 839 1227 } 840 1228 } … … 876 1264 877 1265 /** 878 * HELPER: Cleans and formats block content for an answer.1266 * HELPER: Cleans and formats block content for an answer. 879 1267 */ 880 1268 private function clean_and_format_answer($blocks) { … … 909 1297 // Clean up remaining HTML, extra whitespace, and decode entities 910 1298 $clean_text = trim(wp_strip_all_tags(html_entity_decode($answer_html))); 911 return preg_replace('/\n\s*\n/', "\n", $clean_text); // Collapse multiple newlines912 } 913 1299 return preg_replace('/\n\s*\n/', "\n", $clean_text); 1300 } 1301 914 1302 // --- MEDIA PARSING FUNCTION --- 915 1303 … … 927 1315 $content = $post->post_content; 928 1316 $found_block_image_urls = []; 929 $found_video_urls = []; // Fixed: Added missing variable declaration 1317 $found_video_urls = []; 1318 1319 $default_description = get_the_title($post_id) . ' Video'; 930 1320 931 1321 if (has_blocks($content)) { 932 1322 $blocks = parse_blocks($content); 933 1323 foreach ($blocks as $block) { 1324 934 1325 // Handle Image Blocks 935 1326 if ($block['blockName'] === 'core/image' && !empty($block['attrs']['id'])) { … … 947 1338 } 948 1339 $images[] = $image_object; 949 $found_block_image_urls[] = $image_data[0]; // Keep track of URLs we've already added1340 $found_block_image_urls[] = $image_data[0]; 950 1341 } 951 1342 } 952 1343 953 // Handle YouTube Embed Blocks using the oEmbed API 1344 // Handle YouTube Embed Blocks using the oEmbed API + API/Scraping 954 1345 if ($block['blockName'] === 'core/embed' && isset($block['attrs']['providerNameSlug']) && $block['attrs']['providerNameSlug'] === 'youtube') { 955 $oembed_url = 'https://www.youtube.com/oembed?format=json&url=' . urlencode($block['attrs']['url']); 1346 $video_url = $block['attrs']['url']; 1347 1348 // 1. Get Accurate Date 1349 $video_id = self::extract_youtube_video_id($video_url); 1350 $upload_date = self::get_youtube_upload_date($video_id, $post_id); 1351 1352 // 2. Get Metadata 1353 $oembed_url = 'https://www.youtube.com/oembed?format=json&url=' . urlencode($video_url); 956 1354 $response = wp_remote_get($oembed_url); 1355 957 1356 if (!is_wp_error($response)) { 958 1357 $data = json_decode(wp_remote_retrieve_body($response), true); … … 962 1361 'name' => $data['title'], 963 1362 'thumbnailUrl' => $data['thumbnail_url'], 964 'embedUrl' => $block['attrs']['url'], 965 'author' => ['@type' => 'Person', 'name' => $data['author_name']] 1363 'embedUrl' => $video_url, 1364 'contentUrl' => $video_url, 1365 'author' => ['@type' => 'Person', 'name' => $data['author_name']], 1366 'uploadDate' => $upload_date, // Uses API/Scraped date 1367 'description' => $default_description 966 1368 ]; 967 $found_video_urls[] = $ block['attrs']['url']; // Track processed videos1369 $found_video_urls[] = $video_url; 968 1370 } 969 1371 } … … 972 1374 } 973 1375 974 // --- Universal Fallback for <img> and <iframe> tags in Classic Editor or HTML Blocks---975 976 // Find Images (logic is unchanged)1376 // --- Universal Fallback for <img> and <iframe> tags --- 1377 1378 // Find Images 977 1379 if (preg_match_all('/<img[^>]+>/i', $content, $img_matches)) { 978 1380 foreach ($img_matches[0] as $img_tag) { … … 982 1384 if (preg_match('/src\s*=\s*[\'"]([^\'"]+)[\'"]/i', $img_tag, $src_matches)) { 983 1385 $url = $src_matches[1]; 984 // Add the image ONLY if we haven't already added it from a core/image block985 1386 if (!in_array($url, $found_block_image_urls)) { 986 1387 $images[] = ['@type' => 'ImageObject', 'url' => $url]; 987 1388 } 988 1389 } 989 }990 } 991 992 // RESTORED: Find iframes1390 } 1391 } 1392 1393 // Find iframes for YouTube 993 1394 if (preg_match_all('/<iframe[^>]+src="([^"]+)"[^>]*>/i', $content, $iframe_matches)) { 994 1395 foreach($iframe_matches[1] as $iframe_src) { 995 // Skip if we already processed this video from a block 996 if (in_array($iframe_src, $found_video_urls)) { 997 continue; 998 } 999 1000 if (strpos($iframe_src, 'youtube.com/embed') !== false) { 1001 preg_match('/embed\/([a-zA-Z0-9_-]+)/i', $iframe_src, $id_matches); 1002 $video_id = $id_matches[1] ?? null; 1003 if ($video_id) { 1004 $videos[] = [ 1005 '@type' => 'VideoObject', 1006 'name' => 'Embedded YouTube Video', // Title isn't available from a raw iframe 1007 'thumbnailUrl' => 'https://i.ytimg.com/vi/' . $video_id . '/hqdefault.jpg', 1008 'embedUrl' => 'https://www.youtube.com/watch?v=' . $video_id, 1009 ]; 1010 } 1396 if (in_array($iframe_src, $found_video_urls)) { continue; } 1397 1398 // Use the static helper to check ID 1399 $video_id = self::extract_youtube_video_id($iframe_src); 1400 1401 if ($video_id) { 1402 $upload_date = self::get_youtube_upload_date($video_id, $post_id); 1403 1404 $videos[] = [ 1405 '@type' => 'VideoObject', 1406 'name' => 'Embedded YouTube Video', 1407 'description' => $default_description, 1408 'uploadDate' => $upload_date, 1409 'thumbnailUrl' => 'https://i.ytimg.com/vi/' . $video_id . '/hqdefault.jpg', 1410 'embedUrl' => 'https://www.youtube.com/watch?v=' . $video_id, 1411 'contentUrl' => 'https://www.youtube.com/watch?v=' . $video_id 1412 ]; 1011 1413 } 1012 1414 } … … 1015 1417 return ['images' => $images, 'videos' => $videos]; 1016 1418 } 1017 1018 // Function to generate BreadcrumbList schema for Taxonomy pages (not posts & Pages)1419 1420 // Function to generate BreadcrumbList schema for Taxonomy pages (not posts & Pages) 1019 1421 public function get_schema_for_taxonomy() { 1020 1422 $term = get_queried_object(); … … 1063 1465 return $schema; 1064 1466 } 1065 1066 // Function to create author name 1067 1068 public function get_author_name( $author_id ) { 1069 $format = seo44_get_option('author_format', 'display_name'); 1070 switch ($format) { 1071 case 'first_last': return get_the_author_meta('first_name', $author_id) . ' ' . get_the_author_meta('last_name', $author_id); 1072 case 'last_first': return get_the_author_meta('last_name', $author_id) . ', ' . get_the_author_meta('first_name', $author_id); 1073 default: return get_the_author_meta('display_name', $author_id); 1074 } 1075 } 1076 public static function get_author_name_static( $author_id ) { 1077 $frontend = new self(); 1078 return $frontend->get_author_name( $author_id ); 1079 } 1467 1468 // Function to create author name 1469 // With more graceful logic for missing name fields 1470 public function get_author_name( $author_id ) { 1471 $format = seo44_get_option('author_format', 'display_name'); 1472 1473 // 1. Get the raw values first 1474 $first_name = get_the_author_meta( 'first_name', $author_id ); 1475 $last_name = get_the_author_meta( 'last_name', $author_id ); 1476 1477 $formatted_name = ''; 1478 1479 switch ( $format ) { 1480 case 'first_last': 1481 // 2. Concatenate and trim. 1482 // If both are empty, trim() ensures we get "", not " ". 1483 // If one is missing, it removes the leading/trailing space. 1484 $formatted_name = trim( $first_name . ' ' . $last_name ); 1485 break; 1486 1487 case 'last_first': 1488 // 3. Smart comma handling. 1489 // Only add the comma if BOTH fields have data. 1490 if ( ! empty( $last_name ) && ! empty( $first_name ) ) { 1491 $formatted_name = $last_name . ', ' . $first_name; 1492 } else { 1493 // If one is missing, fall back to simple concatenation/trim without the comma 1494 $formatted_name = trim( $last_name . ' ' . $first_name ); 1495 } 1496 break; 1497 1498 // Default case is handled by the fallback check below 1499 } 1500 1501 // 4. The Fallback Check 1502 // If formatted_name is empty (because fields were empty) OR format was 'display_name' 1503 if ( empty( $formatted_name ) ) { 1504 return get_the_author_meta( 'display_name', $author_id ); 1505 } 1506 1507 return $formatted_name; 1508 } 1509 1510 public static function get_author_name_static( $author_id ) { 1511 $frontend = new self(); 1512 return $frontend->get_author_name( $author_id ); 1513 } 1514 1515 /** 1516 * Parses content for headings with IDs to create a "Table of Contents" schema. 1517 * @param string $content The post content. 1518 * @return array Array of WebPageElement objects. 1519 */ 1520 1521 // This could be updated to use IDS passed through to field from jump links block to be more efficient than scan and remove need for checkbox. 1522 // UPDATED: Now respects whitelist to match "included" jump links 1523 private static function generate_has_part_schema($content, $post_id) { 1524 $has_part = []; 1525 1526 // Retrieve the whitelist of IDs (generated by JS when saving the Jump Links block) 1527 $whitelist_ids = get_post_meta($post_id, '_seo44_howto_step_ids', true); 1528 1529 // Regex to find H2-H4 tags that have an ID attribute 1530 if (preg_match_all('/<h[2-4][^>]*id="([^"]+)"[^>]*>(.*?)<\/h[2-4]>/i', $content, $matches, PREG_SET_ORDER)) { 1531 1532 foreach ($matches as $match) { 1533 $anchor_id = $match[1]; 1534 $heading_text = wp_strip_all_tags($match[2]); 1535 1536 // --- FILTER LOGIC --- 1537 // Default to include everything if no whitelist exists (e.g. old post, no jump links block) 1538 // But if a whitelist exists (array), enforce it strictly. 1539 $should_include = true; 1540 1541 if ( ! empty($whitelist_ids) && is_array($whitelist_ids) ) { 1542 if ( ! in_array($anchor_id, $whitelist_ids) ) { 1543 $should_include = false; 1544 } 1545 } 1546 1547 if ($should_include && !empty($anchor_id) && !empty($heading_text)) { 1548 $has_part[] = [ 1549 '@type' => 'WebPageElement', 1550 'name' => $heading_text, 1551 'url' => get_permalink($post_id) . '#' . $anchor_id, 1552 'cssSelector' => '#' . $anchor_id 1553 ]; 1554 } 1555 } 1556 } 1557 return $has_part; 1558 } 1559 1560 1561 /** 1562 * Extracts YouTube video ID from various URL formats 1563 */ 1564 private static function extract_youtube_video_id($url) { 1565 $patterns = [ 1566 '/youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)/', // Standard 1567 '/youtube\.com\/embed\/([a-zA-Z0-9_-]+)/', // Embed 1568 '/youtu\.be\/([a-zA-Z0-9_-]+)/', // Short URL 1569 '/youtube\.com\/v\/([a-zA-Z0-9_-]+)/', // Old format 1570 ]; 1571 1572 foreach ($patterns as $pattern) { 1573 if (preg_match($pattern, $url, $matches)) { 1574 return $matches[1]; 1575 } 1576 } 1577 1578 return null; 1579 } 1580 1581 /** 1582 * Gets YouTube video upload date with multiple fallback strategies 1583 * @param string $video_id YouTube video ID 1584 * @param int $post_id WordPress post ID for fallback 1585 * @return string ISO 8601 formatted date 1586 */ 1587 private static function get_youtube_upload_date($video_id, $post_id) { 1588 if (empty($video_id)) { 1589 return get_the_date('c', $post_id); 1590 } 1591 1592 // Strategy 1: Check transient cache (24 hour cache to avoid API limits) 1593 $cache_key = 'seo44_yt_date_' . $video_id; 1594 $cached_date = get_transient($cache_key); 1595 1596 if ($cached_date !== false) { 1597 return $cached_date; 1598 } 1599 1600 // Strategy 2: Try YouTube Data API (if configured) 1601 $api_key = seo44_get_option('youtube_api_key'); 1602 1603 if (!empty($api_key)) { 1604 $api_date = self::fetch_youtube_date_from_api($video_id, $api_key); 1605 1606 if ($api_date) { 1607 // Cache for 24 hours 1608 set_transient($cache_key, $api_date, DAY_IN_SECONDS); 1609 return $api_date; 1610 } 1611 } 1612 1613 // Strategy 3: Try scraping YouTube page (no API key needed, but less reliable) 1614 $scraped_date = self::scrape_youtube_upload_date($video_id); 1615 1616 if ($scraped_date) { 1617 // Cache for 24 hours 1618 set_transient($cache_key, $scraped_date, DAY_IN_SECONDS); 1619 return $scraped_date; 1620 } 1621 1622 // Strategy 4: Fallback to post publication date 1623 return get_the_date('c', $post_id); 1624 } 1625 1626 /** 1627 * Fetches upload date from YouTube Data API v3 1628 */ 1629 private static function fetch_youtube_date_from_api($video_id, $api_key) { 1630 $api_url = sprintf( 1631 'https://www.googleapis.com/youtube/v3/videos?id=%s&key=%s&part=snippet', 1632 urlencode($video_id), 1633 urlencode($api_key) 1634 ); 1635 1636 $response = wp_remote_get($api_url, [ 1637 'timeout' => 5, 1638 'sslverify' => true 1639 ]); 1640 1641 if (is_wp_error($response)) { 1642 return null; 1643 } 1644 1645 $body = wp_remote_retrieve_body($response); 1646 $data = json_decode($body, true); 1647 1648 if (isset($data['items'][0]['snippet']['publishedAt'])) { 1649 return $data['items'][0]['snippet']['publishedAt']; 1650 } 1651 1652 return null; 1653 } 1654 1655 /** 1656 * Scrapes upload date from YouTube page HTML (fallback method, no API key needed) 1657 * IMPPROVED: Scans Meta tags first, then JSON-LD, then internal player data. 1658 */ 1659 private static function scrape_youtube_upload_date($video_id) { 1660 $video_page_url = 'https://www.youtube.com/watch?v=' . $video_id; 1661 1662 $response = wp_remote_get($video_page_url, [ 1663 'timeout' => 10, 1664 // Use a generic browser agent to avoid being served a "bare" bot page 1665 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 1666 'sslverify' => true 1667 ]); 1668 1669 if (is_wp_error($response)) { 1670 return null; 1671 } 1672 1673 $html = wp_remote_retrieve_body($response); 1674 1675 // Strategy A: Look for Meta Tags (Most Reliable) 1676 // YouTube usually includes: <meta itemprop="datePublished" content="YYYY-MM-DD"> 1677 if (preg_match('/<meta\s+itemprop="datePublished"\s+content="([^"]+)"/i', $html, $matches)) { 1678 return $matches[1]; 1679 } 1680 if (preg_match('/<meta\s+itemprop="uploadDate"\s+content="([^"]+)"/i', $html, $matches)) { 1681 return $matches[1]; 1682 } 1683 1684 // Strategy B: JSON-LD Schema (Your original method) 1685 if (preg_match('/"uploadDate":"([^"]+)"/', $html, $matches)) { 1686 return $matches[1]; 1687 } 1688 1689 // Strategy C: Look for microformatDataRenderer (Internal YouTube Data) 1690 // This is often found inside the large ytInitialPlayerResponse JSON object 1691 if (preg_match('/"publishDate":"([^"]+)"/', $html, $matches)) { 1692 $date_string = $matches[1]; // Usually YYYY-MM-DD 1693 if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_string)) { 1694 return $date_string; 1695 } 1696 // Fallback for compact format YYYYMMDD 1697 if (preg_match('/^(\d{4})(\d{2})(\d{2})$/', $date_string, $date_parts)) { 1698 return sprintf('%s-%s-%s', $date_parts[1], $date_parts[2], $date_parts[3]); 1699 } 1700 } 1701 1702 return null; 1703 } 1080 1704 } -
search-appearance-toolkit-seo-44/trunk/includes/class-seo44-metabox.php
r3389330 r3423355 1 1 <?php 2 // Version 4.3 - Added a Generate HowTo Schema checkbox that appears via activation word 2 3 class SEO44_Metabox { 3 4 … … 23 24 $description = get_post_meta($post->ID, seo44_get_option('description_key'), true); 24 25 $keywords = get_post_meta($post->ID, seo44_get_option('keywords_key'), true); 25 $jump_link_headings = get_post_meta($post->ID, '_seo44_jump_link_headings', true); 26 wp_nonce_field('seo44_jump_links_nonce', 'seo44_jump_links_nonce_field'); 26 27 // NEW: Retrieve the HowTo toggle state 28 // We use 'yes' for checked, empty for unchecked 29 $enable_howto = get_post_meta($post->ID, '_seo44_enable_howto', true); 30 31 // Logic to determine initial visibility: Show if already checked 32 $howto_wrapper_style = ($enable_howto === 'yes') ? '' : 'display:none;'; 33 27 34 ?> 28 <input type="hidden" id="seo44_jump_link_headings_field" name="seo44_jump_link_headings" value="<?php echo esc_attr($jump_link_headings); ?>">35 29 36 <p> 30 37 <label for="seo44_title"><strong><?php esc_html_e('SEO Title', 'search-appearance-toolkit-seo-44'); ?></strong></label><br> … … 54 61 </p> 55 62 <?php endif; ?> 63 <div id="seo44-howto-trigger-wrapper" style="<?php echo esc_attr($howto_wrapper_style); ?>"> 64 <p> 65 <label for="seo44_enable_howto"> 66 <input type="checkbox" id="seo44_enable_howto" name="seo44_enable_howto" value="yes" <?php checked($enable_howto, 'yes'); ?>> 67 <strong><?php esc_html_e('Generate HowTo Schema', 'search-appearance-toolkit-seo-44'); ?></strong> 68 </label> 69 <br> 70 <span class="description"> 71 <?php esc_html_e('It looks like you are publishing a "How-To" guide. When this box is checked, SEO 44 will generate HowTo structured data.', 'search-appearance-toolkit-seo-44'); ?> 72 </span> 73 </p> 74 </div> 56 75 <hr> 57 76 <div id="seo44-snippet-preview"> … … 80 99 public function save_meta_box_data($post_id) { 81 100 if (!isset($_POST['seo44_meta_box_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['seo44_meta_box_nonce'])), 'seo44_save_meta_box_data')) return; 82 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;83 $post_type = get_post_type($post_id);84 // Robust capability checking85 if (!current_user_can("edit_{$post_type}s")) {101 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; 102 $post_type = get_post_type($post_id); 103 // Robust capability checking 104 if (!current_user_can("edit_{$post_type}s")) { 86 105 return; 87 }88 if (isset($_POST['seo44_title'])) {106 } 107 if (isset($_POST['seo44_title'])) { 89 108 update_post_meta($post_id, seo44_get_option('title_key'), sanitize_text_field(wp_unslash($_POST['seo44_title']))); 90 109 } … … 95 114 update_post_meta($post_id, seo44_get_option('keywords_key'), sanitize_text_field(wp_unslash($_POST['seo44_keywords']))); 96 115 } 97 // Save Logic for Hidden Box for Jump Links Block data 98 if (isset($_POST['seo44_jump_links_nonce_field']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['seo44_jump_links_nonce_field'])), 'seo44_jump_links_nonce')) { 99 if (isset($_POST['seo44_jump_link_headings'])) { 100 update_post_meta($post_id, '_seo44_jump_link_headings', sanitize_text_field(wp_unslash($_POST['seo44_jump_link_headings']))); 101 } 116 // NEW: Save HowTo Checkbox 117 if (isset($_POST['seo44_enable_howto'])) { 118 update_post_meta($post_id, '_seo44_enable_howto', 'yes'); 119 } else { 120 // Checkboxes don't send data if unchecked, so we must explicitly delete or update to 'no' 121 // Only do this if our nonce is verified (which implies the form was actually submitted) 122 update_post_meta($post_id, '_seo44_enable_howto', 'no'); 102 123 } 124 103 125 } 104 126 } -
search-appearance-toolkit-seo-44/trunk/includes/class-seo44-settings.php
r3405454 r3423355 1 1 <?php 2 // Version 4.3 3 // Added hasPart Table of Contents Schema, advanced HowTo Schema 4 // Added YouTube Data API to Integrations 2 5 class SEO44_Settings { 3 6 … … 59 62 add_settings_field('seo44_schema_tools', __('Schema Scanner', 'search-appearance-toolkit-seo-44'), [$this, 'render_schema_tools'], 'seo-44_schema', 'seo44_schema_settings_section'); 60 63 add_settings_field('seo44_enable_schema', __('Enable Schema', 'search-appearance-toolkit-seo-44'), [$this, 'render_checkbox_field'], 'seo-44_schema', 'seo44_schema_settings_section', ['id' => 'enable_schema', 'label' => __('Output JSON-LD to your webpages.', 'search-appearance-toolkit-seo-44')]); 61 // NEW: Add checkbox for scanning content62 64 add_settings_field('seo44_scan_content_for_schema', __('Scan Content for Media', 'search-appearance-toolkit-seo-44'), [$this, 'render_checkbox_field'], 'seo-44_schema', 'seo44_schema_settings_section', [ 63 65 'id' => 'scan_content_for_schema', … … 65 67 'tooltip' => 'This provides more detail to search engines but can be slightly more resource-intensive.' 66 68 ]); 67 // NEW: Add checkbox for advanced schema detection 68 add_settings_field('seo44_enable_advanced_schema', __('Enable Advanced Schema Detection', 'search-appearance-toolkit-seo-44'), [$this, 'render_checkbox_field'], 'seo-44_schema', 'seo44_schema_settings_section', [ 69 'id' => 'enable_advanced_schema', 70 'label' => __('Scan content for patterns to generate FAQ and How-To schema.', 'search-appearance-toolkit-seo-44'), 71 'tooltip' => 'If special formats are detected, they will be added to your page\'s schema structured data.' 72 ]); 69 // UPDATED: Using custom callback to include the Jump Links Block recommendation 70 add_settings_field('seo44_enable_advanced_schema', __('Enable Advanced Schema Detection', 'search-appearance-toolkit-seo-44'), [$this, 'render_advanced_schema_checkbox'], 'seo-44_schema', 'seo44_schema_settings_section'); 71 72 // NEW: Jump Links Integration Field 73 add_settings_field('seo44_enable_jumplinks_schema', 74 __('Generate "Table of Contents" Schema', 'search-appearance-toolkit-seo-44'), 75 [$this, 'render_checkbox_field'], 76 'seo-44_schema', 77 'seo44_schema_settings_section', 78 [ 79 'id' => 'enable_jumplinks_schema', 80 'label' => __('Automatically create "hasPart" schema for headings highlighted in Jump Links Blocks, aligning structured data with Jump Links to reinforce support for Jump-to links in search results.', 'search-appearance-toolkit-seo-44'), 81 'tooltip' => 'This helps Google understand the deep structure of your article by mapping your headings (H2-H4) as distinct sections, complementing the visual Jump Links Block on your page.' 82 ] 83 ); 73 84 add_settings_field('seo44_enable_schema_on_taxonomies', __('Enable Schema on Taxonomies', 'search-appearance-toolkit-seo-44'), [$this, 'render_checkbox_field'], 'seo-44_schema', 'seo44_schema_settings_section', [ 74 85 'id' => 'enable_schema_on_taxonomies', … … 145 156 ] 146 157 ); 147 148 149 150 158 // 3. Add Address Fields (Crucial for Local SEO & Disambiguation) 151 159 add_settings_field( … … 190 198 ); 191 199 192 193 200 add_settings_field( 194 201 'org_phone', … … 391 398 ] 392 399 ); 400 // --- Site APIs Header --- 401 add_settings_field( 402 'seo44_site_apis_header', 403 '', // Empty label for full-width header 404 [$this, 'render_site_apis_header_field'], 405 'seo-44_integrations', 406 'seo44_integrations_section' 407 ); 408 409 // --- YouTube API Key --- 410 add_settings_field( 411 'youtube_api_key', 412 __('YouTube API Key', 'search-appearance-toolkit-seo-44'), 413 [$this, 'render_youtube_api_key_field'], 414 'seo-44_integrations', 415 'seo44_integrations_section', 416 [ 417 'label_for' => 'youtube_api_key', 418 'tooltip' => __('This will be used to improve the Video Object schema created for embedded YouTube Videos.', 'search-appearance-toolkit-seo-44') 419 ] 420 ); 393 421 // --- end of settings_init --- 394 422 } … … 400 428 'enable_tags', 'include_keywords', 'include_author', 401 429 'enable_og_tags', 'enable_twitter_tags', 402 'enable_schema', 'scan_content_for_schema', 'enable_ advanced_schema', 'enable_schema_on_cpts', 'enable_schema_on_taxonomies','enable_organization_schema',430 'enable_schema', 'scan_content_for_schema', 'enable_jumplinks_schema', 'enable_advanced_schema', 'enable_schema_on_cpts', 'enable_schema_on_taxonomies','enable_organization_schema', 403 431 'enable_sitemaps', 'enable_sitemap_ping', 404 432 'sitemap_include_images', 'sitemap_include_content_images', … … 423 451 'org_address_street', 'org_address_city', 'org_address_state', 'org_address_zip', 'org_address_country', 424 452 'org_phone', 'org_email', 'org_area_served', 453 'youtube_api_key', 425 454 ]; 426 455 foreach ($text_fields as $tf) { … … 594 623 <?php 595 624 } 625 // UPDATED: Custom renderer for the Advanced Schema Checkbox 626 public function render_advanced_schema_checkbox() { 627 $id = 'enable_advanced_schema'; 628 $checked = seo44_get_option($id); 629 630 // Output the standard checkbox 631 printf( 632 '<label for="%s"><input type="checkbox" id="%s" name="seo44_settings[%s]" value="1" %s /> %s</label>', 633 esc_attr($id), 634 esc_attr($id), 635 esc_attr($id), 636 checked($checked, 1, false), 637 esc_html__('Scan content for patterns to generate FAQ and How-To schema.', 'search-appearance-toolkit-seo-44') 638 ); 639 seo44_render_tooltip('If special formats are detected, they will be added to your page\'s schema structured data.'); 640 641 // Add the new recommendation text with the link 642 echo '<p class="description">'; 643 echo esc_html__('For the most robust HowTo Schema, use a Jump Links Blocks for your How To steps. ', 'search-appearance-toolkit-seo-44'); 644 echo '<a href="https://seo44plugin.com/search-appearance-toolkit-seo-44/schema-structured-data/" target="_blank">' . esc_html__('(Learn more)', 'search-appearance-toolkit-seo-44') . '</a>'; 645 echo '</p>'; 646 647 } 596 648 597 649 public function render_textarea_field($args) { … … 775 827 echo '<p class="description">' . esc_html__('These tags are used to prove you own your site to search engines. Paste in your verification codes here and they will be added to your site\'s <head>.', 'search-appearance-toolkit-seo-44') . '</p>'; 776 828 } 829 public function render_site_apis_header_field() { 830 echo '<h3>' . esc_html__('Site APIs', 'search-appearance-toolkit-seo-44') . '</h3>'; 831 echo '<p class="description">' . esc_html__('Adding a YouTube API key fetches accurate video upload dates for schema structured data. Without this, video upload dates may fall back to your post publication date.', 'search-appearance-toolkit-seo-44') . '</p>'; 832 } 833 public function render_youtube_api_key_field($args) { 834 $value = seo44_get_option('youtube_api_key'); 835 $tooltip = isset($args['tooltip']) ? $args['tooltip'] : ''; 836 ?> 837 <input type="text" 838 id="youtube_api_key" 839 name="seo44_settings[youtube_api_key]" 840 value="<?php echo esc_attr($value); ?>" 841 class="regular-text code"> 842 843 <?php if ($tooltip) { seo44_render_tooltip($tooltip); } ?> 844 845 <p class="description"> 846 <?php esc_html_e('Enter your YouTube Data API v3 key.', 'search-appearance-toolkit-seo-44'); ?> 847 <a href="https://developers.google.com/youtube/v3/getting-started" target="_blank"> 848 <?php esc_html_e('Get API Key', 'search-appearance-toolkit-seo-44'); ?> 849 </a> 850 </p> 851 <?php 852 } 777 853 778 854 public function settings_page_html() { … … 780 856 return; 781 857 } 782 858 783 859 // --- START: SECURE TAB SWITCHING LOGIC --- 784 860 -
search-appearance-toolkit-seo-44/trunk/js/admin-script.js
r3389330 r3423355 1 1 jQuery(document).ready(function($) { 2 2 3 // --- NEW:Add custom class to metabox heading ---3 // --- Add custom class to metabox heading --- 4 4 // Find the metabox by its ID and then find the title element inside it. 5 5 var metabox = $('#seo44_meta_box'); … … 7 7 metabox.find('h2.hndle').addClass('seo44-metabox-heading'); 8 8 } 9 10 // --- "Use Example" Title Button --- 11 $('#seo44-use-example-title').on('click', function() { 12 var exampleText = $('#seo44-title-example').text(); 13 var titleInput = $('#seo44_title'); 14 titleInput.val(exampleText); 15 16 // Trigger the keyup event to update the snippet preview and character counter 17 titleInput.trigger('keyup'); 18 }); 9 19 10 20 // --- Character Counter Functionality (from v1.4) --- … … 36 46 createCounter('seo44_description', 'seo44_description_char_count', 160); 37 47 38 // --- NEW: "Use Example" Title Button --- 39 $('#seo44-use-example-title').on('click', function() { 40 var exampleText = $('#seo44-title-example').text(); 41 var titleInput = $('#seo44_title'); 42 titleInput.val(exampleText); 48 49 // --- HowTo Schema Trigger Logic --- 50 const howToWrapper = $('#seo44-howto-trigger-wrapper'); 51 const howToCheckbox = $('#seo44_enable_howto'); 52 const descriptionField = $('#seo44_description'); 53 54 // List of triggers (lowercase) 55 const triggers = ['step guide', 'step-by-step', 'how to', 'how-to', 'guide', 'walkthrough', 'directions', 'instructions', 'tutorial']; 56 57 // Flag to track if user has manually overridden the automation 58 let userHasInteracted = false; 59 60 // Listen for manual clicks 61 howToCheckbox.on('change', function() { 62 userHasInteracted = true; 63 }); 64 65 function checkHowToTriggers() { 66 // 1. If user manually touched the box, do nothing. 67 if (userHasInteracted) return; 68 69 // 2. Get description text 70 const text = descriptionField.val().toLowerCase(); 43 71 44 // Trigger the keyup event to update the snippet preview and character counter 45 titleInput.trigger('keyup'); 46 }); 72 // 3. Check for triggers 73 const hasTrigger = triggers.some(trigger => text.includes(trigger)); 74 75 if (hasTrigger) { 76 // Found a trigger! 77 howToWrapper.show(); // Make visible 78 79 // Only check it if it wasn't already checked (to avoid redundant events) 80 if (!howToCheckbox.is(':checked')) { 81 howToCheckbox.prop('checked', true); 82 } 83 } else { 84 // No trigger found. 85 // Behavior: If we are purely automated (user hasn't clicked), 86 // we hide it and uncheck it. 87 howToWrapper.hide(); 88 howToCheckbox.prop('checked', false); 89 } 90 } 91 92 // Run on keyup in description 93 descriptionField.on('keyup change paste', checkHowToTriggers); 94 95 // Run once on load to handle pre-filled content (if user hasn't saved yet) 96 // We delay slightly to ensure values are populated 97 setTimeout(function() { 98 // Only run auto-check if the box is currently hidden/unchecked. 99 // If it's already visible/checked from DB, we respect that state. 100 if (!howToCheckbox.is(':checked')) { 101 checkHowToTriggers(); 102 } else { 103 // If it IS checked from DB, mark as "interacted" so we don't auto-uncheck it 104 // just because the user edits the description and removes a keyword. 105 userHasInteracted = true; 106 } 107 }, 500); 108 47 109 48 110 // --- Snippet Preview Functionality --- 49 111 const titleInput = $('#seo44_title'); 50 112 const descriptionInput = $('#seo44_description'); 51 113 52 114 const previewHeaderUrl = $('.seo44-preview-breadcrumb-url'); 53 115 const previewTitle = $('.seo44-preview-title'); … … 58 120 const siteName = seo44_data.site_name; 59 121 const permalink = seo44_data.permalink; 60 122 61 123 function updatePreview() { 62 124 // Update Title 63 125 let titleVal = titleInput.val(); 64 previewTitle.text(titleVal ? titleVal : defaultTitle); 65 66 67 // if (titleVal) { 68 // previewTitle.text(titleVal); 69 // } else { 70 // If the input is empty, show "Post Title - Site Name" 71 // previewTitle.text(defaultTitle + ' - ' + siteName); 72 //} 126 previewTitle.text(titleVal ? titleVal : defaultTitle); 73 127 74 128 // Update Description … … 80 134 previewDescription.text('Enter a meta description to see how it will appear in search results...'); 81 135 } 82 83 // Update Breadcrumb URL136 137 // Update Breadcrumb URL 84 138 let breadcrumb = permalink.replace(/^https?:\/\//, '').replace(/\/$/, ''); 85 139 breadcrumb = breadcrumb.replace(/\//g, ' › '); // › is the > symbol -
search-appearance-toolkit-seo-44/trunk/readme.txt
r3405454 r3423355 3 3 Tags: seo, on-page seo, schema, structured data, xml sitemaps 4 4 Requires at least: 5.5 5 Tested up to: 6. 86 Stable tag: 4. 25 Tested up to: 6.9 6 Stable tag: 4.3 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 72 72 * **Knowledge Graph Control:** A dedicated interface to manage your brand's digital identity. Define your Founder, Founding Date, Contact Info, and professional Credentials to improve E-E-A-T signals. 73 73 * **Include Images and Videos:** A built-in tool automatically finds all images and embedded YouTube videos in your content and adds them to the schema, boosting their appearance in search results. 74 * **FAQ and How-To Detection:** Enable a smart scanner to detect patterns in your content for FAQ and How-To sections on your website and incorporate this useful format into the schema. 74 * **FAQ and How-To Detection:** Enable smart scanners to detect patterns in your content for FAQ and How-To sections on your website and incorporate this useful format into the schema. 75 * **Jump Links HowTo Scanner:** The plugin can use a Jump Links Block as a "Map" to generate detailed HowTo schema steps, while simultaneously scanning your content for Prep Time, Yield, Supplies, and Tools. 76 * **Table of Contents Schema:** Automatically generates `hasPart` structured data that mirrors your Jump Links Block, helping search engines understand your article's deep structure. 75 77 * **Modern Output:** All structured data is generated in the modern JSON-LD format preferred by search engines, following the guidelines set by [Schema.org](https://schema.org/). 76 78 * **Granular Control:** Tailor the Schema settings to fit your site's needs through Enable/disable settings, including on Custom Post Types and Taxonomies. … … 101 103 The Search Appearance Toolkit serves as a hub for connecting your site to essential third-party services, helping you to create valuable analytics data and site insights. 102 104 103 = Site Verification =104 Verify your site with search engines quickly and easily. Paste your verification codes for **Google Search Console** and **Bing Webmaster Tools** into the corresponding fields in the Integrations tab. The plugin handles the rest, adding site verification meta tags to your website's head. No coding required.105 106 105 = Google Tag Manager (GTM) Integration = 107 106 Easily integrate with Google Tag Manager by pasting your `GTM-XXXXXXX` ID into the settings field. The plugin will correctly and safely inject the GTM scripts into your site's `<head>` and `<body>` on every page. No coding required. … … 115 114 * **Jump Link Click Tracking:** Tracks engagement with your Jump Links Block by pushing a `jump_link_click` event, letting you see which sections your users are most interested in. 116 115 117 SEO 44's integration with Google Tag Manager pushes valuable events to the `dataLayer` for use in Google Analytics. The plugin includes an import file and detailed [Instructions for setting up Google Tag Manager and Google Analytics to receive event-tracking data](https://seo44plugin.com/search-appearance-toolkit-seo-44/integrations-setup-guide/). 116 SEO 44's integration with Google Tag Manager pushes valuable events to the `dataLayer` for use in Google Analytics. The plugin includes an import file and detailed [Instructions for setting up Google Tag Manager and Google Analytics to receive event-tracking data](https://seo44plugin.com/search-appearance-toolkit-seo-44/integrations-setup-guide/). 117 118 = Site Verification = 119 Verify your site with search engines quickly and easily. Paste your verification codes for **Google Search Console** and **Bing Webmaster Tools** into the corresponding fields in the Integrations tab. The plugin handles the rest, adding site verification meta tags to your website's head. No coding required. 120 121 = YouTube Data API = 122 To ensure that your site's video schema is as accurate as possible, you may add your YouTube Data API Key. The plugin uses this key to fetch the upload date for any YouTube video embedded in your content, replacing less reliable page scraping options and fallbacks to the post publish date. 118 123 119 124 == Frequently Asked Questions == … … 122 127 Search Appearance Toolkit (SEO 44) gives you control over the technical, on-page SEO factors that help search engines understand and rank your content. Key benefits include: 123 128 * **Optimized Snippets:** Control how your titles and descriptions appear in search results. 124 * **Rich Results:** The advanced Schema.org data helps you earn rich results like FAQs, How-Tos, and breadcrumbs in Google.129 * **Rich Results:** The advanced Schema.org data improves indexing and helps you earn rich results in Google. 125 130 * **Crawlability:** A clean and comprehensive XML sitemap ensures search engines can find and index all of your important content and images. 126 131 * **User Engagement:** The Jump Links Block improves user experience, which is a positive ranking signal, and can help you earn "Jump to" links in search results. … … 154 159 SEO 44 helps your content look great when shared on social media platforms. You can enable the automatic generation of **Open Graph** (og:) tags, which Facebook, LinkedIn, and Pinterest use, and **Twitter Card** meta tags for when your content appears on X (formerly Twitter). This ensures your posts have the correct title, description, and preview image when shared. 155 160 156 Use the plugin's Google Tag Manager Integration to facilitate additional connections with soc lal media platforms. Use the Organization Schema settings to associate all of your social media profiles with your website.161 Use the plugin's Google Tag Manager Integration to facilitate additional connections with social media platforms. Use the Organization Schema settings to associate all of your social media profiles with your website. 157 162 158 163 = How are social media images handled? = … … 177 182 178 183 = What are the benefits of using FAQPage and HowTo schema? = 179 The plugin intelligently scans your content for patterns that match question-and-answer formats or step-by-step instructions (when this option is enabled). The plugin locates this content and automatically generates FAQPage or HowTo schema that presents this content within the JSON-LD.180 181 The benefit of this is that Google can use this structured data to display your content in special, highly visible formats in the search results. An FAQ page might appear as a rich snippet with expandable questions , while a How-To article can be featured in a step-by-step guide. Rich snippets make your search results stand out, which can significantlyimprove your click-through rate (CTR).184 The plugin intelligently scans your content for patterns that match question-and-answer formats or step-by-step instructions (when this option is enabled). The plugin locates this content and automatically generates FAQPage or HowTo schema that presents this content as structured data. 185 186 The benefit of this is that Google can use this structured data to display your content in special, highly visible formats in the search results. An FAQ page might appear as a rich snippet with expandable questions or feature within a People Also Ask (PAA) result, while a How-To article can be featured in a step-by-step guide in AI Overviews. Search results that stand out can improve your click-through rate (CTR). 182 187 183 188 Read more about the [FAQPage and HowTo Schema](https://seo44plugin.com/search-appearance-toolkit-seo-44/schema-structured-data/#benefits-of-faqpage-and-howto-schema) created by SEO 44. … … 285 290 * **Google:** [Terms of Service](https://policies.google.com/terms), [Privacy Policy](https://policies.google.com/privacy) 286 291 * **Bing (Microsoft):** [Microsoft Services Agreement](https://www.microsoft.com/en-us/servicesagreement/), [Microsoft Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) 292 293 = YouTube Data API Integration & Video Metadata = 294 295 * **Service Description:** This plugin connects to YouTube to fetch the "Upload Date" for videos embedded in your content. This ensures your VideoObject schema is accurate. 296 * **Data Sent and Conditions:** 297 * **Method 1 (API):** If you provide a YouTube API Key in settings, the plugin sends the Video ID to the Google Data API. 298 * **Method 2 (Public Fallback):** If no API Key is present, the plugin acts as a standard browser and fetches the public video page (via `wp_remote_get`) to locate the upload date in the page meta tags. 299 * **Conditions:** This occurs automatically when a post with a YouTube embed is updated, provided that Schema generation is enabled. 300 * **Service Provider Links:** 301 * **YouTube:** [Terms of Service](https://www.youtube.com/t/terms), [Google Privacy Policy](https://policies.google.com/privacy) 287 302 288 303 == Screenshots == … … 315 330 == Changelog == 316 331 332 = 4.3.0 = 333 * FEATURE: **Table of Contents Schema:** Automatically generates `hasPart` schema synchronized with the Jump Links Block to support "Jump-to" links in search results. 334 * FEATURE: **Advanced HowTo Schema:** A new "Block-Aware" scanner uses the Jump Links Block to strictly define steps while intelligently mining the introduction for tools, time, yield, and video data. 335 * TWEAK: Added a "Generate HowTo Schema" checkbox to the SEO 44 metabox, giving users explicit control over when the advanced scanner runs. 336 * FEATURE: **YouTube Integration:** Added support for the YouTube Data API v3 to fetch accurate video upload dates for VideoObject schema. 337 * TESTED: Tested to WordPress Version 6.9 338 317 339 = 4.2.0 = 318 340 * FEATURE: **Rich Organization Schema:** Added a comprehensive settings section to generate Organization structured data for the Knowledge Graph. … … 325 347 * TWEAK: Updated the image uploader JavaScript to support multiple distinct upload buttons on the settings page. 326 348 327 = 4.1.0 =328 **Jump Links Block Updates:**329 * FEATURE: **Sticky Positioning:** Keep your table of contents visible while users scroll. Includes a "Top Offset" slider to clear sticky headers, a "Jump Offset" slider to ensure that the sticky header does not cover the heading text, and a "Disable on Mobile" toggle to preserve screen space on small devices.330 * FEATURE: **Auto-Hide Title:** Implemented a smart "sticky state" detection. When the block sticks, the title gently collapses and fades out to keep the interface clean (this occurs when a block title is used alongside sticky positioning).331 * FEATURE: **Smart Indentation:** Added a "Create Visual Hierarchy" toggle. When enabled, H3 and H4 sub-headings are visually indented to create a clear, nested outline structure.332 * FEATURE: **Block Background:** You can now set a background color for the entire block container, perfect for creating "card-style" floating navigation.333 * FEATURE: **ScrollSpy:** Automatically highlights the active link in the table of contents as the user scrolls through the corresponding section of the post.334 * FEATURE: Added support for Border and Spacing controls. You can now add borders, rounded corners, margins, and padding to the Jump Links block directly from the editor settings.335 * FEATURE: Added a "Title tag" control. You can now choose the specific HTML tag (H2, H3, H4, H5, Paragraph, or Div) for the "On This Page" heading to better match your document structure.336 * REFACTOR: Optimized the block's styling logic to use CSS variables on the parent container instead of inline styles for every link. This reduces the block's HTML size and improves rendering performance.337 * FIX: Resolved an accessibility and HTML validation issue where using multiple Jump Links blocks on a single page created duplicate element IDs. Each block now generates a unique instance ID.338 * PERFORMANCE: Refactored the front-end JavaScript to use event delegation for smooth scrolling. This reduces memory usage by attaching a single event listener to the block instead of individual listeners for every link.339 * TWEAK: Reorganized the sidebar settings for better clarity between Block Title settings and Content Inclusion settings.340 341 = 4.0.0 =342 * FEATURE: Added a new "Integrations" tab for third-party services like Google Tag Manager and Webmaster Tools.343 * FEATURE: Added Google Tag Manager (GTM) integration. The plugin can now automatically inject the GTM container script into the site's <head> and <body> based on your ID.344 * FEATURE: Added Webmaster Verification. You can now add your Google Search Console and Bing Webmaster Tools verification codes directly from the plugin settings.345 * FEATURE: Added automatic GTM event tracking. When enabled, the plugin can push the following events to the dataLayer:346 * Rich SEO dataLayer: Pushes page type, category, author, and tags on page load for advanced GTM triggers.347 * Scroll Depth Tracking: Pushes 'scroll_depth' events at 25%, 50%, 75%, and 100% of the page.348 * External & Affiliate Clicks:* Pushes 'external_link_click' or 'affiliate_link_click' (for `rel="sponsored"` links).349 * Jump Link Clicks: Pushes a 'jump_link_click' event when a user clicks a link in the Jump Links Block.350 * FEATURE: Added a Google Tag Manager recipe import file (`seo44-gtm-recipe-importer.json`) and new FAQ instructions to fully configure GTM and GA4 event tracking.351 * ENHANCEMENT: Centralized all GTM event tracking into a new, efficient `global-tracker.js` file that uses event delegation for better performance.352 * TWEAK: Improved the "Integrations" settings page UI for clarity, adding clarifying tooltips and a file downloader.353 354 349 For a complete list of changes, please see the [full changelog](https://seo44plugin.com/search-appearance-toolkit-seo-44/changelog/) or the `changelog.txt` file included with the plugin. 355 350 -
search-appearance-toolkit-seo-44/trunk/seo-44.php
r3405454 r3423355 4 4 * Plugin URI: https://www.sethcreates.com/plugins-for-wordpress/search-appearance-toolkit-seo-44/ 5 5 * Description: A lightweight, powerful SEO plugin for essential meta tags, advanced schema, XML sitemaps, jump links, and easy migration from other plugins. 6 * Version: 4. 2.06 * Version: 4.3 7 7 * Author: Seth Smigelski 8 8 * Author URI: https://www.sethcreates.com/plugins-for-wordpress/ … … 14 14 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly 15 15 16 define( 'SEO44_VERSION', '4. 2.0' );16 define( 'SEO44_VERSION', '4.3' ); 17 17 define( 'SEO44_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); 18 18
Note: See TracChangeset
for help on using the changeset viewer.