Przejdź do strony

29 lipca 2026 • Wojciech Smolarek

Przekierowania na krawędzi. Jak obsłużyć 100k URL-i w Next.js i Cloudflare

Przekierowania na krawędzi. Jak obsłużyć 100k URL-i w Next.js i Cloudflare

Powiem Ci od razu, bez owijania w bawełnę: jeśli masz mapę 100 tysięcy przekierowań po migracji i pierwsze, co przychodzi Ci do głowy, to wrzucić ją do next.config.js — odłóż to na chwilę. Zaraz pokażę Ci, dlaczego to jest droga donikąd i gdzie te przekierowania naprawdę powinny wisieć, żeby Twój serwer nawet nie zauważył, że ktoś je odpytuje.

Kontekst jest prosty i pewnie znajomy. Migracja dużego sklepu albo serwisu, zmienia się struktura URL-i, stare adresy trzeba spiąć z nowymi tak, żeby ani ruch, ani crawl budget nie wyparował. Piotr wyciągnął z SaaS-a mapę: 100 000 par stary URL → nowy URL, ładny CSV, wszystko posprzątane. Teraz zaczyna się moja działka — wystawić to na produkcję tak, żeby w poniedziałek rano nikt nie dzwonił, że strona muli.

Dlaczego 100k to inna liga niż 100

Sto przekierowań to nudna robota, którą zrobisz na kolanie. Sto tysięcy to już decyzja architektoniczna. Różnica nie polega na tym, że jest ich „więcej” — polega na tym, że przy tej skali miejsce, w którym trzymasz mapę, decyduje o wydajności całej strony. Każdy request, który wchodzi na serwis, musi w jakiś sposób sprawdzić: „czy ta ścieżka ma przekierowanie?”. Zrób to źle, a dokładasz ten koszt do każdego wejścia — również tych 99% wejść, które żadnego przekierowania nie potrzebują.

I tu leży pies pogrzebany.

Najczęstszy błąd: wszystko do next.config.js

Wbudowana tablica redirects() jest świetna do kilku, kilkunastu reguł. Do 100k — nie. Dwa twarde powody:

  • Limit platformy. Na Vercelu obowiązuje limit 1 024 przekierowań w konfiguracji. Twoje 100 000 się tam po prostu nie zmieści.
  • Redeploy przy każdej zmianie. Reguły z next.config.js są wpinane na etapie builda. Chcesz dodać jedno przekierowanie? Cały deploy od nowa. Przy żywej migracji, gdzie mapę poprawiasz kilka razy dziennie, to jest gwóźdź do trumny.

Sama dokumentacja Next.js mówi wprost: powyżej ~1000 przekierowań idź w stronę własnego rozwiązania na Middleware, zamiast tłuc to w konfiguracji. To nie moja fanaberia — to zalecenie twórców frameworka.

Druga pułapka: 100k JSON-a w Middleware

„Dobra, to wrzucę mapę do Middleware i tam będę sprawdzał.” Krok w dobrą stronę, ale jest haczyk: jeśli zaimportujesz cały plik ze 100 000 wpisów prosto do middleware.ts, spuchnie Ci bundle Edge’a, a odczyt tej struktury poleci przy każdym requeście — także przy tych, które przekierowania nie mają. Zbijasz sobie czas odpowiedzi na całej stronie po to, żeby obsłużyć ułamek ruchu. Bez sensu.

Zapamiętaj więc prostą zasadę, wokół której zbudujemy całą resztę:

Dopasuj warstwę do kształtu przekierowania. Płaska mapa 1:1 (stary adres → nowy adres) należy do krawędzi, przed serwerem. Przekierowania z logiką (regex, parametry, język, warunki) należą do Middleware.

Rozbijmy to na dwie warstwy.


Warstwa 1: Cloudflare, zanim request w ogóle dotknie serwera

Skoro 95% Twojej mapy to nudne pary 1:1, to nie ma żadnego powodu, żeby te requesty w ogóle docierały do Next.js. Cloudflare stoi przed originem — niech tam się skończą. Serwer ich nie zobaczy, nie policzy, nie zapłaci za nie ani milisekundą. To jest dokładnie ten „edge”, o który chodzi.

Masz dwie sensowne drogi.

Opcja A: Bulk Redirects (bez linijki kodu)

Cloudflare ma natywną funkcję Bulk Redirects — wgrywasz listę, włączasz regułę i tyle. Zero kodu, zero deployu Next.js. Idealne, jeśli lubisz mieć przekierowania odklejone od aplikacji.

Jest tylko jedno „ale”, które musisz sprawdzić zanim obiecasz komukolwiek termin — limity zależą od planu:

PlanLimit URL-i w Bulk Redirects
Free10 000
Pro25 000
Business50 000
Enterprise1 000 000

Widzisz problem? Twoje 100 000 nie zmieści się nawet w Business (50k). Żeby obsłużyć całą mapę samymi Bulk Redirects, potrzebujesz Enterprise. Na niższych planach ta droga Ci odpada — i dlatego zaraz jest opcja B.

Drobna, ale wredna pułapka z praktyki: na planie Free limit oficjalnie podniesiono do 10 000, ale rozjazd we wdrożeniu tego podbicia potrafił zostawić część kont zablokowanych na starych 20 sztukach. Jak wgrywasz listę i dostajesz „maximum number of items” przy absurdalnie niskiej liczbie — to nie Twój błąd, to zaległy rollout. Zgłoszenie do supportu załatwia sprawę, ale lepiej wiedzieć wcześniej niż w noc wdrożenia.

Opcja B: Worker + KV (działa na każdym planie)

To moja domyślna droga przy tej skali, zwłaszcza gdy klient nie siedzi na Enterprise. Mapę trzymasz w Workers KV — globalnym magazynie klucz–wartość na krawędzi Cloudflare. Klucz to stara ścieżka, wartość to nowy adres. Mały Worker robi jeden lookup i, jeśli trafi, zwraca 301. Jeśli nie trafi — przepuszcza request dalej do Next.js.

Dlaczego akurat KV? Bo redirecty to workload czytany, nie pisany. Mapę wgrywasz raz (i aktualizujesz z doskoku), a odczytujesz miliony razy. KV jest zbudowane dokładnie pod to: gorące klucze schodzą poniżej 5 ms, liczba kluczy praktycznie Cię nie ogranicza (100k par to grosze w 1 GB storage’u), a wartość może mieć do 25 MB — czyli z zapasem.

Kod Workera jest banalnie prosty i o to chodzi — im mniej się tu dzieje, tym lepiej:

// worker.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // normalizacja klucza: bez końcowego slasha, żeby /buty i /buty/ trafiały tak samo
    const key = url.pathname.replace(/\/+$/, "") || "/";

    const target = await env.REDIRECTS.get(key);
    if (target) {
      // 301 = permanentne, przekazuje sygnały SEO na nowy adres
      return Response.redirect(new URL(target, url.origin).toString(), 301);
    }

    // brak trafienia → puszczamy do originu (Next.js), serwer robi swoje
    return fetch(request);
  },
};

Powiązanie namespace’u KV wpinasz w wrangler.toml:

name = "redirects-edge"
main = "worker.js"

[[kv_namespaces]]
binding = "REDIRECTS"
id = "twoj_kv_namespace_id"

A teraz najlepsze — załadowanie tych 100k par. Nie klikasz ich ręcznie (nie po to Piotr wyciągał dane, żebym ja je wklepywał). CSV z SaaS-a przerabiasz na JSON w formacie, który KV łyka hurtem:

[
  { "key": "/stary-produkt-123", "value": "/nowy-produkt-abc" },
  { "key": "/kategoria/wyprzedaz", "value": "/promocje" }
]

I wgrywasz jednym strzałem przez Wrangler:

npx wrangler kv bulk put redirects.json --binding=REDIRECTS --remote

Sto tysięcy wpisów ląduje na krawędzi w kilka chwil. Origin cały ten ruch po prostu mija.

Jedna szczera uwaga o kosztach, bo nie lubię, jak ktoś przemilcza rachunek: darmowy tier KV daje 100 000 odczytów na dobę. Dla realnego serwisu z ruchem to za mało — plan Workers Paid (rząd wielkości: kilka dolarów miesięcznie) zdejmuje ten limit. To nie jest przekręt, to normalny koszt infrastruktury, tyle że śmiesznie niski w porównaniu z tym, co ratujesz na wydajności originu.


Warstwa 2: Middleware Next.js — tylko dla przekierowań z logiką

Część mapy nie da się zapisać jako płaska para adresów. Regexy, przenoszenie parametrów zapytania, przekierowania zależne od języka albo od tego, czy user jest zalogowany — to jest robota dla Middleware, bo tu potrzebujesz kodu, nie tablicy.

Ale trzymamy się zasady z góry: do Middleware nie wrzucamy wielkiej mapy. Zamiast tego korzystamy z wzorca, który poleca sama dokumentacja Next.js — filtra Blooma.

Idea jest sprytna. Filtr Blooma to malutka struktura (mieści się w bundlu bez problemu), która na pytanie „czy ta ścieżka ma przekierowanie?” odpowiada albo „na pewno nie”, albo „prawdopodobnie tak”. Kluczowe: fałszywy negatyw nie istnieje — jeśli filtr mówi „nie”, to naprawdę nie ma i nigdy nie zgubisz realnego przekierowania. Fałszywy pozytyw czasem się zdarzy, ale to nic nie kosztuje, bo wtedy dopiero sięgamy po prawdziwą mapę i sprawdzamy.

Efekt: dla 99% ruchu Middleware kończy pracę na jednym tanim teście w pamięci. Po ciężką mapę (KV, Edge Config, Redis) sięga tylko wtedy, gdy filtr da sygnał „prawdopodobnie tak”.

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { ScalableBloomFilter } from "bloom-filters";
import bloomData from "./redirects/bloom-filter.json";

// wygenerowany offline z Twojej mapy, mały i statyczny
const filter = ScalableBloomFilter.fromJSON(bloomData as any);

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1. tani test: czy ta ścieżka W OGÓLE może mieć przekierowanie?
  if (filter.has(pathname)) {
    // 2. dopiero teraz odpytujemy prawdziwą mapę przez Route Handler / API
    const res = await fetch(
      new URL(`/api/redirect?path=${encodeURIComponent(pathname)}`, request.url)
    );

    if (res.ok) {
      const data = await res.json();
      if (data?.destination) {
        return NextResponse.redirect(
          new URL(data.destination, request.url),
          data.permanent ? 308 : 307
        );
      }
    }
  }

  return NextResponse.next();
}

// nie odpalaj Middleware na statykach i assetach — to marnowanie cykli
export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
};

Ten matcher na końcu to nie ozdoba. Bez niego Middleware odpala się na każdym obrazku i pliku _next — a to dokładnie ten narzut, którego chcemy uniknąć.


Pułapki migracyjne, na których ludzie się przewracają

Sama mechanika serwowania to połowa roboty. Druga połowa to niedopuszczenie do tego, żeby przekierowania zjadły Ci SEO, po które w ogóle je robisz.

  • 301, nie 302. Do trwałej migracji używasz 301 (albo 308, jeśli chcesz zachować metodę HTTP). 302/307 to „tymczasowe” — Google nie przenosi po nich sygnałów tak chętnie. To najczęstszy cichy sabotaż widoczności po migracji.
  • Spłaszczaj łańcuchy. Jeśli stary URL prowadzi do pośredniego, a ten dopiero do finalnego (A → B → C), to marnujesz crawl budget i dokładasz opóźnienie użytkownikowi. Mapuj od razu A → C. Przy 100k par przelać to skryptem przez mapę to kwestia minut, a różnicę widać w logach.
  • Normalizacja kluczy. Końcowy slash, wielkość liter, parametry zapytania — zdecyduj, jak je traktujesz, i trzymaj się tego po obu stronach (w mapie i w kodzie). Inaczej /Buty/ i /buty rozjadą Ci się w martwe trafienia.
  • Zero pętli. A → B i gdzieś w tej samej paczce B → A to gwarantowany ERR_TOO_MANY_REDIRECTS. Zanim wgrasz cokolwiek, przelać mapę przez prosty walidator wykrywający cykle. Lepiej złapać to na laptopie niż na produkcji.

Checklista przed i po wdrożeniu

Zanim klikniesz „deploy”:

  •  CSV z SaaS-a przerobiony na docelowy format (JSON dla KV / lista dla Bulk Redirects)
  •  Łańcuchy spłaszczone do stary → finalny
  •  Wykryte i usunięte pętle oraz duplikaty kluczy
  •  Ustalona i wymuszona normalizacja (slash / wielkość liter / parametry)
  •  Sprawdzone limity planu Cloudflare pod liczbę Twoich przekierowań
  •  Kody statusu: 301/308 dla trwałych, świadomie i celowo

Po wdrożeniu:

  •  Próbka 20–30 starych URL-i przeklikana ręcznie — czy 301 leci na właściwy adres
  •  Sprawdzone w logach, że originu nie zalewają requesty, które miała zdjąć krawędź
  •  Zaktualizowana i wysłana sitemapa z nowymi adresami
  •  GSC obserwowany na spokojnie — indeksacja przeliczy się przez kilka dni, nerwowe odświeżanie nic nie przyśpieszy

Podsumowując po ludzku

Sto tysięcy przekierowań to nie jest problem „ile”, tylko „gdzie”. Płaskie pary 1:1 spychasz na krawędź Cloudflare — Bulk Redirects, jeśli plan udźwignie liczbę, albo Worker + KV, jeśli chcesz to mieć niezależnie od planu i limitów. Serwer ich wtedy nie widzi i o to cały czas chodziło. Garść przekierowań z realną logiką zostaje w Middleware Next.js, ale odchudzonym filtrem Blooma, żeby 99% ruchu nawet nie musnęło ciężkiej mapy.

Match tool to the shape of the redirect. Reszta to porządek: 301, spłaszczone łańcuchy, brak pętli, czysta normalizacja. Zrób to raz porządnie, a w poniedziałek po migracji zamkniesz laptopa z czystym sumieniem — zamiast gasić pożar w GSC.

Wojciech Smolarek

O autorze: Wojciech Smolarek

Web Developer

Web Developer, Technical SEO Specialist i specjalista od automatyzacji. W Studio Algorytm odpowiada za development, zamieniając skomplikowane procesy biznesowe i marzenia klientów w czysty, zoptymalizowany kod.

Zobacz wszystkie artykuły autora