seoedgeai.com Blog

Using Cloudflare Workers for SEO: 7 Practical Examples

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.

Laptop displaying code in a modern workspace, representing the developer-focused audience for Cloudflare Workers SEO techniques
Pexels License, via Pexels (dkomov)

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.

1. Rewrite title tags by URL pattern

Different sections of a site often need different title conventions: a product page might want “Buy [Name] | Store”, while a blog post should read “[Title] – Blog”. Rather than editing templates, use HTMLRewriter to swap the title based on the URL path.

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const res = await fetch(request);
    const contentType = res.headers.get('Content-Type') || '';

    if (!contentType.startsWith('text/html')) return res;

    const titleMap = {
      '/blog/': (path) => {
        const slug = path.replace('/blog/', '').replace(/\/$/, '').replace(/-/g, ' ');
        return `${slug.charAt(0).toUpperCase() + slug.slice(1)} | Blog`;
      },
      '/products/': (path) => `Buy ${path.replace('/products/', '').replace(/\/$/, '')} | Store`,
    };

    let newTitle = null;
    for (const [prefix, fn] of Object.entries(titleMap)) {
      if (url.pathname.startsWith(prefix)) {
        newTitle = fn(url.pathname);
        break;
      }
    }

    if (!newTitle) return res;

    class TitleHandler {
      element(element) {
        element.setInnerContent(newTitle);
      }
    }

    return new HTMLRewriter()
      .on('title', new TitleHandler())
      .transform(res);
  },
};

Every request to /blog/how-to-use-workers gets a descriptive title without touching your CMS templates.

2. Inject JSON-LD structured data

Rich results in Google depend on structured data in the <head>. A Worker can inject per-page JSON-LD based on the URL, product SKU in the path, or article metadata fetched from an API.

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const res = await fetch(request);
    const contentType = res.headers.get('Content-Type') || '';
    if (!contentType.startsWith('text/html')) return res;

    const ld = url.pathname.startsWith('/products/')
      ? {
          '@context': 'https://schema.org',
          '@type': 'Product',
          name: url.pathname.split('/').pop().replace(/-/g, ' '),
          offers: { '@type': 'Offer', priceCurrency: 'USD', availability: 'https://schema.org/InStock' },
        }
      : url.pathname.startsWith('/blog/')
        ? {
            '@context': 'https://schema.org',
            '@type': 'Article',
            headline: url.pathname.split('/').pop().replace(/-/g, ' '),
          }
        : null;

    if (!ld) return res;

    class LdHandler {
      element(element) {
        element.append(
          `<script type="application/ld+json">${JSON.stringify(ld)}</script>`,
          { html: true }
        );
      }
    }

    return new HTMLRewriter()
      .on('head', new LdHandler())
      .transform(res);
  },
};

No plugin, no theme edit. Every page type gets the schema type it needs, served at the edge.

3. Add canonical URLs dynamically

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.

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const res = await fetch(request);
    const contentType = res.headers.get('Content-Type') || '';
    if (!contentType.startsWith('text/html')) return res;

    // Normalise: lowercase, strip trailing slash (except root), strip query params
    let canonicalPath = url.pathname.toLowerCase();
    if (canonicalPath.length > 1 && canonicalPath.endsWith('/')) {
      canonicalPath = canonicalPath.slice(0, -1);
    }
    const canonical = `${url.origin}${canonicalPath}`;

    class CanonicalHandler {
      element(element) {
        const existing = element.getAttribute('rel');
        if (existing === 'canonical') {
          element.setAttribute('href', canonical);
        } else {
          element.append(
            `<link rel="canonical" href="${canonical}">`,
            { html: true }
          );
        }
      }
    }

    return new HTMLRewriter()
      .on('link[rel="canonical"]', new CanonicalHandler())
      .transform(res);
  },
};

If your CMS already outputs a canonical, the Worker updates it. If not, it inserts one.

4. Redirect crawlers without affecting real visitors

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 User-Agent header at the edge and respond differently per audience.

const BOT_PATTERNS = /(Googlebot|bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot)/i;

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const ua = request.headers.get('User-Agent') || '';

    // Send bots from old paths to the new location
    if (BOT_PATTERNS.test(ua) && url.pathname.startsWith('/old-section/')) {
      const newPath = url.pathname.replace('/old-section/', '/new-section/');
      return Response.redirect(`${url.origin}${newPath}`, 301);
    }

    // Everyone else gets the normal page
    return fetch(request);
  },
};

This keeps your redirect chain short for search engines while avoiding unnecessary round trips for real users.

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.

5. A/B test meta descriptions at the edge

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.

const COOKIE_NAME = 'meta_variant';

const VARIANTS = {
  A: 'Original meta description for this page.',
  B: 'New meta description that we hypothesise will increase CTR.',
};

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const res = await fetch(request);
    const contentType = res.headers.get('Content-Type') || '';
    if (!contentType.startsWith('text/html')) return res;

    const cookie = request.headers.get('Cookie') || '';
    let variant = cookie.includes(`${COOKIE_NAME}=B`) ? 'B' : 'A';

    if (!cookie.includes(`${COOKIE_NAME}=`)) {
      variant = Math.random() < 0.5 ? 'B' : 'A';
      const newRes = new Response(res.body, res);
      newRes.headers.append('Set-Cookie', `${COOKIE_NAME}=${variant}; path=/; max-age=86400`);
      res = newRes;
    }

    class MetaHandler {
      element(element) {
        if (element.getAttribute('name') === 'description') {
          element.setAttribute('content', VARIANTS[variant]);
        }
      }
    }

    return new HTMLRewriter()
      .on('meta[name="description"]', new MetaHandler())
      .transform(res);
  },
};

A 50/50 split, sticky via cookie. Check Search Console after two weeks to see which variant drove more clicks.

6. Set SEO-related response headers

Certain headers affect how search engines index your pages. X-Robots-Tag can set noindex per URL pattern, and Link headers signal paginated series (prev/next). Workers set these without touching server config.

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const res = await fetch(request);
    const newRes = new Response(res.body, res);

    // Block indexing of staging or parameter-heavy URLs
    if (url.hostname === 'staging.example.com' || url.searchParams.has('print')) {
      newRes.headers.set('X-Robots-Tag', 'noindex, nofollow');
    }

    // Tag paginated content
    if (url.pathname.startsWith('/category/')) {
      const page = parseInt(url.searchParams.get('page')) || 1;
      if (page > 1) {
        const prev = new URL(url);
        prev.searchParams.set('page', String(page - 1));
        newRes.headers.set('Link', `<${prev.pathname}${prev.search}>; rel="prev"`);
      }
    }

    return newRes;
  },
};

Headers set this way are invisible to users but fully respected by Googlebot.

7. Combine patterns by page type

A single Worker can dispatch different SEO transformations based on the page category, keeping all your edge SEO logic in one place.

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const res = await fetch(request);
    const contentType = res.headers.get('Content-Type') || '';
    if (!contentType.startsWith('text/html')) return res;

    const pageType = url.pathname.startsWith('/products/') ? 'product'
      : url.pathname.startsWith('/blog/') ? 'article'
      : url.pathname.startsWith('/category/') ? 'category'
      : 'page';

    const rewriter = new HTMLRewriter();

    // Title rules per type
    const titles = {
      product: `Buy ${url.pathname.split('/').pop().replace(/-/g, ' ')} | Store`,
      article: `${url.pathname.split('/').pop().replace(/-/g, ' ')} | Blog`,
      category: `Shop ${url.pathname.split('/').pop().replace(/-/g, ' ')}`,
    };

    if (titles[pageType]) {
      class TitleRewriter {
        element(el) { el.setInnerContent(titles[pageType]); }
      }
      rewriter.on('title', new TitleRewriter());
    }

    // Canonical
    let canon = `${url.origin}${url.pathname.replace(/\/$/, '') || '/'}`;
    class CanonicalRewriter {
      element(el) { el.setAttribute('href', canon); }
    }
    rewriter.on('link[rel="canonical"]', new CanonicalRewriter());

    // JSON-LD
    const ld = pageType === 'product'
      ? { '@context': 'https://schema.org', '@type': 'Product', name: url.pathname.split('/').pop() }
      : pageType === 'article'
        ? { '@context': 'https://schema.org', '@type': 'Article' }
        : null;

    if (ld) {
      class LdInjector {
        element(el) {
          el.append(`<script type="application/ld+json">${JSON.stringify(ld)}</script>`, { html: true });
        }
      }
      rewriter.on('head', new LdInjector());
    }

    return rewriter.transform(res);
  },
};

This is the pattern that tools like SEOEdgeAI 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, the agent handles it automatically.

Close-up of fiber optic cables plugged into network switches
Pexels License, via Pexels (brett-sayles)

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.


What these examples have in common: 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.

If you are building Worker-based SEO for your own site, you might also need competitive intelligence automation to track how your competitors’ sites are changing their own SEO strategies over time.

Published by seoedgeai.com.

Visit seoedgeai.com

Made with AI.