Edge SEO for WordPress: Fix Titles, Meta Descriptions and Structured Data Without a Plugin
A Cloudflare Worker in front of your WordPress site rewrites titles, meta descriptions, canonicals, hreflang and structured data per URL, with no plugin or theme edit.
WordPress is good at managing your content and bad at managing your <title> tags. The tag is assembled from parts (the page title, the site name, a page number), a theme template or an SEO plugin then stamps one site-wide format on all of it, and block themes hide the fields you would want to edit. Edge SEO removes that whole layer of pain. A Cloudflare Worker that sits in front of your WordPress site rewrites the <title>, the meta description, the canonical, the hreflang tags and the JSON-LD in the HTML as it passes through, per URL, with no theme edit and no plugin. WordPress keeps running exactly as it does today. The crawler just sees better metadata.
Below: the pain points the edge removes, what a Worker actually rewrites, how the changes survive the CDN cache, a step-by-step setup, and how this blog does exactly that.
What edge SEO means for a WordPress site
Edge SEO is rewriting what search engines see on a page at the CDN edge, between the visitor and your origin server, instead of editing the CMS, the template or the code that produced the page. For a WordPress site on Cloudflare, the “edge” is a Cloudflare Worker: a small program that runs on every request to your domain, fetches your WordPress site, and edits the HTML response on its way out. Nothing on your origin changes. Nothing in your theme’s header.php or in the site editor changes. Publish a new post in WordPress normally, and the Worker applies the same rules to it automatically.
WordPress is a big target for this. According to W3Techs it now powers 40.7% of all websites (58.9% of sites whose CMS is known), the largest of any CMS. Most of those sites run on the default title plumbing that follows.
The five WordPress SEO headaches the edge removes
1. Title templates that fit every page and match none. WordPress core assembles the document title from parts, filtered through the document_title_parts hook: the page title, an optional page number on paginated views, the site name, and a tagline on the home page. Your theme or SEO plugin then wraps that in one template for every URL, so you get Post Name | Site Name everywhere. That is fine for a blog and wrong for a business with different kinds of content. Making the convention vary per URL means editing template files or fighting a plugin’s title templates.
2. Thin archive, tag and pagination titles. WordPress appends the page number itself on paginated views, and archives inherit the format Category: News | Site Name or Croissants | Site Name. On an ecommerce-flavored blog where tag pages can rank, that is a whole tier of URLs sent to Google with titles nobody would click.
3. Block themes that hide the fields. Block themes arrived with WordPress 5.9, as the WordPress documentation explains: every part of the site, including the header, is a block to be edited in the Site Editor. There is no SEO panel there, and no meta description field. The header template part decides what ends up in the <head> of every page, so a site editor who “hides the title” changes the visible heading, not the <title> tag the crawler reads. The edge fixes what the template emits.
4. Two SEO plugins that fight each other. The usual answer to all of the above is an SEO plugin: Yoast SEO or Rank Math. They work, up to a point. But install two, or keep one half-enabled while a theme adds its own tags, and you get duplicate meta descriptions, duplicate Open Graph tags and two sets of JSON-LD in the same <head>, leaving search engines to pick a winner. Even one plugin is a big surface on a site you care about, and its schema is templated, not per-page.
5. Category descriptions, multilingual hreflang and JSON-LD conflicts. WordPress stores a description per category, but whether that description reaches the HTML depends on the theme template, and it is not a meta description. Multilingual sites need hreflang tags that are mutual (each language version lists itself and every other version, with fully qualified URLs and an x-default catchall, per Google Search Central); keep that in a plugin and it can quietly vanish on your next theme change. And WordPress core has no structured data settings, so valid, per-page JSON-LD comes down to whatever your plugins happen to output.
None of these are reasons to abandon WordPress. They are reasons to stop solving them in the CMS.
What a Worker actually rewrites, with before and after
The three tags that decide your click-through rate are the <title>, the meta description and the canonical, and all three are plain elements in <head>. A Worker rewrites them per URL, at the moment the page is served. The before and after on a typical WordPress blog:
| URL | Before (theme or plugin default) | After (edge rewrite) |
|---|---|---|
Single post /blog/making-croissants/ |
Making croissants – Example Blog |
Making Croissants: A Step-by-Step Recipe With Video |
Tag page /tag/butter/ |
Butter | Example Blog |
Butter: 9 Recipes That Start With Butter |
Category /category/news/ |
Category: News – Example Blog |
News: What's New at Example Blog |
Paginated /blog/page/2/ |
Blog – Page 2 – Example Blog |
Blog (Page 2 of 9) – Example Blog |
What makes this practical is HTMLRewriter, Cloudflare’s streaming HTML parser, which the docs describe as “a jQuery-like experience directly inside of your Workers application.” You select elements with CSS selectors and rewrite them as the response streams through. A minimal Worker that fixes titles and injects JSON-LD on blog posts looks like this:
export default {
async fetch(request) {
const url = new URL(request.url);
const res = await fetch(request);
const type = res.headers.get("Content-Type") || "";
if (!type.startsWith("text/html")) return res; // only touch pages
const m = url.pathname.match(/^\/blog\/([a-z0-9-]+)\/?$/);
if (!m) return res;
const slug = m[1];
const title = slug.charAt(0).toUpperCase() + slug.slice(1).replace(/-/g, " ") + " | Example Blog";
const ld = {
"@context": "https://schema.org",
"@type": "Article",
headline: slug.replace(/-/g, " "),
};
class TitleHandler { element(e) { e.setInnerContent(title); } }
class JsonLdHandler {
element(e) {
e.append(`<script type="application/ld+json">${JSON.stringify(ld)}</script>`, { html: true });
}
}
return new HTMLRewriter()
.on("title", new TitleHandler())
.on("head", new JsonLdHandler())
.transform(res);
},
};
Meta descriptions, canonicals and hreflang are the same pattern: setInnerContent for the description, setAttribute("href", ...) for a canonical, and append or replace for link elements. Because the rules key off the URL path, each section of the site gets its own convention, and changing 50,000 titles is editing one script, not running a database migration.
If you want to see the approach demonstrated before you wire anything, Chris Lever’s talk at brightonSEO in April 2025 walks through Worker-based SEO changes that need no developer work on the origin side.
Make the edge rewrites survive the CDN cache
A WordPress site on Cloudflare is cached at two layers, and the interaction matters. Your WordPress host runs a page cache, and Cloudflare’s CDN runs another. The important fact, per Cloudflare’s cache documentation: Workers “run before the cache but can also be utilized to modify assets once they are returned from the cache.” In practice, a Worker that rewrites the HTML runs whether the response comes from your origin or from Cloudflare’s cache, so the rewritten <title> is what gets served and what gets stored. That is the whole game: the cached document is the transformed document.

Two things to get right anyway:
- Decide whether HTML is cached at all. Cloudflare’s default is to cache static assets such as images and CSS; HTML is generally not cached until you say so. WordPress regenerates a page for every request that reaches it. Add a Cache Rule that makes your HTML URLs cacheable (the docs call this adjusting “what is eligible to cache, how long it should be cached and where”). Your origin renders once per cache entry; the Worker rewrites on every served copy for free.
- Vary the cache by the things your rules depend on. If your rewrites differ by language or device, one cached HTML document cannot serve them all. Cache Rules let you set a custom cache key that includes a request header, so you can keep one cached French page and one English page, or separate cached variants per language. And when you change the Worker’s rules, purge the affected URLs, or your old titles keep coming out of the cache until it expires.
Step by step: put a Cloudflare Worker in front of WordPress
- Make sure your domain is proxied through Cloudflare. Create the Worker in Cloudflare’s dashboard under Workers & Pages, then Create application, and either paste the script above or start from a template. Per Cloudflare’s dashboard guide you can deploy from the dashboard in a few clicks.
- Attach the Worker to your WordPress domain. A Worker only runs when a route matches: in your Worker’s settings, under Domains & Routes, add a route with the pattern
example.com/*. The route requires an active zone and a DNS record that is proxied (orange-clouded), which is how requests reach Cloudflare before they reach WordPress. - Start narrowly. Match only the paths you understand,
/blog/*or a specific page type, and return the unchanged response for everything else. TheContent-Typecheck in the example above protects your admin, your REST API and your uploads, which are not HTML pages. - Test the served HTML. View source and check
<title>, the meta description, the canonical and the JSON-LD. Test again with the page cache warm, since that is the document Googlebot downloads. - Add the Cache Rule for HTML. Rules, Cache Rules in the dashboard, matching your site’s HTML URLs, so WordPress renders once and Cloudflare serves the rewritten copies.
- Purge after every change. When you edit the Worker’s title map, purge the affected URLs before comparing, so you are looking at the new rules and not a cached older title.
This is a deliberately boring setup: one Worker, one route, one cache rule.
This blog is optimized exactly this way
The blog you are reading is a WordPress site, and it uses edge SEO itself. A Cloudflare Worker fronts the domain and hands requests to an edge proxy that injects the titles, meta descriptions, response headers and JSON-LD that a strategy decided for each URL, without anyone editing this theme. The difference from the manual Worker above is that the decisions are automated: an agent reads Google Search Console, plans which titles and structured data to change, applies them through the proxy, and adjusts from the results. That whole architecture is documented on this site, and it is the product of SEOEdgeAI: connect your Cloudflare account, a Worker goes up in front of your WordPress site, and the agent rewrites titles, descriptions and structured data and then learns from Search Console. The free plan covers one site, edge rewriting and JSON-LD, no credit card.
If you take one thing from this article: your WordPress theme and your plugin list no longer have to be the only place titles come from. The last mile between your server and the search engine is code you can run, and for WordPress that is where title problems actually get fixed.
Published by seoedgeai.com.
Visit seoedgeai.comMade with AI.