=== Marcelo Site Connector ===
Contributors: itay12222
Tags: analytics, search console, redirects, meta description, canonical
Requires at least: 6.0
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.15.26
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Places the Google tags a site needs, gives pages the description search engines show, and sends dead addresses to the page that replaced them.

== Description ==

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 `<head>`.

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 — and the person being asked is usually a business owner who paid somebody to build the site two years ago.

This plugin removes that step. Install it once, and Marcelo can place those tags itself.

= What it does =

* Places up to three Google tags in the site's `<head>` — a Search Console verification, an Analytics tag and a Tag Manager container — built from the identifier of each rather than from any markup it was handed.
* Stores a description for a page that has none, and prints it as that page's `meta description` — 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.
* Stores the title a page should go by in search results and in a browser tab, and hands it to WordPress through `pre_get_document_title` — the filter core publishes for exactly this, and the one every common SEO plugin uses. This is not the post's own title: `post_title` 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.
* Stores a `LocalBusiness` schema block describing the business, as JSON-LD in the head of every page.
* Stores redirects for addresses that no longer exist, and sends visitors on to the page that replaced them.
* 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 `post_content`; Elementor keeps its own copy in a protected postmeta field and prints that instead, so a write to `post_content` on an Elementor page succeeds and changes nothing a visitor reads.
* Puts a tap-to-call bar in front of visitors: one fixed element carrying the business's phone number as a `tel:` link, printed at `wp_footer` on every page. It is Marcelo's own element and touches nothing the site already had, so clearing it removes it completely.
* Draws links Marcelo measured as too small to tap at least 44 pixels tall and wide on phone-sized screens, by a minimum size on each named link in a stylesheet of its own. This is the one thing here that changes how the site's own elements are drawn, and only their size: nothing is reworded or moved, and clearing it removes every rule.
* Reports how this site serves robots.txt, and — only where WordPress generates it — lets a named crawler back in that robots.txt was turning away. A `robots.txt` 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.
* Points a page's `canonical` 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 — see below for exactly how far that reaches, and where it stops.
* Adds a translated page's own link among its `hreflang` alternates where it is missing one — 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 — the address is always built from the page's current request, never stored, so it stays correct if the domain ever changes.
* 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.
* Draws a page that does not exist yet in this site's own theme — header, navigation, styling and footer — 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.
* On a page Marcelo created with its own phone number, shows that number wherever the site's own header and footer print the business's main one — on that page only. This is the one place the plugin rewrites a page's finished HTML, and it is bounded accordingly: it opens only on a page carrying the post meta Marcelo sets, it replaces only that one number, in words and in `tel:` links, and it never touches a script, a style, a comment or any other address.
* Counts, per day, how many of the site's own emails WordPress handed on and how many it reported failing, through core's `wp_mail_succeeded` and `wp_mail_failed` actions, so Marcelo can tell whether a contact form's messages leave the site at all. The last failure's reason is kept with every email address taken out of it. No recipient, subject or message is ever stored, nothing is sent anywhere, and the count stays on the site — thirty days of it — until a signed-in request reads it.
* Exposes all of it over the REST API to users who can already `manage_options`.

= What it does not do =

* 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.
* It contains no tracking, analytics or telemetry of its own.
* It stores no markup at all. Three identifiers are kept — a verification token, a `G-` measurement id, a `GTM-` container id — and nothing else. Google's scripts are then loaded through `wp_enqueue_script`, 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.
* The schema field is held to a far narrower rule than the rest: exactly one `<script type="application/ld+json">` 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.
* 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 — a description truncated mid-sentence is worse than the one the page did not have.
* A page title is held to the same rule, and for a sharper reason: it is printed inside a `<title>` element, where a surviving `<` would close the element early and put whatever followed onto the page. It is never written to the post itself — nothing here calls `wp_update_post`, and no page is renamed.
* A title element of its own is printed only on a theme that does not declare `title-tag` 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 `<title>` into `header.php`, 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.
* 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 — 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.
* 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 — and if a page ever returns at that address, the redirect stops on its own.
* It cannot send a visitor to another website. Destinations are paths on this site and go through `wp_safe_redirect`.
* 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.
* 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 — through the filters each of them publishes for this purpose — 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.
* It stores no robots.txt and no line of one. The setting holds crawler names — "somebody decided GPTBot may read this site" — 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.
* It never writes a file. The only file it will ever name is the `robots.txt` in the site's own root, worked out from `ABSPATH` and never taken from a request, and it is named in order to be refused rather than written.
* 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.
* It never overrides "Discourage search engines from indexing this site" in Settings → Reading. That setting is what closes robots.txt to everybody on nearly every WordPress site, it also puts a `noindex` on every page, and it is a decision somebody made on purpose — on a staging site or a site not ready to launch, the right one.
* 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.
* The call bar edits nothing. It adds one element of its own at `wp_footer`, with no JavaScript at all and with every style rule namespaced under `.marcelo-call-bar`, 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.

= Do I need a Marcelo account? =

To use it as intended, yes — 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.

== External services ==

This plugin can place Google tags built from identifiers stored in its own settings — 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:

* `https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX` — Google Analytics, where a measurement id is stored.
* `https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX` — Google Tag Manager, where a container id is stored.
* `https://www.googletagmanager.com/gtag/js?id=AW-XXXXXXXXX` — Google Ads, where a conversion id is stored.

This is the ordinary way any site places these tags, by hand or through any other plugin — the plugin itself never contacts Google or sends it anything; it only writes the `<script src>` that causes the visitor's browser to. A stored Search Console verification token loads nothing at all: it is only ever printed as a `<meta>` tag.

This service is provided by Google. Terms of service: https://policies.google.com/terms. Privacy policy: https://policies.google.com/privacy.

== Installation ==

1. Install and activate the plugin.
2. In Marcelo, connect this website. The tags are placed for you.

== Frequently Asked Questions ==

= Does this send my data anywhere? =

No. The plugin makes no outbound connections at all — 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.

= What happens if I deactivate it? =

The tags stop being printed, and the stored snippets are kept — so reactivating restores them without reconnecting. Deleting the plugin removes them.

= Does the call bar change my theme? =

No. Nothing in your theme is read, edited or replaced — the plugin adds one element of its own at the `wp_footer` 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.

= Does it edit my robots.txt file? =

It cannot, and it will not try. If your site has a real `robots.txt` file in its root, your webserver serves that file and WordPress is never asked for one — 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.

= What happens to my redirects if I delete the plugin? =

They go with it, and every address that was a 404 before is a 404 again. Deactivating keeps them, like the snippets.

= Can it inject anything into my site? =

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 — a Google site-verification token, a `G-` Analytics measurement id, a `GTM-` container id — 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.

A caller must also already be able to `manage_options`, 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.

= Where do the profile logos come from? =

The "Find us on" row draws each platform's logo as inline SVG. The Google Maps mark is from Simple Icons (https://simpleicons.org, CC0). The others are Font Awesome Free icons by Fonticons, Inc. (https://fontawesome.com), licensed CC BY 4.0. Each logo is a trademark of its owner and is used only to link to the business's own profile there.

== Changelog ==

= 1.15.26 =
* An update that cannot finish now leaves the plugin working: it is no longer stopped by the browser closing the connection or by the server's time limit, and where it ends part-way through, the build that was installed before is put back and switched on again.

= 1.15.25 =
* Menus are read the way a browser reads them across every popular theme and builder: a new page's line now reaches an ElementsKit menu, a heading with no link of its own (Beaver Builder, Kadence Blocks) or drawn as a button (block themes) opens its section's page, links written without quotes are read, Blocksy and Max Mega Menu columns keep their pages and get a "See more" line, and a builder's template panel under a heading is left whole instead of emptied.

= 1.15.24 =
* A menu whose dropdown lines are never closed, as ElementsKit prints them, is read line by line: a section past fifteen pages shows its first six and a "See more" line again, and new pages reach it.

= 1.15.23 =
* The preview of a change to an existing Elementor page (a block of questions and answers added to it, say) keeps each element's own colours, sizes and alignment. Before, the page's headings were drawn in Elementor's default light blue, at the theme's size, while the header and footer were right.
* After Marcelo changes the site's menu, it asks the site to clear its page caches, so visitors see the new menu straight away instead of after the cache expires.
* Where the menu's Blog is a plain link, the dropdown arrows ElementsKit adds come off it too.

= 1.15.22 =
* The menu's Blog opens the site's blog page, with no dropdown of article titles. Services and Service Areas open their own page too, and past fifteen pages a dropdown shows its first six and a "See more" line. Nothing in the site's WordPress menus is changed or deleted; the shape is drawn over the menu visitors see, and taking it off in Marcelo puts the dropdown back.
* A section's page shows a picture card for every published page in it, three across on a computer, two on a tablet and one on a phone, twelve to a page with plain page links. It takes the site's own header, footer, colours and fonts, and is drawn from the site's published pages each time it is opened, so a new page appears and a removed one goes by itself.
* WordPress's own blog page shows the articles Marcelo once wrote as pages beside the posts.
* An article Marcelo publishes on a site with no SEO plugin now carries its own sharing card: Open Graph and Twitter card tags naming its title, description, address and featured image, so a link to it on Facebook or X shows the article's picture rather than the first image on the page. Only on a post Marcelo wrote, and never where Yoast SEO, Rank Math, All in One SEO, SEOPress, The SEO Framework or Jetpack already prints sharing tags.

= 1.15.21 =
* The preview of a new page on a site whose pages carry their own header and footer (Elementor Canvas) is drawn with the header and footer of its sibling pages, exactly as the published page will be, instead of the theme's.

= 1.15.20 =
* Where the menu visitors see has no Blog, Services or Service Areas dropdown, the first page Marcelo publishes of that kind makes one, after the row's last dropdown and in its own markup, and the next pages join it. Where the row has no dropdown at all, the section is a plain entry opening the section's own page.
* A page Marcelo publishes on a site whose pages carry their own header and footer (Elementor Canvas) now gets the same header and footer as its sibling pages, read from a sibling's own layout, instead of the theme's.

= 1.15.19 =
* A page Marcelo publishes reaches the menu visitors actually see, however that menu is built. Where a site's header or footer lists its pages by hand or through its own code rather than through a WordPress menu, the new page's link is drawn into each list that names the pages beside it, in the same markup as its neighbours. Taking the page off in Marcelo takes the link off every page.

= 1.15.18 =
* The "Find us on" row takes the look of the site it is on, when Marcelo drafts it: a quiet line, an inset panel or a compact capsule, chosen from the shape of the site's own buttons and drawn in its footer's colours, font and width. The platform marks are coloured to suit the site too. Each link is a 48-pixel button, names print on paper, and keyboard focus is always visible.
* Rows published before this version keep exactly the look they were approved in.

= 1.15.17 =
* The "Find us on" row draws each profile as its platform's logo — a round button in the platform's own colour, opening that profile — with the name kept for screen readers and printed on paper.
* One logo per platform: a second address on a platform already in the row (two Google Maps links to one listing) is left out of the drawing. The stored row is unchanged.

= 1.15.16 =
* Every setting Marcelo writes — a title, a description, a canonical, hreflang, a schema block, the Ads and Analytics tags, the call button, larger buttons, profile links, the availability line, the guide at /llms.txt, a redirect, an Elementor layout — now clears the site's page cache the moment it is written, so visitors are handed the changed page rather than the cached copy. It goes through the public door of every cache plugin and host cache that has one: LiteSpeed Cache, WP Rocket, WP Super Cache, W3 Total Cache, WP Fastest Cache, SiteGround Optimizer, WP-Optimize, Cache Enabler, Hummingbird, Breeze, Nginx Helper, WP Engine, Kinsta, Pantheon and Cloudflare's plugin, each only where it is installed. `marcelo_sc_page_caches_purged` fires after, for a cache not named here. Nothing is cleared on a refused write, and the plugin's own email count clears nothing.
* One description per page. On a page Marcelo holds a description for, the description Yoast SEO, Rank Math, SEOPress, All in One SEO or The SEO Framework would print beside it is switched off for that request through the filter each plugin publishes, the way the canonical already was. The SEO Framework 5 is reached through its render data, for the canonical as well.
* A redirect's rule fires on a 410 as well as a 404 — a *gone* status another plugin set ahead of it is the same verdict, and Marcelo proposes redirects for both.
* An address a Marcelo redirect forwards is left out of the sitemaps the plugin reaches while the rule is stored — WordPress's own, by the post the address resolves to, and Yoast's and Rank Math's through their entry filters — and both sitemap caches are asked to build again when a rule is stored or taken off. The write route answers `sitemaps` naming which it reaches on the site, so Marcelo prints only what the site said. A sitemap written by anything else still lists the address until it is taken off by hand.

= 1.15.15 =
* Counts the site's own outgoing email, so Marcelo can say whether a contact form's messages leave the site at all. Core's `wp_mail_succeeded` and `wp_mail_failed` actions are counted per day for thirty days, with which of the two came last and the last failure's reason — every email address taken out of it, entities decoded first so none hides inside one. No recipient, subject or message is stored, nothing is sent anywhere, and nothing is ever sent through a form to find out.
* `GET /mail-health` reads the count to a user who can `manage_options` and writes nothing. Uninstalling deletes it.

= 1.15.14 =
* A page previewed on an Elementor site keeps the site's own design on Elementor 4.3.5 and later, which skip every template's CSS inside a REST request — the preview is drawn in one. The header, the footer and the kit's global styles are printed inline through Elementor's own `before_print_css` door, generated in memory, only for the documents Elementor skipped; on earlier Elementor nothing is printed twice.
* A page previewed on an Elementor site keeps the site's own design where Elementor's CSS cache had just been cleared. Elementor regenerates a template's CSS from a missing `_elementor_css` row, writes it back and reads it again before printing; inside the preview's read-only hold that write was refused, so the header, the footer and the global styles were left off the page — the logo white on white, the menu a bare list, every button grey. For the length of the draw the regenerated CSS is now kept in memory and answered back to Elementor through the `update_post_metadata` and `get_post_metadata` filters. Nothing is written on the site, and the filters go with the hold on every path out.

= 1.15.13 =
* Larger buttons on phones. Links Marcelo measured as too small for a thumb are drawn at least 44 pixels tall and wide on screens up to 820 pixels wide, through one stylesheet of the plugin's own registered with `wp_add_inline_style`. Each link is named by its own address — `a[href="…"]` — and a link inside a line of text is drawn as an inline box so the minimum applies. Nothing in the theme is edited, no markup is touched, computers are not reached, and clearing the setting removes every rule.
* Nothing that arrives becomes CSS. What is stored is a link address and one yes-or-no per link, and the address has to be made only of the characters a web address uses, so no quote, backslash, angle bracket, brace or space reaches the selector the plugin writes around it. Every declaration is a template in the plugin, and every stored address is checked again on the way out.
* The route takes the same option lock the redirect and title routes take, reads the row back past the cache, and refuses rather than reporting a write the site did not take.

= 1.15.12 =
* A page title Marcelo sets on a block theme — Twenty Twenty-Two and every default theme since — no longer arrives as a second `<title>` beside the theme's own. A block theme prints its title tag without declaring `title-tag` support, so the plugin printed one too, read two back, and undid itself every time. The title now goes through the document-title filter alone on those themes, which is where the block theme reads it from.

= 1.15.11 =
* An article Marcelo writes, which goes up as a post in the site's blog, is previewed as a post: the private preview draws it in the theme's single-post frame and the template the site's own posts use, rather than as a page. The preview route takes `postType` and says what it drew.
* A post can carry its own phone number across the site's header and footer, and its own search settings — noindex, out of the sitemap, a title of its own — the way a page Marcelo writes already could. Both are set only by Marcelo's create, on the post it creates, by somebody who may edit that post. The core, Rank Math and Yoast sitemaps leave such a post out as they leave out such a page.
* The theme's second copy of the title comes off a post Marcelo designed whose content carries its title as the H1, as it already did on a page. Posts Marcelo did not write are untouched.

= 1.15.10 =
* A page whose own content already carries its title as its main heading shows the title once even after it is re-saved in a page builder. Elementor's text widget drops the hidden marker 1.15.8 recognised Marcelo's pages by, so a page re-saved there showed the theme's copy of the title again. The theme's copy is now taken off wherever the page's own content carries an H1 with exactly the title's words; a page whose content carries no such heading is untouched.

= 1.15.9 =
* Updating the plugin from Marcelo installs the build WordPress.org publishes, even in the minutes after a release when WordPress's own update index is still offering the one before it. The press, and Marcelo's twice-daily update, used to install only what that index offered — so a site asked to update right after a release answered that it was already current, or moved one build short. Now the directory's own answer for this one plugin is read as well, and where it publishes a newer build the update goes to that build. Only a build newer than the one installed, only this plugin's own package, and only from downloads.wordpress.org; anything else leaves WordPress's answer exactly as it was.

= 1.15.8 =
* A page Marcelo designed shows its title once. Where the theme prints a page's title as something other than its main heading — a small H2 above the content, say — Marcelo puts the title in the page's own banner as the page's one H1, and the theme's copy is now taken off that page: the first heading with a title class whose words are the page's title, and the empty title wrapper with it. Only on a page whose own content carries the Marcelo design marker and an H1; every other page on the site is untouched, and a failure inside answers with the page exactly as it was drawn.
* The page preview takes the title off the same way, so the page a reviewer approves is the one that goes up.

= 1.15.7 =
* A page Marcelo creates can carry its own search settings, when the person asked for them for that page: kept out of search results (noindex), left out of the sitemap, and a browser and search title of its own that differs from the headline on the page — a thank-you page is the usual case. The page carries them as post meta Marcelo sets when it creates the page. Where Rank Math or Yoast SEO runs on the site, the page's own settings in that plugin are written when the page is created, so the box on the page says what the page does and a change made there is obeyed; where neither runs, WordPress's own robots tag and document title are answered from the meta. Leaving a page out of the sitemap is asked of WordPress's own sitemap, Rank Math's and Yoast's. Every other page on the site is untouched.

= 1.15.6 =
* A page Marcelo creates can carry its own phone number — a campaign's, say — in place of the business's main one, and now the whole page shows it. The page's own sections already did; the theme's header and footer around it went on printing the main number. The page carries the swap as post meta Marcelo sets when it creates the page, and on that page alone, after WordPress knows which page it is drawing, the finished HTML has the main number — however it is written: brackets, dashes, dots, spaces, a +1 — replaced with the page's, in the words and in the tel: link it dials. Scripts, styles, text areas, comments and every other address are left exactly as they were, a longer number that merely contains the main one is never touched, and every other page on the site is untouched. A failure inside answers with the page exactly as it was drawn.
* The page preview takes the same swap, so the header and footer a reviewer approves are the ones that go up.

= 1.15.5 =
* A page preview is drawn for a visitor, the way the page will really be seen. It used to be drawn for the administrator whose application password Marcelo connects with — but WordPress only accepts that password once the request is known to be an API request, which is after every plugin on the site has loaded, so the site's plugins were loaded for a visitor and then asked to draw for an administrator. A plugin that loads its admin-only half for administrators at start-up, and calls into it later on the page, then failed on a function its own start-up had never loaded, and every preview on a site running one had to be drawn a second time without it, under a note saying the real page might look different. That was never the plugin's fault. Drawn for a visitor, such a plugin draws normally, and nothing a visitor would never see — an admin toolbar, an edit or duplicate button — appears in a page a reviewer is asked to approve. The user is switched while nothing may be written to the database and switched back before the answer, and the plugin's recovery for a plugin that is really broken is unchanged.

= 1.15.4 =
* A page preview the site had to draw a second time — because one of its plugins failed the first time — now keeps the page builder's own design. Elementor prints the CSS of a site's header, footer and forms through a list of its own that it keeps for the whole request, and the discarded first attempt had spent it: the second attempt was told that CSS was already on a page nobody would see, and the preview came back with the header and footer in no design at all. That list is now put back before the second attempt, along with any inline CSS the first attempt attached, so nothing is printed twice either.
* A page preview is drawn in the real file of the page template the site's pages use. 1.15.2 answered the template through the post-meta filter, and WordPress never asks that filter about a post with no id — which is what a preview post is — so the preview went on being drawn in the theme's default template, with the title printed above the page's own headline. The template's file is now found directly, from the theme or from the page builder that owns it, and the page's body carries that template's classes for the length of the draw.

= 1.15.3 =
* An enquiry form for the pages Marcelo writes: the shortcode [marcelo_enquiry_form] draws a name, phone, email and message form, and what it receives is mailed from the site itself to the one address Marcelo stored — the business's own, out of its confirmed details — with the visitor's address as reply-to. With no address stored the shortcode draws nothing, so a form can never send to nobody. A nonce, a hidden field no person fills and a small per-address ceiling stand between it and a script; nothing of a third party's runs on the page.
* The plugin's own preview draws that form so a reviewer sees the page as it will be, and runs no other shortcode.

= 1.15.2 =
* A page drawn for preview now carries the page template most of the site's published pages use, so it is drawn in the same frame the page will really be created in. It was drawn by the theme's default page template, which prints the title above the content, while the page itself would be created in a page builder's content-only template that prints none — so a page carrying its own headline showed the title twice in the preview and once on the site. The template is answered for the in-memory page through the metadata filter, for the length of the draw and no longer; nothing is stored. The answer names the slug it used.

= 1.15.1 =
* The four routes that store a whole row — the call button, the profile links, the availability line and the guide at /llms.txt — now refuse a write the site did not take, rather than answering that it worked. Each read the row back afterwards, which is right, and each then reported that row as what the press had stored: on a first write that is honestly empty, and on an overwrite it is the previous row, so a caller was told their press landed when the site had refused it. Found by running the routes against a database holding every option write.
* The refusal says whose side it happened on. A full or read-only database, a crashed table or a plugin holding option writes is the site rather than anything wrong with what was sent, so it is a 500 with a sentence that does not send somebody to rewrite a number, a list or a line that was never the problem.

= 1.15.0 =
* A site can now serve an llms.txt — the plain-text guide the tools that answer questions about a business read, at /llms.txt. WordPress has no route that serves one and no filter to hook, so a site with no file there answers 404; this serves it from the site itself, only where nothing else claimed the request.
* A real llms.txt file in the site root is refused by name, with its full path. The webserver serves that file directly and WordPress is never asked for it, so anything stored here would be a document no reader ever sees — reported as published and invisible, which is the worst outcome available.
* Sent as text/plain with nosniff, bounded, and refused outright if it carries a control character rather than repaired: a guide with something taken out of the middle of it is a document nobody approved.

= 1.14.9 =
* Page preview: the check on what a caller may ask to be left out was a pattern PHP could not compile, so it rejected nothing and printed a warning every time it ran. On a site that shows warnings, that warning arrives in front of the answer and the preview fails to read on a site where nothing is wrong.

= 1.14.8 =
* Updating the plugin from Marcelo now asks WordPress.org rather than repeating what the site last heard. WordPress keeps the answer to "is there an update?" for up to twelve hours, and the request to refresh it respects that cache — so a site pressed minutes after a release answers "already on the newest published build" about a version that did not exist when it last looked. Seen on a live site an hour after 1.14.7 was published, with the directory and its own API both serving the new version.
* The stored answer is cleared before asking, which is what the Check again button on the site's own Updates screen does. One request to WordPress.org, on a press somebody made deliberately.
= 1.14.7 =
* Page preview on a site with a broken plugin: the plugin that broke it is remembered, and the next preview takes it off before drawing rather than after. A site in that state used to draw every preview twice, and the second draw is where its own design went missing — a stylesheet is printed once per request, so the discarded first attempt had already claimed what it managed to print. 1.14.5 gave the second attempt the enqueue lists back and a real site still came back short: nine stylesheet links against forty-two on the live page, the theme's own among the missing.
* So the second draw stops being how that site draws. One attempt, from a request that has spent nothing, and the page cannot come up short on its own stylesheets however the recovery behaves. The page still says which plugin it was drawn without.
* The memory is dated and lasts a week, because a plugin somebody has since fixed has to come back and nothing on the site says it was fixed. After that it is included again, and if it is still broken the ordinary recovery catches it and writes the date forward.
= 1.14.6 =
* The plugin can update itself when Marcelo asks it to. WordPress can install a plugin from outside and has no way at all to update one, so until now every fix in a new build reached a site only when somebody downloaded a ZIP file and replaced it by hand — for a fix they did not ask for, on software they did not choose to install.
* Only this plugin, and only from the WordPress.org directory: the route takes no plugin name, so it cannot be asked to install anything else. It asks for the `update_plugins` permission separately from the settings permission, and a site set not to modify its own files (`DISALLOW_FILE_MODS`) is told so in those words rather than shown a filesystem error — that is usually a managed host doing the updating for you.
* The version it reports is read back off the file afterwards rather than claimed: the running request is still executing the old copy, so the only honest answer is what is on disk when it has finished. It also checks the plugin is still switched on, because an update that quietly left it off would take every Marcelo feature off this site at once.
* This arrives *in* a version, so it cannot update the version that does not have it: this is the last update that has to be done by hand.

= 1.14.5 =
* Page preview: a page that had to be drawn twice now comes back wearing this site's own design. 1.14.4 added a second attempt when another plugin breaks the first one; a stylesheet is printed once per request, so the first attempt had already claimed every one of them before it fell over, and the second attempt printed none — the page came back as its own words with no theme, no header styling and no colour, on precisely the sites the second attempt exists to rescue.
* The enqueue registers are read before the first attempt and put back before the second, so attempt two starts where attempt one did and this site's theme is asked for its stylesheets again. Nothing else about the attempt changes, and a page drawn once — every site where nothing is broken — is untouched.

= 1.14.4 =
* 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 — and the answer says which plugin was left out, so nobody is shown a page that is quietly missing something without being told what.
* 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 — nothing on the site is switched off, and nothing about it is stored.
* 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.
* 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.

= 1.14.3 =
* 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 — the part of the path under this site's own `wp-content`, nothing about the server above it — 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.
* 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.

= 1.14.2 =
* 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 — `wp` and `template_redirect` — 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.
* 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 …", 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.
* 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.
* 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 — 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.
* 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.

= 1.14.1 =
* Subdirectory installs: a site living at `example.com/blog` sent visitors to `example.com/blog/blog/the-new-page`. 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 — 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.
* 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.
* 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.
* No change to what the plugin can do. Every route, every feature name and every answer is the same as 1.14.0 — so a site on 1.14.0 loses nothing by staying, and gains these two fixes by updating.

= 1.14.0 =
* 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 — the point being that somebody can see what a new page will look like on their own site before agreeing to have it added.
* Nothing is created and nothing is changed. The page is a `WP_Post` built in memory for the length of one request, never `wp_insert_post`, 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.
* 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.
* The real template hierarchy and the real `template_include` 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 — a half-drawn page presented as a preview is worse than none.
* The title is sanitised as text and the content with `wp_kses_post`, the same rule WordPress core applies to post content from anybody without `unfiltered_html`, so the preview is drawn from the characters the page would really carry.
* A statement is sent only when its first word is one that cannot change a row — `SELECT`, `SHOW`, `DESCRIBE`, `EXPLAIN`, `SET` 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.
* 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 — so a preview of text nobody has agreed to yet displays `[the-shortcode]` where the published page would display what it produces.
* 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 — a plugin calling `exit`, 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.

= 1.13.0 =
* 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 — search engines are told what the other language versions are but never told this page is one of them.
* Additive, unlike the canonical route beside it: nothing already in the head is displaced. Whatever else on the page prints its other hreflang alternates — a theme, an SEO plugin — is left exactly as it is, and this adds one more tag naming the page itself.
* 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.
* The code is checked against a loose BCP 47 shape before it is stored — a language subtag, optionally a region or script — and lower-cased on the way in, because every reader that matters compares hreflang values case-insensitively.
* 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.

= 1.12.0 =
* 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.
* It is only ever sent for a business whose confirmed hours are seven days of `24 hours`. That decision is made against a record a person filled in and cannot be made here — 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.
* It is not the `openingHours` 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.
* `wp_footer` rather than `wp_body_open`, though the top of the page is where a line like this reads best. `wp_body_open` 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.
* 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.
* Removing it is one request, and it says what it took off.

= 1.11.0 =
* Profile links: a short row of links to the accounts a business has confirmed are its own — Facebook, Instagram, Google and the rest — 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.
* It is not the `sameAs` 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.
* No address is ever composed here. Each one is checked for an `http` or `https` 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.
* The words a link reads come from its own host — "facebook.com" reads Facebook — and an address on a host nobody recognises is labelled by its bare host rather than by a name invented for it.
* 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.
* Removing them is one request, and it says what it took off.

= 1.10.0 =
* 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 **file** rather than a value in the database, and everything below follows from that one difference.
* 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 — there is no arithmetic that turns one back into the original — so a compression with no way back is not one this makes.
* 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 — `photo-1024x768.jpg` — is resolved back to the attachment it is a size of, and the original is what gets resaved.
* 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.
* 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.
* JPEG, PNG and WebP only. A GIF may be animated and every image library WordPress ships flattens one to a single frame — a "compression" that destroys the thing the image was for — and an SVG is text with no raster editor at all. Both refused by mime type, with the reason.
* The server is asked whether it can do this at all, through `wp_image_editor_supports()`. 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 — 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.
* The sized copies `srcset` 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 — the page no faster, and the record saying it was fixed.
* 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.
* 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.

= 1.9.0 =
* 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 `AW-` id before it is accepted — a Search Console token or a bare customer id is refused rather than fired on every page as a tag belonging to nobody.

= 1.8.0 =
* 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 `pre_get_document_title`, 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.
* It is not the post's title, and it is never written as one. `post_title` is the name of the page in the admin, in the permalink and — on most themes — the heading a visitor reads at the top of the page. Every SEO plugin overrides the rendered `<title>` independently of it, so writing `post_title` to change a search result would change what a visitor reads and change nothing in Google. Nothing here calls `wp_update_post`.
* 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 `title-tag` support never calls `wp_get_document_title()`, so the filter never fires there; where such a theme prints no title at all — which is the finding this most often answers — 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.
* Text only, refused rather than cut. Every tag is stripped on the way in and the value is escaped again on the way out — which matters more here than for a description, because the value goes inside a `<title>` element where a surviving `<` closes it early.
* Written in batches, answered per page, so one bad line does not throw away the rest and the caller is told which one.
* 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.

= 1.7.0 =
* 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 — the one WordPress generates, or a file in its root. Those two are identical over HTTP and completely different underneath: WordPress's `robots_txt` 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.
* 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.
* 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 — so removing the setting puts the file back to exactly what it said, with nothing of the plugin's left in it.
* A rule that names other crawlers is left alone. One `Disallow: /` under two `User-agent` 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.
* "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 `noindex` on every page, and it is a deliberate decision — refused with the setting named, so it can be changed in WordPress by whoever made it.
* 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.
* Canonicals: a page whose `canonical` 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 — 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 `header.php` 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.
* 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 — a route that could do it would be able to cause the exact thing it exists to repair.
* The schema block is finally printed. From 1.2.0 the block was accepted, validated, re-encoded, stored — 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.
* Structured data is now encoded with `JSON_HEX_TAG`. 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 — which was harmless only for as long as the block was never printed.

= 1.6.0 =
* A tap-to-call bar. One fixed element carrying the business's confirmed phone number as a `tel:` link, printed on every page at `wp_footer`, so somebody who lands on a page can call without scrolling or hunting for a number.
* 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.
* No JavaScript of any kind, and nothing that animates. The styles go through `wp_add_inline_style` on a registered handle, and every selector is namespaced under `.marcelo-call-bar` so it cannot restyle anything the site already had.
* 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.
* Phone screens only unless asked otherwise, because that is where the number being far down the page actually costs an enquiry.
* The number is checked against a strict pattern — digits, spaces, brackets, dots, hyphens, one optional leading plus, 7 to 15 digits — before it is stored, and escaped again on the way out. A number that fails is refused with a reason rather than stored empty.

= 1.5.0 =
* Elementor layouts can be read and written. A page built with Elementor keeps its whole content — sections, columns, widgets and every setting — 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.
* 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.
* 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 — it makes it blank.
* 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.
* 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.

= 1.4.0 =
* Nothing but identifiers is stored. Three values are kept — a Search Console verification token, a `G-` measurement id and a `GTM-` container id — and no markup of any kind. A full tag can still be sent, and only the identifier inside it is taken.
* Google's scripts are loaded through `wp_enqueue_script` 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 — 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.
* 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 `script`: any JavaScript passed, and so did `onclick`, which simply was not on the list. A deny list is only ever as good as the imagination of whoever wrote it.
* An Analytics snippet that loads one property and configures another is refused rather than repaired — it is the shape of a copy-paste error, and a site reporting to a property nobody reads looks instrumented and is not.
* 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.

= 1.3.0 =
* 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 — it succeeds, changes nothing, and reports success.
* 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.
* 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.
* Written in batches, answered per page, so one bad line does not throw away the rest and the caller is told which one.
* Removing one is clearing it. Nothing is left on the page.

= 1.2.0 =
* 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 — exactly one `script` of type `application/ld+json`, 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.
* Removing it is clearing the field. Nothing is left on any page.

= 1.1.0 =
* 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.
* The status route now names what this build can do, so Marcelo does not have to infer it from a version number.

= 1.0.0 =
* First release.
