Home
Stop Blogger “?m=1” Redirect Errors with Cloudflare Settings (2026 Updated Fix)

Stop Blogger “?m=1” Redirect Errors with Cloudflare Settings (2026 Updated Fix)

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.

100% Indexation Recovery
0ms Latency Penalty
200 Status Resolution

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:

  1. Googlebot Smartphone requests your clean URL (e.g., yoursite.com/post.html).
  2. Blogger's backend server detects the smartphone User-Agent and forcefully issues a 302 Redirect to yoursite.com/post.html?m=1.
  3. Googlebot lands on the ?m=1 page and reads your canonical tag, which says the clean URL is the official page.
  4. 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

🤖
Zero Redirect Errors for All Bots
The script identifies Googlebot, AhrefsBot, SemrushBot, and more. When it detects a bot, it intercepts the request, grabs the content, and serves it directly on the clean URL.
⚙️
On-the-Fly DOM Scrubbing
Utilizes Cloudflare's HTMLRewriter API to surgically scrub the HTML stream in real-time, ripping ?m=1 out of every link before the bot sees it.
🗄️
Defeats Cache Fragmentation
Injects a smart cache key to force Cloudflare to neatly organize your traffic into just two high-performance buckets: Mobile and Desktop.
🧹
The 301 Vacuum Cleaner
If legacy ?m=1 URLs are floating around, this Worker intercepts them and throws a strict 301 Permanent Redirect back to the clean URL.
Static Asset Bypass
Protects your LCP and saves Worker execution limits by passing images, CSS, and fonts straight through.

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

Workers routes dashboard interface
Step 1
Workers Routes

Log into your Cloudflare dashboard and navigate to the Workers routes section.

Manage workers button interface
Step 2
Manage Workers

Click the button to manage your Cloudflare Workers.

Create application initialization
Step 3
Create Application

Click the button to create a new application.

Basic template selection screen
Step 4
Start with Hello World

Choose the option to start with a basic "Hello world" template.

Naming and deployment confirmation
Step 5
Pick a Name and Deploy

Give your Worker a recognizable name (e.g., blogger-seo-proxy) and click deploy.

Phase 2: Inject the Code

Code editor execution button
Step 6
Edit Code

Once deployed, hit the "Edit code" button to open the Cloudflare code editor.

Clearing the editor completely
Step 7
Clear Existing Code

Clear out all the default "Hello World" code currently in the editor.

Pasting the custom proxy script
Step 8
Paste New Worker Code

Paste the complete custom V5 Worker code into the editor, then click "Save and deploy".

Worker Script — V5 Edge Proxy
/**
 * 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

Returning to main dashboard
Step 9
Go Back to Account Home

Exit the editor, go back to your Cloudflare Account Home, and click on your active domain name.

Clicking workers routes tab again
Step 10
Workers Routes Tab

In your domain's sidebar, hit the "Workers Routes" tab again.

Add route button
Step 11
Add Route

Click the button to add a new route.

Final route mapping and save
Step 12
Configure Route & Save

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.

You Might Like

Handpicked utilities everyone is using right now