Cloudflare Workers let you intercept and modify HTTP responses at the edge — between your origin server and the user (or crawler). For SEO, this means you can fix title tags, inject structured data, add hreflang tags, and test meta descriptions without touching your backend code or waiting for a deploy cycle.
This guide covers practical Worker patterns for SEO teams.
Why Edge-Based SEO Matters
Speed of Implementation
Traditional SEO fixes require a developer, a code review, a deploy, and cache invalidation. A Cloudflare Worker can modify HTML in production within minutes. For SEO teams blocked by development queues, this is transformative.
Backend-Agnostic
Workers operate on the HTTP response regardless of your backend technology. WordPress, Shopify, Next.js, custom PHP — the Worker sees HTML and modifies it. This makes edge SEO viable for teams that can't easily modify their CMS or application code.
Reversibility
Every Worker change can be rolled back instantly by modifying or disabling the Worker route. No database migrations, no git reverts, no cache busting.
Setting Up Your First SEO Worker
Prerequisites
- Cloudflare account with your domain proxied (orange cloud enabled)
- Wrangler CLI installed (
npm install -g wrangler) - Basic JavaScript/TypeScript knowledge
Project Structure
seo-worker/
├── src/
│ └── index.ts
├── wrangler.toml
└── package.json
Basic Worker Template
export default {
async fetch(request: Request): Promise<Response> {
const response = await fetch(request)
// Only modify HTML responses
const contentType = response.headers.get("content-type") || ""
if (!contentType.includes("text/html")) {
return response
}
// Use HTMLRewriter for efficient streaming transformation
return new HTMLRewriter()
.on("title", new TitleHandler())
.transform(response)
}
}
class TitleHandler {
element(element: Element) {
// Modify title tag content
element.setInnerContent("New Title | Brand Name")
}
}
SEO Worker Patterns
Pattern 1: Dynamic Title Tags
Modify title tags based on URL path, query parameters, or A/B test groups:
class DynamicTitleHandler {
private path: string
private titleMap: Record<string, string>
constructor(path: string) {
this.path = path
this.titleMap = {
"/products/widget": "Best Widget for Teams | 2026 Reviews & Pricing",
"/blog/seo-guide": "SEO Guide: 15 Strategies That Work in 2026"
}
}
element(element: Element) {
const newTitle = this.titleMap[this.path]
if (newTitle) {
element.setInnerContent(newTitle)
}
}
}
Pattern 2: Inject Structured Data
Add JSON-LD structured data without modifying your templates:
class StructuredDataInjector {
private schema: object
constructor(schema: object) {
this.schema = schema
}
element(element: Element) {
const script = `<script type="application/ld+json">${JSON.stringify(this.schema)}</script>`
element.append(script, { html: true })
}
}
// Usage
const faqSchema = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: [
{
"@type": "Question",
name: "What is edge SEO?",
acceptedAnswer: {
"@type": "Answer",
text: "Edge SEO uses CDN workers to modify HTML for search optimization."
}
}
]
}
new HTMLRewriter()
.on("head", new StructuredDataInjector(faqSchema))
.transform(response)
Pattern 3: Hreflang Tag Injection
Add hreflang tags for international SEO without modifying every page template:
class HreflangInjector {
private url: URL
constructor(url: URL) {
this.url = url
}
element(element: Element) {
const path = this.url.pathname
const domain = this.url.hostname
const hreflangs = [
{ lang: "en", path: `/en${path}` },
{ lang: "es", path: `/es${path}` },
{ lang: "de", path: `/de${path}` },
{ lang: "x-default", path: path }
]
const tags = hreflangs
.map(
({ lang, path }) =>
`<link rel="alternate" hreflang="${lang}" href="https://${domain}${path}" />`
)
.join("\n")
element.append(tags, { html: true })
}
}
Pattern 4: Canonical URL Fixes
Fix canonical tags programmatically when your CMS generates incorrect canonicals:
class CanonicalFixer {
private correctCanonical: string
constructor(url: URL) {
// Remove trailing slashes, force lowercase, strip query params
this.correctCanonical = `https://${url.hostname}${url.pathname.replace(/\/$/, "").toLowerCase()}`
}
element(element: Element) {
if (element.getAttribute("rel") === "canonical") {
element.setAttribute("href", this.correctCanonical)
}
}
}
Pattern 5: Meta Description A/B Testing
Test different meta descriptions to optimize click-through rate:
class MetaDescriptionTest {
private variants: string[]
constructor(variants: string[]) {
this.variants = variants
}
element(element: Element) {
if (element.getAttribute("name") === "description") {
// Simple hash-based split (consistent per URL)
const variantIndex =
Math.abs(hashCode(element.getAttribute("content") || "")) %
this.variants.length
element.setAttribute("content", this.variants[variantIndex])
}
}
}
Performance Considerations
Worker Execution Time
Cloudflare Workers have a 30-second CPU time limit (50ms on the free plan). HTMLRewriter is streaming-based, so it handles large pages efficiently without buffering the entire response.
Caching Strategy
Workers execute before Cloudflare's cache by default. For SEO modifications that don't vary by user, cache the Worker's output:
const cacheKey = new Request(request.url, request)
const cache = caches.default
let response = await cache.match(cacheKey)
if (!response) {
response = await fetch(request)
response = new HTMLRewriter()
.on("title", new TitleHandler())
.transform(response)
// Cache for 1 hour
response = new Response(response.body, response)
response.headers.set("Cache-Control", "public, max-age=3600")
await cache.put(cacheKey, response.clone())
}
return response
Bot Detection
Serve different modifications to crawlers vs. users (carefully — cloaking violations apply):
const userAgent = request.headers.get("user-agent") || ""
const isBot = /googlebot|bingbot|yandex/i.test(userAgent)
// Only inject structured data for bots (to avoid client-side JS conflicts)
if (isBot) {
return new HTMLRewriter()
.on("head", new StructuredDataInjector(schema))
.transform(response)
}
Warning: Only use bot detection for additive improvements (like structured data injection), never for showing fundamentally different content. Google considers content cloaking a violation.
Deployment and Monitoring
Wrangler Configuration
name = "seo-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[routes]]
pattern = "example.com/blog/*"
zone_name = "example.com"
Gradual Rollout
Start with a specific URL pattern (e.g., /blog/*) before expanding to the entire site. Monitor:
- Google Search Console for indexing errors after deployment
- Page speed metrics (Workers add minimal latency but verify)
- Crawl stats in Search Console for any crawl rate changes
Logging and Debugging
Use console.log in Workers for debugging — logs appear in wrangler tail:
wrangler tail --format pretty
FAQ
Does a Cloudflare Worker add latency? Minimal — typically 1-5ms. Workers execute at the edge, geographically close to the user. The latency is negligible compared to origin server response time.
Can Google detect Worker-modified content? Google renders pages with JavaScript but also caches the initial HTML response. Worker modifications are part of the initial response, so Google sees them reliably — more reliably than client-side JavaScript modifications.
Is this considered cloaking? Not if you serve the same content to users and crawlers. Workers that modify HTML uniformly (same title tag, same structured data for everyone) are fine. Workers that show fundamentally different content to bots vs. users are cloaking.
Can I use Workers with Shopify, WordPress, or other managed platforms? Yes, as long as your domain is proxied through Cloudflare. Workers intercept the response after it leaves the origin, regardless of what generates it.