{"id":350408,"date":"2026-08-12T03:56:17","date_gmt":"2026-08-12T03:56:17","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/marcelo-site-connector\/"},"modified":"2026-08-12T03:55:59","modified_gmt":"2026-08-12T03:55:59","slug":"marcelo-site-connector","status":"publish","type":"plugin","link":"https:\/\/mfe.wordpress.org\/plugins\/marcelo-site-connector\/","author":23543832,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.14.4","stable_tag":"1.14.4","tested":"7.0.4","requires":"6.0","requires_php":"7.4","requires_plugins":null,"header_name":"Marcelo Site Connector","header_author":"Marcelo","header_description":"Lets Marcelo add the verification and measurement tags a site needs \u2014 Search Console, Analytics, Tag Manager, the Google Ads conversion tag \u2014 send addresses that no longer exist to the page that replaced them, give a page the search-result description it is missing, give a page the search-result title it is missing or is sharing with another page, edit the text of a page built with Elementor, put a tap-to-call bar in front of visitors, link the profiles a business has confirmed are theirs from every page, say on every page that a business answers around the clock where its confirmed hours say exactly that, let a named crawler back in where robots.txt was turning it away, point a page's canonical back at itself where it names another website, add a translated page's own link among its hreflang alternates where it is missing, and shrink an oversized image in the site's own media library while keeping its original, without anyone editing theme files by hand.","assets_banners_color":"","last_updated":"2026-08-12 03:55:59","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"https:\/\/meetmarcelo.ai","rating":0,"author_block_rating":0,"active_installs":0,"downloads":31,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.14.4":{"tag":"1.14.4","author":"itay12222","date":"2026-08-12 03:55:59"}},"upgrade_notice":[],"ratings":[],"assets_icons":[],"assets_banners":[],"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.14.4"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[232,6363,5777,726,127115],"plugin_category":[36],"plugin_contributors":[275275],"plugin_business_model":[],"class_list":["post-350408","plugin","type-plugin","status-publish","hentry","plugin_tags-analytics","plugin_tags-canonical","plugin_tags-meta-description","plugin_tags-redirects","plugin_tags-search-console","plugin_category-analytics","plugin_contributors-itay12222","plugin_committers-itay12222"],"banners":[],"icons":{"svg":false,"icon":"https:\/\/s.w.org\/plugins\/geopattern-icon\/marcelo-site-connector.svg","icon_2x":false,"generated":true},"screenshots":[],"raw_content":"<!--section=description-->\n<p>Connecting a website to Google Search Console, Google Analytics or Google Tag Manager always ends the same way: a line of code has to go into the site's <code>&lt;head&gt;<\/code>.<\/p>\n\n<p>WordPress can be given permission to write posts, pages and media over its own REST API, but not to write there. So the last step of connecting any of those three is a person being asked to paste a snippet into a theme file or into an SEO plugin's settings \u2014 and the person being asked is usually a business owner who paid somebody to build the site two years ago.<\/p>\n\n<p>This plugin removes that step. Install it once, and Marcelo can place those tags itself.<\/p>\n\n<h4>What it does<\/h4>\n\n<ul>\n<li>Places up to three Google tags in the site's <code>&lt;head&gt;<\/code> \u2014 a Search Console verification, an Analytics tag and a Tag Manager container \u2014 built from the identifier of each rather than from any markup it was handed.<\/li>\n<li>Stores a description for a page that has none, and prints it as that page's <code>meta description<\/code> \u2014 the sentence a search engine shows under the page in its results. WordPress core keeps no such field, and every SEO plugin uses its own hidden one, so a description written into any of those keys is invisible on a site running a different plugin.<\/li>\n<li>Stores the title a page should go by in search results and in a browser tab, and hands it to WordPress through <code>pre_get_document_title<\/code> \u2014 the filter core publishes for exactly this, and the one every common SEO plugin uses. This is not the post's own title: <code>post_title<\/code> is the name of the page in the admin and usually the heading a visitor reads, and changing it does not change the title a search engine shows on a site running any SEO plugin.<\/li>\n<li>Stores a <code>LocalBusiness<\/code> schema block describing the business, as JSON-LD in the head of every page.<\/li>\n<li>Stores redirects for addresses that no longer exist, and sends visitors on to the page that replaced them.<\/li>\n<li>Reads and writes the layout of a page built with Elementor, so the text of such a page can be edited at all. WordPress keeps a page's text in <code>post_content<\/code>; Elementor keeps its own copy in a protected postmeta field and prints that instead, so a write to <code>post_content<\/code> on an Elementor page succeeds and changes nothing a visitor reads.<\/li>\n<li>Puts a tap-to-call bar in front of visitors: one fixed element carrying the business's phone number as a <code>tel:<\/code> link, printed at <code>wp_footer<\/code> on every page. It is Marcelo's own element and touches nothing the site already had, so clearing it removes it completely.<\/li>\n<li>Reports how this site serves robots.txt, and \u2014 only where WordPress generates it \u2014 lets a named crawler back in that robots.txt was turning away. A <code>robots.txt<\/code> file sitting in the site's root is refused rather than touched: the webserver serves that file directly and WordPress is never asked for one, so nothing a plugin does could change what a crawler reads.<\/li>\n<li>Points a page's <code>canonical<\/code> link back at the page itself, where the one on it names another website. This is the only thing here that displaces markup the site was already printing rather than adding markup of its own \u2014 see below for exactly how far that reaches, and where it stops.<\/li>\n<li>Adds a translated page's own link among its <code>hreflang<\/code> alternates where it is missing one \u2014 the one entry that says \"this page is itself\". Nothing else in the head is touched: whatever else prints the page's other alternates is left exactly as it is, and only the language code is stored \u2014 the address is always built from the page's current request, never stored, so it stays correct if the domain ever changes.<\/li>\n<li>Saves an oversized picture in the site's own media library again at a smaller size, keeping the untouched original beside it so it can be put back. This is the only thing here that overwrites a file rather than a value in the database, and it is bounded accordingly: only an attachment this site really owns, only a file inside its own uploads directory, only when the file still weighs what the caller says it weighed, never a GIF or an SVG, and never at all on a host with no image library. A re-encode that came out larger is undone in full.<\/li>\n<li>Draws a page that does not exist yet in this site's own theme \u2014 header, navigation, styling and footer \u2014 from a title and some text, and hands the finished page back without storing any of it. Nothing is created: no post, no draft, no revision, no row of any kind. The page is assembled in memory for the length of one request, and the database is held read-only underneath it, so anything on the site that would ordinarily write while a page renders is refused rather than trusted.<\/li>\n<li>Exposes all of it over the REST API to users who can already <code>manage_options<\/code>.<\/li>\n<\/ul>\n\n<h4>What it does not do<\/h4>\n\n<ul>\n<li>It does not phone home. No connection is ever made from this plugin to any server, including ours. Everything travels the other way, over your site's own authenticated REST API.<\/li>\n<li>It contains no tracking, analytics or telemetry of its own.<\/li>\n<li>It stores no markup at all. Three identifiers are kept \u2014 a verification token, a <code>G-<\/code> measurement id, a <code>GTM-<\/code> container id \u2014 and nothing else. Google's scripts are then loaded through <code>wp_enqueue_script<\/code>, from a URL this plugin builds, so no text a caller sends can reach a visitor's browser: there is no path by which any is stored.<\/li>\n<li>The schema field is held to a far narrower rule than the rest: exactly one <code>&lt;script type=\"application\/ld+json\"&gt;<\/code> whose body parses as JSON, re-encoded from the parsed value rather than stored as received. A field named for structured data that accepted arbitrary script would be a larger plugin than this one.<\/li>\n<li>A page description is text and nothing else. Every tag is stripped on the way in and the value is escaped again on the way out. One that is too long is refused rather than cut \u2014 a description truncated mid-sentence is worse than the one the page did not have.<\/li>\n<li>A page title is held to the same rule, and for a sharper reason: it is printed inside a <code>&lt;title&gt;<\/code> element, where a surviving <code>&lt;<\/code> would close the element early and put whatever followed onto the page. It is never written to the post itself \u2014 nothing here calls <code>wp_update_post<\/code>, and no page is renamed.<\/li>\n<li>A title element of its own is printed only on a theme that does not declare <code>title-tag<\/code> support, because on every other theme WordPress prints one itself and a second would give the page two. Where a theme with no such support echoes its own <code>&lt;title&gt;<\/code> into <code>header.php<\/code>, nothing here can reach it and the page ends up with two; Marcelo reads the page afterwards, sees the two, and takes its own title off again.<\/li>\n<li>An Elementor layout is checked before a byte of it is stored: it has to parse as a JSON list of elements, each naming its own type. One that does not is refused, and one that is too large is refused rather than stored in part \u2014 half a layout is a blank page, and a blank page is worse than everything this route exists to fix put together. A page with no Elementor layout on it is refused too, because storing one would switch the builder on and replace what visitors already see.<\/li>\n<li>It never redirects an address the site can answer. A stored redirect fires only where WordPress was about to show a 404, so it cannot hide a page that exists \u2014 and if a page ever returns at that address, the redirect stops on its own.<\/li>\n<li>It cannot send a visitor to another website. Destinations are paths on this site and go through <code>wp_safe_redirect<\/code>.<\/li>\n<li>A canonical override cannot point anywhere but this site. The fault it exists to correct is a page handing its ranking to a different website, so a route that could store one would be able to cause the exact thing it repairs.<\/li>\n<li>It does not rewrite your pages to remove a canonical it did not put there, and it will not. Removing a tag that another hook has already printed means buffering and find-and-replacing the whole page's HTML, which is a far larger and more dangerous mechanism than anything else in this plugin. Instead it removes WordPress core's own canonical for that request and asks Yoast, Rank Math, SEOPress, All in One SEO and The SEO Framework \u2014 through the filters each of them publishes for this purpose \u2014 not to print theirs, then prints one. Where the tag comes from a theme with no filter, none of that reaches it and the page ends up with two; Marcelo reads the page afterwards, sees the two, and takes its own override off again.<\/li>\n<li>It stores no robots.txt and no line of one. The setting holds crawler names \u2014 \"somebody decided GPTBot may read this site\" \u2014 and the change is made to the text WordPress itself generates, as it generates it. There is no path by which text a caller sends reaches that file.<\/li>\n<li>It never writes a file. The only file it will ever name is the <code>robots.txt<\/code> in the site's own root, worked out from <code>ABSPATH<\/code> and never taken from a request, and it is named in order to be refused rather than written.<\/li>\n<li>It never removes a robots.txt rule that names a crawler nobody asked about. A rule shared between two crawlers is left exactly as it is, and the caller is told which others share it.<\/li>\n<li>It never overrides \"Discourage search engines from indexing this site\" in Settings \u2192 Reading. That setting is what closes robots.txt to everybody on nearly every WordPress site, it also puts a <code>noindex<\/code> on every page, and it is a decision somebody made on purpose \u2014 on a staging site or a site not ready to launch, the right one.<\/li>\n<li>The availability notice and the profile links edit nothing either, and follow every rule below that the call bar does. Neither is pinned: both sit in the flow of the document at the end of the page, so nothing is covered and no space is reserved.<\/li>\n<li>The call bar edits nothing. It adds one element of its own at <code>wp_footer<\/code>, with no JavaScript at all and with every style rule namespaced under <code>.marcelo-call-bar<\/code>, so it cannot restyle a single element the site already had. It reserves its own height as padding on the body rather than sitting permanently on top of the page's last line, and it hides itself when the page is printed. The phone number is checked against a strict pattern before it is stored and escaped again on the way out.<\/li>\n<\/ul>\n\n<h4>Do I need a Marcelo account?<\/h4>\n\n<p>To use it as intended, yes \u2014 the plugin is the site-side half of connecting a website to https:\/\/meetmarcelo.ai. The plugin itself is free and GPL, works on its own, and sends nothing anywhere.<\/p>\n\n<h3>External services<\/h3>\n\n<p>This plugin can place Google tags built from identifiers stored in its own settings \u2014 a Google Analytics measurement id, a Google Tag Manager container id, a Google Ads conversion id. Wherever one of those is stored, the plugin enqueues Google's own tag script for it, and that script is fetched by the visitor's own browser, directly from Google, every time a page loads:<\/p>\n\n<ul>\n<li><code>https:\/\/www.googletagmanager.com\/gtag\/js?id=G-XXXXXXX<\/code> \u2014 Google Analytics, where a measurement id is stored.<\/li>\n<li><code>https:\/\/www.googletagmanager.com\/gtm.js?id=GTM-XXXXXXX<\/code> \u2014 Google Tag Manager, where a container id is stored.<\/li>\n<li><code>https:\/\/www.googletagmanager.com\/gtag\/js?id=AW-XXXXXXXXX<\/code> \u2014 Google Ads, where a conversion id is stored.<\/li>\n<\/ul>\n\n<p>This is the ordinary way any site places these tags, by hand or through any other plugin \u2014 the plugin itself never contacts Google or sends it anything; it only writes the <code>&lt;script src&gt;<\/code> that causes the visitor's browser to. A stored Search Console verification token loads nothing at all: it is only ever printed as a <code>&lt;meta&gt;<\/code> tag.<\/p>\n\n<p>This service is provided by Google. Terms of service: https:\/\/policies.google.com\/terms. Privacy policy: https:\/\/policies.google.com\/privacy.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Install and activate the plugin.<\/li>\n<li>In Marcelo, connect this website. The tags are placed for you.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20this%20send%20my%20data%20anywhere%3F\"><h3>Does this send my data anywhere?<\/h3><\/dt>\n<dd><p>No. The plugin makes no outbound connections at all \u2014 not to Google, and not to Marcelo. Everything travels the other way: your site's own administrator sends it what to place, over your site's own login, and it places it.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20i%20deactivate%20it%3F\"><h3>What happens if I deactivate it?<\/h3><\/dt>\n<dd><p>The tags stop being printed, and the stored snippets are kept \u2014 so reactivating restores them without reconnecting. Deleting the plugin removes them.<\/p><\/dd>\n<dt id=\"does%20the%20call%20bar%20change%20my%20theme%3F\"><h3>Does the call bar change my theme?<\/h3><\/dt>\n<dd><p>No. Nothing in your theme is read, edited or replaced \u2014 the plugin adds one element of its own at the <code>wp_footer<\/code> hook, which is the same mechanism it uses for the Google tags. Turning it off removes that element and the page is exactly what it was before.<\/p><\/dd>\n<dt id=\"does%20it%20edit%20my%20robots.txt%20file%3F\"><h3>Does it edit my robots.txt file?<\/h3><\/dt>\n<dd><p>It cannot, and it will not try. If your site has a real <code>robots.txt<\/code> file in its root, your webserver serves that file and WordPress is never asked for one \u2014 so this plugin has no way in, and it says so, with the file's full path, rather than reporting a change that would never have happened. Where there is no such file, WordPress generates robots.txt on every request and this plugin changes what it generates.<\/p><\/dd>\n<dt id=\"what%20happens%20to%20my%20redirects%20if%20i%20delete%20the%20plugin%3F\"><h3>What happens to my redirects if I delete the plugin?<\/h3><\/dt>\n<dd><p>They go with it, and every address that was a 404 before is a 404 again. Deactivating keeps them, like the snippets.<\/p><\/dd>\n<dt id=\"can%20it%20inject%20anything%20into%20my%20site%3F\"><h3>Can it inject anything into my site?<\/h3><\/dt>\n<dd><p>No, and the reason is structural rather than a promise. The plugin does not store what it is sent. It looks for the three things it knows \u2014 a Google site-verification token, a <code>G-<\/code> Analytics measurement id, a <code>GTM-<\/code> container id \u2014 and writes the tag itself from a fixed template with that id in it. Text that is not one of those three is not stored, so there is nothing for it to print.<\/p>\n\n<p>A caller must also already be able to <code>manage_options<\/code>, which is an account that could install a plugin doing far more. Both are true; the first is the one that would still hold if the second failed.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.14.4<\/h4>\n\n<ul>\n<li>Page preview: one broken plugin no longer takes the whole preview with it. If drawing the page falls over inside another plugin on this site, the page is drawn once more with that plugin's own code taken off every hook for the length of the attempt \u2014 and the answer says which plugin was left out, so nobody is shown a page that is quietly missing something without being told what.<\/li>\n<li>This is what WordPress itself does when a plugin breaks a site: work out which one the fault came from and carry on without it. The same idea, for one render, and then it is put straight back \u2014 nothing on the site is switched off, and nothing about it is stored.<\/li>\n<li>Bounded on purpose. It happens once and never in a loop; only for another plugin, never for this site's theme, never for WordPress itself, and never for this plugin; and only where that plugin really had code running. If the second attempt fails too, the answer is the first failure's own message, because that is the one that names the plugin that actually broke.<\/li>\n<li>Everything the preview already promised is unchanged and covers both attempts: the database is held read-only for the whole of it, nothing is stored on any path, and a page that still cannot be drawn is reported as \"could not draw the page\" exactly as before.<\/li>\n<\/ul>\n\n<h4>1.14.3<\/h4>\n\n<ul>\n<li>Page preview: a failure now says where it happened, not only what it said. \"Call to undefined function cdp_vars()\" alone named a function and nothing about which theme or plugin called it. The same message now carries the file \u2014 the part of the path under this site's own <code>wp-content<\/code>, nothing about the server above it \u2014 and the line number, the same information WordPress's own fatal-error screen already shows to a signed-in administrator, which is the trust level a request reaching this route already has.<\/li>\n<li>No change to what draws the preview or to what it may do. This is the 1.14.2 fix reporting itself more precisely, not a second attempt at it.<\/li>\n<\/ul>\n\n<h4>1.14.2<\/h4>\n\n<ul>\n<li>Page preview: a page is now drawn the way this site draws a page, rather than by loading the theme's template on its own. WordPress runs two steps between working out what a request is and drawing it \u2014 <code>wp<\/code> and <code>template_redirect<\/code> \u2014 and a request coming in over the REST API never reaches either. A great many themes and plugins start their front-end half on those two steps, including loading the files that define the functions their own templates then call.<\/li>\n<li>What that looked like: a site whose pages all work perfectly answering every preview with \"This site could not draw the page: Call to undefined function \u2026\", naming a function belonging to some other plugin on the site. Both steps now run before the template does, in the order WordPress runs them.<\/li>\n<li>Everything the preview already promised is unchanged and covers the wider surface: the database is still held read-only for the whole render, so a plugin that writes while starting up is refused exactly as one that writes while a page draws; a plugin that falls over while starting up is reported as \"could not draw the page\" rather than breaking the request; and nothing is stored, on any path.<\/li>\n<li>Two things that only make sense for a real visitor are declined for the length of the render. The admin bar is not drawn into a preview \u2014 the published page will not have one, and it would otherwise carry the account name of whoever is signed in. And a plugin asking to redirect during a preview is not sent anywhere: there is no visitor to send, only a request asking this site to draw a page.<\/li>\n<li>No change to what the plugin can do. Every route, every feature name and every answer is the same as 1.14.0 and 1.14.1.<\/li>\n<\/ul>\n\n<h4>1.14.1<\/h4>\n\n<ul>\n<li>Subdirectory installs: a site living at <code>example.com\/blog<\/code> sent visitors to <code>example.com\/blog\/blog\/the-new-page<\/code>. Every path this plugin stores is measured from the domain root, and one function turned it back into an address by appending to the site root \u2014 so on a root install it was right and on a subdirectory install it doubled the folder. The self-referencing hreflang tag named an address the same way and had the same fault. Both build the address from the site's own origin now.<\/li>\n<li>Worse than a broken link, which is why this is a release of its own: the rule really was stored, so asking the site for its rules again returned exactly what was written, and the fix was recorded as applied. A visitor followed a redirect to a page that does not exist while the record said it landed.<\/li>\n<li>Page preview: the read-only hold over the database is let go of after the render is completely finished rather than one step before, so nothing a theme does at the very end of drawing a page can write to a site that was only being looked at.<\/li>\n<li>No change to what the plugin can do. Every route, every feature name and every answer is the same as 1.14.0 \u2014 so a site on 1.14.0 loses nothing by staying, and gains these two fixes by updating.<\/li>\n<\/ul>\n\n<h4>1.14.0<\/h4>\n\n<ul>\n<li>Page preview: draws a page that does not exist yet, in this site's own theme, and hands the finished page back. It is the only route here whose answer is a document rather than a record, and the only one that stores nothing at all \u2014 the point being that somebody can see what a new page will look like on their own site before agreeing to have it added.<\/li>\n<li>Nothing is created and nothing is changed. The page is a <code>WP_Post<\/code> built in memory for the length of one request, never <code>wp_insert_post<\/code>, so there is no post, no draft, no auto-draft, no revision and no row in any table. It has no id, because it has no row.<\/li>\n<li>The database is held read-only while the page is drawn, at the one filter every write in WordPress passes through. A theme or plugin that counts a view, warms a cache or stamps a \"last seen\" while a page renders is refused rather than asked nicely, and the answer says how many writes were turned away.<\/li>\n<li>The real template hierarchy and the real <code>template_include<\/code> filter, so what comes back is the page this site would have served rather than a generic frame with some text in it. A site whose theme cannot draw a page it has no row for is told so plainly, and the caller shows no preview at all \u2014 a half-drawn page presented as a preview is worse than none.<\/li>\n<li>The title is sanitised as text and the content with <code>wp_kses_post<\/code>, the same rule WordPress core applies to post content from anybody without <code>unfiltered_html<\/code>, so the preview is drawn from the characters the page would really carry.<\/li>\n<li>A statement is sent only when its first word is one that cannot change a row \u2014 <code>SELECT<\/code>, <code>SHOW<\/code>, <code>DESCRIBE<\/code>, <code>EXPLAIN<\/code>, <code>SET<\/code> and the transaction words. Anything else is turned away, rather than the other way round: the ways of writing to MySQL are not a list this plugin could finish, and the code running while a page draws belongs to somebody else.<\/li>\n<li>A shortcode in the text is shown rather than run. Shortcode handlers are ordinary PHP and can send mail or call another service, and the read-only hold does not cover either of those \u2014 so a preview of text nobody has agreed to yet displays <code>[the-shortcode]<\/code> where the published page would display what it produces.<\/li>\n<li>The hold is let go of last, after the page is drawn and after every global the render borrowed is put back, and there is a safety net on shutdown for the endings PHP cannot catch \u2014 a plugin calling <code>exit<\/code>, a memory limit, a time limit. A site left read-only by a preview that never finished would be a site where publishing silently does nothing, which is worse than a preview that failed.<\/li>\n<\/ul>\n\n<h4>1.13.0<\/h4>\n\n<ul>\n<li>hreflang self-reference: adds a translated page's own link among its own alternates, where the set is missing one. A page that lists French and Spanish alternates but not itself breaks the whole set \u2014 search engines are told what the other language versions are but never told this page is one of them.<\/li>\n<li>Additive, unlike the canonical route beside it: nothing already in the head is displaced. Whatever else on the page prints its other hreflang alternates \u2014 a theme, an SEO plugin \u2014 is left exactly as it is, and this adds one more tag naming the page itself.<\/li>\n<li>Only a language code is stored, never a URL. The address in the tag is always built from the request being served, the same way the canonical route builds its value from the site's own parts rather than storing what it was sent, so the tag stays correct if the domain ever changes.<\/li>\n<li>The code is checked against a loose BCP 47 shape before it is stored \u2014 a language subtag, optionally a region or script \u2014 and lower-cased on the way in, because every reader that matters compares hreflang values case-insensitively.<\/li>\n<li>Batched like descriptions: one request answers for every page in it separately, so one bad line does not throw the rest away, and the caller is told which page it was.<\/li>\n<\/ul>\n\n<h4>1.12.0<\/h4>\n\n<ul>\n<li>Availability notice: one short line at the bottom of every page saying the business answers around the clock. The third thing this plugin puts in front of a visitor, after the tap-to-call bar and the profile links, and built to the same rules: one element of its own, one namespaced class, no JavaScript at all, nothing animating, and nothing on the site read, moved or rewritten. Clearing the option removes it completely and the page is byte for byte what it was.<\/li>\n<li>It is only ever sent for a business whose confirmed hours are seven days of <code>24 hours<\/code>. That decision is made against a record a person filled in and cannot be made here \u2014 this plugin stores and prints words that arrived and refuses the ones that are the wrong shape. A business that closes at five is never sent anything by this route, because a firm advertised as always available on the strength of its opening hours is the worst thing this feature could do.<\/li>\n<li>It is not the <code>openingHours<\/code> in the schema block, and the difference is the whole point. That is markup in the head of the page and no visitor will ever see a word of it; this is a line somebody reads. Answering \"the page never says when you answer\" with a block in the head would tick the finding without changing a word anybody reads.<\/li>\n<li><code>wp_footer<\/code> rather than <code>wp_body_open<\/code>, though the top of the page is where a line like this reads best. <code>wp_body_open<\/code> is a hook a theme may or may not call, and an element that silently fails to print is worse than one printed lower down: it would be recorded as published and be nowhere on the page.<\/li>\n<li>Sixty characters at most, plain text only, refused rather than cut. It sits in the flow of the document so it covers nothing, inherits the site's own colour rather than arriving as a band of somebody else's brand, and reserves no thumb-sized height when the page is printed.<\/li>\n<li>Removing it is one request, and it says what it took off.<\/li>\n<\/ul>\n\n<h4>1.11.0<\/h4>\n\n<ul>\n<li>Profile links: a short row of links to the accounts a business has confirmed are its own \u2014 Facebook, Instagram, Google and the rest \u2014 printed at the very bottom of every page. The second thing this plugin puts in front of a visitor, after the tap-to-call bar, and built to the same rules: one element of its own, one namespaced class, no JavaScript at all, nothing animating, and nothing on the site read, moved or rewritten. Clearing the option removes it completely and the page is byte for byte what it was.<\/li>\n<li>It is not the <code>sameAs<\/code> property in the schema block, and the difference is the whole point. That is markup in the head of the page and no visitor will ever see a word of it; this is a link somebody can click. A site can have one and not the other, and answering \"your website links to none of your profiles\" with a block in the head would tick the finding without putting a link anywhere anybody could see.<\/li>\n<li>No address is ever composed here. Each one is checked for an <code>http<\/code> or <code>https<\/code> scheme and a real host and is stored as it arrived; nothing goes looking for a business's Facebook page, guesses one from a name, or turns a host into an address. One refused address refuses the whole request, because a site linking to three of somebody's four profiles with nothing saying which one was dropped is worse than a site linking to none.<\/li>\n<li>The words a link reads come from its own host \u2014 \"facebook.com\" reads Facebook \u2014 and an address on a host nobody recognises is labelled by its bare host rather than by a name invented for it.<\/li>\n<li>Every link is at least 44 pixels tall, which is Apple's minimum and is the size a row of small footer links usually is not. The row inherits the site's own colour rather than arriving as a stripe of somebody else's brand, sits in the flow of the document so it covers nothing, and reserves no space when the page is printed.<\/li>\n<li>Removing them is one request, and it says what it took off.<\/li>\n<\/ul>\n\n<h4>1.10.0<\/h4>\n\n<ul>\n<li>Oversized images: a picture in this site's own media library can be saved again at a smaller size. This is the first thing this plugin does that overwrites a <strong>file<\/strong> rather than a value in the database, and everything below follows from that one difference.<\/li>\n<li>The original is copied aside before a single byte is written, and a copy that fails refuses the whole request. A re-encoded JPEG is the only copy of itself \u2014 there is no arithmetic that turns one back into the original \u2014 so a compression with no way back is not one this makes.<\/li>\n<li>Only ever an attachment in this site's own media library, proved by WordPress's own lookup rather than guessed from the address. An image served from a CDN, a stock photo host or another website is refused by name: this plugin will not rewrite a file it cannot show belongs to the site it is installed on. An address naming a generated size \u2014 <code>photo-1024x768.jpg<\/code> \u2014 is resolved back to the attachment it is a size of, and the original is what gets resaved.<\/li>\n<li>Only ever a file inside this site's uploads directory, checked against the resolved real path. That check is the last thing standing between a REST parameter and a write to an arbitrary file on the server.<\/li>\n<li>The file has to still weigh what the caller says it weighed. A picture somebody has replaced since the measurement is a picture nobody asked about, and it is refused with both numbers rather than resaved.<\/li>\n<li>JPEG, PNG and WebP only. A GIF may be animated and every image library WordPress ships flattens one to a single frame \u2014 a \"compression\" that destroys the thing the image was for \u2014 and an SVG is text with no raster editor at all. Both refused by mime type, with the reason.<\/li>\n<li>The server is asked whether it can do this at all, through <code>wp_image_editor_supports()<\/code>. WordPress resizes with whichever of Imagick or GD the host has and a host can have neither, or a build of GD without JPEG support \u2014 which answers yes to \"is GD loaded\" and no to the question that matters. Reported as a hosting fact, because that is where it is fixed.<\/li>\n<li>The sized copies <code>srcset<\/code> points at are rebuilt from the new file. Those are separate files generated at upload time, and a compression that fixed only the original would leave every copy a visitor is actually served exactly as heavy as it was \u2014 the page no faster, and the record saying it was fixed.<\/li>\n<li>A re-encode that came out larger is undone in full and refused with both numbers. That happens often on an already-optimised image, and making a customer's picture heavier while reporting a fix is the worst outcome this route has available.<\/li>\n<li>Putting the original back is one request, and it refuses rather than answering \"nothing to do\" when there is no saved original beside the file. Every other DELETE here removes something of the plugin's, where \"it was not there\" is the state the caller asked for; this one is asked to restore a file, and a 200 saying it did not would read as done.<\/li>\n<\/ul>\n\n<h4>1.9.0<\/h4>\n\n<ul>\n<li>The Google Ads conversion tag: installed from the client's own connected Google Ads account, never typed in or guessed. Stored under its own key so writing it can never overwrite the Search Console, Analytics or Tag Manager tag stored beside it, and validated as an <code>AW-<\/code> id before it is accepted \u2014 a Search Console token or a bare customer id is refused rather than fired on every page as a tag belonging to nobody.<\/li>\n<\/ul>\n\n<h4>1.8.0<\/h4>\n\n<ul>\n<li>Page titles: the line a search engine shows as the name of a page, and the words in a browser tab. Stored beside the address it belongs to and handed to WordPress through <code>pre_get_document_title<\/code>, registered last so it is the final opinion asked for. That filter is core's own way for a plugin to own the title and is the one Yoast, Rank Math, SEOPress, All in One SEO and The SEO Framework each use, so one hook wins against all of them without this plugin knowing anything about any of them.<\/li>\n<li>It is not the post's title, and it is never written as one. <code>post_title<\/code> is the name of the page in the admin, in the permalink and \u2014 on most themes \u2014 the heading a visitor reads at the top of the page. Every SEO plugin overrides the rendered <code>&lt;title&gt;<\/code> independently of it, so writing <code>post_title<\/code> to change a search result would change what a visitor reads and change nothing in Google. Nothing here calls <code>wp_update_post<\/code>.<\/li>\n<li>The second thing this plugin does that displaces markup the site was already printing, and bounded the same way as the canonical override. A theme that does not declare <code>title-tag<\/code> support never calls <code>wp_get_document_title()<\/code>, so the filter never fires there; where such a theme prints no title at all \u2014 which is the finding this most often answers \u2014 a title element of its own is printed instead, and where it prints one the page ends up with two. Marcelo reads every page back after writing and removes its own title from any page that does not come back carrying exactly one. Undoing is a normal step here rather than an error path.<\/li>\n<li>Text only, refused rather than cut. Every tag is stripped on the way in and the value is escaped again on the way out \u2014 which matters more here than for a description, because the value goes inside a <code>&lt;title&gt;<\/code> element where a surviving <code>&lt;<\/code> closes it early.<\/li>\n<li>Written in batches, answered per page, so one bad line does not throw away the rest and the caller is told which one.<\/li>\n<li>Removing one is clearing it. The site goes back to assembling the title exactly as it did before, with nothing of the plugin's left on any page.<\/li>\n<\/ul>\n\n<h4>1.7.0<\/h4>\n\n<ul>\n<li>robots.txt: a named crawler that robots.txt was turning away can be let back in, and a site can be asked which of the two kinds of robots.txt it has \u2014 the one WordPress generates, or a file in its root. Those two are identical over HTTP and completely different underneath: WordPress's <code>robots_txt<\/code> filter runs for the first and is never called for the second, so a plugin hooking it changes the first and does nothing at all to the second.<\/li>\n<li>A real file in the root is refused rather than attempted, named by its full path. Writing to a file outside WordPress's own APIs is a far larger surface than this plugin has any business opening, and a change reported but not made is worse than no change at all.<\/li>\n<li>Nothing is stored in robots.txt. The setting holds crawler names, and the change is made to the text WordPress generates, as it generates it \u2014 so removing the setting puts the file back to exactly what it said, with nothing of the plugin's left in it.<\/li>\n<li>A rule that names other crawlers is left alone. One <code>Disallow: \/<\/code> under two <code>User-agent<\/code> lines is one rule about both of them, and taking it out for one would let the other in as a side effect nobody asked for.<\/li>\n<li>\"Discourage search engines from indexing this site\" is never overridden. It is what closes robots.txt to everybody on nearly every WordPress site, it also puts a <code>noindex<\/code> on every page, and it is a deliberate decision \u2014 refused with the setting named, so it can be changed in WordPress by whoever made it.<\/li>\n<li>The result is read back before it is stored. The file that would be produced is built and parsed the way a crawler parses it, and a crawler still turned away after the change is reported as refused rather than recorded as let in.<\/li>\n<li>Canonicals: a page whose <code>canonical<\/code> link names another website can be pointed back at itself. This is the first thing this plugin does that displaces markup the site was already printing, and it is deliberately only half a mechanism \u2014 it removes WordPress core's canonical for that request and asks each of the five common SEO plugins, through the filter each publishes for exactly this, not to print theirs. A canonical a theme echoes directly into <code>header.php<\/code> is reachable by nothing, so the page then carries two; Marcelo reads every page back after writing, and removes its own override from any page that does not come back carrying exactly one. Undoing is a normal step here rather than an error path.<\/li>\n<li>A canonical override can only ever name an address on this site. The fault being corrected is a page handing its ranking away, so storing an off-site canonical is refused \u2014 a route that could do it would be able to cause the exact thing it exists to repair.<\/li>\n<li>The schema block is finally printed. From 1.2.0 the block was accepted, validated, re-encoded, stored \u2014 and never output, so a site could hold one, be told it was live for search engines, and have nothing on any page. That is the failure this plugin is built against, in the plugin itself, and it is the reason this line is here rather than in a quiet commit.<\/li>\n<li>Structured data is now encoded with <code>JSON_HEX_TAG<\/code>. A JSON string containing an escaped closing script tag survives the check that refuses a literal one, and re-encoding without this flag turns it back into a literal \u2014 which was harmless only for as long as the block was never printed.<\/li>\n<\/ul>\n\n<h4>1.6.0<\/h4>\n\n<ul>\n<li>A tap-to-call bar. One fixed element carrying the business's confirmed phone number as a <code>tel:<\/code> link, printed on every page at <code>wp_footer<\/code>, so somebody who lands on a page can call without scrolling or hunting for a number.<\/li>\n<li>Nothing on the page is read, moved or rewritten, and no theme file is touched. The element is the plugin's own, wrapped in comments naming it, and clearing the setting removes it with nothing left behind.<\/li>\n<li>No JavaScript of any kind, and nothing that animates. The styles go through <code>wp_add_inline_style<\/code> on a registered handle, and every selector is namespaced under <code>.marcelo-call-bar<\/code> so it cannot restyle anything the site already had.<\/li>\n<li>It reserves its own height as padding on the body rather than covering the last line of the page, keeps clear of the iPhone home indicator and of the WordPress admin bar, and hides itself when a page is printed.<\/li>\n<li>Phone screens only unless asked otherwise, because that is where the number being far down the page actually costs an enquiry.<\/li>\n<li>The number is checked against a strict pattern \u2014 digits, spaces, brackets, dots, hyphens, one optional leading plus, 7 to 15 digits \u2014 before it is stored, and escaped again on the way out. A number that fails is refused with a reason rather than stored empty.<\/li>\n<\/ul>\n\n<h4>1.5.0<\/h4>\n\n<ul>\n<li>Elementor layouts can be read and written. A page built with Elementor keeps its whole content \u2014 sections, columns, widgets and every setting \u2014 in a protected postmeta field, and prints that rather than the text WordPress stores. Until now a write to such a page succeeded and changed nothing anybody could see, which is the one kind of failure this plugin exists to make impossible.<\/li>\n<li>Nothing composes a layout here. What arrives is checked and stored, and the stored value is handed straight back so the caller can verify what is on the page rather than assume it.<\/li>\n<li>Refused rather than repaired, and refused rather than truncated. A layout that does not parse as a JSON list of elements is not stored at all, because a half-written layout does not make a page look wrong \u2014 it makes it blank.<\/li>\n<li>A page that has no Elementor layout is refused. Storing one would switch the builder on for that page and replace what visitors see, as a side effect of a text edit nobody connected to it.<\/li>\n<li>The generated CSS for a changed page is cleared, so new content arrives styled rather than as correct text in a page that looks broken.<\/li>\n<\/ul>\n\n<h4>1.4.0<\/h4>\n\n<ul>\n<li>Nothing but identifiers is stored. Three values are kept \u2014 a Search Console verification token, a <code>G-<\/code> measurement id and a <code>GTM-<\/code> container id \u2014 and no markup of any kind. A full tag can still be sent, and only the identifier inside it is taken.<\/li>\n<li>Google's scripts are loaded through <code>wp_enqueue_script<\/code> instead of being written into the head. That means they can be deregistered and deferred like any other script, and are not loaded a second time when another plugin enqueues the same file \u2014 a site with Analytics installed twice counts every visit twice, and nobody finds that out by looking at the page. The Analytics configuration is attached to its own handle, so it can no longer run before the file it configures.<\/li>\n<li>Together these replace a rule that refused a list of dangerous strings and accepted anything beginning with an approved tag. That is a deny list guarding a hole the size of <code>script<\/code>: any JavaScript passed, and so did <code>onclick<\/code>, which simply was not on the list. A deny list is only ever as good as the imagination of whoever wrote it.<\/li>\n<li>An Analytics snippet that loads one property and configures another is refused rather than repaired \u2014 it is the shape of a copy-paste error, and a site reporting to a property nobody reads looks instrumented and is not.<\/li>\n<li>Upgrading needs nothing. A value stored by an earlier version is a full tag, and the identifier is read out of it the first time it is used.<\/li>\n<\/ul>\n\n<h4>1.3.0<\/h4>\n\n<ul>\n<li>Page descriptions: the sentence a search engine shows under a page, stored beside the address it belongs to and printed for that page only. WordPress keeps no description of its own and every SEO plugin uses its own hidden field, so writing one of those keys on a site running a different plugin stores a value nothing reads \u2014 it succeeds, changes nothing, and reports success.<\/li>\n<li>Text only. Every tag is stripped on the way in and the value is escaped again on the way out, because a description is prose by nature and there is no markup it could legitimately need.<\/li>\n<li>Refused rather than cut when it is too long. A description truncated mid-sentence by a plugin nobody asked is worse than the one the page did not have.<\/li>\n<li>Written in batches, answered per page, so one bad line does not throw away the rest and the caller is told which one.<\/li>\n<li>Removing one is clearing it. Nothing is left on the page.<\/li>\n<\/ul>\n\n<h4>1.2.0<\/h4>\n\n<ul>\n<li>Business schema markup: a LocalBusiness block describing the business, printed into the head of every page. Held to its own rule rather than the one the tags above use \u2014 exactly one <code>script<\/code> of type <code>application\/ld+json<\/code>, whose contents must parse as JSON, re-encoded from the parsed value rather than stored as received. A field named for structured data that accepted arbitrary script would be a larger plugin than this needs to be.<\/li>\n<li>Removing it is clearing the field. Nothing is left on any page.<\/li>\n<\/ul>\n\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>Redirects for addresses that no longer exist, stored and served by the plugin. Only ever applied where the site would otherwise show a 404, and only ever to a path on the same site.<\/li>\n<li>The status route now names what this build can do, so Marcelo does not have to infer it from a version number.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>First release.<\/li>\n<\/ul>","raw_excerpt":"Places the Google tags a site needs, gives pages the description search engines show, and sends dead addresses to the page that replaced them.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/mfe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/350408","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mfe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/mfe.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/mfe.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=350408"}],"author":[{"embeddable":true,"href":"https:\/\/mfe.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/itay12222"}],"wp:attachment":[{"href":"https:\/\/mfe.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=350408"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/mfe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=350408"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/mfe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=350408"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/mfe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=350408"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/mfe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=350408"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/mfe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=350408"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}