{"id":105,"date":"2026-08-28T21:35:54","date_gmt":"2026-08-28T21:35:54","guid":{"rendered":"https:\/\/seoedgeai.com\/blog\/edge-seo-wordpress\/"},"modified":"2026-08-28T21:36:15","modified_gmt":"2026-08-28T21:36:15","slug":"edge-seo-wordpress","status":"publish","type":"post","link":"https:\/\/seoedgeai.com\/blog\/edge-seo-wordpress\/","title":{"rendered":"Edge SEO for WordPress: Fix Titles, Meta Descriptions and Structured Data Without a Plugin"},"content":{"rendered":"<p>WordPress is good at managing your content and bad at managing your <code>&lt;title&gt;<\/code> 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 <code>&lt;title&gt;<\/code>, 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.<\/p>\n<p>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.<\/p>\n<h2>What edge SEO means for a WordPress site<\/h2>\n<p><a href=\"https:\/\/seoedgeai.com\/blog\/what-is-edge-seo\/\">Edge SEO<\/a> 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 &#8220;edge&#8221; is a <a href=\"https:\/\/developers.cloudflare.com\/workers\/runtime-apis\/html-rewriter\/\">Cloudflare Worker<\/a>: 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&#8217;s <code>header.php<\/code> or in the site editor changes. Publish a new post in WordPress normally, and the Worker applies the same rules to it automatically.<\/p>\n<p>WordPress is a big target for this. According to <a href=\"https:\/\/w3techs.com\/technologies\/details\/cm-wordpress\">W3Techs<\/a> 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.<\/p>\n<h2>The five WordPress SEO headaches the edge removes<\/h2>\n<p><strong>1. Title templates that fit every page and match none.<\/strong> WordPress core assembles the document title from parts, filtered through the <code>document_title_parts<\/code> 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 <code>Post Name | Site Name<\/code> 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&#8217;s title templates.<\/p>\n<p><strong>2. Thin archive, tag and pagination titles.<\/strong> WordPress appends the page number itself on paginated views, and archives inherit the format <code>Category: News | Site Name<\/code> or <code>Croissants | Site Name<\/code>. 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.<\/p>\n<p><strong>3. Block themes that hide the fields.<\/strong> Block themes arrived with WordPress 5.9, as the <a href=\"https:\/\/wordpress.org\/documentation\/article\/block-themes\/\">WordPress documentation<\/a> 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 <code>&lt;head&gt;<\/code> of every page, so a site editor who &#8220;hides the title&#8221; changes the visible heading, not the <code>&lt;title&gt;<\/code> tag the crawler reads. The edge fixes what the template emits.<\/p>\n<p><strong>4. Two SEO plugins that fight each other.<\/strong> 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 <code>&lt;head&gt;<\/code>, 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.<\/p>\n<p><strong>5. Category descriptions, multilingual hreflang and JSON-LD conflicts.<\/strong> 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 <code>hreflang<\/code> tags that are mutual (each language version lists itself and every other version, with fully qualified URLs and an <code>x-default<\/code> catchall, per <a href=\"https:\/\/developers.google.com\/search\/docs\/specialty\/international\/localized-versions\">Google Search Central<\/a>); 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.<\/p>\n<p>None of these are reasons to abandon WordPress. They are reasons to stop solving them in the CMS.<\/p>\n<h2>What a Worker actually rewrites, with before and after<\/h2>\n<p>The three tags that decide your click-through rate are the <code>&lt;title&gt;<\/code>, the meta description and the canonical, and all three are plain elements in <code>&lt;head&gt;<\/code>. A Worker rewrites them per URL, at the moment the page is served. The before and after on a typical WordPress blog:<\/p>\n<div class=\"table-wrap\"><table>\n<thead>\n<tr>\n<th>URL<\/th>\n<th>Before (theme or plugin default)<\/th>\n<th>After (edge rewrite)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Single post <code>\/blog\/making-croissants\/<\/code><\/td>\n<td><code>Making croissants \u2013 Example Blog<\/code><\/td>\n<td><code>Making Croissants: A Step-by-Step Recipe With Video<\/code><\/td>\n<\/tr>\n<tr>\n<td>Tag page <code>\/tag\/butter\/<\/code><\/td>\n<td><code>Butter | Example Blog<\/code><\/td>\n<td><code>Butter: 9 Recipes That Start With Butter<\/code><\/td>\n<\/tr>\n<tr>\n<td>Category <code>\/category\/news\/<\/code><\/td>\n<td><code>Category: News \u2013 Example Blog<\/code><\/td>\n<td><code>News: What's New at Example Blog<\/code><\/td>\n<\/tr>\n<tr>\n<td>Paginated <code>\/blog\/page\/2\/<\/code><\/td>\n<td><code>Blog \u2013 Page 2 \u2013 Example Blog<\/code><\/td>\n<td><code>Blog (Page 2 of 9) \u2013 Example Blog<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/div>\n<p>What makes this practical is <a href=\"https:\/\/developers.cloudflare.com\/workers\/runtime-apis\/html-rewriter\/\">HTMLRewriter<\/a>, Cloudflare&#8217;s streaming HTML parser, which the docs describe as &#8220;a jQuery-like experience directly inside of your Workers application.&#8221; 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:<\/p>\n<pre><code class=\"language-js\">export default {\n  async fetch(request) {\n    const url = new URL(request.url);\n    const res = await fetch(request);\n    const type = res.headers.get(&quot;Content-Type&quot;) || &quot;&quot;;\n    if (!type.startsWith(&quot;text\/html&quot;)) return res; \/\/ only touch pages\n\n    const m = url.pathname.match(\/^\\\/blog\\\/([a-z0-9-]+)\\\/?$\/);\n    if (!m) return res;\n\n    const slug = m[1];\n    const title = slug.charAt(0).toUpperCase() + slug.slice(1).replace(\/-\/g, &quot; &quot;) + &quot; | Example Blog&quot;;\n\n    const ld = {\n      &quot;@context&quot;: &quot;https:\/\/schema.org&quot;,\n      &quot;@type&quot;: &quot;Article&quot;,\n      headline: slug.replace(\/-\/g, &quot; &quot;),\n    };\n\n    class TitleHandler { element(e) { e.setInnerContent(title); } }\n    class JsonLdHandler {\n      element(e) {\n        e.append(`&lt;script type=&quot;application\/ld+json&quot;&gt;${JSON.stringify(ld)}&lt;\/script&gt;`, { html: true });\n      }\n    }\n\n    return new HTMLRewriter()\n      .on(&quot;title&quot;, new TitleHandler())\n      .on(&quot;head&quot;, new JsonLdHandler())\n      .transform(res);\n  },\n};\n<\/code><\/pre>\n<p>Meta descriptions, canonicals and hreflang are the same pattern: <code>setInnerContent<\/code> for the description, <code>setAttribute(\"href\", ...)<\/code> for a canonical, and <code>append<\/code> or <code>replace<\/code> 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.<\/p>\n<p>If you want to see the approach demonstrated before you wire anything, Chris Lever&#8217;s talk at brightonSEO in April 2025 walks through Worker-based SEO changes that need no developer work on the origin side.<\/p>\n<p><iframe loading=\"lazy\" title=\"Unlocking SEO wins with Cloudflare workers: no devs required! - Chris Lever - brightonSEO April 2025\" width=\"500\" height=\"281\" src=\"https:\/\/www.youtube.com\/embed\/lDW05YR8h9k?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/p>\n<h2>Make the edge rewrites survive the CDN cache<\/h2>\n<p>A WordPress site on Cloudflare is cached at two layers, and the interaction matters. Your WordPress host runs a page cache, and Cloudflare&#8217;s CDN runs another. The important fact, per <a href=\"https:\/\/developers.cloudflare.com\/workers\/reference\/how-the-cache-works\/\">Cloudflare&#8217;s cache documentation<\/a>: Workers &#8220;run before the cache but can also be utilized to modify assets once they are returned from the cache.&#8221; In practice, a Worker that rewrites the HTML runs whether the response comes from your origin or from Cloudflare&#8217;s cache, so the rewritten <code>&lt;title&gt;<\/code> is what gets served and what gets stored. That is the whole game: the cached document is the transformed document.<\/p>\n<figure class=\"post-figure\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1703\" class=\"wp-image-107\" alt=\"Aisles of server racks in a data center, where the Cloudflare network caches and serves the rewritten pages\" src=\"https:\/\/seoedgeai.com/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-5480781-2-scaled.jpeg\" title=\"Credit: brett-sayles, Pexels License, via Pexels\" srcset=\"https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-5480781-2-scaled.jpeg 2560w, https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-5480781-2-300x200.jpeg 300w, https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-5480781-2-1024x681.jpeg 1024w, https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-5480781-2-768x511.jpeg 768w, https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-5480781-2-1536x1022.jpeg 1536w, https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-5480781-2-2048x1363.jpeg 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><figcaption>Credit: brett-sayles, Pexels License, via Pexels<\/figcaption><\/figure>\n<p>Two things to get right anyway:<\/p>\n<ul>\n<li><strong>Decide whether HTML is cached at all.<\/strong> Cloudflare&#8217;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 <a href=\"https:\/\/developers.cloudflare.com\/cache\/how-to\/cache-rules\/\">Cache Rule<\/a> that makes your HTML URLs cacheable (the docs call this adjusting &#8220;what is eligible to cache, how long it should be cached and where&#8221;). Your origin renders once per cache entry; the Worker rewrites on every served copy for free.<\/li>\n<li><strong>Vary the cache by the things your rules depend on.<\/strong> 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&#8217;s rules, purge the affected URLs, or your old titles keep coming out of the cache until it expires.<\/li>\n<\/ul>\n<h2>Step by step: put a Cloudflare Worker in front of WordPress<\/h2>\n<ol>\n<li><strong>Make sure your domain is proxied through Cloudflare.<\/strong> Create the Worker in Cloudflare&#8217;s dashboard under Workers &amp; Pages, then Create application, and either paste the script above or start from a template. Per <a href=\"https:\/\/developers.cloudflare.com\/workers\/get-started\/dashboard\/\">Cloudflare&#8217;s dashboard guide<\/a> you can deploy from the dashboard in a few clicks.<\/li>\n<li><strong>Attach the Worker to your WordPress domain.<\/strong> A Worker only runs when a <a href=\"https:\/\/developers.cloudflare.com\/workers\/configuration\/routing\/routes\/\">route<\/a> matches: in your Worker&#8217;s settings, under Domains &amp; Routes, add a route with the pattern <code>example.com\/*<\/code>. 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.<\/li>\n<li><strong>Start narrowly.<\/strong> Match only the paths you understand, <code>\/blog\/*<\/code> or a specific page type, and return the unchanged response for everything else. The <code>Content-Type<\/code> check in the example above protects your admin, your REST API and your uploads, which are not HTML pages.<\/li>\n<li><strong>Test the served HTML.<\/strong> View source and check <code>&lt;title&gt;<\/code>, the meta description, the canonical and the JSON-LD. Test again with the page cache warm, since that is the document Googlebot downloads.<\/li>\n<li><strong>Add the Cache Rule for HTML.<\/strong> Rules, Cache Rules in the dashboard, matching your site&#8217;s HTML URLs, so WordPress renders once and Cloudflare serves the rewritten copies.<\/li>\n<li><strong>Purge after every change.<\/strong> When you edit the Worker&#8217;s title map, purge the affected URLs before comparing, so you are looking at the new rules and not a cached older title.<\/li>\n<\/ol>\n<p>This is a deliberately boring setup: one Worker, one route, one cache rule.<\/p>\n<h2>This blog is optimized exactly this way<\/h2>\n<p>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 <a href=\"https:\/\/seoedgeai.com\/seo\/how-it-works\">documented on this site<\/a>, and it is the product of <a href=\"https:\/\/seoedgeai.com\/seo\">SEOEdgeAI<\/a>: 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 <a href=\"https:\/\/seoedgeai.com\/pricing\">free plan<\/a> covers one site, edge rewriting and JSON-LD, no credit card.<\/p>\n<p>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.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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.<\/p>\n","protected":false},"author":1,"featured_media":106,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-105","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/posts\/105","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/comments?post=105"}],"version-history":[{"count":1,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/posts\/105\/revisions"}],"predecessor-version":[{"id":108,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/posts\/105\/revisions\/108"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/media\/106"}],"wp:attachment":[{"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/media?parent=105"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/categories?post=105"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/tags?post=105"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}