# Przekierowanie 301 w .htaccess: mapa URL, testy i rollback

> Zbuduj mapę starych i nowych adresów, wybierz właściwe reguły Apache i sprawdź każdy kierunek przekierowania. Z kodem, testami negatywnymi, wynikami lokalnego laboratorium oraz procedurą bezpiecznego wycofania zmiany.

Autor: WebAlpha (WebAlpha). Opublikowano: 2026-09-07. Zaktualizowano: 2026-09-07. Kanoniczny adres: https://webalpha.pl/blog/przekierowanie-301-htaccess

Około 19 min czytania (szacunek; bez czasu wykonania instrukcji).

Przekierowanie 301 informuje, że adres został przeniesiony na stałe. Poprawne wdrożenie to jednak nie tylko odpowiedź 301: stary URL powinien prowadzić do właściwego odpowiednika, bez pętli i zbędnych skoków, a końcowy adres musi działać. W tym tutorialu przechodzimy od mapy URL do testu HTTP, wdrożenia i rollbacku.

<a id="sekcja-szybka-sciezka-jeden-stary-adres-nowa-strona"></a>

## Szybka ścieżka: jeden stary adres → nowa strona

Jeżeli potrzebujesz jednego przekierowania, zacznij tutaj. Wariant wymaga potwierdzonego Apache 2.4 z mod_rewrite i obsługą .htaccess w katalogu głównym witryny. Jeśli masz tylko panel przekierowań hostingu, użyj instrukcji operatora dla tego panelu; nie dodawaj równolegle drugiej reguły w pliku. Brak dostępu do plików lub niepewność co do serwera oznacza, że ten fragment należy przekazać administratorowi, a nie wklejać w edytor treści WordPressa.

1. Najpierw otwórz nową stronę i potwierdź, że zastępuje starą. W poniższym przykładzie jest to /oferta.html. Zapisz stan starego i nowego adresu przed zmianą.
2. Przez SFTP lub menedżer plików ustal DocumentRoot domeny; włącz widoczność plików ukrytych. Pobierz istniejący .htaccess jako prywatną kopię na komputer, poza WWW. Nie zastępuj całego pliku przykładem.
3. Na stagingu dodaj poniższy fragment poza blokiem generowanym przez CMS i przed jego wewnętrznym przepisaniem do aplikacji. Podstaw zatwierdzony stary adres i własną stałą domenę HTTPS. Istniejące RewriteEngine On nie musi być dopisywane drugi raz.
4. Wykonaj GET opisany w sekcji 6: stary adres ma dać 301 i dokładny Location, a nowy 200 bez następnego przekierowania. Sprawdź wariant z ukośnikiem, UTM oraz adres, który nie powinien pasować, np. /stara-oferta-dodatkowa.
5. Dopiero po teście wdrażaj uzgodniony fragment na produkcji i powtórz kontrolę. Przy 500, pętli lub błędnym celu przywróć prywatną kopię w tej samej warstwie; sprawdź też cache. Pełna procedura odbioru i wycofania znajduje się w sekcjach 8–9.

Fragment dla jednego adresu — ta reguła została wykonana w naszym lokalnym teście Apache po podstawieniu stałego lokalnego originu. Nie jest testem Twojego hostingu.

```apache
RewriteEngine On
RewriteRule ^stara-oferta/?$ https://sklep.example/oferta.html [R=301,END]
```

Ten wariant zachowuje przychodzące parametry, w tym UTM. Nie obejmuje całej domeny ani /stara-oferta/podstrona. Jeśli migrujesz wiele adresów, identyfikatory typu ?p=123 lub zmieniasz politykę parametrów, przejdź przez mapę i testy poniżej. Nie rozszerzaj regexu na całą witrynę bez takiej mapy.

> **Co naprawdę przetestowaliśmy:** 7 września 2026 r.: pierwszy zestaw na natywnym Apache 2.4.66, curl 8.7.1 i Python 3.14.6 dał 41 PASS. Rozszerzenie mapy i HTML dało 47 PASS. Osobna integracja na WordPress 7.1 / Apache 2.4.68 w lokalnym Dockerze: 12 PASS i 2 kontrole rollbacku. Każda próba była lokalna, na danych syntetycznych. Nie testowano produkcji, PrestaShop, TLS, CDN, LiteSpeed ani całej migracji WordPressa. Warunki i wyniki poszczególnych prób są rozdzielone poniżej.

[Pobierz źródła i wyniki: Apache, mapa URL, HTML oraz test WordPress](https://webalpha.pl/downloads/blog/przekierowania-301-apache-lab.zip)

ZIP edukacyjny. Test WordPress wymaga osobnego, już przygotowanego lokalnego laboratorium WP. Pakiet zawiera także celowo błędne scenariusze bad-*. Nie wgrywaj go na hosting produkcyjny.

<a id="sekcja-1-ustal-gdzie-dziala-przekierowanie"></a>

## 1. Ustal, gdzie działa przekierowanie

Ten kod dotyczy Apache 2.4 i pliku .htaccess w katalogu głównym witryny, czyli DocumentRoot. Nginx nie odczytuje .htaccess. Zgodności konkretnej instalacji LiteSpeed nie zakładamy na podstawie testu Apache. Zanim zmienisz plik, potwierdź serwer i warstwę obsługującą dany URL w panelu hostingu lub u administratora. Sam nagłówek Server może pochodzić z proxy i nie rozstrzyga tej kwestii.

- Sprawdź: CDN/proxy → serwer WWW → reguły aplikacji/CMS-a. Każda warstwa może dodawać własne 301. Zmiana .htaccess nie usunie przekierowania ustawionego wcześniej w CDN.
- Potwierdź aktywny mod_rewrite albo mod_alias oraz uprawnienia AllowOverride/AllowOverrideList. Nie włączaj globalnie AllowOverride All tylko po to, aby przykład zaczął działać.
- Zidentyfikuj reguły HTTPS, www, końcowego ukośnika, front controllera i katalogi z własnym .htaccess. Kod dla DocumentRoot nie jest gotową konfiguracją witryny w podkatalogu /sklep/.
- Jeśli masz dostęp do konfiguracji serwera, rozważ umieszczenie reguł właśnie tam; kontekst wzorców jest wtedy inny i wymaga osobnego testu. Jeśli zarządzasz tylko plikami hostingu, potwierdź obsługę .htaccess z operatorem.

Apache opisuje kontekst i ograniczenia pliku w [dokumentacji .htaccess](https://httpd.apache.org/docs/2.4/howto/htaccess.html). W naszym laboratorium serwer dopuszczał tylko konkretne dyrektywy przekierowań, bez zmiany ustawień systemowego Apache.

<a id="sekcja-2-zbuduj-mape-url-zanim-napiszesz-regex"></a>

## 2. Zbuduj mapę URL, zanim napiszesz regex

Połącz adresy z istniejącej sitemap, crawla, stron docelowych w GSC i analityce oraz istotnych linków przychodzących. Przechowaj źródło informacji i decyzję właściciela treści. Brak kliknięć w krótkim okresie nie oznacza automatycznie, że URL można usunąć. Mapa ma wyjaśniać, który nowy zasób faktycznie zastępuje stary; podobieństwo nazwy sluga nie wystarcza.

**Przykładowa mapa użyta w lokalnym teście — nie adresy klientów ani plan migracji WebAlpha**

| Stary URL / wariant | Decyzja | Oczekiwana odpowiedź |
| --- | --- | --- |
| /stara-oferta i /stara-oferta/ | Ta sama oferta pod /oferta.html | 301 → /oferta.html → 200, jeden skok |
| /stara-oferta?utm_source=test | Zachować parametr pomiarowy | 301 → /oferta.html?utm_source=test |
| /index.php?p=123 | Konkretny identyfikator poradnika | 301 → /poradnik.html, bez p=123 |
| /stara-kampania?utm_source=test | W tym przykładzie świadomie usunąć stare parametry | 301 → /oferta.html, bez query string |
| /wycofane-bez-zamiennika | Usunięty zasób bez odpowiednika | 410, bez Location |
| /nie-istnieje | Nieznany adres | 404, bez Location |
| /oferta.html | Aktualny adres końcowy | 200, bez przekierowania |

Dla każdego wiersza dopisz warianty HTTP/HTTPS i www/bez www, jeśli są obsługiwane, politykę parametrów, odbiorcę treści oraz kryterium sukcesu. W pliku testowym zachowuj wielkość liter i kodowanie ścieżek. Część po #, np. #cennik, nie jest wysyłana do serwera i nie może stanowić warunku RewriteRule. Nazwę pliku z kropką trzeba potraktować jako dokładny tekst, a nie dowolny znak regex.

> **Nie kieruj wszystkich 404 na stronę główną:** Jeżeli nie ma trafnego zamiennika, pozostaw odpowiedni 404 lub 410. W naszym przykładzie 410 dotyczy jednego zatwierdzonego, trwale usuniętego zasobu. Nie jest metodą masowego pozbywania się problemów z indeksacją.

Zasady mapowania, unikania nietrafnych przekierowań i aktualizacji sygnałów opisuje [Google: migracja witryny ze zmianą URL](https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes).

<a id="sekcja-3-wybierz-301-308-albo-brak-przekierowania"></a>

## 3. Wybierz 301, 308 albo brak przekierowania

| Sytuacja | Wybór | Co sprawdzić |
| --- | --- | --- |
| Stała zmiana adresu dokumentu dostępnego przez GET | 301 lub 308 | Zgodność treści, finalne 200 i jeden skok |
| Stałe przeniesienie endpointu z zachowaniem metody i body | 308, po weryfikacji kontraktu API | Obsługę klienta, autoryzację i bezpieczeństwo ponowienia żądania |
| Zmiana tylko tymczasowa | Rozważ 302 lub 307, nie utrwalaj jej jako 301 | Czas obowiązywania oraz wymóg zachowania metody |
| Usunięcie bez trafnego odpowiednika | 404 lub 410 | Czy zasób naprawdę usunięto; brak catch-all na homepage |

Google traktuje zarówno 301, jak i 308 jako przekierowania stałe i sygnał wyboru adresu kanonicznego. To nie gwarancja pozycji, indeksacji ani zachowania konkretnej liczby kliknięć. Źródło: [Google Search Central: przekierowania i wyszukiwarka](https://developers.google.com/search/docs/crawling-indexing/301-redirects).

W eksperymencie curl po 301 zmienił POST na GET i nie przesłał body; po 308 zachował POST oraz syntetyczne body. Nie używaliśmy opcji -X POST ani --post301, które zmieniłyby warunki testu. HTTP dopuszcza zmianę POST na GET przy 301, natomiast 308 zachowuje metodę. Sprawdź [RFC 9110: 301](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.2) i [308](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.9). Nie testuj tego przez wysyłanie zamówień lub prawdziwych formularzy na produkcji.

<a id="sekcja-4-redirect-czy-rewriterule-roznica-ma-znaczenie"></a>

## 4. Redirect czy RewriteRule? Różnica ma znaczenie

Dla prostych map bez warunków query string wystarcza mod_alias. Uważaj jednak: Redirect operuje na prefiksie pełnych segmentów ścieżki. Reguła dla /alias-prefix obejmuje też /alias-prefix/child i dołącza końcówkę do celu. Nie obejmuje /alias-prefix-other. Jeśli chcesz pojedynczy adres, użyj zakotwiczonego RedirectMatch albo dokładnego RewriteRule.

Dwa osobne przykłady mod_alias, przetestowane po zastąpieniu stałej domeny originem lokalnego laboratorium.

```apache
# Prefiks: obejmuje także /alias-prefix/child.
Redirect 301 /alias-prefix https://sklep.example/nowy-prefix

# Dokładna ścieżka, z opcjonalnym końcowym ukośnikiem.
RedirectMatch 301 ^/alias-exact/?$ https://sklep.example/oferta.html
```

W obrębie tej samej konfiguracji mod_alias pierwsze pasujące przekierowanie ma pierwszeństwo, więc wyjątki zapisuj przed szerokimi regułami. Nie zakładaj jednak, że samo przeniesienie linii mod_alias nad mod_rewrite rozstrzygnie kolejność między modułami. Dla tej samej grupy adresów stosuj jeden mechanizm i sprawdź rzeczywistą odpowiedź. Źródło: [Apache mod_alias: kolejność, Redirect i RedirectMatch](https://httpd.apache.org/docs/2.4/mod/mod_alias.html).

<a id="sekcja-5-dokladna-mapa-w-mod-rewrite-i-obsluga-parametrow"></a>

## 5. Dokładna mapa w mod_rewrite i obsługa parametrów

Poniższy blok jest fragmentem reguł, nie zamiennikiem całego pliku .htaccess. Domena sklep.example jest celowo przykładowa. Wstaw własny, stały adres kanoniczny oraz zatwierdzoną mapę. Zachowaj istniejące zabezpieczenia i konfigurację hostingu. W typowym układzie z front controllerem własne przekierowania muszą zostać rozpatrzone przed wewnętrznym przepisaniem do aplikacji; ustal to w swojej konfiguracji i nie edytuj sekcji automatycznie nadpisywanej przez CMS.

Dyrektywy mapowania zgodne z przetestowaną fixture Apache. W laboratorium stały origin był HTTP na 127.0.0.1; produkcyjnego HTTPS/CDN tym testem nie objęto.

```apache
# Apache 2.4, .htaccess w DocumentRoot, stała domena bez końcowego /.
# Podstaw WŁASNĄ domenę i ZATWIERDZONĄ mapę. To nie cały plik CMS-a.
RewriteEngine On

RewriteCond %{QUERY_STRING} ^p=123$
RewriteRule ^index\.php$ https://sklep.example/poradnik.html [R=301,END,QSD]

RewriteRule ^stara-oferta/?$ https://sklep.example/oferta.html [R=301,END]
RewriteRule ^stary-poradnik\.html$ https://sklep.example/poradnik.html [R=301,END]
RewriteRule ^stare-zażółć$ https://sklep.example/poradnik.html [R=301,END]

RewriteRule ^stara-kampania$ https://sklep.example/oferta.html [R=301,END,QSD]
RewriteRule ^stary-filtr$ https://sklep.example/oferta.html?kolor=czarny [R=301,END]
RewriteRule ^stary-filtr-qsa$ https://sklep.example/oferta.html?kolor=czarny [R=301,END,QSA]

# Tylko ten konkretny zasób usunięto bez odpowiednika.
RewriteRule ^wycofane-bez-zamiennika$ - [G,END]
```

- W .htaccess wzorzec RewriteRule nie zaczyna się od /. Apache usuwa prefiks katalogu przed dopasowaniem. Cel może być pełnym URL-em. W teście błędne ^/stara-oferta$ nie pasowało i zwracało 404.
- ^ i $ ograniczają dopasowanie. /? dopuszcza tylko opcjonalny końcowy ukośnik. Bez tych ograniczeń łatwo objąć dodatkowe adresy. Nie dodawaj [NC] bez decyzji, czy wielkość liter rzeczywiście ma być ignorowana.
- Query string nie jest częścią wzorca RewriteRule. Dokładny warunek ^p=123$ pasuje tylko do takiej postaci parametrów. Wariant p=123&utm_source=test celowo NIE pasuje — w naszym statycznym teście zwracał 404. W prawdziwym CMS-ie może obsłużyć go aplikacja; jeśli ma migrować, dopisz jawną politykę i kolejne testy zamiast zakładać zgodność.
- R=301 wskazuje kod wprost. Samo R oznacza domyślnie 302. END kończy przetwarzanie mod_rewrite dla bieżącego żądania w kontekście katalogowym; nie zapobiega kolejnemu żądaniu klienta, więc nie naprawia pętli A → B → A.
- Cel jest stały: nie korzystamy z %{HTTP_HOST}, dowolnego parametru next ani backreference tworzącego domenę. Reguły nie mogą stać się otwartym przekierowaniem na obcy serwis.

**Query string: trzy różne decyzje, nie trzy zamienne flagi**

| Reguła | Zachowanie | Wynik dla parametrów testowych |
| --- | --- | --- |
| Cel bez ? i bez QSD | Kopiuje przychodzący query string | /stara-oferta?utm_source=test → /oferta.html?utm_source=test |
| Cel bez ? z QSD | Usuwa cały przychodzący query string | /stara-kampania?utm_source=test → /oferta.html |
| Cel z własnym ? bez QSA | Zastępuje przychodzący query string | /stary-filtr?utm_source=test → /oferta.html?kolor=czarny |
| Cel z własnym ? oraz QSA | Dołącza parametry wejściowe do docelowych | /stary-filtr-qsa?utm_source=test → /oferta.html?kolor=czarny&utm_source=test |

QSD usuwa także parametry mające znaczenie dla aplikacji lub analityki: nie dodawaj go globalnie. QSA może z kolei utworzyć powtarzające się klucze, których interpretacja zależy od aplikacji. Testuj osobno parametry kampanii, identyfikatory treści i filtry. Nie przenoś tokenów logowania do nowego publicznego URL-a. W przykładzie z polskimi znakami żądanie zawierało kodowanie procentowe, a wzorzec dopasował zdekodowaną ścieżkę; dowolne backreference i złożone kodowanie wymagają odrębnych testów.

Podstawy: [Apache mod_rewrite: kontekst katalogowy i zmienne](https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html) oraz [flagi R, END, QSD i QSA](https://httpd.apache.org/docs/2.4/rewrite/flags.html).

<a id="sekcja-6-sprawdz-http-sam-koncowy-status-200-nie-wystarczy"></a>

## 6. Sprawdź HTTP: sam końcowy status 200 nie wystarczy

Odczyt GET, bez formularzy i bez -k. Najpierw obejrzyj Location. W drugim kroku oczekujesz status=200, redirects=1 i dokładnego adresu z mapy.

```bash
# Terminal na Twoim komputerze. Podstaw własny publiczny URL HTTPS.
redirect_url='https://sklep.example/stara-oferta?utm_source=test'

# 1. Rzeczywisty GET: status i Location pierwszej odpowiedzi, bez podążania.
curl --silent --show-error --globoff --connect-timeout 5 --max-time 15 \
  --proto '=https' --dump-header - --output /dev/null "$redirect_url"

# 2. Dopiero po sprawdzeniu domeny w Location: docelowy status i liczba skoków.
curl --silent --show-error --globoff --location --max-redirs 5 \
  --connect-timeout 5 --max-time 20 \
  --proto '=https' --proto-redir '=https' --output /dev/null \
  --write-out 'status=%{http_code}\nredirects=%{num_redirects}\nurl=%{url_effective}\n' \
  "$redirect_url"
```

Nie zastępuj pierwszego testu samym curl -I: to metoda HEAD, której obsługa może różnić się od GET. Nie dodajemy --fail, bo 404 i 410 są poprawnymi oczekiwanymi wynikami niektórych wierszy mapy. Dla pętli curl z ograniczeniem liczby przekierowań zakończy się błędem; w naszym teście było to wyjście 47 po trzech skokach. Nie usuwaj limitu, aby wymusić zielony wynik.

Przy większej mapie użyj check-map.py z paczki. Dla każdego źródła sprawdza oczekiwany status i dokładny Location, a potem pobiera osobno zatwierdzony cel. Jeśli cel również przekierowuje, test nie przechodzi. Skrypt nie podąża za niesprawdzonym adresem z nagłówka Location i nie wysyła danych autoryzacji. Wersja z paczki dotyczy mapy w obrębie jednego originu, nie migracji między domenami.

Zapisz własny plik moja-mapa.json. Zostaw zarówno przykłady pozytywne, jak i adresy, które nie powinny się przekierowywać.

```json
[
  {"source":"/stara-oferta","status":301,"target":"/oferta.html"},
  {"source":"/stara-oferta?utm_source=test","status":301,"target":"/oferta.html?utm_source=test"},
  {"source":"/oferta.html","status":200},
  {"source":"/stara-oferta-dodatkowa","status":404}
]
```

W katalogu rozpakowanej paczki; domena bez końcowego ukośnika. Podstaw własną domenę oraz własną mapę. Kod wyjścia 0: wszystkie pozycje PASS; 1: wykryty problem.

```bash
python3 check-map.py --origin 'https://sklep.example' --map moja-mapa.json --delay 1
```

<a id="sekcja-najpierw-dry-run-wykryj-konflikt-mapy-bez-http"></a>

### Najpierw dry-run: wykryj konflikt mapy bez HTTP

To polecenie nie wysyła żądań i nie zmienia konfiguracji. Oczekujesz result=PASS oraz httpRequests=0. Tę kontrolę wykonaj przed wdrożeniem reguł.

```bash
python3 check-map.py --origin 'https://sklep.example' --map moja-mapa.json --dry-run
```

Dry-run odrzuca brak celu, powtórzone źródła (również identyczne), przekierowanie na siebie, cykl A → B → A, łańcuch do kolejnego przekierowania oraz cel, który ta sama mapa opisuje jako 404/410. Walidacja działa też przed każdym testem HTTP. Dokładne warianty query string są oddzielnymi wierszami; automat nie zgaduje równoważności parametrów, kodowania, wielkości liter lub treści. Cel niewystępujący jako źródło w mapie wymaga późniejszego GET — samo poprawne parsowanie JSON nie potwierdza jego istnienia.

> **Dwie różne kontrole:** check-map.py bada strukturę mapy i HTTP. Oddzielny check-destinations.py z sekcji 9 bada zatwierdzone strony HTML. Żaden nie zastępuje oceny sensowności przekierowania, robots.txt, renderowania JavaScript ani indeksu Google. Checker mapy wykonuje maksymalnie dwa GET na wiersz i czeka między wierszami: domyślnie 0,25 s, powyżej 1 s. Wymaga Python 3 i curl. Bezpośrednie połączenie bez proxy musi być zgodne z polityką Twojej sieci. Testuj wyłącznie własne lub powierzone witryny.

<a id="sekcja-7-odtworz-nasze-testy-w-tym-celowo-bledne-reguly"></a>

## 7. Odtwórz nasze testy, w tym celowo błędne reguły

Rozpakuj pobrany ZIP do nowego katalogu na własnym komputerze, poza katalogiem jakiejkolwiek strony. Przejrzyj README i kod. Na macOS z natywnym /usr/sbin/httpd, dostępnymi modułami Apache, Pythonem 3 i curl uruchom poniższe polecenie jako zwykły użytkownik. Nie używaj sudo. Skrypt tworzy własny katalog tymczasowy, uruchamia odseparowany Apache na losowym wysokim porcie 127.0.0.1 i po teście zatrzymuje wyłącznie własny proces.

Terminal otwarty w folderze przekierowania-301-apache-lab po rozpakowaniu ZIP. Brak instalacji pakietów i brak Dockera.

```bash
python3 run-lab.py
```

Osobno uruchom 47 kontroli mapy i HTML. Ten zestaw używa Python 3 oraz własnego serwera na 127.0.0.1; nie wymaga Apache ani Dockera. Oczekujesz 47 PASS, 0 FAIL.

```bash
python3 test-seo-checks.py
```

**Wybrane obserwacje z faktycznego przebiegu; komplet 41 testów w results.json**

| Test | Zaobserwowano | Znaczenie |
| --- | --- | --- |
| Dokładna mapa + parametry | 19 pozytywnych i negatywnych wierszy PASS | Ukośniki, litery, kodowanie, query, 200/301/404/410 |
| Stary URL → właściwy cel | 200, num_redirects=1 | Brak dodatkowego skoku |
| Podmieniony Host i parametr next | Stały lokalny origin w Location | Te konkretne reguły nie przekazały sterowania na obcy host |
| POST z body synthetic=alpha | 301: GET i puste body; 308: POST i zachowane body | Zachowanie curl 8.7.1 w opisanym trybie |
| Szeroka reguła przed wyjątkiem | Przekierowanie do nieprawidłowej oferty | Odtworzony błąd kolejności, nie dobry wynik biznesowy |
| A → B → oferta | 200, num_redirects=2; checker odrzucił mapę | Samo finalne 200 nie wykrywa łańcucha |
| A → B → A, również z END | curl exit=47 po 3 skokach | Pętla zewnętrzna została wykryta i zatrzymana limitem |
| Błędna dyrektywa w .htaccess | httpd -t: sukces; rzeczywisty GET: 500 | Test konfiguracji głównej nie wystarczył |
| Przywrócenie poprzedniego .htaccess | Stary URL ponownie 404; aktualna oferta nadal 200 | Rollback przywrócił baseline tego laboratorium |

PASS przy scenariuszu bad-* oznacza, że test poprawnie odtworzył i wykrył oczekiwany błąd. Nie oznacza dopuszczenia tej konfiguracji do publikacji. Nie uruchamiaj całej paczki na serwerze swojej strony. Laboratoryjny HTTP nie zastępuje testu certyfikatu, CDN ani wariantów produkcyjnej domeny.

<a id="sekcja-osobny-test-regula-przed-blokiem-wordpressa"></a>

### Osobny test: reguła przed blokiem WordPressa

Na WordPress 7.1 z Apache 2.4.68 wykonaliśmy dodatkową próbę przez lokalną bramę Nginx. Utworzyliśmy jedną własną stronę testową i sprawdziliśmy jej treść oraz canonical. Reguła umieszczona przed istniejącym front controllerem WordPressa zwracała 301 → właściwa strona 200 dla starego URL-a, końcowego ukośnika i parametru UTM. Sąsiedni, nieobjęty wzorcem adres pozostał 404. Ta sama reguła dopisana za blokiem CMS-a nie obsłużyła starego URL-a — otrzymaliśmy 404. Po poprawieniu kolejności wróciło 301.

Wynik: 12 kontroli integracji oraz 2 kontrole wycofania. Przywrócono dokładną zawartość wcześniejszego .htaccess i usunięto wyłącznie własną stronę testową; zbiór wcześniejszych identyfikatorów stron pozostał bez zmian. To dowód jednego scenariusza kolejności z działającym CMS-em, nie gwarancja zachowania każdej wtyczki przekierowań, konfiguracji multisite lub proxy.

Aby odtworzyć opcjonalny test, najpierw przygotuj i ukończ izolowane laboratorium source/restore z [poradnika kopii i odtwarzania WordPress](https://webalpha.pl/blog/kopia-zapasowa-wordpress-przywracanie). Następnie użyj test-wordpress.mjs z tego ZIP-a, przekazując ścieżkę do folderu z compose.yaml. Szczegóły i warunki wyłącznego dostępu opisuje README. Nie przekazuj ścieżki swojego projektu produkcyjnego.

<a id="sekcja-8-wdrozenie-i-rollback-przygotuj-je-przed-zmiana"></a>

## 8. Wdrożenie i rollback: przygotuj je przed zmianą

1. Zapisz aktualny .htaccess i inne zmieniane ustawienia w wersjonowanej kopii poza publicznym katalogiem strony. Jeśli pracujesz przez SFTP, pobierz plik lokalnie i sprawdź jego zawartość. Nie przechowuj kopii .htaccess.bak ani plików konfiguracyjnych do pobrania z WWW.
2. Zachowaj wyniki bazowe: źródła i cele z mapy, działanie strony głównej, formularzy oraz kluczowych ścieżek aplikacji. Dla funkcji zapisujących dane stosuj testowe środowisko i dane; nie odtwarzaj prawdziwych zamówień.
3. Przygotuj poprawkę jako mały diff istniejącego pliku. Nie usuwaj przy okazji reguł zabezpieczeń, obsługi aplikacji, cache ani konfiguracji operatora. Ustal też, czy CMS nie nadpisze Twojego bloku.
4. Odtwórz reguły i pełną mapę na stagingu odpowiadającym konfiguracji produkcyjnej. Sprawdź GET oraz log błędów. Jeżeli zmieniasz konfigurację główną serwera, sprawdź jej składnię zgodnie z procedurą operatora; test .htaccess nadal wymaga żądania HTTP.
5. Wgraj uzgodnioną zmianę w oknie z dostępnym rollbackiem. Sposób bezpiecznej podmiany pliku ustal z hostingiem. Konfiguracja .htaccess jest odczytywana przy żądaniach, więc jej edycja nie jest powodem do globalnego restartu Apache.
6. Natychmiast wykonaj własną mapę i testy stron, które miały pozostać bez zmian. Sprawdź HTTP/HTTPS, www/bez www, slash, parametry i kodowanie. Wyeliminuj łańcuchy także między CDN, serwerem i aplikacją: stary adres powinien kierować od razu do docelowej kanonicznej wersji.
7. Przy 500, pętli, obcym Location, błędnym celu lub uszkodzeniu aplikacji przywróć zatwierdzoną kopię oraz inne zmienione reguły w tej samej warstwie. Powtórz baseline i testy funkcjonalne. Nie rozwiązuj błędu przez wyłączenie zabezpieczeń ani przekierowanie całej domeny na homepage.

301 i 308 mogą być przechowywane w cache. Przywrócenie pliku na serwerze nie cofa natychmiast odpowiedzi zapisanej wcześniej w przeglądarce lub CDN. Dlatego po rollbacku porównaj świeży odczyt curl z przeglądarką i w razie potrzeby usuń właściwe wpisy cache zgodnie z instrukcją operatora. Nie twórz odwrotnego przekierowania B → A w ciemno, bo klient z zapamiętanym A → B może wejść w pętlę. Podstawa: [RFC 9110: statusy i cache](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.1).

<a id="sekcja-9-po-301-sprawdz-canonical-linki-i-google-search-console"></a>

## 9. Po 301 sprawdź canonical, linki i Google Search Console

Nie oceniaj nowego adresu wyłącznie po HTTP 200. W pakiecie dodaliśmy check-destinations.py: odczytuje dokładnie wskazane strony, sprawdza canonical w head, meta robots/googlebot i nagłówki X-Robots-Tag, tytuł, H1 oraz zatwierdzone frazy treści. To własny profil odbioru statycznego HTML, nie implementacja algorytmu Google ani automatyczna ocena jakości. Dobierz wymagane fragmenty tak, aby odróżniały właściwą ofertę od homepage lub komunikatu błędu.

Zapisz cele-seo.json i zastąp wszystkie przykłady treścią swojej strony. Canonical jest celowy: URL z UTM może wskazywać wersję bez parametrów. Nie wyliczamy go automatycznie ze starego adresu.

```json
[
  {
    "path": "/oferta.html",
    "canonical": "https://sklep.example/oferta.html",
    "titleContains": "Integracja magazynu",
    "h1Contains": "Integracja magazynu",
    "requiredText": ["Synchronizacja stanów", "Obsługiwane systemy"]
  }
]
```

Odczyt własnych stron: jeden GET na cel, bez podążania za przekierowaniami, bez logowania i z sekundową przerwą. Kod 0 oznacza zgodność tych kryteriów; 1 oznacza wykryty problem, nie wynik SEO.

```bash
python3 check-destinations.py --origin 'https://sklep.example' --manifest cele-seo.json
```

Ten ścisły wariant wymaga jednego absolutnego canonicala w HTML head w tym samym originie, jednego H1 i jawnie wskazanej treści. Odrzuca noindex/none dla Googlebota oraz łańcuch. HTTP Link canonical, element base i unavailable_after kieruje do osobnego przeglądu zamiast zgadywać wynik. Czyta do 1 MiB HTML; większa odpowiedź nie otrzyma częściowego PASS. Nie renderuje JS, nie ocenia widoczności CSS i nie interpretuje robots.txt. Zgodność kilku fraz nie dowodzi równoważności całej treści — tę decyzję nadal zatwierdza właściciel mapy.

Kryteria odbioru opieramy na dokumentacji [Google: canonical](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls) i [meta robots oraz X-Robots-Tag](https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag). Zestaw 47 testów obejmuje również celowo zły canonical, blokadę indeksacji w nagłówku, duplikat H1, treść obecną tylko w skrypcie, limit rozmiaru odpowiedzi i brak podążania do obcej domeny.

- Nowy adres zwraca 200 i odpowiednią treść. Jeśli ma wejść do indeksu, nie jest blokowany przez przypadkowe noindex ani robots.txt. Canonical wskazuje nowy właściwy adres, a nie starą wersję.
- Linki wewnętrzne oraz hreflang, jeśli występuje, prowadzą bezpośrednio do nowych adresów. Nie zostawiaj nawigacji i kart produktów opartych na stałych 301.
- Sitemap zawiera aktualne adresy kanoniczne, a nie przekierowania i błędy. Prześlij aktualną mapę w GSC. Przy większej migracji zachowaj listę starych adresów do monitoringu poza publiczną sitemap.
- W Inspekcji URL sprawdź reprezentatywne źródła i cele. Test aktualnego URL-a mówi o pobraniu w danym momencie; raport indeksu i wybrany przez Google canonical mogą odzwierciedlać wcześniejszy crawl. Sam 301 nie potwierdza zakończenia migracji.
- Monitoruj błędy przekierowań, 404/5xx i logi po wdrożeniu; porównuj widoczność starego i nowego zestawu adresów, a nie tylko pojedyncze strony. Datę wdrożenia zaznacz w swojej dokumentacji pomiaru. Reaguj od razu na błąd techniczny, ale nie zmieniaj mapy codziennie na podstawie pojedynczego dnia ruchu.

Google zaleca utrzymywać przekierowania możliwie długo, zwykle co najmniej rok; dla użytkowników starych linków mogą być potrzebne bezterminowo. Jednocześnie warto aktualizować własne linki oraz ważne odsyłacze zewnętrzne, aby nie opierały się na dodatkowym skoku. Źródło: [Google: wdrożenie i monitoring migracji URL](https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes).

Jeżeli po migracji adres końcowy działa, ale ma błędny canonical lub noindex, kolejny krok to diagnostyka HTML. Pokazujemy ją na przykładzie sklepu w [poradniku technicznego SEO PrestaShop](https://webalpha.pl/blog/seo-prestashop-poradnik). Przy zmianie struktury całej witryny zakres mapy i odbioru możesz uzgodnić w ramach [wdrożenia technicznego SEO](https://webalpha.pl/seo/techniczne).

## Zmieniają się adresy Twojej strony lub sklepu?

Prześlij starą i planowaną strukturę URL oraz informację o hostingu i CMS-ie. Ustalimy zakres mapy przekierowań, testów oraz monitoringu po migracji. Sam kod 301 nie zastępuje sprawdzenia treści i indeksacji.

[Sprawdź zakres technicznego SEO](https://webalpha.pl/seo/techniczne)

## Najczęstsze pytania (FAQ)

### Dlaczego przekierowanie 301 w .htaccess nie działa?

Najpierw ustal, czy żądanie dociera do Apache i właściwego katalogu. Sprawdź obsługę .htaccess, moduł oraz dozwolone dyrektywy, brak początkowego / we wzorcu RewriteRule w kontekście katalogowym i warunki query string. Porównaj pierwszy GET z regułami CDN oraz aplikacji i zajrzyj do logu błędów. Nie włączaj globalnie AllowOverride All jako uniwersalnej naprawy.

### Czy 301 usuwa parametry UTM?

Nie automatycznie. W mod_rewrite cel bez własnego query string domyślnie przejmuje parametry wejściowe. QSD je usuwa, własne ? w celu je zastępuje, a QSA może je dołączyć. Dobierz zachowanie do mapy i analityki; nie dodawaj QSD do wszystkich reguł.

### Czy lepiej użyć 301 czy 308?

Dla Google oba są przekierowaniami stałymi. Ważną różnicą jest metoda HTTP: 301 dopuszcza zmianę POST na GET, natomiast 308 zachowuje metodę. Endpointy formularzy i API wymagają testu kontraktu klienta oraz body; nie wybieraj kodu tylko ze względu na SEO.

### Czy httpd -t wystarczy przed zmianą .htaccess?

Nie. W naszym rzeczywistym teście konfiguracja główna przeszła httpd -t, a nieprawidłowa dyrektywa w .htaccess spowodowała 500 przy GET. Sprawdź odpowiedź HTTP dla mapy i log błędów po wczytaniu pliku.

### Jak długo zostawić przekierowania po migracji?

Google zaleca utrzymać je możliwie długo, zwykle co najmniej rok. Jeżeli stare adresy nadal mają użytkowników lub wartościowe odsyłacze, mogą być potrzebne dłużej. Aktualizuj przy tym własne linki i sitemap, aby prowadziły bezpośrednio do nowych URL-i.

### Czy laboratorium potwierdza działanie na WordPress, PrestaShop i LiteSpeed?

Osobno przetestowaliśmy jedną integrację na WordPress 7.1 i Apache 2.4.68: kolejność reguły przed front controllerem, trzy warianty URL-a, finalną treść i canonical oraz rollback. Pierwsze 41 testów dotyczy natywnego Apache ze statycznymi stronami. Nie testowaliśmy PrestaShop, LiteSpeed, TLS, CDN ani kompletnej migracji CMS-a; wymagają odbioru na stagingu odpowiadającym Twojej konfiguracji.
