{"id":60,"date":"2026-08-14T20:36:47","date_gmt":"2026-08-14T20:36:47","guid":{"rendered":"https:\/\/seoedgeai.com\/blog\/cloudflare-workers-seo-7-practical-examples\/"},"modified":"2026-08-14T20:37:10","modified_gmt":"2026-08-14T20:37:10","slug":"cloudflare-workers-seo-7-practical-examples","status":"publish","type":"post","link":"https:\/\/seoedgeai.com\/blog\/cloudflare-workers-seo-7-practical-examples\/","title":{"rendered":"Using Cloudflare Workers for SEO: 7 Practical Examples"},"content":{"rendered":"<p>A Cloudflare Worker is a piece of JavaScript that runs on every HTTP request to your site, before it reaches your server. Because it sits between the visitor and your origin, it can read, modify, or redirect any request or response in flight, making it one of the most flexible tools available for technical SEO. This article walks through seven patterns you can copy, adapt, and deploy today.<\/p>\n<h2>1. Rewrite title tags by URL pattern<\/h2>\n<p>Different sections of a site often need different title conventions: a product page might want &#8220;Buy [Name] | Store&#8221;, while a blog post should read &#8220;[Title] &#8211; Blog&#8221;. Rather than editing templates, use <code>HTMLRewriter<\/code> to swap the title based on the URL path.<\/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 contentType = res.headers.get('Content-Type') || '';\n\n    if (!contentType.startsWith('text\/html')) return res;\n\n    const titleMap = {\n      '\/blog\/': (path) =&gt; {\n        const slug = path.replace('\/blog\/', '').replace(\/\\\/$\/, '').replace(\/-\/g, ' ');\n        return `${slug.charAt(0).toUpperCase() + slug.slice(1)} | Blog`;\n      },\n      '\/products\/': (path) =&gt; `Buy ${path.replace('\/products\/', '').replace(\/\\\/$\/, '')} | Store`,\n    };\n\n    let newTitle = null;\n    for (const [prefix, fn] of Object.entries(titleMap)) {\n      if (url.pathname.startsWith(prefix)) {\n        newTitle = fn(url.pathname);\n        break;\n      }\n    }\n\n    if (!newTitle) return res;\n\n    class TitleHandler {\n      element(element) {\n        element.setInnerContent(newTitle);\n      }\n    }\n\n    return new HTMLRewriter()\n      .on('title', new TitleHandler())\n      .transform(res);\n  },\n};\n<\/code><\/pre>\n<p>Every request to <code>\/blog\/how-to-use-workers<\/code> gets a descriptive title without touching your CMS templates.<\/p>\n<h2>2. Inject JSON-LD structured data<\/h2>\n<p>Rich results in Google depend on structured data in the <code>&lt;head&gt;<\/code>. A Worker can inject per-page JSON-LD based on the URL, product SKU in the path, or article metadata fetched from an API.<\/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 contentType = res.headers.get('Content-Type') || '';\n    if (!contentType.startsWith('text\/html')) return res;\n\n    const ld = url.pathname.startsWith('\/products\/')\n      ? {\n          '@context': 'https:\/\/schema.org',\n          '@type': 'Product',\n          name: url.pathname.split('\/').pop().replace(\/-\/g, ' '),\n          offers: { '@type': 'Offer', priceCurrency: 'USD', availability: 'https:\/\/schema.org\/InStock' },\n        }\n      : url.pathname.startsWith('\/blog\/')\n        ? {\n            '@context': 'https:\/\/schema.org',\n            '@type': 'Article',\n            headline: url.pathname.split('\/').pop().replace(\/-\/g, ' '),\n          }\n        : null;\n\n    if (!ld) return res;\n\n    class LdHandler {\n      element(element) {\n        element.append(\n          `&lt;script type=&quot;application\/ld+json&quot;&gt;${JSON.stringify(ld)}&lt;\/script&gt;`,\n          { html: true }\n        );\n      }\n    }\n\n    return new HTMLRewriter()\n      .on('head', new LdHandler())\n      .transform(res);\n  },\n};\n<\/code><\/pre>\n<p>No plugin, no theme edit. Every page type gets the schema type it needs, served at the edge.<\/p>\n<h2>3. Add canonical URLs dynamically<\/h2>\n<p>Duplicate content from URL parameters, session IDs, or trailing-slash variations can dilute ranking signals. A Worker can compute and inject a canonical URL that normalises the path.<\/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 contentType = res.headers.get('Content-Type') || '';\n    if (!contentType.startsWith('text\/html')) return res;\n\n    \/\/ Normalise: lowercase, strip trailing slash (except root), strip query params\n    let canonicalPath = url.pathname.toLowerCase();\n    if (canonicalPath.length &gt; 1 &amp;&amp; canonicalPath.endsWith('\/')) {\n      canonicalPath = canonicalPath.slice(0, -1);\n    }\n    const canonical = `${url.origin}${canonicalPath}`;\n\n    class CanonicalHandler {\n      element(element) {\n        const existing = element.getAttribute('rel');\n        if (existing === 'canonical') {\n          element.setAttribute('href', canonical);\n        } else {\n          element.append(\n            `&lt;link rel=&quot;canonical&quot; href=&quot;${canonical}&quot;&gt;`,\n            { html: true }\n          );\n        }\n      }\n    }\n\n    return new HTMLRewriter()\n      .on('link[rel=&quot;canonical&quot;]', new CanonicalHandler())\n      .transform(res);\n  },\n};\n<\/code><\/pre>\n<p>If your CMS already outputs a canonical, the Worker updates it. If not, it inserts one.<\/p>\n<h2>4. Redirect crawlers without affecting real visitors<\/h2>\n<p>Sometimes you need search bots to see one URL while human visitors stay on another: retiring old URLs, serving prerendered content to Googlebot, or separating mobile indexing. Check the <code>User-Agent<\/code> header at the edge and respond differently per audience.<\/p>\n<pre><code class=\"language-js\">const BOT_PATTERNS = \/(Googlebot|bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot)\/i;\n\nexport default {\n  async fetch(request) {\n    const url = new URL(request.url);\n    const ua = request.headers.get('User-Agent') || '';\n\n    \/\/ Send bots from old paths to the new location\n    if (BOT_PATTERNS.test(ua) &amp;&amp; url.pathname.startsWith('\/old-section\/')) {\n      const newPath = url.pathname.replace('\/old-section\/', '\/new-section\/');\n      return Response.redirect(`${url.origin}${newPath}`, 301);\n    }\n\n    \/\/ Everyone else gets the normal page\n    return fetch(request);\n  },\n};\n<\/code><\/pre>\n<p>This keeps your redirect chain short for search engines while avoiding unnecessary round trips for real users.<\/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<p>In this brightonSEO talk, Chris Lever walks through practical Worker-based SEO optimisations that require no developer involvement on the origin side. The session covers several of the patterns shown here, including crawler routing and on-the-fly content changes.<\/p>\n<h2>5. A\/B test meta descriptions at the edge<\/h2>\n<p>Meta description experiments are slow when they require CMS deploys. With a Worker, you split traffic by cookie, serve variant B to half your visitors, and measure the click-through rate in Search Console.<\/p>\n<pre><code class=\"language-js\">const COOKIE_NAME = 'meta_variant';\n\nconst VARIANTS = {\n  A: 'Original meta description for this page.',\n  B: 'New meta description that we hypothesise will increase CTR.',\n};\n\nexport default {\n  async fetch(request) {\n    const url = new URL(request.url);\n    const res = await fetch(request);\n    const contentType = res.headers.get('Content-Type') || '';\n    if (!contentType.startsWith('text\/html')) return res;\n\n    const cookie = request.headers.get('Cookie') || '';\n    let variant = cookie.includes(`${COOKIE_NAME}=B`) ? 'B' : 'A';\n\n    if (!cookie.includes(`${COOKIE_NAME}=`)) {\n      variant = Math.random() &lt; 0.5 ? 'B' : 'A';\n      const newRes = new Response(res.body, res);\n      newRes.headers.append('Set-Cookie', `${COOKIE_NAME}=${variant}; path=\/; max-age=86400`);\n      res = newRes;\n    }\n\n    class MetaHandler {\n      element(element) {\n        if (element.getAttribute('name') === 'description') {\n          element.setAttribute('content', VARIANTS[variant]);\n        }\n      }\n    }\n\n    return new HTMLRewriter()\n      .on('meta[name=&quot;description&quot;]', new MetaHandler())\n      .transform(res);\n  },\n};\n<\/code><\/pre>\n<p>A 50\/50 split, sticky via cookie. Check Search Console after two weeks to see which variant drove more clicks.<\/p>\n<h2>6. Set SEO-related response headers<\/h2>\n<p>Certain headers affect how search engines index your pages. <code>X-Robots-Tag<\/code> can set noindex per URL pattern, and <code>Link<\/code> headers signal paginated series (<code>prev<\/code>\/<code>next<\/code>). Workers set these without touching server config.<\/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 newRes = new Response(res.body, res);\n\n    \/\/ Block indexing of staging or parameter-heavy URLs\n    if (url.hostname === 'staging.example.com' || url.searchParams.has('print')) {\n      newRes.headers.set('X-Robots-Tag', 'noindex, nofollow');\n    }\n\n    \/\/ Tag paginated content\n    if (url.pathname.startsWith('\/category\/')) {\n      const page = parseInt(url.searchParams.get('page')) || 1;\n      if (page &gt; 1) {\n        const prev = new URL(url);\n        prev.searchParams.set('page', String(page - 1));\n        newRes.headers.set('Link', `&lt;${prev.pathname}${prev.search}&gt;; rel=&quot;prev&quot;`);\n      }\n    }\n\n    return newRes;\n  },\n};\n<\/code><\/pre>\n<p>Headers set this way are invisible to users but fully respected by Googlebot.<\/p>\n<h2>7. Combine patterns by page type<\/h2>\n<p>A single Worker can dispatch different SEO transformations based on the page category, keeping all your edge SEO logic in one place.<\/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 contentType = res.headers.get('Content-Type') || '';\n    if (!contentType.startsWith('text\/html')) return res;\n\n    const pageType = url.pathname.startsWith('\/products\/') ? 'product'\n      : url.pathname.startsWith('\/blog\/') ? 'article'\n      : url.pathname.startsWith('\/category\/') ? 'category'\n      : 'page';\n\n    const rewriter = new HTMLRewriter();\n\n    \/\/ Title rules per type\n    const titles = {\n      product: `Buy ${url.pathname.split('\/').pop().replace(\/-\/g, ' ')} | Store`,\n      article: `${url.pathname.split('\/').pop().replace(\/-\/g, ' ')} | Blog`,\n      category: `Shop ${url.pathname.split('\/').pop().replace(\/-\/g, ' ')}`,\n    };\n\n    if (titles[pageType]) {\n      class TitleRewriter {\n        element(el) { el.setInnerContent(titles[pageType]); }\n      }\n      rewriter.on('title', new TitleRewriter());\n    }\n\n    \/\/ Canonical\n    let canon = `${url.origin}${url.pathname.replace(\/\\\/$\/, '') || '\/'}`;\n    class CanonicalRewriter {\n      element(el) { el.setAttribute('href', canon); }\n    }\n    rewriter.on('link[rel=&quot;canonical&quot;]', new CanonicalRewriter());\n\n    \/\/ JSON-LD\n    const ld = pageType === 'product'\n      ? { '@context': 'https:\/\/schema.org', '@type': 'Product', name: url.pathname.split('\/').pop() }\n      : pageType === 'article'\n        ? { '@context': 'https:\/\/schema.org', '@type': 'Article' }\n        : null;\n\n    if (ld) {\n      class LdInjector {\n        element(el) {\n          el.append(`&lt;script type=&quot;application\/ld+json&quot;&gt;${JSON.stringify(ld)}&lt;\/script&gt;`, { html: true });\n        }\n      }\n      rewriter.on('head', new LdInjector());\n    }\n\n    return rewriter.transform(res);\n  },\n};\n<\/code><\/pre>\n<p>This is the pattern that tools like <a href=\"https:\/\/seoedgeai.com\/seo\">SEOEdgeAI<\/a> automate: a single edge Worker that rewrites titles, descriptions, structured data, and headers differently for every page type, with no code changes at the origin. For sites that want the benefits without maintaining the Worker themselves, <a href=\"https:\/\/seoedgeai.com\/seo\/how-it-works\">the agent handles it automatically<\/a>.<\/p>\n<figure class=\"post-figure\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1703\" class=\"wp-image-62\" alt=\"Close-up of fiber optic cables plugged into network switches\" src=\"https:\/\/seoedgeai.com/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-2881233-scaled.jpeg\" title=\"Pexels License, via Pexels (brett-sayles)\" srcset=\"https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-2881233-scaled.jpeg 2560w, https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-2881233-300x200.jpeg 300w, https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-2881233-1024x681.jpeg 1024w, https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-2881233-768x511.jpeg 768w, https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-2881233-1536x1022.jpeg 1536w, https:\/\/seoedgeai.com\/blog\/wp-content\/uploads\/sites\/4\/2026\/08\/pexels-photo-2881233-2048x1363.jpeg 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><figcaption>Pexels License, via Pexels (brett-sayles)<\/figcaption><\/figure>\n<p>Edge SEO happens at the network layer, between the visitor and your origin server. A Worker runs in this space, which is why it can transform responses without modifying your application codebase.<\/p>\n<hr \/>\n<p><strong>What these examples have in common:<\/strong> they all run before your origin server sends a byte, which means they add zero latency to your application code, they deploy independently of your CMS, and they can be tested and rolled back in seconds. For teams that want to move faster on technical SEO without waiting on deploy cycles, the edge is the right place to do it.<\/p>\n<p>If you are building Worker-based SEO for your own site, you might also need <a href=\"https:\/\/insightmoves.com\/\">competitive intelligence automation<\/a> to track how your competitors&#8217; sites are changing their own SEO strategies over time.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Seven practical Cloudflare Worker examples for technical SEO: rewrite title tags, inject JSON-LD, add canonical URLs, redirect crawlers, A\/B test descriptions, and control headers at the edge.<\/p>\n","protected":false},"author":1,"featured_media":61,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-60","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\/60","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=60"}],"version-history":[{"count":1,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/posts\/60\/revisions"}],"predecessor-version":[{"id":63,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/posts\/60\/revisions\/63"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/media\/61"}],"wp:attachment":[{"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/media?parent=60"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/categories?post=60"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/seoedgeai.com\/blog\/wp-json\/wp\/v2\/tags?post=60"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}