If you run a Blogger site, you already know the nightmare. You open Google Search Console (GSC), check your indexing status, and see a massive wall of red: "Redirect Error" or "Page with redirect." Your beautiful, clean URLs aren't getting indexed, your search traffic is stalling, and traditional SEO advice isn't working.
The culprit? Blogger's ancient, hardcoded ?m=1 mobile redirect.
In this guide, we are going to break down exactly why this error happens in the era of Mobile-First Indexing, why standard fixes fail, and how to permanently eliminate the issue using a custom Cloudflare Worker Edge Proxy—without sacrificing your site's speed or mobile usability.
Why the Blogger "?m=1" Redirect Error Happens
To understand the solution, you have to understand the trap. Google now operates almost entirely on Mobile-First Indexing. This means the crawler analyzing your website is "Googlebot Smartphone." Here is exactly what happens when Googlebot crawls a standard Blogger site architecture:
- Googlebot Smartphone requests your clean URL (e.g.,
yoursite.com/post.html). - Blogger's backend server detects the smartphone User-Agent and forcefully issues a 302 Redirect to
yoursite.com/post.html?m=1. - Googlebot lands on the
?m=1page and reads your canonical tag, which says the clean URL is the official page. - Googlebot gets trapped in a logic loop: "The canonical tag says the clean URL is the master, but the server won't let me reach it without redirecting me." The result? Google abandons the crawl and slaps your URL with a fatal Redirect Error.
The Solution: The Edge Reverse Proxy
For years, SEOs tried to fight this by spoofing User-Agents, blocking m=1 in robots.txt, or changing canonical tags. Those methods either created infinite server loops, ruined cache performance, or resulted in ugly ?m=1 URLs ranking in Google.
The real solution is Edge Computing.
By deploying a specifically engineered Cloudflare Worker, we can intercept Googlebot before it reaches Blogger. Instead of redirecting the bot, the Worker secretly fetches the mobile HTML on the backend, scrubs the code clean of any ?m=1 traces, and hands it back to Googlebot as a flawless, redirect-free 200 OK.
The Masterclass Advantages of This Script
HTMLRewriter API to surgically scrub the HTML stream in real-time, ripping ?m=1 out of every link before the bot sees it.?m=1 URLs are floating around, this Worker intercepts them and throws a strict 301 Permanent Redirect back to the clean URL.How to Deploy the Worker on Cloudflare (Step-by-Step)
Deploying this script takes less than five minutes and works perfectly on Cloudflare's Free tier. Here is the exact setup process to completely restructure your mobile proxy environment.
Phase 1: Create the Worker
Log into your Cloudflare dashboard and navigate to the Workers routes section.
Click the button to manage your Cloudflare Workers.
Click the button to create a new application.
Choose the option to start with a basic "Hello world" template.
Give your Worker a recognizable name (e.g., blogger-seo-proxy) and click deploy.
Phase 2: Inject the Code
Once deployed, hit the "Edit code" button to open the Cloudflare code editor.
Clear out all the default "Hello World" code currently in the editor.
Paste the complete custom V5 Worker code into the editor, then click "Save and deploy".
/**
* DOXLAYER CYBORG EDGE PROXY (V5)
*
* Request flow:
*
* Client → GET /post.html
* ↓
* Worker detects mobile UA → silently adds ?m=1 to origin request
* ↓
* Blogger sees ?m=1 → serves 200 (no redirect)
* ↓
* Worker rewrites HTML → strips ?m=1 from all internal links
* ↓
* Client sees 200 on clean URL. No redirect. No ?m=1 anywhere.
*
* Direct ?m=1 requests → 301 to clean URL.
* Edge cache → device-aware, aligned TTL, stale-while-revalidate.
*/
// ─── DETECTION PATTERNS ────────────────────────────────────────────────────────
// SEO crawlers + audit tools — served mobile content but must get 200 on clean URL
// Checked BEFORE mobile UA so bot intent is clear in logic
const BOT_UA = /googlebot|bingbot|yandexbot|duckduckbot|baiduspider|facebot|linkedinbot|pinterestbot|slackbot|twitterbot|whatsapp|telegrambot|applebot|crawler|spider|lighthouse|pagespeed|gtmetrix|pingdom|screaming.?frog|ahrefsbot|semrushbot|dotbot|rogerbot|mj12bot|petalbot|bytespider/i;
// Real mobile devices — Blogger would redirect these to ?m=1 without the worker
const MOBILE_UA = /android.+mobile|iphone|ipod|blackberry|iemobile|opera mini|opera mobi|palm|windows ce|series[46]0|symbian|treo|up\.browser|vodafone|wap|xiino|avantgo|bada|fennec|hiptop|kindle|lge |maemo|midp|mmp|netfront|palm|phone|pixi|plucker|psp|silk|danger|docomo|mot-|samsung|ucweb/i;
// Pass static assets directly to origin — no rewriting needed
const STATIC_RE = /\.(js|css|jpg|jpeg|png|gif|svg|webp|avif|ico|woff2?|ttf|eot|otf|pdf|zip|mp[34]|webm|ogg|xml|txt|csv|map|webmanifest)$/i;
// Blogger internal paths — bypass all logic
const BYPASS = ['/feeds/', '/b/', '/_/', '/gadgets/', '/rpc/', '/.well-known/'];
// ─── URL NORMALIZATION ─────────────────────────────────────────────────────────
// Strips m=1 from any URL string while preserving all other params and hash
function stripM(str) {
if (!str || str.indexOf('m=1') === -1) return str;
var hashIdx = str.indexOf('#');
var hash = hashIdx !== -1 ? str.substring(hashIdx) : '';
var main = hashIdx !== -1 ? str.substring(0, hashIdx) : str;
var qIdx = main.indexOf('?');
if (qIdx === -1) return str;
var base = main.substring(0, qIdx);
var pairs = main.substring(qIdx + 1).split('&');
var kept = [];
for (var i = 0; i < pairs.length; i++) {
if (pairs[i] !== 'm=1' && pairs[i] !== 'm=') kept.push(pairs[i]);
}
var result = base;
if (kept.length > 0) result += '?' + kept.join('&');
if (hash) result += hash;
return result;
}
// ─── HTML REWRITER HANDLERS ────────────────────────────────────────────────────
// Strips ?m=1 from href and action attributes on links and forms
class LinkCleaner {
constructor(attr) { this.attr = attr; }
element(el) {
var val = el.getAttribute(this.attr);
if (val && val.indexOf('m=1') !== -1) {
el.setAttribute(this.attr, stripM(val));
}
}
}
// Strips ?m=1 from OG / Twitter / dox meta content attributes
// NOTE: canonical is a not a tag —
// it is handled by LinkCleaner('href') on link[href], not here
class MetaFixer {
element(el) {
var prop = (el.getAttribute('property') || el.getAttribute('name') || '').toLowerCase();
if (
prop.indexOf('og:') === 0 ||
prop.indexOf('dox:') === 0 ||
prop.indexOf('twitter:') === 0
) {
var content = el.getAttribute('content');
if (content && content.indexOf('m=1') !== -1) {
el.setAttribute('content', stripM(content));
}
}
}
}
// ─── MAIN HANDLER ──────────────────────────────────────────────────────────────
async function handle(request, ctx) {
var url = new URL(request.url);
var ua = request.headers.get('User-Agent') || '';
var path = url.pathname;
// ── Bypass: static assets and Blogger internals ──
if (STATIC_RE.test(path)) return fetch(request);
for (var b = 0; b < BYPASS.length; b++) {
if (path.indexOf(BYPASS[b]) === 0) return fetch(request);
}
// ── Step 1: Redirect any incoming ?m=1 to clean URL ──
// Covers old cached links, shared URLs, browser history.
// 301 tells Google to permanently forget the ?m=1 version.
if (url.searchParams.has('m')) {
url.searchParams.delete('m');
// Preserve all other query params (e.g. ?pg=2 for pagination)
var cleanUrl = url.origin + url.pathname + (url.search || '');
return Response.redirect(cleanUrl, 301);
}
// ── Step 2: Device classification ──
// Bot is checked first — Googlebot Smartphone contains "mobile" in its UA
// so checking bot first makes the intent explicit and avoids ambiguity
var isBot = BOT_UA.test(ua);
var isMobile = MOBILE_UA.test(ua);
var needM = isBot || isMobile;
// ── Step 3: Edge cache lookup ──
// Cache key uses ?__d=1 for mobile/bot, ?__d=0 for desktop.
// This gives device-aware caching without Vary: User-Agent,
// which would fragment the cache across thousands of UA strings.
var cache = caches.default;
var cacheKey = new Request(url.origin + url.pathname + url.search + (url.search ? '&' : '?') + '__d=' + (needM ? '1' : '0'));
var hit = await cache.match(cacheKey);
if (hit) {
var hitHeaders = new Headers(hit.headers);
hitHeaders.set('X-Edge-Cache', 'HIT');
return new Response(hit.body, { status: hit.status, headers: hitHeaders });
}
// ── Step 4: Build origin request ──
// Silently append ?m=1 for mobile/bot so Blogger skips its own 302.
// Desktop gets the clean URL — Blogger serves 200 for desktop UAs.
var originUrl = new URL(url.toString());
if (needM) originUrl.searchParams.set('m', '1');
// ── Step 5: Fetch from Blogger origin ──
// redirect:'manual' gives us full control over any redirects Blogger returns.
var res = await fetch(originUrl.toString(), {
method: request.method,
headers: request.headers,
redirect: 'manual'
});
// ── Step 6: Handle origin redirects (safety net) ──
// If Blogger still redirects despite ?m=1 being present (edge cases),
// follow it once manually. Strip ?m=1 from the target then re-add if needed.
// After one hop, pass any further redirects through — prevents infinite loops.
if (res.status >= 300 && res.status < 400) {
var location = res.headers.get('Location');
if (location) {
var redirUrl = new URL(location, originUrl);
redirUrl.searchParams.delete('m');
if (needM) redirUrl.searchParams.set('m', '1');
res = await fetch(redirUrl.toString(), {
method: request.method,
headers: request.headers,
redirect: 'manual'
});
}
}
// ── Step 7: Pass through 304 Not Modified unchanged ──
if (res.status === 304) {
return new Response(null, { status: 304, headers: res.headers });
}
// ── Step 8: Transform HTML ──
// Stream-rewrite all internal links, form actions, and meta content
// to strip any ?m=1 Blogger may have injected into the served HTML.
var ct = res.headers.get('Content-Type') || '';
var isHTML = ct.indexOf('text/html') !== -1;
var output;
if (isHTML) {
var rewriter = new HTMLRewriter()
.on('a[href]', new LinkCleaner('href')) // internal page links
.on('link[href]', new LinkCleaner('href')) // canonical + RSS links
.on('form[action]', new LinkCleaner('action')) // search forms
.on('meta', new MetaFixer()); // OG / Twitter / dox metas
output = rewriter.transform(res);
} else {
output = res;
}
// ── Step 9: Build final response headers ──
var headers = new Headers(output.headers);
// Security headers
headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
headers.set('X-Content-Type-Options', 'nosniff');
headers.set('X-Frame-Options', 'SAMEORIGIN');
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
headers.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// Cache headers — aligned TTL between browser and CDN edge.
// Both set to 30min so a newly published post appears within 30min everywhere.
// stale-while-revalidate allows serving stale while revalidating in background.
if (isHTML && res.status === 200) {
headers.set('Cache-Control', 'public, max-age=1800, stale-while-revalidate=86400');
headers.set('CDN-Cache-Control', 'public, max-age=1800, stale-while-revalidate=86400');
}
// Strip Vary: User-Agent — device separation is handled via cache key, not Vary.
// Keeping Vary: User-Agent would fragment the cache for every UA string.
var vary = headers.get('Vary');
if (vary) {
var parts = vary.split(',');
var kept = [];
for (var v = 0; v < parts.length; v++) {
if (parts[v].trim().toLowerCase() !== 'user-agent') kept.push(parts[v].trim());
}
if (kept.length === 0) headers.delete('Vary');
else headers.set('Vary', kept.join(', '));
}
// Remove Blogger internal noise headers
headers.delete('X-Blogger-Server');
headers.delete('X-Blogger-Data');
// Debug headers — shows cache and device state, safe to expose
headers.set('X-Edge-Cache', 'MISS');
headers.set('X-Edge-Device', needM ? 'mobile' : 'desktop');
var finalRes = new Response(output.body, {
status: res.status,
statusText: res.statusText,
headers: headers
});
// ── Step 10: Write to edge cache asynchronously ──
// ctx.waitUntil keeps the Worker alive until the write completes
// without blocking or delaying the response already sent to the client.
if (isHTML && res.status === 200) {
ctx.waitUntil(cache.put(cacheKey, finalRes.clone()));
}
return finalRes;
}
// ─── EXPORT ────────────────────────────────────────────────────────────────────
export default {
async fetch(request, env, ctx) {
try {
return await handle(request, ctx);
} catch (err) {
// Ultimate fallback — bypass all worker logic and hit origin directly.
// If origin also fails, return a clean 503 with Retry-After.
return fetch(request).catch(function () {
return new Response('Service Unavailable', {
status: 503,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Retry-After': '60'
}
});
});
}
}
};
Phase 3: Route Your Traffic
Exit the editor, go back to your Cloudflare Account Home, and click on your active domain name.
In your domain's sidebar, hit the "Workers Routes" tab again.
Click the button to add a new route.
In the Route field, type *yoursite.com/* (or your domain). Select the Worker you just created from the dropdown, and hit Save.
Congratulations.
Your edge proxy is now live. Ensure you have Cloudflare's Rocket Loader turned OFF (as it can interfere with HTML rewriting), purge your Cloudflare cache, and head over to Google Search Console.
Run a Live Test on your failing URLs, hit Request Indexing, and watch as your Redirect Errors become a thing of the past.