U&AI SEO Bridge

Description

The U&AI content platform audits client sites and produces a list of SEO issues. This plugin is how those fixes reach your site — and, just as importantly, how you see and control them.

It never renders anything over your own content. If a fix cannot be made to the real value, it is not made: the platform raises it for a person instead. Concretely:

  • Headings are changed in the page’s own stored content (Elementor’s data, or the post content), so the edit is visible and reversible in your own editor. A page the plugin cannot edit raises a task rather than an override.
  • Image alt text is only ever added where alt is empty. Where an image is in the Media Library, its own alt text is what renders — clear it there and nothing is injected.
  • robots directives are merged: the plugin may add noindex, and can never remove or override one you set.
  • Third-party JSON-LD is never deleted. Invalid schema is reported as an issue; what to do about it is your decision.
  • Redirect rules only fire on an address that genuinely returns 404. Republish a page at a path that had a rule and visitors reach your page again.
  • Where your theme or another SEO plugin already prints a meta description, this plugin’s own tag stands down rather than removing theirs.

You can see and change everything it does

On every post, page, category and tag there is a “U&AI SEO” panel listing everything the plugin renders for that item: search title, meta description, canonical, noindex/nofollow, structured data, language alternates, pagination links, injected image alt text, and whether the plugin is managing that item at all. Text fields are editable; machine-generated additions have a “Remove it” checkbox.

Anything you set by hand is yours permanently. The field is marked as owner-edited and no automated write can overwrite it afterwards — enforced inside the plugin at the metadata layer, so it holds for every write path, not only the ones that ask nicely.

Tools U&AI SEO covers the site-wide behaviour: every redirect rule with a delete button, plus switches for image-alt injection, external-link rel attributes, HTTP security headers and the analytics snippet. A switch you turn off stays off — no automated request can turn it back on.

Working alongside another SEO plugin

When another SEO plugin owns the <head>, the bridge does not compete for it.

With Yoast SEO, values are written into Yoast’s own fields, so they appear in Yoast’s sidebar where you already edit, and they stay there if you deactivate this plugin. A value you type in Yoast always wins: the plugin only fills a field you left empty, and it records what it wrote so it can update its own value later without ever touching yours. The same applies to a per-post noindex you set in Yoast — a value from the platform can never re-expose a page you hid.

With other SEO plugins the platform writes their fields over the WordPress REST API where they expose them, and reports the fix as needing manual attention where they do not.

Core REST meta

The SEO title and meta description are also registered with show_in_rest, so they can be written in the same POST /wp-json/wp/v2/posts call that creates a post:

{"title": "...", "content": "...", "meta": {"_uandai_seo_enabled": true, "_uandai_seo_title": "...", "_uandai_meta_description": "..."}}

This is what lets U&AI publish a generated article — body, featured image and SEO fields — in one request. The keys are protected meta, so writes require the edit_post capability for the target post, and they are subject to the same owner-edit protection as everything else. Canonical URL and robots stay bridge-only because their validation depends on the target post.

Post types are every public REST-enabled type (always including post and page); filter with uandai_seo_bridge_rest_meta_post_types.

Conflict behaviour

If the bridge detects an active SEO plugin (Yoast, Rank Math, AIOSEO, SEOPress, Squirrly, The SEO Framework, or any plugin whose folder name contains “seo”), head_injection_safe becomes false in /health and the plugin stops emitting its own head tags, so there are never duplicate <title>, <meta> or <link rel="canonical"> tags.

To force the bridge to coexist with another plugin, hook uandai_seo_bridge_allowed_seo_plugins (returns an allow-list of plugin slugs) or uandai_seo_bridge_head_injection_safe (returns the final boolean).

Canonical escape hatch

By default the bridge rejects canonical URLs whose host differs from the post’s host. Pass force_external_canonical=1 (in the request body or query string) to permit cross-host canonical values when migrating between domains.

Endpoints

All endpoints live under /wp-json/uandai-seo/v1/ and accept and return JSON. Authenticate with the same WordPress Application Password the U&AI dashboard already uses. Site-wide settings and /health require manage_options; per-object routes require edit_post or edit_term on that specific object.

Per post and term:

  • GET|POST /page-meta/{post_id} — read or write a post’s SEO fields
  • POST /page-meta/{post_id}/clear — remove what the plugin manages (values you set by hand are preserved)
  • GET|POST /term-meta/{taxonomy}/{term_id} — the same for a category or tag
  • POST /term-meta/{taxonomy}/{term_id}/clear
  • POST /image-alt/{post_id} — fill empty image alt text

Site-wide:

  • GET /health — version, capabilities, detected SEO plugins, duplicate installs, and which features you have switched off
  • POST /resolve-url — map a crawled URL to a post or term
  • GET|POST /redirects — list or replace redirect rules
  • GET /robots, POST /robots/configure — robots.txt additions
  • GET /security-headers, POST /security-headers/configure
  • GET /dom-normalize, POST /dom-normalize/configure — the body passes (image alts, external-link rel)
  • POST /sitemap/configure
  • POST /snippet-config — analytics snippet configuration
  • POST /purge-cache — best-effort cache purge after a write

External services

This plugin connects your site to U&AI (https://uandai.co), the platform that produces the SEO fixes it writes. A U&AI account is required to use it: you create a WordPress Application Password and enter it in the U&AI dashboard, and that is what authorises everything below.

The plugin’s own PHP never makes an outbound request. Traffic between your site and U&AI happens in two ways.

1. U&AI calls your site. The platform authenticates against the uandai-seo/v1 REST namespace with the Application Password you supplied and writes the SEO fields listed under “Endpoints”. Nothing leaves your server except the responses to those calls.

2. The analytics snippet runs in your visitors’ browsers. Only if you enable the U&AI analytics snippet for this site in the U&AI dashboard: the platform pushes a script URL to POST /uandai-seo/v1/snippet-config, and the plugin then enqueues that script on the front end. The script is served by your U&AI instance — by default https://app.uandai.co/static/platform/snippet.js — reads its collection settings from https://app.uandai.co/api/platform/snippet-config/, and reports to https://app.uandai.co/api/platform/events/.

What the snippet sends, by the collection level configured on your U&AI account:

  • Always: page URL, path, title, canonical URL and meta description; the referrer; UTM parameters and advertising click identifiers; the derived traffic source; device type; and a first-party anonymous visitor id and session id generated in the browser.
  • Interaction level: scroll depth, clicks and call-to-action clicks, and form-field interaction metadata — field name, label and type, typing timing, and whether text was pasted. Field values are sent only for fields your account explicitly allow-lists.
  • Interaction level, when enabled and only for a sampled share of sessions: an rrweb session recording of the page, with input masking applied.
  • Attribution level: the values of third-party analytics cookies already present in the visitor’s browser (by default _ga and _fbp).

The snippet is never loaded in wp-admin and never for a logged-in WordPress user, so staff activity stays out of your visitor numbers.

Consent: whether the snippet waits for consent is a per-account setting in U&AI. With it on, nothing but a page view is recorded until your consent tool calls window.__uandaiOnConsent() — form interaction, session recording, third-party cookie values and the GA4 identify call all stay off until then. With it off, collection starts on page load. Set it to match the consent regime your site operates under.

To stop it: switch the snippet off for the site in the U&AI dashboard (the platform then pushes enabled: false), or deactivate this plugin — deactivating removes the snippet and ends the platform’s ability to write to the site.

U&AI terms of use: https://uandai.co/terms
U&AI privacy policy: https://uandai.co/privacy

Reviews

There are no reviews for this plugin.

Contributors & Developers

“U&AI SEO Bridge” is open source software. The following people have contributed to this plugin.

Contributors

Translate “U&AI SEO Bridge” into your language.

Interested in development?

Browse the code, check out the SVN repository, or subscribe to the development log by RSS.

Changelog

0.15.0

  • This plugin no longer renders anything over your own content. Earlier versions replaced the first <h1> on a page and prepended their own, on every render, after your page builder had finished. Nothing here reacted to a human edit, so an owner who changed the heading in Elementor was silently overridden every time — one site could not edit a heading at all until they deactivated the plugin. Headings are now changed in the page’s own stored content (Elementor’s data or the post content), so the edit is visible and reversible in your own editor. A page this plugin cannot edit raises a task for a person instead of a render-time override.
  • A “U&AI SEO” panel on every post, page, category and tag. Everything this plugin renders for that item is now shown where you edit it: search title, meta description, canonical, noindex/nofollow, structured data, language alternates, pagination links, injected image alt text, and whether the plugin is managing that item at all. Text fields are editable; machine-generated additions have a “Remove it” checkbox. Anything you set by hand is marked as yours, and no automated write can overwrite it afterwards — enforced inside the plugin, so an older backend cannot bypass it.
  • Tools U&AI SEO lists the site-wide behaviour: redirect rules (with a delete button per rule) and switches for image-alt injection, external-link rel attributes, HTTP security headers and the analytics snippet. A switch you turn off stays off — no automated request can turn it back on.
  • With Yoast SEO active, values are written into Yoast’s own fields instead of being rendered over them, so they appear in Yoast’s sidebar where you already edit. A value you type in Yoast always wins: this plugin only fills a field you left empty, and it records what it wrote so it can update its own value later without ever touching yours.
  • Third-party JSON-LD is no longer deleted. Earlier versions removed any ld+json block they judged invalid — schema your own plugins emitted, removed at render time with no trace. Invalid schema is still reported as an issue; deciding what to do about it is yours.
  • Redirect rules now only fire on an address that genuinely 404s. If you republish a page at a path that had a redirect rule, visitors reach your page again instead of being sent away from it.
  • On a page where your theme or another SEO plugin already prints a meta description, this plugin’s own tag stands down rather than deleting theirs.
  • robots directives are merged rather than replaced: this plugin may add noindex, and can never remove or override one you set.
  • Image alt text is only ever added where alt is empty, and where an image is in the Media Library its own alt text is what renders — clear it there and nothing is injected.
  • Fixes: duplicate <title> tags are now actually de-duplicated (the old routine removed nothing and logged PHP notices); injected alt text can no longer produce malformed <img> markup; the forced content-type header uses the site’s configured charset instead of assuming UTF-8; text inside <script>, <template>, <textarea> and HTML comments is left alone by all content passes.

0.14.0

  • Archive URLs resolve on sites whose permalink structure starts with a fixed segment. 0.12.2 matched a taxonomy’s own rewrite slug, but WordPress prepends the permalink FRONT to it: a site using /article/%postname%/ has the front /article/, its category taxonomy still reports its slug as plain category, and the real archive lives at /article/category/<term>. Matching the slug alone found nothing on exactly the sites the fix was written for — one production site had every archive come back unresolved even after upgrading. The front-prefixed form is now tried first, the bare slug second, and the resolved term’s own permalink still has to equal the requested path before it is returned, so neither form can send a fix to the wrong archive.
  • Date archives (/2026/, /2026/04/, /2026/04/15/, with or without a permalink front) now report date_archive_unsupported instead of a bare unresolved. WordPress builds them from a template, so there is no post or term for this plugin to write to and no re-scan will produce one. Saying so stops them being retried run after run — and stops meta descriptions being generated for pages that can never receive them.

0.13.0

  • The analytics snippet is now added with wp_enqueue_script() instead of being printed straight into <head>. WordPress had no record that the tag existed, so caching, script-optimisation and consent plugins could reorder or defer everything around it without accounting for it. It still loads early in <head> and still carries data-site-id on the tag, which the snippet reads as it boots.
  • Read permissions now match what is being read. Site-wide settings — robots rules, security headers, DOM normalisation, snippet config — and /health require manage_options, as the matching write routes always did; previously any user who could draft a post could read them, and /health also lists the site’s active SEO plugins. Reading a single post’s or term’s SEO metadata now requires edit_post / edit_term on that specific object instead of a blanket edit_posts, which had allowed walking post ids to read the SEO fields of other people’s drafts. If the Application Password configured in U&AI belongs to a non-administrator, /health now returns 403 and the bridge will not be used — it belonged to an administrator already for any site where writes worked.
  • The DOM normalisation output buffer is now explicitly closed at shutdown rather than left for PHP to flush at the end of the request, and on WordPress 6.9+ the plugin joins core’s own template output buffer instead of opening a second one. Two buffers opened by two components can be closed in the wrong order, which puts one component’s output inside the other’s.
  • Added an “External services” section documenting exactly what this plugin connects to: the U&AI REST calls into your site, and — when you enable it — what the analytics snippet sends from your visitors’ browsers, field by field, with the consent switch and the terms and privacy policy links. Nothing about the behaviour changed; it was simply never written down.
  • Removed a developer-only test checklist from the plugin package.
  • Passes WordPress’s own Plugin Check with no errors and no warnings, and is tested against WordPress 7.1.

0.12.2

  • POST /resolve-url now resolves archive URLs for ANY registered public taxonomy, at any depth in the path. It previously only recognised category or tag sitting at the very first path segment, so a site that gives its custom post type a permalink base — /article/category/blog — or uses a custom taxonomy — /article/case-study-category/investing — had every archive URL come back unresolved. Those URLs then fell through to a post lookup, which cannot resolve an archive because an archive is not a post. On one production site this single gap accounted for 77% of a remediation run: titles, meta descriptions, H1s and canonicals all computed, then discarded for want of a term id.
  • Matching is against each taxonomy’s real rewrite base, longest first, and the resolved term’s own permalink must equal the requested path before it is returned — so two taxonomies sharing a term slug (/article/case-study-category/strategy and /article/portfolio-category/strategy) resolve to their own terms and a fix cannot land on the wrong archive.
  • Author archives now report reason: author_archive_unsupported instead of a bare unresolved. There is no post or term behind one for our meta to attach to, and saying so keeps genuinely fixable URLs from being buried alongside them.

0.12.1

  • POST /resolve-url now resolves URLs it previously refused. url_to_postid() matches against the site’s canonical home URL and rewrite rules, so it returned “not found” for URLs that are plainly this site’s: a www. host where WordPress is configured without it (or the reverse), an http:// URL on an https site, a trailing slash that disagrees with the permalink structure, and custom post types whose permalink base does not round-trip. Each of those made an applicable SEO fix unresolvable — the fix was right, we just could not name the post to write to.
  • The fallbacks stay exact rather than fuzzy: the host is only ever swapped for this site’s own www/non-www twin, so a foreign URL that happens to share a path still does not resolve; and the custom-post-type slug lookup is limited to public REST-enabled types and refuses when two posts share the slug, because then the path does not identify either of them.

0.12.0

  • Installing the plugin now installs the U&AI analytics snippet too. Previously someone had to paste a script tag into the theme by hand, so most sites never did it and reported no visitor data at all. The platform pushes the site id to POST /uandai-seo/v1/snippet-config and the plugin renders the loader in <head> — no theme editing, and no chance of pasting the wrong site id.
  • The snippet is not rendered in wp-admin or for logged-in users, so staff traffic stays out of the client’s page-view counts. What the snippet may collect is still decided server-side and fetched at runtime, so a site cannot opt itself into heavier collection.
  • /health reports snippet_installed and the snippet_install capability.

0.11.3

  • Every write the bridge exposes is now safe to retry. Send an Idempotency-Key header and a replay of that key returns the STORED response instead of performing the write a second time — so a request that times out AFTER WordPress applied it no longer forces a choice between duplicating the change and skipping it. Applied at the REST dispatch layer, so it covers every write route, including ones added later.
  • /health gains a capabilities array describing what the bridge can DO (as opposed to supports, which lists the meta fields it can write), so a backend can detect idempotency support without matching on version numbers.
  • Keys are kept for 7 days and swept by a daily cron event; a request still in flight answers 409 with Retry-After, and reusing a key with a different body is rejected rather than served the wrong result.

0.11.2

  • A second copy of the plugin is now inert instead of fatal. WordPress installs a zip as a NEW plugin when its folder differs from the installed one, and both copies then load — the second inherited the first’s paths, never declared its own classes, and fataled the moment it called one. First copy to load now wins; any other returns immediately.
  • /health reports the plugin’s actual install path and any duplicate copies, so this is diagnosable without shell access.

0.11.1

  • Fixes a fatal error on activation. 0.11.0 inspected the site while registering — it called the conflict detector and pulled in wp-admin/includes/plugin.php at plugins_loaded, far earlier than any of that code had run before. Registration now adds its filters and inspects nothing; every check happens inside the callbacks, which run at wp_head with the full API available. Yoast detection no longer needs an admin include at all.

0.11.0

  • Yoast passthrough now covers robots directives and JSON-LD as well as title, description and canonical.
  • Schema is MERGED into Yoast’s @graph rather than emitted as a second graph: a type Yoast does not output is appended, a type it does output keeps Yoast’s node and only gains properties Yoast is missing. Yoast’s values always win, so its internal @id references stay intact.
  • The passthrough no longer registers when the bridge itself is rendering the head (an allow-listed SEO plugin), which would have produced our values twice.
  • /health advertises yoast_passthrough_robots and yoast_passthrough_schema separately, so the platform never routes a fix to a version that cannot render it.

0.10.0

  • Coexists with Yoast SEO instead of standing down: when Yoast is active the bridge feeds its stored SEO title, meta description and canonical through Yoast’s own wpseo_title / wpseo_metadesc / wpseo_canonical filters. Yoast still renders the head — it renders your values. Nothing to disable, no duplicate tags, and deactivating this plugin restores Yoast’s own values exactly.
  • Heading fixes (H1/first H2) no longer stand down for a resident SEO plugin. Those are body content; no SEO plugin writes them.
  • /health advertises yoast_passthrough so the platform can route meta fixes to sites where Yoast owns the head.

0.9.0

  • SEO title and meta description are writable through the core REST API (meta on /wp/v2/posts), so a published article carries its SEO fields in the create call.
  • /health advertises core_rest_meta plus the registered meta keys.
  • Sitemap, pagination links and JSON-LD output hardened per WordPress Plugin Check; redirects now use wp_safe_redirect() with an allow-list built from the configured rules.

0.1.0

  • Initial release: REST namespace, head renderer for title/meta/canonical/robots/JSON-LD, conflict detection.