[{"data":1,"prerenderedAt":8991},["ShallowReactive",2],{"wpisy":3},[4,2215,5070,8327],{"id":5,"title":6,"body":7,"date":2201,"description":2202,"extension":2203,"kategoria":2204,"meta":2205,"navigation":248,"obrazek":2206,"obrazekAlt":2207,"path":2208,"seo":2209,"sitemap":2210,"stem":2211,"szkic":2212,"zaktualizowano":2213,"__hash__":2214},"wpisy/pasek-zgod-na-wlasnym-kodzie.md","Pasek zgód na własnym kodzie — dlaczego liczy się moment, a nie kilobajty",{"type":8,"value":9,"toc":2184},"minimark",[10,19,22,25,30,33,39,45,49,61,72,75,81,84,87,91,106,121,124,127,134,138,141,162,169,172,176,367,380,390,393,506,509,513,677,691,698,740,754,761,765,768,1177,1183,1188,1300,1307,1310,1314,1317,1328,1333,1336,1339,1342,1387,1393,1399,1403,1409,1412,1415,1519,1526,1529,1533,1536,1542,1548,1555,1562,1569,1576,1579,1582,1586,1589,1613,1616,1745,1752,1775,1778,1956,1963,1984,1987,1997,2120,2130,2133,2137,2140,2146,2152,2158,2164,2167,2171,2174,2177,2180],[11,12,13,14,18],"p",{},"Skrypt wejściowy popularnej platformy zgód waży 121 kB, po gzipie 28,3 kB, i jest tylko loaderem — konfigurację domeny oraz samo okno dialogowe dociąga osobno. Logika zgód, którą napisałem zamiast niego, ma 1922 bajty po minifikacji i ",[15,16,17],"strong",{},"900 bajtów po gzipie",", wchodzi we wspólny bundle i nie dokłada żadnego żądania.",[11,20,21],{},"To porównanie obejmuje po obu stronach samą logikę. Moje 900 bajtów nie zawiera komponentu rysującego pasek, a te 28 kB nie zawiera okna dialogowego platformy. Komponent u mnie to 2,8 kB źródła Vue przed kompresją, które w bundlu i tak dzieli style z resztą serwisu.",[11,23,24],{},"Różnica w kilobajtach jest tu jednak najmniej ciekawa. Skrypt zgód z definicji musi wystartować przed tagami, które ma blokować, więc ląduje w tym miejscu ładowania strony, gdzie każde żądanie konkuruje z treścią. Ten efekt zmierzyłem — choć na innych skryptach niż platforma zgód.",[26,27,29],"h2",{"id":28},"co-jest-pomiarem-a-co-wnioskiem","Co jest pomiarem, a co wnioskiem",[11,31,32],{},"Chcę to powiedzieć na wstępie, żeby nikt nie musiał się domyślać.",[11,34,35,38],{},[15,36,37],{},"Zmierzone:"," rozmiar loadera platformy zgód, rozmiar mojego kodu, oraz koszt dwóch innych skryptów zewnętrznych w oknie krytycznym mojej strony głównej.",[11,40,41,44],{},[15,42,43],{},"Wywnioskowane przez analogię:"," że gotowa platforma zgód kosztuje podobnie. Nie wstawiłem jej na swoją stronę i nie zmierzyłem. Wniosek opieram na tym, że jest większa od tamtych dwóch skryptów i z konieczności siedzi wcześniej. Traktuj to jako hipotezę popartą sąsiednim pomiarem.",[26,46,48],{"id":47},"dziesięć-kilobajtów-które-kosztowały-450-ms","Dziesięć kilobajtów, które kosztowały 450 ms",[11,50,51,52,56,57,60],{},"Na targetx.pl Cloudflare wstrzykiwał własny beacon do statystyk — ",[53,54,55],"code",{},"beacon.min.js",", ",[15,58,59],{},"10 kB"," przy 332 kB całego skryptu strony. Przy tych proporcjach wyglądał na zaokrąglenie.",[11,62,63,64,67,68,71],{},"Przez większość dnia optymalizacji ",[15,65,66],{},"FCP stało nieruchomo na 2701 ms",", powtarzalne co do milisekundy w dwunastu kolejnych przebiegach PageSpeed Insights. Nie drgnęło po zdjęciu ponad 300 kB skryptu. Nie drgnęło po ucięciu 87 kB HTML-a. Ruszyło dopiero po wyłączeniu beacona: ",[15,69,70],{},"2701 → 2251 ms",".",[11,73,74],{},"Tyle pomiaru. Teraz interpretacja, bo mechanizmu nie udowodniłem.",[11,76,77,80],{},[53,78,79],{},"\u003Cscript type=\"module\">"," jest domyślnie odroczony i renderu nie blokuje, więc te 450 ms nie biorą się z blokowania parsera. Najbardziej prawdopodobne wyjaśnienie to koszt otwarcia połączenia do trzeciej domeny — rozwiązanie DNS, uzgodnienie TLS i transfer — w oknie, w którym przeglądarka zajmuje się obrazkiem hero. Lighthouse symuluje wolne łącze, więc dodatkowy handshake waży tam więcej niż na moim biurku. Pewności nie mam; mam za to powtarzalny wynik i moment, w którym się zmienił.",[11,82,83],{},"Jedno zastrzeżenie do samej powtarzalności. PageSpeed Insights liczy FCP modelem Lantern, na podstawie grafu żądań, a nie z prawdziwego przebiegu na łączu. Dwanaście identycznych wyników to więc cecha symulacji, nie dowód stabilności w terenie. Model wzmacnia moją interpretację, bo dolicza koszt dodatkowego połączenia — i jednocześnie znaczy, że te 450 ms jest wynikiem laboratoryjnym. Dane z prawdziwych przeglądarek mogą pokazać mniej albo więcej.",[11,85,86],{},"Wniosek praktyczny zostaje ten sam niezależnie od mechanizmu: przy skryptach ładowanych wcześnie patrz na pozycję w kolejce, nie na rozmiar pliku.",[26,88,90],{"id":89},"druga-historia-poprawna-konfiguracja-która-nic-nie-robiła","Druga historia: poprawna konfiguracja, która nic nie robiła",[11,92,93,94,97,98,101,102,105],{},"Na tej samej stronie miałem włączoną funkcję podającą kontener Tag Managera z pierwszej domeny, spod ścieżki w rodzaju ",[53,95,96],{},"mojadomena.pl/zig4/",". Kosztowała około 300 kB, ale gorsze było co innego: wstawka ",[53,99,100],{},"google_tags_first_party"," siedziała na ",[15,103,104],{},"bajcie 151"," dokumentu HTML.",[11,107,108,109,112,113,116,117,120],{},"Kontener startował więc przed hydracją aplikacji, a mój kod ustawiający domyślne odmowy zgód trafiał do ",[53,110,111],{},"dataLayer"," dopiero po zdarzeniach ",[53,114,115],{},"gtm.dom"," i ",[53,118,119],{},"gtm.load",". Consent Mode wymaga odwrotnej kolejności — domyślne ustawienia muszą być na miejscu przed startem kontenera, bo inaczej tagi czytają stan, którego jeszcze nie ustawiłeś.",[11,122,123],{},"W panelu wszystko wyglądało poprawnie. W przeglądarce mechanizm zgód był pozorny.",[11,125,126],{},"Po wyłączeniu tej funkcji wynik ogólny poszedł w górę: Performance 70 → 81–85, LCP 5,6 s → 3,35 s. FCP, jak wyżej, nie zareagowało wcale — stąd dwie osobne historie zamiast jednej.",[11,128,129,130,133],{},"Dla porządku co do liczb: 332 kB skryptu to stan ",[15,131,132],{},"po"," wyłączeniu tej funkcji. Beacon mierzyłem już w tym stanie.",[26,135,137],{"id":136},"co-taki-pasek-musi-umieć","Co taki pasek musi umieć",[11,139,140],{},"Zanim napiszesz własny, warto wiedzieć, na co się piszesz. Od strony technicznej:",[142,143,144,148,151,156,159],"ol",{},[145,146,147],"li",{},"Ustawić domyślne odmowy w Consent Mode v2, zanim wystartuje kontener.",[145,149,150],{},"Pokazać wybór, gdy decyzji jeszcze nie ma.",[145,152,153,154,71],{},"Zapamiętać decyzję i przekazać ją do ",[53,155,111],{},[145,157,158],{},"Pozwolić ją wycofać, ze skutkiem natychmiastowym.",[145,160,161],{},"Nie pokazywać się nikomu, kto już zdecydował.",[11,163,164,165,168],{},"Od strony prawnej dochodzą dwa wymogi dotyczące samego interfejsu, o których łatwo zapomnieć przy pisaniu własnego. Przycisk odmowy musi być ",[15,166,167],{},"równorzędny"," z przyciskiem zgody — ten sam poziom, podobna widoczność, bez chowania odmowy w ustawieniach szczegółowych. I żadna zgoda nie może być domyślnie zaznaczona; brak decyzji znaczy odmowę.",[11,170,171],{},"Pasek z jednym przyciskiem „OK\" łamie pierwszy warunek wprost, a drugi omija, bo nie daje żadnego wyboru. Widuje się go wciąż często.",[26,173,175],{"id":174},"domyślne-odmowy","Domyślne odmowy",[177,178,183],"pre",{"className":179,"code":180,"language":181,"meta":182,"style":182},"language-ts shiki shiki-themes one-dark-pro","const gtag: Gtag = function () {\n  warstwa().push(arguments);\n};\n\nexport function ustawDomyslneZgody(): void {\n  gtag('consent', 'default', {\n    ad_storage: 'denied',\n    ad_user_data: 'denied',\n    ad_personalization: 'denied',\n    analytics_storage: 'denied',\n    wait_for_update: 500,\n  });\n}\n","ts","",[53,184,185,216,237,243,250,270,290,305,317,329,341,355,361],{"__ignoreMap":182},[186,187,190,194,198,202,206,210,213],"span",{"class":188,"line":189},"line",1,[186,191,193],{"class":192},"seHd6","const",[186,195,197],{"class":196},"sVbv2"," gtag",[186,199,201],{"class":200},"sn6KH",": ",[186,203,205],{"class":204},"sU0A5","Gtag",[186,207,209],{"class":208},"sjrmR"," =",[186,211,212],{"class":192}," function",[186,214,215],{"class":200}," () {\n",[186,217,219,222,225,228,231,234],{"class":188,"line":218},2,[186,220,221],{"class":196},"  warstwa",[186,223,224],{"class":200},"().",[186,226,227],{"class":196},"push",[186,229,230],{"class":200},"(",[186,232,233],{"class":204},"arguments",[186,235,236],{"class":200},");\n",[186,238,240],{"class":188,"line":239},3,[186,241,242],{"class":200},"};\n",[186,244,246],{"class":188,"line":245},4,[186,247,249],{"emptyLinePlaceholder":248},true,"\n",[186,251,253,256,258,261,264,267],{"class":188,"line":252},5,[186,254,255],{"class":192},"export",[186,257,212],{"class":192},[186,259,260],{"class":196}," ustawDomyslneZgody",[186,262,263],{"class":200},"(): ",[186,265,266],{"class":204},"void",[186,268,269],{"class":200}," {\n",[186,271,273,276,278,282,284,287],{"class":188,"line":272},6,[186,274,275],{"class":196},"  gtag",[186,277,230],{"class":200},[186,279,281],{"class":280},"subq3","'consent'",[186,283,56],{"class":200},[186,285,286],{"class":280},"'default'",[186,288,289],{"class":200},", {\n",[186,291,293,297,299,302],{"class":188,"line":292},7,[186,294,296],{"class":295},"sVyAn","    ad_storage",[186,298,201],{"class":200},[186,300,301],{"class":280},"'denied'",[186,303,304],{"class":200},",\n",[186,306,308,311,313,315],{"class":188,"line":307},8,[186,309,310],{"class":295},"    ad_user_data",[186,312,201],{"class":200},[186,314,301],{"class":280},[186,316,304],{"class":200},[186,318,320,323,325,327],{"class":188,"line":319},9,[186,321,322],{"class":295},"    ad_personalization",[186,324,201],{"class":200},[186,326,301],{"class":280},[186,328,304],{"class":200},[186,330,332,335,337,339],{"class":188,"line":331},10,[186,333,334],{"class":295},"    analytics_storage",[186,336,201],{"class":200},[186,338,301],{"class":280},[186,340,304],{"class":200},[186,342,344,347,349,353],{"class":188,"line":343},11,[186,345,346],{"class":295},"    wait_for_update",[186,348,201],{"class":200},[186,350,352],{"class":351},"sVC51","500",[186,354,304],{"class":200},[186,356,358],{"class":188,"line":357},12,[186,359,360],{"class":200},"  });\n",[186,362,364],{"class":188,"line":363},13,[186,365,366],{"class":200},"}\n",[11,368,369,370,373,374,376,377,379],{},"Funkcja ",[53,371,372],{},"gtag"," wypycha do ",[53,375,111],{}," obiekt ",[53,378,233],{},", a nie zwykłą tablicę. Tak wygląda oryginalna wtyczka Google'a i tak trzeba to zrobić, bo kontener rozpoznaje polecenia zgód po tym kształcie.",[11,381,382,385,386,389],{},[53,383,384],{},"wait_for_update: 500"," każe tagom wstrzymać się pół sekundy na wypadek, gdyby zaraz przyszła aktualizacja odczytana z ciasteczka. W mojej architekturze jest to asekuracja, a nie mechanizm, na którym polegam: kontener startuje dopiero po zdarzeniu ",[53,387,388],{},"load",", więc odczyt z ciasteczka i tak trafia do kolejki wcześniej. Zostawiam to pole na wypadek, gdyby ktoś przestawił moment startu.",[11,391,392],{},"Aktualizacja po decyzji podnosi wyłącznie jedną flagę:",[177,394,396],{"className":179,"code":395,"language":181,"meta":182,"style":182},"export function zaktualizujZgode(wybor: WyborZgody): void {\n  gtag('consent', 'update', {\n    ad_storage: 'denied',\n    ad_user_data: 'denied',\n    ad_personalization: 'denied',\n    analytics_storage: wybor === 'udzielona' ? 'granted' : 'denied',\n  });\n}\n",[53,397,398,425,440,450,460,470,498,502],{"__ignoreMap":182},[186,399,400,402,404,407,409,413,415,418,421,423],{"class":188,"line":189},[186,401,255],{"class":192},[186,403,212],{"class":192},[186,405,406],{"class":196}," zaktualizujZgode",[186,408,230],{"class":200},[186,410,412],{"class":411},"s_ZVi","wybor",[186,414,201],{"class":200},[186,416,417],{"class":204},"WyborZgody",[186,419,420],{"class":200},"): ",[186,422,266],{"class":204},[186,424,269],{"class":200},[186,426,427,429,431,433,435,438],{"class":188,"line":218},[186,428,275],{"class":196},[186,430,230],{"class":200},[186,432,281],{"class":280},[186,434,56],{"class":200},[186,436,437],{"class":280},"'update'",[186,439,289],{"class":200},[186,441,442,444,446,448],{"class":188,"line":239},[186,443,296],{"class":295},[186,445,201],{"class":200},[186,447,301],{"class":280},[186,449,304],{"class":200},[186,451,452,454,456,458],{"class":188,"line":245},[186,453,310],{"class":295},[186,455,201],{"class":200},[186,457,301],{"class":280},[186,459,304],{"class":200},[186,461,462,464,466,468],{"class":188,"line":252},[186,463,322],{"class":295},[186,465,201],{"class":200},[186,467,301],{"class":280},[186,469,304],{"class":200},[186,471,472,474,476,478,481,484,487,490,493,496],{"class":188,"line":272},[186,473,334],{"class":295},[186,475,201],{"class":200},[186,477,412],{"class":295},[186,479,480],{"class":208}," ===",[186,482,483],{"class":280}," 'udzielona'",[186,485,486],{"class":192}," ?",[186,488,489],{"class":280}," 'granted'",[186,491,492],{"class":192}," :",[186,494,495],{"class":280}," 'denied'",[186,497,304],{"class":200},[186,499,500],{"class":188,"line":292},[186,501,360],{"class":200},[186,503,504],{"class":188,"line":307},[186,505,366],{"class":200},[11,507,508],{},"Trzy pozostałe zostają odrzucone niezależnie od wyboru, bo na tych serwisach nie ma reklam. Gdy są, dwa przyciski przestają wystarczać: potrzebujesz osobnych kategorii i ekranu szczegółowego, a to jest moment, w którym własne rozwiązanie zaczyna się robić projektem samym w sobie.",[26,510,512],{"id":511},"zapis-i-wycofanie","Zapis i wycofanie",[177,514,516],{"className":179,"code":515,"language":181,"meta":182,"style":182},"const NAZWA_CIASTECZKA = 'targetx_zgody';\nconst WAZNOSC_DNI = 180;\n\nexport function zapiszZgode(wybor: WyborZgody): void {\n  const bezpieczne = location.protocol === 'https:' ? '; Secure' : '';\n  document.cookie = `${NAZWA_CIASTECZKA}=${wybor}; path=/; max-age=${WAZNOSC_DNI * 86400}; SameSite=Lax${bezpieczne}`;\n}\n",[53,517,518,533,547,551,574,609,673],{"__ignoreMap":182},[186,519,520,522,525,527,530],{"class":188,"line":189},[186,521,193],{"class":192},[186,523,524],{"class":204}," NAZWA_CIASTECZKA",[186,526,209],{"class":208},[186,528,529],{"class":280}," 'targetx_zgody'",[186,531,532],{"class":200},";\n",[186,534,535,537,540,542,545],{"class":188,"line":218},[186,536,193],{"class":192},[186,538,539],{"class":204}," WAZNOSC_DNI",[186,541,209],{"class":208},[186,543,544],{"class":351}," 180",[186,546,532],{"class":200},[186,548,549],{"class":188,"line":239},[186,550,249],{"emptyLinePlaceholder":248},[186,552,553,555,557,560,562,564,566,568,570,572],{"class":188,"line":245},[186,554,255],{"class":192},[186,556,212],{"class":192},[186,558,559],{"class":196}," zapiszZgode",[186,561,230],{"class":200},[186,563,412],{"class":411},[186,565,201],{"class":200},[186,567,417],{"class":204},[186,569,420],{"class":200},[186,571,266],{"class":204},[186,573,269],{"class":200},[186,575,576,579,582,584,587,589,592,594,597,599,602,604,607],{"class":188,"line":252},[186,577,578],{"class":192},"  const",[186,580,581],{"class":204}," bezpieczne",[186,583,209],{"class":208},[186,585,586],{"class":204}," location",[186,588,71],{"class":200},[186,590,591],{"class":295},"protocol",[186,593,480],{"class":208},[186,595,596],{"class":280}," 'https:'",[186,598,486],{"class":192},[186,600,601],{"class":280}," '; Secure'",[186,603,492],{"class":192},[186,605,606],{"class":280}," ''",[186,608,532],{"class":200},[186,610,611,614,616,619,621,624,627,630,633,636,638,640,642,645,647,650,653,656,658,661,663,666,668,671],{"class":188,"line":272},[186,612,613],{"class":204},"  document",[186,615,71],{"class":200},[186,617,618],{"class":295},"cookie",[186,620,209],{"class":208},[186,622,623],{"class":280}," `",[186,625,626],{"class":192},"${",[186,628,629],{"class":204},"NAZWA_CIASTECZKA",[186,631,632],{"class":192},"}",[186,634,635],{"class":280},"=",[186,637,626],{"class":192},[186,639,412],{"class":295},[186,641,632],{"class":192},[186,643,644],{"class":280},"; path=/; max-age=",[186,646,626],{"class":192},[186,648,649],{"class":204},"WAZNOSC_DNI",[186,651,652],{"class":208}," *",[186,654,655],{"class":351}," 86400",[186,657,632],{"class":192},[186,659,660],{"class":280},"; SameSite=Lax",[186,662,626],{"class":192},[186,664,665],{"class":295},"bezpieczne",[186,667,632],{"class":192},[186,669,670],{"class":280},"`",[186,672,532],{"class":200},[186,674,675],{"class":188,"line":292},[186,676,366],{"class":200},[11,678,679,680,683,684,687,688,71],{},"Zwykłe ciasteczko, bez biblioteki. ",[53,681,682],{},"SameSite=Lax"," wystarcza, bo nikt nie czyta tego z żądania między domenami, a ",[53,685,686],{},"Secure"," dokładam warunkowo, żeby mechanizm działał też pod ",[53,689,690],{},"localhost",[11,692,693,694,697],{},"Wycofanie zgody wymaga ",[15,695,696],{},"trzech"," rzeczy, a nie jednej — i tu popełniłem błąd, który wyłapała dopiero recenzja tego wpisu. Moja pierwsza wersja kasowała ciasteczko wyboru i pokazywała pasek, przez co analityka chodziła dalej na udzielonej zgodzie aż do końca bieżącej odsłony. Poprawnie:",[177,699,701],{"className":179,"code":700,"language":181,"meta":182,"style":182},"zapomnijZgode();\nzaktualizujZgode('odrzucona');\nwidoczny.value = true;\n",[53,702,703,711,723],{"__ignoreMap":182},[186,704,705,708],{"class":188,"line":189},[186,706,707],{"class":196},"zapomnijZgode",[186,709,710],{"class":200},"();\n",[186,712,713,716,718,721],{"class":188,"line":218},[186,714,715],{"class":196},"zaktualizujZgode",[186,717,230],{"class":200},[186,719,720],{"class":280},"'odrzucona'",[186,722,236],{"class":200},[186,724,725,728,730,733,735,738],{"class":188,"line":239},[186,726,727],{"class":204},"widoczny",[186,729,71],{"class":200},[186,731,732],{"class":295},"value",[186,734,209],{"class":208},[186,736,737],{"class":351}," true",[186,739,532],{"class":200},[11,741,742,743,116,746,749,750,753],{},"Czego nadal nie robię: ciasteczka ",[53,744,745],{},"_ga",[53,747,748],{},"_ga_*"," zostają w przeglądarce. Po odmowie analityka do nich nie sięga, więc są bezużyteczne, ale fizycznie tam są. Skasowanie ich wymaga odgadnięcia domeny, na której zostały ustawione, co przy adresach w rodzaju ",[53,751,752],{},"co.uk"," bywa zawodne. Jeśli robisz to u siebie, sprawdź, na jakiej domenie faktycznie siedzą.",[11,755,756,757,760],{},"Sam przełącznik w stopce to link z atrybutem ",[53,758,759],{},"data-zmien-zgody",", a komponent nasłuchuje kliknięć na całym dokumencie i reaguje na najbliższy pasujący element. Dzięki temu link można wstawić w dowolnym miejscu układu bez przekazywania czegokolwiek przez propsy.",[26,762,764],{"id":763},"kiedy-startuje-tag-manager","Kiedy startuje Tag Manager",[11,766,767],{},"Tu leży druga połowa zysku na wydajności:",[177,769,771],{"className":179,"code":770,"language":181,"meta":182,"style":182},"const LIMIT_BEZCZYNNOSCI_MS = 3000;\nconst ZAPASOWE_OPOZNIENIE_MS = 2500;\n\nconst odpal = () => zaladujGtm(identyfikator);\n\nconst poBezczynnosci = () => {\n  const okno = window as Window & {\n    requestIdleCallback?: (wywolanie: () => void, opcje?: { timeout: number }) => void;\n  };\n\n  if (typeof okno.requestIdleCallback === 'function') {\n    okno.requestIdleCallback(odpal, { timeout: LIMIT_BEZCZYNNOSCI_MS });\n    return;\n  }\n\n  setTimeout(odpal, ZAPASOWE_OPOZNIENIE_MS);\n};\n\nif (document.readyState === 'complete') {\n  poBezczynnosci();\n} else {\n  window.addEventListener('load', poBezczynnosci, { once: true });\n}\n\nwindow.addEventListener('pointerdown', odpal, { once: true, passive: true });\nwindow.addEventListener('keydown', odpal, { once: true });\n",[53,772,773,787,801,805,830,834,849,870,919,924,928,954,981,988,994,999,1016,1021,1026,1049,1057,1068,1101,1106,1111,1149],{"__ignoreMap":182},[186,774,775,777,780,782,785],{"class":188,"line":189},[186,776,193],{"class":192},[186,778,779],{"class":204}," LIMIT_BEZCZYNNOSCI_MS",[186,781,209],{"class":208},[186,783,784],{"class":351}," 3000",[186,786,532],{"class":200},[186,788,789,791,794,796,799],{"class":188,"line":218},[186,790,193],{"class":192},[186,792,793],{"class":204}," ZAPASOWE_OPOZNIENIE_MS",[186,795,209],{"class":208},[186,797,798],{"class":351}," 2500",[186,800,532],{"class":200},[186,802,803],{"class":188,"line":239},[186,804,249],{"emptyLinePlaceholder":248},[186,806,807,809,812,814,817,820,823,825,828],{"class":188,"line":245},[186,808,193],{"class":192},[186,810,811],{"class":196}," odpal",[186,813,209],{"class":208},[186,815,816],{"class":200}," () ",[186,818,819],{"class":192},"=>",[186,821,822],{"class":196}," zaladujGtm",[186,824,230],{"class":200},[186,826,827],{"class":295},"identyfikator",[186,829,236],{"class":200},[186,831,832],{"class":188,"line":252},[186,833,249],{"emptyLinePlaceholder":248},[186,835,836,838,841,843,845,847],{"class":188,"line":272},[186,837,193],{"class":192},[186,839,840],{"class":196}," poBezczynnosci",[186,842,209],{"class":208},[186,844,816],{"class":200},[186,846,819],{"class":192},[186,848,269],{"class":200},[186,850,851,853,856,858,861,864,867],{"class":188,"line":292},[186,852,578],{"class":192},[186,854,855],{"class":204}," okno",[186,857,209],{"class":208},[186,859,860],{"class":295}," window",[186,862,863],{"class":192}," as",[186,865,866],{"class":204}," Window",[186,868,869],{"class":200}," & {\n",[186,871,872,875,878,881,884,887,889,892,894,897,899,902,905,907,910,913,915,917],{"class":188,"line":307},[186,873,874],{"class":196},"    requestIdleCallback",[186,876,877],{"class":192},"?",[186,879,880],{"class":200},": (",[186,882,883],{"class":196},"wywolanie",[186,885,886],{"class":200},": () ",[186,888,819],{"class":192},[186,890,891],{"class":204}," void",[186,893,56],{"class":200},[186,895,896],{"class":411},"opcje",[186,898,877],{"class":192},[186,900,901],{"class":200},": { ",[186,903,904],{"class":295},"timeout",[186,906,201],{"class":200},[186,908,909],{"class":204},"number",[186,911,912],{"class":200}," }) ",[186,914,819],{"class":192},[186,916,891],{"class":204},[186,918,532],{"class":200},[186,920,921],{"class":188,"line":319},[186,922,923],{"class":200},"  };\n",[186,925,926],{"class":188,"line":331},[186,927,249],{"emptyLinePlaceholder":248},[186,929,930,933,936,939,941,943,946,948,951],{"class":188,"line":343},[186,931,932],{"class":192},"  if",[186,934,935],{"class":200}," (",[186,937,938],{"class":192},"typeof",[186,940,855],{"class":204},[186,942,71],{"class":200},[186,944,945],{"class":295},"requestIdleCallback",[186,947,480],{"class":208},[186,949,950],{"class":280}," 'function'",[186,952,953],{"class":200},") {\n",[186,955,956,959,961,963,965,968,971,973,975,978],{"class":188,"line":357},[186,957,958],{"class":204},"    okno",[186,960,71],{"class":200},[186,962,945],{"class":196},[186,964,230],{"class":200},[186,966,967],{"class":295},"odpal",[186,969,970],{"class":200},", { ",[186,972,904],{"class":295},[186,974,201],{"class":200},[186,976,977],{"class":204},"LIMIT_BEZCZYNNOSCI_MS",[186,979,980],{"class":200}," });\n",[186,982,983,986],{"class":188,"line":363},[186,984,985],{"class":192},"    return",[186,987,532],{"class":200},[186,989,991],{"class":188,"line":990},14,[186,992,993],{"class":200},"  }\n",[186,995,997],{"class":188,"line":996},15,[186,998,249],{"emptyLinePlaceholder":248},[186,1000,1002,1005,1007,1009,1011,1014],{"class":188,"line":1001},16,[186,1003,1004],{"class":196},"  setTimeout",[186,1006,230],{"class":200},[186,1008,967],{"class":295},[186,1010,56],{"class":200},[186,1012,1013],{"class":204},"ZAPASOWE_OPOZNIENIE_MS",[186,1015,236],{"class":200},[186,1017,1019],{"class":188,"line":1018},17,[186,1020,242],{"class":200},[186,1022,1024],{"class":188,"line":1023},18,[186,1025,249],{"emptyLinePlaceholder":248},[186,1027,1029,1032,1034,1037,1039,1042,1044,1047],{"class":188,"line":1028},19,[186,1030,1031],{"class":192},"if",[186,1033,935],{"class":200},[186,1035,1036],{"class":204},"document",[186,1038,71],{"class":200},[186,1040,1041],{"class":295},"readyState",[186,1043,480],{"class":208},[186,1045,1046],{"class":280}," 'complete'",[186,1048,953],{"class":200},[186,1050,1052,1055],{"class":188,"line":1051},20,[186,1053,1054],{"class":196},"  poBezczynnosci",[186,1056,710],{"class":200},[186,1058,1060,1063,1066],{"class":188,"line":1059},21,[186,1061,1062],{"class":200},"} ",[186,1064,1065],{"class":192},"else",[186,1067,269],{"class":200},[186,1069,1071,1074,1076,1079,1081,1084,1086,1089,1091,1094,1096,1099],{"class":188,"line":1070},22,[186,1072,1073],{"class":204},"  window",[186,1075,71],{"class":200},[186,1077,1078],{"class":196},"addEventListener",[186,1080,230],{"class":200},[186,1082,1083],{"class":280},"'load'",[186,1085,56],{"class":200},[186,1087,1088],{"class":295},"poBezczynnosci",[186,1090,970],{"class":200},[186,1092,1093],{"class":295},"once",[186,1095,201],{"class":200},[186,1097,1098],{"class":351},"true",[186,1100,980],{"class":200},[186,1102,1104],{"class":188,"line":1103},23,[186,1105,366],{"class":200},[186,1107,1109],{"class":188,"line":1108},24,[186,1110,249],{"emptyLinePlaceholder":248},[186,1112,1114,1117,1119,1121,1123,1126,1128,1130,1132,1134,1136,1138,1140,1143,1145,1147],{"class":188,"line":1113},25,[186,1115,1116],{"class":204},"window",[186,1118,71],{"class":200},[186,1120,1078],{"class":196},[186,1122,230],{"class":200},[186,1124,1125],{"class":280},"'pointerdown'",[186,1127,56],{"class":200},[186,1129,967],{"class":295},[186,1131,970],{"class":200},[186,1133,1093],{"class":295},[186,1135,201],{"class":200},[186,1137,1098],{"class":351},[186,1139,56],{"class":200},[186,1141,1142],{"class":295},"passive",[186,1144,201],{"class":200},[186,1146,1098],{"class":351},[186,1148,980],{"class":200},[186,1150,1152,1154,1156,1158,1160,1163,1165,1167,1169,1171,1173,1175],{"class":188,"line":1151},26,[186,1153,1116],{"class":204},[186,1155,71],{"class":200},[186,1157,1078],{"class":196},[186,1159,230],{"class":200},[186,1161,1162],{"class":280},"'keydown'",[186,1164,56],{"class":200},[186,1166,967],{"class":295},[186,1168,970],{"class":200},[186,1170,1093],{"class":295},[186,1172,201],{"class":200},[186,1174,1098],{"class":351},[186,1176,980],{"class":200},[11,1178,1179,1180,1182],{},"Kontener czeka na zdarzenie ",[53,1181,388],{},", potem na pierwszą wolną chwilę głównego wątku, z twardym limitem trzech sekund. Równolegle wisi nasłuch na pierwszej interakcji, więc ktoś, kto od razu klika, nie czeka.",[11,1184,1185,1187],{},[53,1186,967],{}," może więc zostać wywołane z trzech miejsc. Przed podwójnym wstrzyknięciem kontenera chroni flaga po stronie funkcji ładującej:",[177,1189,1191],{"className":179,"code":1190,"language":181,"meta":182,"style":182},"export function zaladujGtm(identyfikator: string): void {\n  const okno = window as OknoZWarstwa;\n\n  if (identyfikator === '' || okno.targetxGtmZaladowany) {\n    return;\n  }\n\n  okno.targetxGtmZaladowany = true;\n  // dalej: wstawienie skryptu\n}\n",[53,1192,1193,1216,1233,1237,1261,1267,1271,1275,1290,1296],{"__ignoreMap":182},[186,1194,1195,1197,1199,1201,1203,1205,1207,1210,1212,1214],{"class":188,"line":189},[186,1196,255],{"class":192},[186,1198,212],{"class":192},[186,1200,822],{"class":196},[186,1202,230],{"class":200},[186,1204,827],{"class":411},[186,1206,201],{"class":200},[186,1208,1209],{"class":204},"string",[186,1211,420],{"class":200},[186,1213,266],{"class":204},[186,1215,269],{"class":200},[186,1217,1218,1220,1222,1224,1226,1228,1231],{"class":188,"line":218},[186,1219,578],{"class":192},[186,1221,855],{"class":204},[186,1223,209],{"class":208},[186,1225,860],{"class":295},[186,1227,863],{"class":192},[186,1229,1230],{"class":204}," OknoZWarstwa",[186,1232,532],{"class":200},[186,1234,1235],{"class":188,"line":239},[186,1236,249],{"emptyLinePlaceholder":248},[186,1238,1239,1241,1243,1245,1247,1249,1252,1254,1256,1259],{"class":188,"line":245},[186,1240,932],{"class":192},[186,1242,935],{"class":200},[186,1244,827],{"class":295},[186,1246,480],{"class":208},[186,1248,606],{"class":280},[186,1250,1251],{"class":208}," ||",[186,1253,855],{"class":204},[186,1255,71],{"class":200},[186,1257,1258],{"class":295},"targetxGtmZaladowany",[186,1260,953],{"class":200},[186,1262,1263,1265],{"class":188,"line":252},[186,1264,985],{"class":192},[186,1266,532],{"class":200},[186,1268,1269],{"class":188,"line":272},[186,1270,993],{"class":200},[186,1272,1273],{"class":188,"line":292},[186,1274,249],{"emptyLinePlaceholder":248},[186,1276,1277,1280,1282,1284,1286,1288],{"class":188,"line":307},[186,1278,1279],{"class":204},"  okno",[186,1281,71],{"class":200},[186,1283,1258],{"class":295},[186,1285,209],{"class":208},[186,1287,737],{"class":351},[186,1289,532],{"class":200},[186,1291,1292],{"class":188,"line":319},[186,1293,1295],{"class":1294},"sV9Aq","  // dalej: wstawienie skryptu\n",[186,1297,1298],{"class":188,"line":331},[186,1299,366],{"class":200},[11,1301,1302,1303,1306],{},"Domyślne zgody ustawiam ",[15,1304,1305],{},"przed"," tym wszystkim, w pierwszej linii pluginu. To jest ta kolejność, którą psuła funkcja opisana wcześniej.",[11,1308,1309],{},"Warto uczciwie dodać: samo opóźnienie startu Tag Managera da się zrobić także z gotową platformą zgód. To dwie niezależne decyzje i zysk z nich też jest niezależny. Własny pasek daje kontrolę nad jednym skryptem mniej w oknie krytycznym, opóźnienie GTM-u daje resztę.",[26,1311,1313],{"id":1312},"trzy-pułapki-których-nikt-za-ciebie-nie-obsłuży","Trzy pułapki, których nikt za Ciebie nie obsłuży",[11,1315,1316],{},"Gotowa platforma bierze na siebie także to, co niżej. Przy własnym kodzie trafiasz na to osobiście — ja trafiłem na wszystkie trzy w ciągu jednego popołudnia.",[11,1318,1319,1320,1323,1324,1327],{},"Żeby dalsze przykłady dało się czytać, dwa zdania o tym, co mam w kontenerze. ",[15,1321,1322],{},"Google Analytics 4"," działa w trybie zaawansowanym i ma jeden wyzwalacz, zdarzenie inicjujące. ",[15,1325,1326],{},"Microsoft Clarity",", czyli mapy ciepła i nagrania sesji, działa w trybie podstawowym i wymaga zgody, więc dostał dwa wyzwalacze oraz ustawienie „Raz na stronę\". Te same dwa tagi zobaczysz niżej w JSON-ie kontenera.",[1329,1330,1332],"h3",{"id":1331},"tag-wymagający-zgody-nie-policzy-wizyty-w-której-zgoda-padła","Tag wymagający zgody nie policzy wizyty, w której zgoda padła",[11,1334,1335],{},"Jeśli w Tag Managerze zaznaczysz tagowi „Wymagaj dodatkowej zgody na uruchomienie\", a wyzwalaczem jest zdarzenie inicjujące albo odsłona strony, przy pierwszej wizycie dzieje się tak: kontener startuje, tag zostaje zablokowany brakiem zgody, użytkownik czyta stronę, klika „Zgadzam się\" — i nic się nie dzieje. Wyzwalające zdarzenie dawno minęło, a tag odpala się dopiero przy następnej odsłonie.",[11,1337,1338],{},"Tracisz przez to odsłonę wejścia, czyli tę jedyną, która niesie źródło ruchu.",[11,1340,1341],{},"Poprawka po stronie kodu to jedno zdarzenie:",[177,1343,1345],{"className":179,"code":1344,"language":181,"meta":182,"style":182},"export function zglosUdzielenieZgody(): void {\n  warstwa().push({ event: 'consent_granted' });\n}\n",[53,1346,1347,1362,1383],{"__ignoreMap":182},[186,1348,1349,1351,1353,1356,1358,1360],{"class":188,"line":189},[186,1350,255],{"class":192},[186,1352,212],{"class":192},[186,1354,1355],{"class":196}," zglosUdzielenieZgody",[186,1357,263],{"class":200},[186,1359,266],{"class":204},[186,1361,269],{"class":200},[186,1363,1364,1366,1368,1370,1373,1376,1378,1381],{"class":188,"line":218},[186,1365,221],{"class":196},[186,1367,224],{"class":200},[186,1369,227],{"class":196},[186,1371,1372],{"class":200},"({ ",[186,1374,1375],{"class":295},"event",[186,1377,201],{"class":200},[186,1379,1380],{"class":280},"'consent_granted'",[186,1382,980],{"class":200},[186,1384,1385],{"class":188,"line":239},[186,1386,366],{"class":200},[11,1388,1389,1390,71],{},"Po stronie kontenera dokładasz temu tagowi drugi wyzwalacz typu „zdarzenie niestandardowe\" o nazwie ",[53,1391,1392],{},"consent_granted",[11,1394,1395,1398],{},[15,1396,1397],{},"Razem z tym przestaw „Opcje uruchamiania tagów\" na „Raz na stronę\"."," Domyślne „Raz na zdarzenie\" ogranicza tag do jednego uruchomienia w obrębie jednego zdarzenia, a tutaj masz dwa różne zdarzenia, więc przed dublowaniem nie chroni wcale. Kolejna sekcja pokazuje, jak to wygląda w praktyce.",[1329,1400,1402],{"id":1401},"ta-sama-poprawka-potrafi-liczyć-podwójnie","Ta sama poprawka potrafi liczyć podwójnie",[11,1404,1405,1406,1408],{},"Pierwsza wersja, którą napisałem, wypychała to zdarzenie wewnątrz ",[53,1407,715],{},". Wyglądało logicznie: zgoda się zmienia, więc informuję o tym kontener.",[11,1410,1411],{},"Problem w tym, że ta funkcja woła się w dwóch miejscach. Raz przy kliknięciu w pasku, drugi raz przy starcie strony, gdy plugin odczytuje zapisaną zgodę z ciasteczka. Przy każdym powrocie stałego czytelnika tag odpalał się więc dwa razy: raz ze zdarzenia inicjującego, raz z mojego. Ustawienie „Raz na zdarzenie\" tego nie zatrzymało, bo to były dwa osobne zdarzenia.",[11,1413,1414],{},"Zdarzenie należy do gestu, a nie do stanu. Wypycham je wyłącznie z obsługi kliknięcia:",[177,1416,1418],{"className":179,"code":1417,"language":181,"meta":182,"style":182},"const rozstrzygnij = (wybor: WyborZgody) => {\n  zapiszZgode(wybor);\n  zaktualizujZgode(wybor);\n\n  if (wybor === 'udzielona') {\n    zglosUdzielenieZgody();\n  }\n\n  widoczny.value = false;\n};\n",[53,1419,1420,1444,1455,1466,1470,1484,1491,1495,1499,1515],{"__ignoreMap":182},[186,1421,1422,1424,1427,1429,1431,1433,1435,1437,1440,1442],{"class":188,"line":189},[186,1423,193],{"class":192},[186,1425,1426],{"class":196}," rozstrzygnij",[186,1428,209],{"class":208},[186,1430,935],{"class":200},[186,1432,412],{"class":411},[186,1434,201],{"class":200},[186,1436,417],{"class":204},[186,1438,1439],{"class":200},") ",[186,1441,819],{"class":192},[186,1443,269],{"class":200},[186,1445,1446,1449,1451,1453],{"class":188,"line":218},[186,1447,1448],{"class":196},"  zapiszZgode",[186,1450,230],{"class":200},[186,1452,412],{"class":295},[186,1454,236],{"class":200},[186,1456,1457,1460,1462,1464],{"class":188,"line":239},[186,1458,1459],{"class":196},"  zaktualizujZgode",[186,1461,230],{"class":200},[186,1463,412],{"class":295},[186,1465,236],{"class":200},[186,1467,1468],{"class":188,"line":245},[186,1469,249],{"emptyLinePlaceholder":248},[186,1471,1472,1474,1476,1478,1480,1482],{"class":188,"line":252},[186,1473,932],{"class":192},[186,1475,935],{"class":200},[186,1477,412],{"class":295},[186,1479,480],{"class":208},[186,1481,483],{"class":280},[186,1483,953],{"class":200},[186,1485,1486,1489],{"class":188,"line":272},[186,1487,1488],{"class":196},"    zglosUdzielenieZgody",[186,1490,710],{"class":200},[186,1492,1493],{"class":188,"line":292},[186,1494,993],{"class":200},[186,1496,1497],{"class":188,"line":307},[186,1498,249],{"emptyLinePlaceholder":248},[186,1500,1501,1504,1506,1508,1510,1513],{"class":188,"line":319},[186,1502,1503],{"class":204},"  widoczny",[186,1505,71],{"class":200},[186,1507,732],{"class":295},[186,1509,209],{"class":208},[186,1511,1512],{"class":351}," false",[186,1514,532],{"class":200},[186,1516,1517],{"class":188,"line":331},[186,1518,242],{"class":200},[11,1520,1521,1522,1525],{},"Jest jeszcze jeden układ, w którym to samo wraca: ktoś klika zgodę, ",[15,1523,1524],{},"zanim kontener zdąży się pobrać",". Mierzyłem taki przebieg — kliknięcie padło w 275 ms, kontener zaczął się ładować w 312 ms. Oba zdarzenia trafiają wtedy do kolejki jeszcze przed startem kontenera, a ten przetwarza je już ze zgodą udzieloną i może uruchomić tag dwukrotnie.",[11,1527,1528],{},"Mierzyłem ten przebieg, gdy tag stał jeszcze na „Raz na zdarzenie\", i dublowania nie zobaczyłem — ale zawdzięczałem to zabezpieczeniu wewnątrz samego Clarity, a nie własnej konfiguracji. Po przestawieniu na „Raz na stronę\" chroni już ustawienie, na które mam wpływ. Stąd to zalecenie jako wymóg, nie ozdoba.",[1329,1530,1532],{"id":1531},"poprawna-poprawka-zależy-od-trybu-zgody","Poprawna poprawka zależy od trybu zgody",[11,1534,1535],{},"Consent Mode ma dwa tryby i decyduje o nich jedno pole w konfiguracji tagu.",[11,1537,1538,1541],{},[15,1539,1540],{},"Tryb podstawowy",", czyli zaznaczone „Wymagaj dodatkowej zgody\": przed decyzją nie leci nic. Sprawdziłem to na drugim swoim serwisie, gdzie Analytics stał wtedy właśnie w tym trybie — zero żądań do Google, zero ciasteczek. To opisana wyżej sytuacja, w której drugi wyzwalacz jest konieczny.",[11,1543,1544,1547],{},[15,1545,1546],{},"Tryb zaawansowany",", czyli brak takiego wymogu: tag startuje zawsze, ale do czasu zgody wysyła sygnał bez ciasteczek i bez identyfikatorów.",[11,1549,1550,1551,1554],{},"Tu potrzebne jest zastrzeżenie, którego w poradnikach zwykle brakuje. Takie sygnały ",[15,1552,1553],{},"nie pojawiają się w raportach jako zwykłe odsłony",". Zasilają modelowanie zachowań, a modelowanie włącza się dopiero po przekroczeniu progów ruchu, których mały serwis nie osiągnie. Przy kilkudziesięciu wizytach miesięcznie różnica między trybami jest w praktyce mniejsza, niż wynikałoby z opisu.",[11,1556,1557,1558,1561],{},"Trzeba z tego wyciągnąć wniosek, który mnie samego kosztował chwilę: ",[15,1559,1560],{},"przy małym ruchu wybór trybu jest w praktyce decyzją głównie teoretyczną",". Ani jeden, ani drugi nie pokaże Ci w raportach wejść od osób, które paska nie dotknęły. Wybrałem zaawansowany, bo nic nie kosztuje technicznie, bo mój pasek i tak mówi użytkownikowi, że statystyki chodzą bez ciasteczek, i bo przy większym ruchu modelowanie zacznie działać bez przestawiania czegokolwiek. Gdyby serwis miał reklamy albo dane wrażliwe, wybrałbym odwrotnie.",[11,1563,1564,1565,1568],{},"Dochodzi do tego kwestia prawna. Argument, że sygnał przed zgodą niczego nie zapisuje na urządzeniu i nie niesie identyfikatora, jest ",[15,1566,1567],{},"moją interpretacją, a nie ustalonym stanowiskiem",". Wytyczne EDPB 2/2023 czytają artykuł 5 ustęp 3 dyrektywy ePrivacy szeroko i obejmują nim także piksele oraz śledzenie przez parametry adresu, czyli techniki bez zapisu po stronie przeglądarki. Jeśli doradzasz w tej sprawie klientowi, warto o tym wiedzieć, zanim powtórzysz moje uzasadnienie.",[11,1570,1571,1572,1575],{},"W trybie zaawansowanym drugi wyzwalacz wysyła ",[15,1573,1574],{},"drugie trafienie dla tej samej odsłony",", bo tag odpalił się już na zdarzeniu inicjującym. Dokładnie to zrobiłem, przenosząc rozwiązanie z jednego serwisu na drugi bez sprawdzenia, że tagi są tam skonfigurowane inaczej.",[11,1577,1578],{},"Czy to znaczy dwie odsłony w raportach, czy odzyskanie tej, która wcześniej poszła bez zgody i do raportów nie trafiła — tego z przeglądarki nie rozstrzygniesz. Widzisz dwa żądania, a co z nimi zrobi Google, zależy od obsługi trybu zgody po jego stronie. Dlatego w trybie zaawansowanym drugiego wyzwalacza nie dokładam: przy niepewności wolę jedno trafienie, o którym wiem, co znaczy.",[11,1580,1581],{},"Zasada, która z tego zostaje: najpierw wybierasz tryb, dopiero potem dobierasz wyzwalacze. W trybie podstawowym drugi wyzwalacz jest konieczny i jego skutek jest jednoznaczny. W zaawansowanym daj sobie z nim spokój.",[26,1583,1585],{"id":1584},"jak-to-sprawdzić-zamiast-zakładać","Jak to sprawdzić, zamiast zakładać",[11,1587,1588],{},"Panel Tag Managera pokazuje, co zamierzałeś. Do sprawdzenia, co naprawdę jest opublikowane, wystarczy pobrać kontener:",[177,1590,1594],{"className":1591,"code":1592,"language":1593,"meta":182,"style":182},"language-bash shiki shiki-themes one-dark-pro","curl -s \"https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX\" -o kontener.js\n","bash",[53,1595,1596],{"__ignoreMap":182},[186,1597,1598,1601,1604,1607,1610],{"class":188,"line":189},[186,1599,1600],{"class":196},"curl",[186,1602,1603],{"class":351}," -s",[186,1605,1606],{"class":280}," \"https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX\"",[186,1608,1609],{"class":351}," -o",[186,1611,1612],{"class":280}," kontener.js\n",[11,1614,1615],{},"W środku siedzi czytelny JSON. Lista tagów mówi, czego każdy wymaga i jak często może odpalić:",[177,1617,1621],{"className":1618,"code":1619,"language":1620,"meta":182,"style":182},"language-json shiki shiki-themes one-dark-pro","\"tags\":[{\"function\":\"__googtag\",\"consent\":[\"list\"],\n         \"once_per_event\":true,\"vtp_tagId\":\"G-XXXXXXXXXX\",\"tag_id\":3},\n        {\"function\":\"__cvt_MQDKZ\",\"consent\":[\"list\",\"analytics_storage\"],\n         \"once_per_load\":true,\"vtp_projectId\":\"xxxxxxxxxx\",\"tag_id\":6}]\n","json",[53,1622,1623,1655,1687,1714],{"__ignoreMap":182},[186,1624,1625,1628,1631,1634,1637,1640,1643,1646,1649,1652],{"class":188,"line":189},[186,1626,1627],{"class":280},"\"tags\"",[186,1629,1630],{"class":200},":[{",[186,1632,1633],{"class":295},"\"function\"",[186,1635,1636],{"class":200},":",[186,1638,1639],{"class":280},"\"__googtag\"",[186,1641,1642],{"class":200},",",[186,1644,1645],{"class":295},"\"consent\"",[186,1647,1648],{"class":200},":[",[186,1650,1651],{"class":280},"\"list\"",[186,1653,1654],{"class":200},"],\n",[186,1656,1657,1660,1662,1664,1666,1669,1671,1674,1676,1679,1681,1684],{"class":188,"line":218},[186,1658,1659],{"class":295},"         \"once_per_event\"",[186,1661,1636],{"class":200},[186,1663,1098],{"class":351},[186,1665,1642],{"class":200},[186,1667,1668],{"class":295},"\"vtp_tagId\"",[186,1670,1636],{"class":200},[186,1672,1673],{"class":280},"\"G-XXXXXXXXXX\"",[186,1675,1642],{"class":200},[186,1677,1678],{"class":295},"\"tag_id\"",[186,1680,1636],{"class":200},[186,1682,1683],{"class":351},"3",[186,1685,1686],{"class":200},"},\n",[186,1688,1689,1692,1694,1696,1699,1701,1703,1705,1707,1709,1712],{"class":188,"line":239},[186,1690,1691],{"class":200},"        {",[186,1693,1633],{"class":295},[186,1695,1636],{"class":200},[186,1697,1698],{"class":280},"\"__cvt_MQDKZ\"",[186,1700,1642],{"class":200},[186,1702,1645],{"class":295},[186,1704,1648],{"class":200},[186,1706,1651],{"class":280},[186,1708,1642],{"class":200},[186,1710,1711],{"class":280},"\"analytics_storage\"",[186,1713,1654],{"class":200},[186,1715,1716,1719,1721,1723,1725,1728,1730,1733,1735,1737,1739,1742],{"class":188,"line":245},[186,1717,1718],{"class":295},"         \"once_per_load\"",[186,1720,1636],{"class":200},[186,1722,1098],{"class":351},[186,1724,1642],{"class":200},[186,1726,1727],{"class":295},"\"vtp_projectId\"",[186,1729,1636],{"class":200},[186,1731,1732],{"class":280},"\"xxxxxxxxxx\"",[186,1734,1642],{"class":200},[186,1736,1678],{"class":295},[186,1738,1636],{"class":200},[186,1740,1741],{"class":351},"6",[186,1743,1744],{"class":200},"}]\n",[11,1746,1747,1748,1751],{},"To jest mój prawdziwy kontener, z zaślepionymi identyfikatorami. Indeks 0 to Google Analytics, indeks 1 to Clarity — ",[53,1749,1750],{},"__cvt_MQDKZ"," oznacza szablon z galerii, a nie wbudowany typ tagu.",[11,1753,1754,1755,1758,1759,1762,1763,1766,1767,1770,1771,1774],{},"Pole ",[53,1756,1757],{},"consent"," z wpisem ",[53,1760,1761],{},"analytics_storage"," znaczy tryb podstawowy, sama ",[53,1764,1765],{},"[\"list\"]"," — zaawansowany. ",[53,1768,1769],{},"once_per_event"," to „Raz na zdarzenie\", ",[53,1772,1773],{},"once_per_load"," to „Raz na stronę\". Widać więc od razu, że Analytics działa w trybie zaawansowanym, a Clarity w podstawowym.",[11,1776,1777],{},"Obok znajdziesz warunki wyzwalaczy i mapowanie jednych na drugie:",[177,1779,1781],{"className":1618,"code":1780,"language":1620,"meta":182,"style":182},"\"predicates\":[{\"function\":\"_eq\",\"arg0\":[\"macro\",0],\"arg1\":\"gtm.init\"},\n              {\"function\":\"_eq\",\"arg0\":[\"macro\",0],\"arg1\":\"gtm.js\"},\n              {\"function\":\"_eq\",\"arg0\":[\"macro\",0],\"arg1\":\"consent_granted\"}],\n\"rules\":[[[\"if\",0],[\"add\",0]],[[\"if\",1],[\"add\",1]],[[\"if\",2],[\"add\",1]]]\n",[53,1782,1783,1825,1859,1893],{"__ignoreMap":182},[186,1784,1785,1788,1790,1792,1794,1797,1799,1802,1804,1807,1809,1812,1815,1818,1820,1823],{"class":188,"line":189},[186,1786,1787],{"class":280},"\"predicates\"",[186,1789,1630],{"class":200},[186,1791,1633],{"class":295},[186,1793,1636],{"class":200},[186,1795,1796],{"class":280},"\"_eq\"",[186,1798,1642],{"class":200},[186,1800,1801],{"class":295},"\"arg0\"",[186,1803,1648],{"class":200},[186,1805,1806],{"class":280},"\"macro\"",[186,1808,1642],{"class":200},[186,1810,1811],{"class":351},"0",[186,1813,1814],{"class":200},"],",[186,1816,1817],{"class":295},"\"arg1\"",[186,1819,1636],{"class":200},[186,1821,1822],{"class":280},"\"gtm.init\"",[186,1824,1686],{"class":200},[186,1826,1827,1830,1832,1834,1836,1838,1840,1842,1844,1846,1848,1850,1852,1854,1857],{"class":188,"line":218},[186,1828,1829],{"class":200},"              {",[186,1831,1633],{"class":295},[186,1833,1636],{"class":200},[186,1835,1796],{"class":280},[186,1837,1642],{"class":200},[186,1839,1801],{"class":295},[186,1841,1648],{"class":200},[186,1843,1806],{"class":280},[186,1845,1642],{"class":200},[186,1847,1811],{"class":351},[186,1849,1814],{"class":200},[186,1851,1817],{"class":295},[186,1853,1636],{"class":200},[186,1855,1856],{"class":280},"\"gtm.js\"",[186,1858,1686],{"class":200},[186,1860,1861,1863,1865,1867,1869,1871,1873,1875,1877,1879,1881,1883,1885,1887,1890],{"class":188,"line":239},[186,1862,1829],{"class":200},[186,1864,1633],{"class":295},[186,1866,1636],{"class":200},[186,1868,1796],{"class":280},[186,1870,1642],{"class":200},[186,1872,1801],{"class":295},[186,1874,1648],{"class":200},[186,1876,1806],{"class":280},[186,1878,1642],{"class":200},[186,1880,1811],{"class":351},[186,1882,1814],{"class":200},[186,1884,1817],{"class":295},[186,1886,1636],{"class":200},[186,1888,1889],{"class":280},"\"consent_granted\"",[186,1891,1892],{"class":200},"}],\n",[186,1894,1895,1898,1901,1904,1906,1908,1911,1914,1916,1918,1921,1923,1925,1928,1930,1932,1934,1936,1938,1940,1942,1945,1947,1949,1951,1953],{"class":188,"line":245},[186,1896,1897],{"class":280},"\"rules\"",[186,1899,1900],{"class":200},":[[[",[186,1902,1903],{"class":280},"\"if\"",[186,1905,1642],{"class":200},[186,1907,1811],{"class":351},[186,1909,1910],{"class":200},"],[",[186,1912,1913],{"class":280},"\"add\"",[186,1915,1642],{"class":200},[186,1917,1811],{"class":351},[186,1919,1920],{"class":200},"]],[[",[186,1922,1903],{"class":280},[186,1924,1642],{"class":200},[186,1926,1927],{"class":351},"1",[186,1929,1910],{"class":200},[186,1931,1913],{"class":280},[186,1933,1642],{"class":200},[186,1935,1927],{"class":351},[186,1937,1920],{"class":200},[186,1939,1903],{"class":280},[186,1941,1642],{"class":200},[186,1943,1944],{"class":351},"2",[186,1946,1910],{"class":200},[186,1948,1913],{"class":280},[186,1950,1642],{"class":200},[186,1952,1927],{"class":351},[186,1954,1955],{"class":200},"]]]\n",[11,1957,1958,1959,1962],{},"Czyta się to tak: warunek 0 uruchamia tag 0, a warunki 1 i 2 uruchamiają tag 1. Czyli Analytics na zdarzeniu inicjującym, Clarity na odsłonie strony ",[15,1960,1961],{},"oraz"," na zdarzeniu zgody — dokładnie ten układ dwóch wyzwalaczy, o którym była mowa wyżej.",[11,1964,1965,1966,1969,1970,1976,1977,1980,1981,1983],{},"Jedna pułapka przy czytaniu: liczba w ",[53,1967,1968],{},"add"," to ",[15,1971,1972,1973],{},"indeks w tablicy ",[53,1974,1975],{},"tags",", a nie ",[53,1978,1979],{},"tag_id",". Obok stoją ",[53,1982,1979],{}," równe 3 i 6, więc łatwo je pomylić.",[11,1985,1986],{},"Nazwa zdarzenia jest widoczna wprost, więc od razu sprawdzisz, czy zgadza się co do znaku z tym, co wypycha strona.",[11,1988,1989,1990,1993,1994,1636],{},"Po stronie przeglądarki najszybszy pomiar daje parametr ",[53,1991,1992],{},"gcs"," w żądaniu do ",[53,1995,1996],{},"/g/collect",[177,1998,2002],{"className":1999,"code":2000,"language":2001,"meta":182,"style":182},"language-js shiki shiki-themes one-dark-pro","performance.getEntriesByType('resource')\n  .filter(z => /g\\/collect/.test(z.name))\n  .map(z => z.name.match(/[?&]gcs=([^&]*)/)?.[1]);\n","js",[53,2003,2004,2022,2064],{"__ignoreMap":182},[186,2005,2006,2009,2011,2014,2016,2019],{"class":188,"line":189},[186,2007,2008],{"class":204},"performance",[186,2010,71],{"class":200},[186,2012,2013],{"class":196},"getEntriesByType",[186,2015,230],{"class":200},[186,2017,2018],{"class":280},"'resource'",[186,2020,2021],{"class":200},")\n",[186,2023,2024,2027,2030,2032,2035,2038,2041,2044,2047,2049,2052,2054,2056,2058,2061],{"class":188,"line":218},[186,2025,2026],{"class":200},"  .",[186,2028,2029],{"class":196},"filter",[186,2031,230],{"class":200},[186,2033,2034],{"class":411},"z",[186,2036,2037],{"class":192}," =>",[186,2039,2040],{"class":295}," /g",[186,2042,2043],{"class":208},"\\/",[186,2045,2046],{"class":295},"collect/",[186,2048,71],{"class":200},[186,2050,2051],{"class":196},"test",[186,2053,230],{"class":200},[186,2055,2034],{"class":204},[186,2057,71],{"class":200},[186,2059,2060],{"class":295},"name",[186,2062,2063],{"class":200},"))\n",[186,2065,2066,2068,2071,2073,2075,2077,2080,2082,2084,2086,2089,2091,2094,2097,2100,2103,2106,2109,2112,2115,2117],{"class":188,"line":239},[186,2067,2026],{"class":200},[186,2069,2070],{"class":196},"map",[186,2072,230],{"class":200},[186,2074,2034],{"class":411},[186,2076,2037],{"class":192},[186,2078,2079],{"class":204}," z",[186,2081,71],{"class":200},[186,2083,2060],{"class":204},[186,2085,71],{"class":200},[186,2087,2088],{"class":196},"match",[186,2090,230],{"class":200},[186,2092,2093],{"class":295},"/",[186,2095,2096],{"class":351},"[?&]",[186,2098,2099],{"class":295},"gcs=(",[186,2101,2102],{"class":351},"[",[186,2104,2105],{"class":200},"^",[186,2107,2108],{"class":351},"&]*",[186,2110,2111],{"class":295},")/",[186,2113,2114],{"class":200},")?.[",[186,2116,1927],{"class":351},[186,2118,2119],{"class":200},"]);\n",[11,2121,2122,2125,2126,2129],{},[53,2123,2124],{},"G100"," oznacza oba magazyny odrzucone, ",[53,2127,2128],{},"G101"," — analitykę udzieloną. Liczba samych żądań mówi o dublowaniu.",[11,2131,2132],{},"Cztery stany warto przejść za każdym razem, na czystej sesji: przed decyzją, zaraz po kliknięciu, po przeładowaniu z zapisaną zgodą i po wycofaniu. Ten ostatni jest tym, o którym zapomniałem.",[26,2134,2136],{"id":2135},"kiedy-gotowa-platforma-jest-właściwym-wyborem","Kiedy gotowa platforma jest właściwym wyborem",[11,2138,2139],{},"Własny pasek zgód ma sens przy serwisie, który zbiera statystyki odwiedzin i nic ponadto. Gotowe rozwiązanie wygrywa w czterech sytuacjach:",[11,2141,2142,2145],{},[15,2143,2144],{},"AdSense, Ad Manager albo AdMob."," W Europejskim Obszarze Gospodarczym, Wielkiej Brytanii i Szwajcarii Google wymaga przy nich certyfikowanej platformy zgód obsługującej framework TCF od IAB. To setki stron specyfikacji i rejestr dostawców, który zmienia się co tydzień. Tego nie pisze się samodzielnie.",[11,2147,2148,2151],{},[15,2149,2150],{},"Obowiązek okresowego skanowania."," Platformy przeglądają serwis i wykrywają ciasteczka, które ktoś dołożył wtyczką bez informowania nikogo. Przy serwisie prowadzonym przez zespół to realna wartość.",[11,2153,2154,2157],{},[15,2155,2156],{},"Wiele języków i jurysdykcji."," Inny zakres zgód dla Unii, inny dla Kalifornii, inny dla Brazylii, do tego tłumaczenia.",[11,2159,2160,2163],{},[15,2161,2162],{},"Dowód zgodności na papierze."," Część klientów potrzebuje dokumentu z logo dostawcy do audytu. Własne 900 bajtów tego nie zastąpi, choćby działały lepiej.",[11,2165,2166],{},"W pozostałych przypadkach płacisz abonamentem i miejscem w kolejce ładowania za funkcje, których nie używasz.",[26,2168,2170],{"id":2169},"co-z-tego-zostaje","Co z tego zostaje",[11,2172,2173],{},"Skrypt zgód jest jedynym elementem strony, który z definicji musi wystartować przed wszystkim innym. Dlatego jego ocena po rozmiarze pliku prowadzi na manowce — liczy się, co przez niego czeka. Dziesięciokilobajtowy beacon kosztował mnie 450 ms FCP, a trzystukilobajtowa paczka skryptu nie kosztowała ani milisekundy tej samej metryki.",[11,2175,2176],{},"Własna implementacja daje kontrolę nad tym momentem. Kosztuje za to trzy pułapki opisane wyżej i obowiązek sprawdzenia w przeglądarce, czy to, co widzisz w panelu, faktycznie się dzieje.",[11,2178,2179],{},"Na trzy pułapki trafiłem sam, przy wdrożeniu. Kolejne trzy błędy — w tym jeden w opisie samej poprawki i jeden w kodzie wycofywania zgody — znalazł dopiero ktoś, kto przeczytał ten tekst uważniej ode mnie. Taki jest koszt rozwiązania, którego nikt za Ciebie nie utrzymuje.",[2181,2182,2183],"style",{},"html pre.shiki code .sVbv2, html code.shiki .sVbv2{--shiki-default:#61AFEF}html pre.shiki code .sVC51, html code.shiki .sVC51{--shiki-default:#D19A66}html pre.shiki code .subq3, html code.shiki .subq3{--shiki-default:#98C379}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sn6KH, html code.shiki .sn6KH{--shiki-default:#ABB2BF}html pre.shiki code .sVyAn, html code.shiki .sVyAn{--shiki-default:#E06C75}html pre.shiki code .sU0A5, html code.shiki .sU0A5{--shiki-default:#E5C07B}html pre.shiki code .s_ZVi, html code.shiki .s_ZVi{--shiki-default:#E06C75;--shiki-default-font-style:italic}html pre.shiki code .seHd6, html code.shiki .seHd6{--shiki-default:#C678DD}html pre.shiki code .sjrmR, html code.shiki .sjrmR{--shiki-default:#56B6C2}html pre.shiki code .sV9Aq, html code.shiki .sV9Aq{--shiki-default:#7F848E;--shiki-default-font-style:italic}",{"title":182,"searchDepth":218,"depth":218,"links":2185},[2186,2187,2188,2189,2190,2191,2192,2193,2198,2199,2200],{"id":28,"depth":218,"text":29},{"id":47,"depth":218,"text":48},{"id":89,"depth":218,"text":90},{"id":136,"depth":218,"text":137},{"id":174,"depth":218,"text":175},{"id":511,"depth":218,"text":512},{"id":763,"depth":218,"text":764},{"id":1312,"depth":218,"text":1313,"children":2194},[2195,2196,2197],{"id":1331,"depth":239,"text":1332},{"id":1401,"depth":239,"text":1402},{"id":1531,"depth":239,"text":1532},{"id":1584,"depth":218,"text":1585},{"id":2135,"depth":218,"text":2136},{"id":2169,"depth":218,"text":2170},"2026-09-23","Pasek zgód z Consent Mode v2 w 900 bajtach wobec 28 kB gotowej platformy. Co zrobić dobrze, na czym się przewróciłem i kiedy wziąć gotowca.","md","Wydajność",{},"/img/blog/pasek-zgod-na-wlasnym-kodzie.webp","Pięć ciastek na otwartym laptopie — cztery ułożone w stos i jedno obok, na gładziku","/pasek-zgod-na-wlasnym-kodzie",{"title":6,"description":2202},{"loc":2208},"pasek-zgod-na-wlasnym-kodzie",false,null,"6D3KK_FJu46pdLSZpRHOhy5CbUrKrq0pB9nMCmeWooI",{"id":2216,"title":2217,"body":2218,"date":5059,"description":5060,"extension":2203,"kategoria":5061,"meta":5062,"navigation":248,"obrazek":5063,"obrazekAlt":5064,"path":5065,"seo":5066,"sitemap":5067,"stem":5068,"szkic":2212,"zaktualizowano":2213,"__hash__":5069},"wpisy/zapytania-n-plus-1-filtry-wordpress.md","Zapytania N+1 w filtrach WordPressa — jak je znaleźć i naprawić",{"type":8,"value":2219,"toc":5043},[2220,2227,2236,2240,2243,2249,2272,2278,2430,2444,2451,2462,2469,2473,2476,2993,3004,3015,3030,3033,3047,3051,3058,3064,3067,3071,3082,3430,3441,3456,3459,3690,3696,3718,3724,3731,3735,3754,3757,3809,3822,3864,3885,3892,3898,3919,3922,3934,3938,3951,3965,3969,3972,4309,4319,4326,4340,4347,4356,4363,4367,4370,4536,4546,4553,4556,4761,4781,4791,4813,4816,4863,4886,4890,4893,4899,4908,4914,4918,4921,4943,4956,4969,4973,4979,4991,4994,5000,5015,5028,5031,5040],[11,2221,2222,2223,2226],{},"Filtr ",[53,2224,2225],{},"wp_nav_menu_objects",", który dobudowuje menu z kategorii rekurencyjnie, wykonuje osobne zapytanie dla każdego poziomu drzewa i drugie dla każdej pozycji — tylko po to, żeby sprawdzić, czy ma dzieci. Przy stu kategoriach to kilkaset zapytań SQL przy każdym wyświetleniu dowolnej strony witryny. Poprawka: pobrać wszystkie terminy jednym zapytaniem, złożyć drzewo w PHP i wynik odłożyć w transiencie.",[11,2228,2229,2230,2235],{},"Ten kod znalazłem w motywie na koncie, które diagnozowałem z powodu ",[2231,2232,2234],"a",{"href":2233},"/limit-cpu-hosting-wspoldzielony-wordpress/","przekroczeń limitu CPU",". Z tamtym problemem nie miał nic wspólnego i to jest osobna historia. Warto go było naprawić z własnego powodu: strona generowała się zauważalnie dłużej, niż powinna.",[26,2237,2239],{"id":2238},"skąd-wiadomo-że-to-n1","Skąd wiadomo, że to N+1",[11,2241,2242],{},"N+1 to jedno zapytanie po listę i po jednym dodatkowym dla każdego elementu z tej listy. Rośnie liniowo z liczbą danych, więc na instalacji deweloperskiej z pięcioma kategoriami jest niewidoczne, a na produkcji ze stoma zjada sekundę.",[11,2244,2245,2246,1636],{},"Najszybszy pomiar bez wtyczek — stała w ",[53,2247,2248],{},"wp-config.php",[177,2250,2254],{"className":2251,"code":2252,"language":2253,"meta":182,"style":182},"language-php shiki shiki-themes one-dark-pro","define('POMIAR_ZAPYTAN', true);\n","php",[53,2255,2256],{"__ignoreMap":182},[186,2257,2258,2261,2263,2266,2268,2270],{"class":188,"line":189},[186,2259,2260],{"class":208},"define",[186,2262,230],{"class":200},[186,2264,2265],{"class":280},"'POMIAR_ZAPYTAN'",[186,2267,1642],{"class":200},[186,2269,737],{"class":351},[186,2271,236],{"class":200},[11,2273,2274,2275,1636],{},"i jedna funkcja w ",[53,2276,2277],{},"functions.php",[177,2279,2281],{"className":2251,"code":2280,"language":2253,"meta":182,"style":182},"add_action('shutdown', function (): void {\n  if (!defined('POMIAR_ZAPYTAN') || !POMIAR_ZAPYTAN) {\n    return;\n  }\n\n  error_log(sprintf(\n    '%s — %d zapytań, %s s',\n    sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? '-')),\n    get_num_queries(),\n    timer_stop(0, 3)\n  ));\n});\n",[53,2282,2283,2304,2330,2336,2340,2344,2357,2364,2396,2404,2420,2425],{"__ignoreMap":182},[186,2284,2285,2288,2290,2293,2295,2297,2300,2302],{"class":188,"line":189},[186,2286,2287],{"class":196},"add_action",[186,2289,230],{"class":200},[186,2291,2292],{"class":280},"'shutdown'",[186,2294,1642],{"class":200},[186,2296,212],{"class":192},[186,2298,2299],{"class":200}," ():",[186,2301,891],{"class":204},[186,2303,269],{"class":200},[186,2305,2306,2308,2310,2313,2315,2317,2320,2322,2325,2328],{"class":188,"line":218},[186,2307,932],{"class":192},[186,2309,935],{"class":200},[186,2311,2312],{"class":208},"!defined",[186,2314,230],{"class":200},[186,2316,2265],{"class":280},[186,2318,2319],{"class":200},")",[186,2321,1251],{"class":208},[186,2323,2324],{"class":208}," !",[186,2326,2327],{"class":200},"POMIAR_ZAPYTAN)",[186,2329,269],{"class":200},[186,2331,2332,2334],{"class":188,"line":239},[186,2333,985],{"class":192},[186,2335,532],{"class":200},[186,2337,2338],{"class":188,"line":245},[186,2339,993],{"class":200},[186,2341,2342],{"class":188,"line":252},[186,2343,249],{"emptyLinePlaceholder":248},[186,2345,2346,2349,2351,2354],{"class":188,"line":272},[186,2347,2348],{"class":208},"  error_log",[186,2350,230],{"class":200},[186,2352,2353],{"class":208},"sprintf",[186,2355,2356],{"class":200},"(\n",[186,2358,2359,2362],{"class":188,"line":292},[186,2360,2361],{"class":280},"    '%s — %d zapytań, %s s'",[186,2363,304],{"class":200},[186,2365,2366,2369,2371,2374,2376,2379,2381,2384,2387,2390,2393],{"class":188,"line":307},[186,2367,2368],{"class":196},"    sanitize_text_field",[186,2370,230],{"class":200},[186,2372,2373],{"class":196},"wp_unslash",[186,2375,230],{"class":200},[186,2377,2378],{"class":295},"$_SERVER",[186,2380,2102],{"class":200},[186,2382,2383],{"class":280},"'REQUEST_URI'",[186,2385,2386],{"class":200},"]",[186,2388,2389],{"class":200}," ??",[186,2391,2392],{"class":280}," '-'",[186,2394,2395],{"class":200},")),\n",[186,2397,2398,2401],{"class":188,"line":319},[186,2399,2400],{"class":196},"    get_num_queries",[186,2402,2403],{"class":200},"(),\n",[186,2405,2406,2409,2411,2413,2415,2418],{"class":188,"line":331},[186,2407,2408],{"class":196},"    timer_stop",[186,2410,230],{"class":200},[186,2412,1811],{"class":351},[186,2414,1642],{"class":200},[186,2416,2417],{"class":351}," 3",[186,2419,2021],{"class":200},[186,2421,2422],{"class":188,"line":343},[186,2423,2424],{"class":200},"  ));\n",[186,2426,2427],{"class":188,"line":357},[186,2428,2429],{"class":200},"});\n",[11,2431,2432,2433,2436,2437,2439,2440,2443],{},"Wynik ląduje w ",[53,2434,2435],{},"error_log"," obok ",[53,2438,2248],{}," albo w logu skonfigurowanym przez ",[53,2441,2442],{},"WP_DEBUG_LOG",". Odśwież kilka podstron i porównaj liczby. Zdrowa strona na WordPressie z kilkoma wtyczkami mieści się w przedziale 30–80 zapytań. Trzysta na każdej podstronie oznacza, że coś liczy w pętli.",[11,2445,2446,2447,2450],{},"Dwie uwagi do tego licznika. Po włączeniu stałej ",[15,2448,2449],{},"loguje każde żądanie",", także boty, więc zostawiony na dobę sam urośnie w problem — zapal go na kilka minut i zgaś.",[11,2452,2453,2454,2457,2458,2461],{},"I rzecz ważniejsza: mierz ",[15,2455,2456],{},"wyjście anonimowe",", nie własną sesję. Odruch podpowiada bramkę na ",[53,2459,2460],{},"current_user_can('manage_options')",", ale zalogowany administrator ma pasek administracyjny, sprawdzanie uprawnień, powiadomienia o aktualizacjach i omija cache stron — to dwadzieścia parę zapytań ponad to, co widzi zwykły odwiedzający. Do porównania „przed i po\" różnica się skraca, ale liczby bezwzględne przestają się odnosić do przedziału wyżej. Otwórz stronę w oknie prywatnym.",[11,2463,2464,2465,2468],{},"Gdy już wiesz, że jest źle, do znalezienia miejsca służy ",[15,2466,2467],{},"Query Monitor",". Zakładka „Queries\" grupuje zapytania po funkcji wywołującej i po komponencie, więc filtr motywu wychodzi na wierzch bez czytania kodu. Wtyczkę instalujesz na czas diagnozy i zdejmujesz po niej — sama dokłada kilkanaście zapytań.",[26,2470,2472],{"id":2471},"filtr-menu-co-robił-kod","Filtr menu: co robił kod",[11,2474,2475],{},"Motyw miał w menu jedną pozycję wskazującą na kategorię z własnej taksonomii, a pod nią miało się rozwijać całe drzewo podkategorii. Autor rozwiązał to filtrem, który dla każdego węzła pyta bazę o jego dzieci:",[177,2477,2479],{"className":2251,"code":2478,"language":2253,"meta":182,"style":182},"add_filter('wp_nav_menu_objects', 'motyw_dobuduj_kategorie');\n\nfunction motyw_dobuduj_kategorie(array $pozycje): array {\n  foreach ($pozycje as $pozycja) {\n    if ($pozycja->object !== 'oferta_kategoria') {\n      continue;\n    }\n\n    $pozycje = array_merge($pozycje, motyw_galezie((int) $pozycja->object_id, $pozycja->ID));\n  }\n\n  return $pozycje;\n}\n\nfunction motyw_galezie(int $rodzic, int $rodzicMenu): array {\n  $dzieci = get_terms([\n    'taxonomy'   => 'oferta_kategoria',\n    'parent'     => $rodzic,\n    'hide_empty' => false,\n  ]);\n\n  $wynik = [];\n\n  foreach ($dzieci as $dziecko) {\n    $wnuki = get_terms([\n      'taxonomy'   => 'oferta_kategoria',\n      'parent'     => $dziecko->term_id,\n      'hide_empty' => false,\n      'fields'     => 'ids',\n    ]);\n\n    $pozycja = motyw_pozycja_menu($dziecko, $rodzicMenu);\n    $pozycja->classes[] = $wnuki ? 'menu-item-has-children' : '';\n    $pozycja->url = get_term_link($dziecko);\n\n    $wynik[] = $pozycja;\n    $wynik = array_merge($wynik, motyw_galezie($dziecko->term_id, $pozycja->ID));\n  }\n\n  return $wynik;\n}\n",[53,2480,2481,2498,2502,2524,2542,2565,2572,2577,2581,2628,2632,2636,2645,2649,2653,2679,2692,2704,2716,2727,2732,2736,2746,2750,2766,2777,2788,2806,2818,2831,2837,2842,2863,2892,2913,2918,2932,2968,2973,2978,2988],{"__ignoreMap":182},[186,2482,2483,2486,2488,2491,2493,2496],{"class":188,"line":189},[186,2484,2485],{"class":196},"add_filter",[186,2487,230],{"class":200},[186,2489,2490],{"class":280},"'wp_nav_menu_objects'",[186,2492,1642],{"class":200},[186,2494,2495],{"class":280}," 'motyw_dobuduj_kategorie'",[186,2497,236],{"class":200},[186,2499,2500],{"class":188,"line":218},[186,2501,249],{"emptyLinePlaceholder":248},[186,2503,2504,2507,2510,2512,2515,2518,2520,2522],{"class":188,"line":239},[186,2505,2506],{"class":192},"function",[186,2508,2509],{"class":196}," motyw_dobuduj_kategorie",[186,2511,230],{"class":200},[186,2513,2514],{"class":204},"array",[186,2516,2517],{"class":295}," $pozycje",[186,2519,420],{"class":200},[186,2521,2514],{"class":204},[186,2523,269],{"class":200},[186,2525,2526,2529,2531,2534,2537,2540],{"class":188,"line":245},[186,2527,2528],{"class":192},"  foreach",[186,2530,935],{"class":200},[186,2532,2533],{"class":295},"$pozycje",[186,2535,2536],{"class":200}," as ",[186,2538,2539],{"class":295},"$pozycja",[186,2541,953],{"class":200},[186,2543,2544,2547,2549,2551,2554,2557,2560,2563],{"class":188,"line":252},[186,2545,2546],{"class":192},"    if",[186,2548,935],{"class":200},[186,2550,2539],{"class":295},[186,2552,2553],{"class":200},"->",[186,2555,2556],{"class":295},"object",[186,2558,2559],{"class":208}," !==",[186,2561,2562],{"class":280}," 'oferta_kategoria'",[186,2564,953],{"class":200},[186,2566,2567,2570],{"class":188,"line":272},[186,2568,2569],{"class":192},"      continue",[186,2571,532],{"class":200},[186,2573,2574],{"class":188,"line":292},[186,2575,2576],{"class":200},"    }\n",[186,2578,2579],{"class":188,"line":307},[186,2580,249],{"emptyLinePlaceholder":248},[186,2582,2583,2586,2588,2591,2593,2595,2597,2600,2603,2606,2608,2611,2613,2616,2618,2620,2622,2625],{"class":188,"line":319},[186,2584,2585],{"class":295},"    $pozycje",[186,2587,209],{"class":208},[186,2589,2590],{"class":208}," array_merge",[186,2592,230],{"class":200},[186,2594,2533],{"class":295},[186,2596,1642],{"class":200},[186,2598,2599],{"class":196}," motyw_galezie",[186,2601,2602],{"class":200},"((",[186,2604,2605],{"class":192},"int",[186,2607,2319],{"class":200},[186,2609,2610],{"class":295}," $pozycja",[186,2612,2553],{"class":200},[186,2614,2615],{"class":295},"object_id",[186,2617,1642],{"class":200},[186,2619,2610],{"class":295},[186,2621,2553],{"class":200},[186,2623,2624],{"class":295},"ID",[186,2626,2627],{"class":200},"));\n",[186,2629,2630],{"class":188,"line":331},[186,2631,993],{"class":200},[186,2633,2634],{"class":188,"line":343},[186,2635,249],{"emptyLinePlaceholder":248},[186,2637,2638,2641,2643],{"class":188,"line":357},[186,2639,2640],{"class":192},"  return",[186,2642,2517],{"class":295},[186,2644,532],{"class":200},[186,2646,2647],{"class":188,"line":363},[186,2648,366],{"class":200},[186,2650,2651],{"class":188,"line":990},[186,2652,249],{"emptyLinePlaceholder":248},[186,2654,2655,2657,2659,2661,2663,2666,2668,2670,2673,2675,2677],{"class":188,"line":996},[186,2656,2506],{"class":192},[186,2658,2599],{"class":196},[186,2660,230],{"class":200},[186,2662,2605],{"class":204},[186,2664,2665],{"class":295}," $rodzic",[186,2667,56],{"class":200},[186,2669,2605],{"class":204},[186,2671,2672],{"class":295}," $rodzicMenu",[186,2674,420],{"class":200},[186,2676,2514],{"class":204},[186,2678,269],{"class":200},[186,2680,2681,2684,2686,2689],{"class":188,"line":1001},[186,2682,2683],{"class":295},"  $dzieci",[186,2685,209],{"class":208},[186,2687,2688],{"class":196}," get_terms",[186,2690,2691],{"class":200},"([\n",[186,2693,2694,2697,2700,2702],{"class":188,"line":1018},[186,2695,2696],{"class":280},"    'taxonomy'",[186,2698,2699],{"class":200},"   =>",[186,2701,2562],{"class":280},[186,2703,304],{"class":200},[186,2705,2706,2709,2712,2714],{"class":188,"line":1023},[186,2707,2708],{"class":280},"    'parent'",[186,2710,2711],{"class":200},"     =>",[186,2713,2665],{"class":295},[186,2715,304],{"class":200},[186,2717,2718,2721,2723,2725],{"class":188,"line":1028},[186,2719,2720],{"class":280},"    'hide_empty'",[186,2722,2037],{"class":200},[186,2724,1512],{"class":351},[186,2726,304],{"class":200},[186,2728,2729],{"class":188,"line":1051},[186,2730,2731],{"class":200},"  ]);\n",[186,2733,2734],{"class":188,"line":1059},[186,2735,249],{"emptyLinePlaceholder":248},[186,2737,2738,2741,2743],{"class":188,"line":1070},[186,2739,2740],{"class":295},"  $wynik",[186,2742,209],{"class":208},[186,2744,2745],{"class":200}," [];\n",[186,2747,2748],{"class":188,"line":1103},[186,2749,249],{"emptyLinePlaceholder":248},[186,2751,2752,2754,2756,2759,2761,2764],{"class":188,"line":1108},[186,2753,2528],{"class":192},[186,2755,935],{"class":200},[186,2757,2758],{"class":295},"$dzieci",[186,2760,2536],{"class":200},[186,2762,2763],{"class":295},"$dziecko",[186,2765,953],{"class":200},[186,2767,2768,2771,2773,2775],{"class":188,"line":1113},[186,2769,2770],{"class":295},"    $wnuki",[186,2772,209],{"class":208},[186,2774,2688],{"class":196},[186,2776,2691],{"class":200},[186,2778,2779,2782,2784,2786],{"class":188,"line":1151},[186,2780,2781],{"class":280},"      'taxonomy'",[186,2783,2699],{"class":200},[186,2785,2562],{"class":280},[186,2787,304],{"class":200},[186,2789,2791,2794,2796,2799,2801,2804],{"class":188,"line":2790},27,[186,2792,2793],{"class":280},"      'parent'",[186,2795,2711],{"class":200},[186,2797,2798],{"class":295}," $dziecko",[186,2800,2553],{"class":200},[186,2802,2803],{"class":295},"term_id",[186,2805,304],{"class":200},[186,2807,2809,2812,2814,2816],{"class":188,"line":2808},28,[186,2810,2811],{"class":280},"      'hide_empty'",[186,2813,2037],{"class":200},[186,2815,1512],{"class":351},[186,2817,304],{"class":200},[186,2819,2821,2824,2826,2829],{"class":188,"line":2820},29,[186,2822,2823],{"class":280},"      'fields'",[186,2825,2711],{"class":200},[186,2827,2828],{"class":280}," 'ids'",[186,2830,304],{"class":200},[186,2832,2834],{"class":188,"line":2833},30,[186,2835,2836],{"class":200},"    ]);\n",[186,2838,2840],{"class":188,"line":2839},31,[186,2841,249],{"emptyLinePlaceholder":248},[186,2843,2845,2848,2850,2853,2855,2857,2859,2861],{"class":188,"line":2844},32,[186,2846,2847],{"class":295},"    $pozycja",[186,2849,209],{"class":208},[186,2851,2852],{"class":196}," motyw_pozycja_menu",[186,2854,230],{"class":200},[186,2856,2763],{"class":295},[186,2858,1642],{"class":200},[186,2860,2672],{"class":295},[186,2862,236],{"class":200},[186,2864,2866,2868,2870,2873,2876,2878,2881,2883,2886,2888,2890],{"class":188,"line":2865},33,[186,2867,2847],{"class":295},[186,2869,2553],{"class":200},[186,2871,2872],{"class":295},"classes",[186,2874,2875],{"class":200},"[] ",[186,2877,635],{"class":208},[186,2879,2880],{"class":295}," $wnuki",[186,2882,486],{"class":192},[186,2884,2885],{"class":280}," 'menu-item-has-children'",[186,2887,492],{"class":192},[186,2889,606],{"class":280},[186,2891,532],{"class":200},[186,2893,2895,2897,2899,2902,2904,2907,2909,2911],{"class":188,"line":2894},34,[186,2896,2847],{"class":295},[186,2898,2553],{"class":200},[186,2900,2901],{"class":295},"url",[186,2903,209],{"class":208},[186,2905,2906],{"class":196}," get_term_link",[186,2908,230],{"class":200},[186,2910,2763],{"class":295},[186,2912,236],{"class":200},[186,2914,2916],{"class":188,"line":2915},35,[186,2917,249],{"emptyLinePlaceholder":248},[186,2919,2921,2924,2926,2928,2930],{"class":188,"line":2920},36,[186,2922,2923],{"class":295},"    $wynik",[186,2925,2875],{"class":200},[186,2927,635],{"class":208},[186,2929,2610],{"class":295},[186,2931,532],{"class":200},[186,2933,2935,2937,2939,2941,2943,2946,2948,2950,2952,2954,2956,2958,2960,2962,2964,2966],{"class":188,"line":2934},37,[186,2936,2923],{"class":295},[186,2938,209],{"class":208},[186,2940,2590],{"class":208},[186,2942,230],{"class":200},[186,2944,2945],{"class":295},"$wynik",[186,2947,1642],{"class":200},[186,2949,2599],{"class":196},[186,2951,230],{"class":200},[186,2953,2763],{"class":295},[186,2955,2553],{"class":200},[186,2957,2803],{"class":295},[186,2959,1642],{"class":200},[186,2961,2610],{"class":295},[186,2963,2553],{"class":200},[186,2965,2624],{"class":295},[186,2967,2627],{"class":200},[186,2969,2971],{"class":188,"line":2970},38,[186,2972,993],{"class":200},[186,2974,2976],{"class":188,"line":2975},39,[186,2977,249],{"emptyLinePlaceholder":248},[186,2979,2981,2983,2986],{"class":188,"line":2980},40,[186,2982,2640],{"class":192},[186,2984,2985],{"class":295}," $wynik",[186,2987,532],{"class":200},[186,2989,2991],{"class":188,"line":2990},41,[186,2992,366],{"class":200},[11,2994,2995,2996,2999,3000,3003],{},"To wierny przedruk oryginału, razem z ",[53,2997,2998],{},"$pozycje = array_merge(…)"," w środku ",[53,3001,3002],{},"foreach"," po tej samej tablicy. Działa, bo PHP iteruje po kopii zrobionej przy wejściu w pętlę, ale wygląda na błąd i czyta się jak błąd. Zostawiam, bo tekst jest o zapytaniach, a nie o tym, co jeszcze dałoby się w tym kodzie posprzątać.",[11,3005,3006,3007,3010,3011,3014],{},"Każde ",[53,3008,3009],{},"get_terms()"," z parametrem ",[53,3012,3013],{},"parent"," to osobne zapytanie, bo zawęża wynik w SQL-u. Rachunek przy stu kategoriach w trzech poziomach wygląda tak:",[3016,3017,3018,3021,3024],"ul",{},[145,3019,3020],{},"jedno zapytanie na każdy węzeł, żeby pobrać jego dzieci — sto jeden razem z korzeniem",[145,3022,3023],{},"jedno zapytanie na każdą pozycję, żeby stwierdzić, czy ma dzieci — kolejne sto",[145,3025,3026,3029],{},[53,3027,3028],{},"get_term_link()"," dla każdej pozycji, które przy taksonomii hierarchicznej dochodzi po przodkach aż do korzenia",[11,3031,3032],{},"Drugi punkt boli najbardziej, bo odpowiedź na pytanie „czy ten termin ma dzieci\" siedzi już w wyniku pierwszego zapytania o poziom niżej. Pytanie zadaje się dwa razy.",[11,3034,3035,3036,3038,3039,3042,3043,3046],{},"I rzecz, która decyduje o skali: filtr ",[53,3037,2225],{}," odpala się przy ",[15,3040,3041],{},"każdym"," ",[53,3044,3045],{},"wp_nav_menu()",", czyli na każdej podstronie i przy każdym menu na niej. Nagłówek i stopka to już dwa przebiegi.",[26,3048,3050],{"id":3049},"dlaczego-cache-obiektów-tego-nie-łapie","Dlaczego cache obiektów tego nie łapie",[11,3052,3053,3054,3057],{},"Odruchowa odpowiedź brzmi „przecież WordPress cachuje terminy\". Cachuje, tylko ten cache żyje w obrębie jednego żądania PHP. Domyślna implementacja ",[53,3055,3056],{},"WP_Object_Cache"," trzyma dane w tablicy w pamięci procesu i znika razem z nim.",[11,3059,3060,3061,3063],{},"Pomaga to przy powtórzonych odczytach tego samego terminu w jednym żądaniu. Nie pomaga tutaj, bo każde ",[53,3062,3009],{}," ma inny zestaw argumentów, a więc inny klucz cache. Pierwsze wywołanie każdego z nich i tak idzie do bazy.",[11,3065,3066],{},"Cache przeżywa żądanie dopiero z persystentnym backendem: Redis albo Memcached z odpowiednią wtyczką drop-in. Na hostingu współdzielonym zwykle nie ma ani jednego, ani drugiego. Zakładaj, że przy każdym wejściu użytkownika kod startuje z pustym cache.",[26,3068,3070],{"id":3069},"poprawka-drzewo-składane-w-php","Poprawka: drzewo składane w PHP",[11,3072,3073,3075,3076,3078,3079,3081],{},[53,3074,3009],{}," bez parametru ",[53,3077,3013],{}," zwraca całą taksonomię jednym zapytaniem i przy okazji wrzuca wszystkie terminy do cache obiektów. Relacje rodzic–dziecko są w polu ",[53,3080,3013],{}," każdego z nich, więc drzewo składa się w PHP bez dotykania bazy:",[177,3083,3085],{"className":2251,"code":3084,"language":2253,"meta":182,"style":182},"function motyw_drzewo_kategorii(): array {\n  $klucz = 'motyw_drzewo_oferta_kategoria';\n  $drzewo = get_transient($klucz);\n\n  if ($drzewo !== false) {\n    return $drzewo;\n  }\n\n  $terminy = get_terms([\n    'taxonomy'   => 'oferta_kategoria',\n    'hide_empty' => false,\n    'pad_counts' => true,\n  ]);\n\n  if (is_wp_error($terminy)) {\n    return [];\n  }\n\n  $drzewo = [];\n\n  foreach ($terminy as $termin) {\n    if ((int) $termin->count === 0) {\n      continue;\n    }\n\n    $drzewo[$termin->parent][] = [\n      'id'    => $termin->term_id,\n      'nazwa' => $termin->name,\n      'url'   => get_term_link($termin),\n    ];\n  }\n\n  set_transient($klucz, $drzewo, DAY_IN_SECONDS);\n\n  return $drzewo;\n}\n",[53,3086,3087,3100,3112,3129,3133,3148,3157,3161,3165,3176,3186,3196,3207,3211,3215,3232,3238,3242,3246,3254,3258,3273,3298,3304,3308,3312,3333,3349,3365,3383,3388,3392,3396,3414,3418,3426],{"__ignoreMap":182},[186,3088,3089,3091,3094,3096,3098],{"class":188,"line":189},[186,3090,2506],{"class":192},[186,3092,3093],{"class":196}," motyw_drzewo_kategorii",[186,3095,263],{"class":200},[186,3097,2514],{"class":204},[186,3099,269],{"class":200},[186,3101,3102,3105,3107,3110],{"class":188,"line":218},[186,3103,3104],{"class":295},"  $klucz",[186,3106,209],{"class":208},[186,3108,3109],{"class":280}," 'motyw_drzewo_oferta_kategoria'",[186,3111,532],{"class":200},[186,3113,3114,3117,3119,3122,3124,3127],{"class":188,"line":239},[186,3115,3116],{"class":295},"  $drzewo",[186,3118,209],{"class":208},[186,3120,3121],{"class":196}," get_transient",[186,3123,230],{"class":200},[186,3125,3126],{"class":295},"$klucz",[186,3128,236],{"class":200},[186,3130,3131],{"class":188,"line":245},[186,3132,249],{"emptyLinePlaceholder":248},[186,3134,3135,3137,3139,3142,3144,3146],{"class":188,"line":252},[186,3136,932],{"class":192},[186,3138,935],{"class":200},[186,3140,3141],{"class":295},"$drzewo",[186,3143,2559],{"class":208},[186,3145,1512],{"class":351},[186,3147,953],{"class":200},[186,3149,3150,3152,3155],{"class":188,"line":272},[186,3151,985],{"class":192},[186,3153,3154],{"class":295}," $drzewo",[186,3156,532],{"class":200},[186,3158,3159],{"class":188,"line":292},[186,3160,993],{"class":200},[186,3162,3163],{"class":188,"line":307},[186,3164,249],{"emptyLinePlaceholder":248},[186,3166,3167,3170,3172,3174],{"class":188,"line":319},[186,3168,3169],{"class":295},"  $terminy",[186,3171,209],{"class":208},[186,3173,2688],{"class":196},[186,3175,2691],{"class":200},[186,3177,3178,3180,3182,3184],{"class":188,"line":331},[186,3179,2696],{"class":280},[186,3181,2699],{"class":200},[186,3183,2562],{"class":280},[186,3185,304],{"class":200},[186,3187,3188,3190,3192,3194],{"class":188,"line":343},[186,3189,2720],{"class":280},[186,3191,2037],{"class":200},[186,3193,1512],{"class":351},[186,3195,304],{"class":200},[186,3197,3198,3201,3203,3205],{"class":188,"line":357},[186,3199,3200],{"class":280},"    'pad_counts'",[186,3202,2037],{"class":200},[186,3204,737],{"class":351},[186,3206,304],{"class":200},[186,3208,3209],{"class":188,"line":363},[186,3210,2731],{"class":200},[186,3212,3213],{"class":188,"line":990},[186,3214,249],{"emptyLinePlaceholder":248},[186,3216,3217,3219,3221,3224,3226,3229],{"class":188,"line":996},[186,3218,932],{"class":192},[186,3220,935],{"class":200},[186,3222,3223],{"class":196},"is_wp_error",[186,3225,230],{"class":200},[186,3227,3228],{"class":295},"$terminy",[186,3230,3231],{"class":200},")) {\n",[186,3233,3234,3236],{"class":188,"line":1001},[186,3235,985],{"class":192},[186,3237,2745],{"class":200},[186,3239,3240],{"class":188,"line":1018},[186,3241,993],{"class":200},[186,3243,3244],{"class":188,"line":1023},[186,3245,249],{"emptyLinePlaceholder":248},[186,3247,3248,3250,3252],{"class":188,"line":1028},[186,3249,3116],{"class":295},[186,3251,209],{"class":208},[186,3253,2745],{"class":200},[186,3255,3256],{"class":188,"line":1051},[186,3257,249],{"emptyLinePlaceholder":248},[186,3259,3260,3262,3264,3266,3268,3271],{"class":188,"line":1059},[186,3261,2528],{"class":192},[186,3263,935],{"class":200},[186,3265,3228],{"class":295},[186,3267,2536],{"class":200},[186,3269,3270],{"class":295},"$termin",[186,3272,953],{"class":200},[186,3274,3275,3277,3280,3282,3284,3286,3288,3291,3293,3296],{"class":188,"line":1070},[186,3276,2546],{"class":192},[186,3278,3279],{"class":200}," ((",[186,3281,2605],{"class":192},[186,3283,1439],{"class":200},[186,3285,3270],{"class":295},[186,3287,2553],{"class":200},[186,3289,3290],{"class":295},"count",[186,3292,480],{"class":208},[186,3294,3295],{"class":351}," 0",[186,3297,953],{"class":200},[186,3299,3300,3302],{"class":188,"line":1103},[186,3301,2569],{"class":192},[186,3303,532],{"class":200},[186,3305,3306],{"class":188,"line":1108},[186,3307,2576],{"class":200},[186,3309,3310],{"class":188,"line":1113},[186,3311,249],{"emptyLinePlaceholder":248},[186,3313,3314,3317,3319,3321,3323,3325,3328,3330],{"class":188,"line":1151},[186,3315,3316],{"class":295},"    $drzewo",[186,3318,2102],{"class":200},[186,3320,3270],{"class":295},[186,3322,2553],{"class":200},[186,3324,3013],{"class":295},[186,3326,3327],{"class":200},"][] ",[186,3329,635],{"class":208},[186,3331,3332],{"class":200}," [\n",[186,3334,3335,3338,3341,3343,3345,3347],{"class":188,"line":2790},[186,3336,3337],{"class":280},"      'id'",[186,3339,3340],{"class":200},"    => ",[186,3342,3270],{"class":295},[186,3344,2553],{"class":200},[186,3346,2803],{"class":295},[186,3348,304],{"class":200},[186,3350,3351,3354,3357,3359,3361,3363],{"class":188,"line":2808},[186,3352,3353],{"class":280},"      'nazwa'",[186,3355,3356],{"class":200}," => ",[186,3358,3270],{"class":295},[186,3360,2553],{"class":200},[186,3362,2060],{"class":295},[186,3364,304],{"class":200},[186,3366,3367,3370,3373,3376,3378,3380],{"class":188,"line":2820},[186,3368,3369],{"class":280},"      'url'",[186,3371,3372],{"class":200},"   => ",[186,3374,3375],{"class":196},"get_term_link",[186,3377,230],{"class":200},[186,3379,3270],{"class":295},[186,3381,3382],{"class":200},"),\n",[186,3384,3385],{"class":188,"line":2833},[186,3386,3387],{"class":200},"    ];\n",[186,3389,3390],{"class":188,"line":2839},[186,3391,993],{"class":200},[186,3393,3394],{"class":188,"line":2844},[186,3395,249],{"emptyLinePlaceholder":248},[186,3397,3398,3401,3403,3405,3407,3409,3411],{"class":188,"line":2865},[186,3399,3400],{"class":196},"  set_transient",[186,3402,230],{"class":200},[186,3404,3126],{"class":295},[186,3406,1642],{"class":200},[186,3408,3154],{"class":295},[186,3410,1642],{"class":200},[186,3412,3413],{"class":200}," DAY_IN_SECONDS);\n",[186,3415,3416],{"class":188,"line":2894},[186,3417,249],{"emptyLinePlaceholder":248},[186,3419,3420,3422,3424],{"class":188,"line":2915},[186,3421,2640],{"class":192},[186,3423,3154],{"class":295},[186,3425,532],{"class":200},[186,3427,3428],{"class":188,"line":2920},[186,3429,366],{"class":200},[11,3431,3432,3433,3436,3437,3440],{},"Tablica jest indeksowana identyfikatorem rodzica, więc dzieci dowolnego węzła to ",[53,3434,3435],{},"$drzewo[$id] ?? []",", a pytanie „czy ma dzieci\" to ",[53,3438,3439],{},"isset($drzewo[$id])",". Oba bez zapytania.",[11,3442,3443,3445,3446,3448,3449,3452,3453,3455],{},[53,3444,3028],{}," w pętli nie wraca po nic do bazy, bo ",[53,3447,3009],{}," zdążyło już zapełnić cache terminów w tym żądaniu. Po co tu ",[53,3450,3451],{},"pad_counts"," i dlaczego bez niego filtr po ",[53,3454,3290],{}," jest błędny — dwie sekcje niżej.",[11,3457,3458],{},"Rekurencja zostaje, ale chodzi po tablicy w pamięci:",[177,3460,3462],{"className":2251,"code":3461,"language":2253,"meta":182,"style":182},"function motyw_galezie(array $drzewo, int $rodzic, int $rodzicMenu): array {\n  $wynik = [];\n\n  foreach ($drzewo[$rodzic] ?? [] as $dziecko) {\n    $pozycja = motyw_pozycja_menu($dziecko, $rodzicMenu);\n\n    if (isset($drzewo[$dziecko['id']])) {\n      $pozycja->classes[] = 'menu-item-has-children';\n    }\n\n    $wynik[] = $pozycja;\n\n    foreach (motyw_galezie($drzewo, $dziecko['id'], $pozycja->ID) as $wnuk) {\n      $wynik[] = $wnuk;\n    }\n  }\n\n  return $wynik;\n}\n",[53,3463,3464,3494,3502,3506,3526,3544,3548,3573,3590,3594,3598,3610,3614,3652,3666,3670,3674,3678,3686],{"__ignoreMap":182},[186,3465,3466,3468,3470,3472,3474,3476,3478,3480,3482,3484,3486,3488,3490,3492],{"class":188,"line":189},[186,3467,2506],{"class":192},[186,3469,2599],{"class":196},[186,3471,230],{"class":200},[186,3473,2514],{"class":204},[186,3475,3154],{"class":295},[186,3477,56],{"class":200},[186,3479,2605],{"class":204},[186,3481,2665],{"class":295},[186,3483,56],{"class":200},[186,3485,2605],{"class":204},[186,3487,2672],{"class":295},[186,3489,420],{"class":200},[186,3491,2514],{"class":204},[186,3493,269],{"class":200},[186,3495,3496,3498,3500],{"class":188,"line":218},[186,3497,2740],{"class":295},[186,3499,209],{"class":208},[186,3501,2745],{"class":200},[186,3503,3504],{"class":188,"line":239},[186,3505,249],{"emptyLinePlaceholder":248},[186,3507,3508,3510,3512,3514,3516,3519,3522,3524],{"class":188,"line":245},[186,3509,2528],{"class":192},[186,3511,935],{"class":200},[186,3513,3141],{"class":295},[186,3515,2102],{"class":200},[186,3517,3518],{"class":295},"$rodzic",[186,3520,3521],{"class":200},"] ?? [] as ",[186,3523,2763],{"class":295},[186,3525,953],{"class":200},[186,3527,3528,3530,3532,3534,3536,3538,3540,3542],{"class":188,"line":252},[186,3529,2847],{"class":295},[186,3531,209],{"class":208},[186,3533,2852],{"class":196},[186,3535,230],{"class":200},[186,3537,2763],{"class":295},[186,3539,1642],{"class":200},[186,3541,2672],{"class":295},[186,3543,236],{"class":200},[186,3545,3546],{"class":188,"line":272},[186,3547,249],{"emptyLinePlaceholder":248},[186,3549,3550,3552,3554,3557,3559,3561,3563,3565,3567,3570],{"class":188,"line":292},[186,3551,2546],{"class":192},[186,3553,935],{"class":200},[186,3555,3556],{"class":208},"isset",[186,3558,230],{"class":200},[186,3560,3141],{"class":295},[186,3562,2102],{"class":200},[186,3564,2763],{"class":295},[186,3566,2102],{"class":200},[186,3568,3569],{"class":280},"'id'",[186,3571,3572],{"class":200},"]])) {\n",[186,3574,3575,3578,3580,3582,3584,3586,3588],{"class":188,"line":307},[186,3576,3577],{"class":295},"      $pozycja",[186,3579,2553],{"class":200},[186,3581,2872],{"class":295},[186,3583,2875],{"class":200},[186,3585,635],{"class":208},[186,3587,2885],{"class":280},[186,3589,532],{"class":200},[186,3591,3592],{"class":188,"line":319},[186,3593,2576],{"class":200},[186,3595,3596],{"class":188,"line":331},[186,3597,249],{"emptyLinePlaceholder":248},[186,3599,3600,3602,3604,3606,3608],{"class":188,"line":343},[186,3601,2923],{"class":295},[186,3603,2875],{"class":200},[186,3605,635],{"class":208},[186,3607,2610],{"class":295},[186,3609,532],{"class":200},[186,3611,3612],{"class":188,"line":357},[186,3613,249],{"emptyLinePlaceholder":248},[186,3615,3616,3619,3621,3624,3626,3628,3630,3632,3634,3636,3638,3640,3642,3644,3647,3650],{"class":188,"line":363},[186,3617,3618],{"class":192},"    foreach",[186,3620,935],{"class":200},[186,3622,3623],{"class":196},"motyw_galezie",[186,3625,230],{"class":200},[186,3627,3141],{"class":295},[186,3629,1642],{"class":200},[186,3631,2798],{"class":295},[186,3633,2102],{"class":200},[186,3635,3569],{"class":280},[186,3637,1814],{"class":200},[186,3639,2610],{"class":295},[186,3641,2553],{"class":200},[186,3643,2624],{"class":295},[186,3645,3646],{"class":200},") as ",[186,3648,3649],{"class":295},"$wnuk",[186,3651,953],{"class":200},[186,3653,3654,3657,3659,3661,3664],{"class":188,"line":990},[186,3655,3656],{"class":295},"      $wynik",[186,3658,2875],{"class":200},[186,3660,635],{"class":208},[186,3662,3663],{"class":295}," $wnuk",[186,3665,532],{"class":200},[186,3667,3668],{"class":188,"line":996},[186,3669,2576],{"class":200},[186,3671,3672],{"class":188,"line":1001},[186,3673,993],{"class":200},[186,3675,3676],{"class":188,"line":1018},[186,3677,249],{"emptyLinePlaceholder":248},[186,3679,3680,3682,3684],{"class":188,"line":1023},[186,3681,2640],{"class":192},[186,3683,2985],{"class":295},[186,3685,532],{"class":200},[186,3687,3688],{"class":188,"line":1028},[186,3689,366],{"class":200},[11,3691,3692,3693,3695],{},"Dwieście zapytań schodzi do kilku: jedno po terminy, jedno do dwóch od ",[53,3694,3451],{},", plus odczyt i zapis transientu. Każde następne wyświetlenie strony kosztuje już tylko sam odczyt transientu.",[11,3697,3698,3699,3042,3702,3705,3706,3709,3710,3713,3714,3717],{},"To jest dobre miejsce, żeby nie zaokrąglić w swoją stronę. ",[15,3700,3701],{},"Transient nie kosztuje zera zapytań.",[53,3703,3704],{},"set_transient()"," z czasem wygaśnięcia zapisuje dwie opcje — wartość i ",[53,3707,3708],{},"_transient_timeout_…"," — obie z ",[53,3711,3712],{},"autoload"," ustawionym na „nie\". Nie jadą więc w zbiorczym zapytaniu o opcje autoładowane, tylko ",[53,3715,3716],{},"get_transient()"," pobiera po kolei timeout i wartość. To dwa zapytania na żądanie zamiast dwustu.",[11,3719,3720,3721,3723],{},"Można zejść do zera, zapisując transient bez czasu wygaśnięcia — wtedy opcja jest autoładowana i przyjeżdża razem z resztą. Cena jest taka, że drzewo ładuje się przy ",[15,3722,3041],{}," żądaniu, także przy tych, w których menu nikomu nie jest potrzebne, i znika zabezpieczenie na wypadek nieunieważnionej zmiany. Przy stu kategoriach ta zamiana się nie opłaca.",[11,3725,3726,3727,3730],{},"Wersja sprzed poprawki dokładała do rekurencji ",[53,3728,3729],{},"array_merge($wynik, …)",", co na każdym poziomie kopiuje całą zebraną dotąd tablicę. Przy stu pozycjach to koszt pomijalny obok dwustu zapytań, ale w kodzie, który właśnie przepisujesz pod wydajność, zostawianie tego wygląda niepoważnie. Dopisywanie po jednym elemencie kosztuje tyle samo linii.",[1329,3732,3734],{"id":3733},"czego-walker-wymaga-od-dokładanej-pozycji","Czego walker wymaga od dokładanej pozycji",[11,3736,3737,3740,3741,3747,3748,3750,3751,3753],{},[53,3738,3739],{},"motyw_pozycja_menu()"," chowa w moich przykładach składanie obiektu pozycji i to jest miejsce, w którym najłatwiej się przejechać. Pozycje wstawiane przez ten filtr ",[15,3742,3743,3744],{},"nie przeszły przez ",[53,3745,3746],{},"_wp_menu_item_classes_by_context()",", bo ta funkcja odpala się w ",[53,3749,3045],{}," wcześniej, przed ",[53,3752,2225],{},". Wszystko, co ona normalnie dokłada, musisz dołożyć sam.",[11,3755,3756],{},"Trzy pola, o których zapomina się najczęściej:",[177,3758,3760],{"className":2251,"code":3759,"language":2253,"meta":182,"style":182},"$pozycja->current               = false;\n$pozycja->current_item_ancestor = false;\n$pozycja->current_item_parent   = false;\n",[53,3761,3762,3778,3793],{"__ignoreMap":182},[186,3763,3764,3766,3768,3771,3774,3776],{"class":188,"line":189},[186,3765,2539],{"class":295},[186,3767,2553],{"class":200},[186,3769,3770],{"class":295},"current",[186,3772,3773],{"class":208},"               =",[186,3775,1512],{"class":351},[186,3777,532],{"class":200},[186,3779,3780,3782,3784,3787,3789,3791],{"class":188,"line":218},[186,3781,2539],{"class":295},[186,3783,2553],{"class":200},[186,3785,3786],{"class":295},"current_item_ancestor",[186,3788,209],{"class":208},[186,3790,1512],{"class":351},[186,3792,532],{"class":200},[186,3794,3795,3797,3799,3802,3805,3807],{"class":188,"line":239},[186,3796,2539],{"class":295},[186,3798,2553],{"class":200},[186,3800,3801],{"class":295},"current_item_parent",[186,3803,3804],{"class":208},"   =",[186,3806,1512],{"class":351},[186,3808,532],{"class":200},[11,3810,3811,3814,3815,3818,3819,3821],{},[53,3812,3813],{},"Walker_Nav_Menu::start_el()"," czyta ",[53,3816,3817],{},"$menu_item->current"," bez sprawdzania, czy pole istnieje. Brak tych trzech linii nie psuje menu wizualnie i dlatego przechodzi niezauważony — za to sypie ostrzeżeniem na każdą pozycję przy każdym renderze. W opisywanym motywie dawało to około czterech tysięcy wpisów do ",[53,3820,2435],{}," na dobę.",[11,3823,3824,3825,56,3827,56,3830,56,3833,56,3835,56,3837,56,3840,56,3843,56,3845,56,3847,56,3850,56,3853,116,3856,3859,3860,3863],{},"Poza tym pozycja potrzebuje kompletu pól, po których chodzi walker: ",[53,3826,2624],{},[53,3828,3829],{},"db_id",[53,3831,3832],{},"menu_item_parent",[53,3834,2556],{},[53,3836,2615],{},[53,3838,3839],{},"type",[53,3841,3842],{},"title",[53,3844,2901],{},[53,3846,2872],{},[53,3848,3849],{},"target",[53,3851,3852],{},"attr_title",[53,3854,3855],{},"xfn",[53,3857,3858],{},"description",". Najprościej wziąć ",[53,3861,3862],{},"(object) []"," i wypełnić wszystkie, zamiast liczyć na to, że walker sobie poradzi z brakami.",[11,3865,3866,3869,3870,3873,3874,3876,3877,3880,3881,3884],{},[15,3867,3868],{},"Uwaga przy przepisywaniu: ta funkcja zmienia typ argumentu."," W wersji sprzed poprawki dostawała obiekt ",[53,3871,3872],{},"WP_Term"," prosto z ",[53,3875,3009],{},", w wersji po — tablicę z sześcioma polami, którą składa ",[53,3878,3879],{},"motyw_drzewo_kategorii()",". Nazwa została ta sama, więc łatwo zostawić starą implementację i dostać błąd przy pierwszym ",[53,3882,3883],{},"$termin->name",". Przepisz ją razem z resztą albo nazwij inaczej.",[11,3886,3887,3888,3891],{},"I tu domyka się historia z ",[2231,3889,3890],{"href":2233},"wpisu o limicie CPU",": sześć tysięcy warningów na dobę, które wyglądały tam na najgorętszy trop, brało się w większości właśnie z tego. Trop był fałszywy co do przyczyny przeciążenia i prawdziwy co do tego, że w motywie siedzi błąd.",[1329,3893,3895,3897],{"id":3894},"pad_counts-to-jedna-linia-bez-której-logika-się-wywraca",[53,3896,3451],{}," to jedna linia, bez której logika się wywraca",[11,3899,3900,3901,3904,3905,3908,3909,3911,3912,3915,3916,71],{},"Oryginał miał ",[53,3902,3903],{},"hide_empty => true"," i puste gałęzie odpadały same, w SQL-u. Po przejściu na ",[53,3906,3907],{},"hide_empty => false"," filtrowanie przenosi się do PHP i tu czeka pułapka: pole ",[53,3910,3290],{}," liczy domyślnie ",[15,3913,3914],{},"wyłącznie wpisy przypisane wprost do danego terminu",". Kategoria nadrzędna, która sama nie ma żadnego wpisu, ale jej podkategorie mają komplet, dostaje ",[53,3917,3918],{},"count === 0",[11,3920,3921],{},"Odfiltrowanie po takim liczniku wycina ją z menu razem z całą gałęzią pod spodem. Na instalacji deweloperskiej, gdzie wpisy siedzą byle gdzie, tego nie widać — na produkcji z uporządkowaną strukturą znika pół nawigacji.",[11,3923,3924,3927,3928,3930,3931,3933],{},[53,3925,3926],{},"pad_counts => true"," każe WordPressowi zsumować liczniki rekurencyjnie w dół drzewa, więc rodzic dostaje sumę swoich potomków. Dopiero wtedy ",[53,3929,3290],{}," znaczy to samo co przy ",[53,3932,3903],{}," i dopiero wtedy wolno po nim filtrować.",[1329,3935,3937],{"id":3936},"sprawdź-czy-taksonomia-ma-w-ogóle-przyjazne-adresy","Sprawdź, czy taksonomia ma w ogóle przyjazne adresy",[11,3939,3940,3942,3943,3946,3947,3950],{},[53,3941,3028],{}," przy taksonomii zarejestrowanej z ",[53,3944,3945],{},"'rewrite' => false"," zwraca adres z parametrem zapytania, w rodzaju ",[53,3948,3949],{},"/?oferta_kategoria=szkolenia",". To nie jest błąd tej funkcji — taksonomia po prostu nie ma własnej struktury adresów.",[11,3952,3953,3954,3956,3957,3960,3961,3964],{},"Kod, który na wyniku ",[53,3955,3028],{}," szuka potem fragmentu ścieżki przez ",[53,3958,3959],{},"strpos()",", nigdy się w takim układzie nie dopasuje i po cichu leci gałęzią awaryjną. Zapytań to nie kosztuje, bo cache jest już pełny, ale to sto wywołań funkcji, której wynik ląduje w koszu, i mylący trop dla następnej osoby czytającej ten plik. Zajrzyj do ",[53,3962,3963],{},"register_taxonomy()",", zanim zaczniesz budować ścieżki z tego, co zwraca.",[1329,3966,3968],{"id":3967},"unieważnianie-transientu","Unieważnianie transientu",[11,3970,3971],{},"Transient bez unieważniania to nowy błąd w miejsce starego: redaktor zmienia nazwę kategorii i przez dobę nie widzi efektu. Drzewo zmienia się przy dwóch rodzajach zdarzeń — przy edycji samych terminów i przy zmianie tego, co jest do nich przypisane, bo od przypisań zależą liczniki:",[177,3973,3975],{"className":2251,"code":3974,"language":2253,"meta":182,"style":182},"add_action('created_oferta_kategoria', 'motyw_wyczysc_drzewo');\nadd_action('edited_oferta_kategoria', 'motyw_wyczysc_drzewo');\nadd_action('delete_oferta_kategoria', 'motyw_wyczysc_drzewo');\n\nadd_action('set_object_terms', function ($id, $terminy, $tt, $taksonomia): void {\n  if ($taksonomia === 'oferta_kategoria') {\n    motyw_wyczysc_drzewo();\n  }\n}, 10, 4);\n\nadd_action('transition_post_status', function ($nowy, $stary, $post): void {\n  if ($nowy !== $stary && $post->post_type === 'oferta') {\n    motyw_wyczysc_drzewo();\n  }\n}, 10, 3);\n\nadd_action('before_delete_post', function ($id, $post): void {\n  if ($post->post_type === 'oferta') {\n    motyw_wyczysc_drzewo();\n  }\n}, 10, 2);\n\nfunction motyw_wyczysc_drzewo(): void {\n  delete_transient('motyw_drzewo_oferta_kategoria');\n}\n",[53,3976,3977,3993,4008,4023,4027,4067,4084,4091,4095,4110,4114,4148,4179,4185,4189,4201,4205,4232,4253,4259,4263,4276,4280,4293,4305],{"__ignoreMap":182},[186,3978,3979,3981,3983,3986,3988,3991],{"class":188,"line":189},[186,3980,2287],{"class":196},[186,3982,230],{"class":200},[186,3984,3985],{"class":280},"'created_oferta_kategoria'",[186,3987,1642],{"class":200},[186,3989,3990],{"class":280}," 'motyw_wyczysc_drzewo'",[186,3992,236],{"class":200},[186,3994,3995,3997,3999,4002,4004,4006],{"class":188,"line":218},[186,3996,2287],{"class":196},[186,3998,230],{"class":200},[186,4000,4001],{"class":280},"'edited_oferta_kategoria'",[186,4003,1642],{"class":200},[186,4005,3990],{"class":280},[186,4007,236],{"class":200},[186,4009,4010,4012,4014,4017,4019,4021],{"class":188,"line":239},[186,4011,2287],{"class":196},[186,4013,230],{"class":200},[186,4015,4016],{"class":280},"'delete_oferta_kategoria'",[186,4018,1642],{"class":200},[186,4020,3990],{"class":280},[186,4022,236],{"class":200},[186,4024,4025],{"class":188,"line":245},[186,4026,249],{"emptyLinePlaceholder":248},[186,4028,4029,4031,4033,4036,4038,4040,4042,4045,4047,4050,4052,4055,4057,4060,4063,4065],{"class":188,"line":252},[186,4030,2287],{"class":196},[186,4032,230],{"class":200},[186,4034,4035],{"class":280},"'set_object_terms'",[186,4037,1642],{"class":200},[186,4039,212],{"class":192},[186,4041,935],{"class":200},[186,4043,4044],{"class":295},"$id",[186,4046,1642],{"class":200},[186,4048,4049],{"class":295}," $terminy",[186,4051,1642],{"class":200},[186,4053,4054],{"class":295}," $tt",[186,4056,1642],{"class":200},[186,4058,4059],{"class":295}," $taksonomia",[186,4061,4062],{"class":200},"):",[186,4064,891],{"class":204},[186,4066,269],{"class":200},[186,4068,4069,4071,4073,4076,4078,4080,4082],{"class":188,"line":272},[186,4070,932],{"class":192},[186,4072,935],{"class":200},[186,4074,4075],{"class":295},"$taksonomia",[186,4077,480],{"class":208},[186,4079,2562],{"class":280},[186,4081,2319],{"class":200},[186,4083,269],{"class":200},[186,4085,4086,4089],{"class":188,"line":292},[186,4087,4088],{"class":196},"    motyw_wyczysc_drzewo",[186,4090,710],{"class":200},[186,4092,4093],{"class":188,"line":307},[186,4094,993],{"class":200},[186,4096,4097,4100,4103,4105,4108],{"class":188,"line":319},[186,4098,4099],{"class":200},"},",[186,4101,4102],{"class":351}," 10",[186,4104,1642],{"class":200},[186,4106,4107],{"class":351}," 4",[186,4109,236],{"class":200},[186,4111,4112],{"class":188,"line":331},[186,4113,249],{"emptyLinePlaceholder":248},[186,4115,4116,4118,4120,4123,4125,4127,4129,4132,4134,4137,4139,4142,4144,4146],{"class":188,"line":343},[186,4117,2287],{"class":196},[186,4119,230],{"class":200},[186,4121,4122],{"class":280},"'transition_post_status'",[186,4124,1642],{"class":200},[186,4126,212],{"class":192},[186,4128,935],{"class":200},[186,4130,4131],{"class":295},"$nowy",[186,4133,1642],{"class":200},[186,4135,4136],{"class":295}," $stary",[186,4138,1642],{"class":200},[186,4140,4141],{"class":295}," $post",[186,4143,4062],{"class":200},[186,4145,891],{"class":204},[186,4147,269],{"class":200},[186,4149,4150,4152,4154,4156,4158,4160,4163,4165,4167,4170,4172,4175,4177],{"class":188,"line":357},[186,4151,932],{"class":192},[186,4153,935],{"class":200},[186,4155,4131],{"class":295},[186,4157,2559],{"class":208},[186,4159,4136],{"class":295},[186,4161,4162],{"class":208}," &&",[186,4164,4141],{"class":295},[186,4166,2553],{"class":200},[186,4168,4169],{"class":295},"post_type",[186,4171,480],{"class":208},[186,4173,4174],{"class":280}," 'oferta'",[186,4176,2319],{"class":200},[186,4178,269],{"class":200},[186,4180,4181,4183],{"class":188,"line":363},[186,4182,4088],{"class":196},[186,4184,710],{"class":200},[186,4186,4187],{"class":188,"line":990},[186,4188,993],{"class":200},[186,4190,4191,4193,4195,4197,4199],{"class":188,"line":996},[186,4192,4099],{"class":200},[186,4194,4102],{"class":351},[186,4196,1642],{"class":200},[186,4198,2417],{"class":351},[186,4200,236],{"class":200},[186,4202,4203],{"class":188,"line":1001},[186,4204,249],{"emptyLinePlaceholder":248},[186,4206,4207,4209,4211,4214,4216,4218,4220,4222,4224,4226,4228,4230],{"class":188,"line":1018},[186,4208,2287],{"class":196},[186,4210,230],{"class":200},[186,4212,4213],{"class":280},"'before_delete_post'",[186,4215,1642],{"class":200},[186,4217,212],{"class":192},[186,4219,935],{"class":200},[186,4221,4044],{"class":295},[186,4223,1642],{"class":200},[186,4225,4141],{"class":295},[186,4227,4062],{"class":200},[186,4229,891],{"class":204},[186,4231,269],{"class":200},[186,4233,4234,4236,4238,4241,4243,4245,4247,4249,4251],{"class":188,"line":1023},[186,4235,932],{"class":192},[186,4237,935],{"class":200},[186,4239,4240],{"class":295},"$post",[186,4242,2553],{"class":200},[186,4244,4169],{"class":295},[186,4246,480],{"class":208},[186,4248,4174],{"class":280},[186,4250,2319],{"class":200},[186,4252,269],{"class":200},[186,4254,4255,4257],{"class":188,"line":1028},[186,4256,4088],{"class":196},[186,4258,710],{"class":200},[186,4260,4261],{"class":188,"line":1051},[186,4262,993],{"class":200},[186,4264,4265,4267,4269,4271,4274],{"class":188,"line":1059},[186,4266,4099],{"class":200},[186,4268,4102],{"class":351},[186,4270,1642],{"class":200},[186,4272,4273],{"class":351}," 2",[186,4275,236],{"class":200},[186,4277,4278],{"class":188,"line":1070},[186,4279,249],{"emptyLinePlaceholder":248},[186,4281,4282,4284,4287,4289,4291],{"class":188,"line":1103},[186,4283,2506],{"class":192},[186,4285,4286],{"class":196}," motyw_wyczysc_drzewo",[186,4288,263],{"class":200},[186,4290,266],{"class":204},[186,4292,269],{"class":200},[186,4294,4295,4298,4300,4303],{"class":188,"line":1108},[186,4296,4297],{"class":196},"  delete_transient",[186,4299,230],{"class":200},[186,4301,4302],{"class":280},"'motyw_drzewo_oferta_kategoria'",[186,4304,236],{"class":200},[186,4306,4307],{"class":188,"line":1113},[186,4308,366],{"class":200},[11,4310,4311,4312,4315,4316,71],{},"Trzy pierwsze to dynamiczne akcje — w nazwie siedzi slug taksonomii, więc dla ",[53,4313,4314],{},"category"," będzie ",[53,4317,4318],{},"edited_category",[11,4320,4321,4322,4325],{},"Kuszące jest zastąpienie trzech ostatnich jednym ",[53,4323,4324],{},"save_post"," bez warunku i sam tak najpierw napisałem. To kasuje transient przy każdej rewizji, przy autozapisie co minutę i przy zapisie dowolnej strony w całym serwisie, także takiej, która z tą taksonomią nie ma nic wspólnego. Na serwisie, w którym ktoś pisze, cache przestaje wtedy istnieć w praktyce, a ty masz złudzenie, że działa.",[11,4327,4328,4331,4332,4335,4336,4339],{},[53,4329,4330],{},"set_object_terms"," łapie dokładnie to zdarzenie, które zmienia drzewo: podpięcie albo odpięcie wpisu od kategorii. ",[53,4333,4334],{},"transition_post_status"," łapie publikację, przeniesienie do kosza i przywrócenie, bo każde z nich rusza licznikiem. ",[53,4337,4338],{},"before_delete_post"," domyka trwałe usunięcie.",[11,4341,4342,4343,4346],{},"Czas życia ustawiony na ",[53,4344,4345],{},"DAY_IN_SECONDS"," jest tu tylko zabezpieczeniem na wypadek zdarzenia, którego nie przewidziałem. Przy sprawnym unieważnianiu transient i tak znika wcześniej.",[11,4348,4349,4350,4352,4353,4355],{},"Jedno takie zdarzenie warto wymienić, bo żaden z powyższych hooków go nie łapie: w transiencie leżą gotowe adresy z ",[53,4351,3028],{},". Ani zmiana struktury bezpośrednich odnośników w ustawieniach, ani zmiana sluga w ",[53,4354,3963],{}," nie rusza terminów ani przypisań, więc menu przez dobę prowadzi pod stare adresy.",[11,4357,4358,4359,4362],{},"Pewne wyjście to nie przechowywać adresów: trzymaj w transiencie same slugi i składaj adres przy renderze. Dopięcie się do ",[53,4360,4361],{},"permalink_structure_changed"," zamyka tylko połowę problemu, bo ta akcja odpala się przy zapisie ustawień odnośników, a zmiana sluga taksonomii dzieje się w kodzie i żadnego zdarzenia w bazie nie zostawia.",[26,4364,4366],{"id":4365},"drugi-filtr-permalinki","Drugi filtr: permalinki",[11,4368,4369],{},"Ten sam motyw miał w permalinkach produktów placeholder podmieniany na slug kategorii:",[177,4371,4373],{"className":2251,"code":4372,"language":2253,"meta":182,"style":182},"add_filter('post_type_link', 'motyw_permalink_produktu', 10, 2);\n\nfunction motyw_permalink_produktu(string $odnosnik, WP_Post $post): string {\n  $terminy = wp_get_object_terms($post->ID, 'oferta_kategoria');\n\n  if (empty($terminy) || is_wp_error($terminy)) {\n    return $odnosnik;\n  }\n\n  return str_replace('%oferta_kategoria%', $terminy[0]->slug, $odnosnik);\n}\n",[53,4374,4375,4399,4403,4430,4453,4457,4484,4492,4496,4500,4532],{"__ignoreMap":182},[186,4376,4377,4379,4381,4384,4386,4389,4391,4393,4395,4397],{"class":188,"line":189},[186,4378,2485],{"class":196},[186,4380,230],{"class":200},[186,4382,4383],{"class":280},"'post_type_link'",[186,4385,1642],{"class":200},[186,4387,4388],{"class":280}," 'motyw_permalink_produktu'",[186,4390,1642],{"class":200},[186,4392,4102],{"class":351},[186,4394,1642],{"class":200},[186,4396,4273],{"class":351},[186,4398,236],{"class":200},[186,4400,4401],{"class":188,"line":218},[186,4402,249],{"emptyLinePlaceholder":248},[186,4404,4405,4407,4410,4412,4414,4417,4419,4422,4424,4426,4428],{"class":188,"line":239},[186,4406,2506],{"class":192},[186,4408,4409],{"class":196}," motyw_permalink_produktu",[186,4411,230],{"class":200},[186,4413,1209],{"class":204},[186,4415,4416],{"class":295}," $odnosnik",[186,4418,56],{"class":200},[186,4420,4421],{"class":204},"WP_Post",[186,4423,4141],{"class":295},[186,4425,420],{"class":200},[186,4427,1209],{"class":204},[186,4429,269],{"class":200},[186,4431,4432,4434,4436,4439,4441,4443,4445,4447,4449,4451],{"class":188,"line":245},[186,4433,3169],{"class":295},[186,4435,209],{"class":208},[186,4437,4438],{"class":196}," wp_get_object_terms",[186,4440,230],{"class":200},[186,4442,4240],{"class":295},[186,4444,2553],{"class":200},[186,4446,2624],{"class":295},[186,4448,1642],{"class":200},[186,4450,2562],{"class":280},[186,4452,236],{"class":200},[186,4454,4455],{"class":188,"line":252},[186,4456,249],{"emptyLinePlaceholder":248},[186,4458,4459,4461,4463,4466,4468,4470,4472,4475,4478,4480,4482],{"class":188,"line":272},[186,4460,932],{"class":192},[186,4462,935],{"class":200},[186,4464,4465],{"class":208},"empty",[186,4467,230],{"class":200},[186,4469,3228],{"class":295},[186,4471,1439],{"class":200},[186,4473,4474],{"class":208},"||",[186,4476,4477],{"class":196}," is_wp_error",[186,4479,230],{"class":200},[186,4481,3228],{"class":295},[186,4483,3231],{"class":200},[186,4485,4486,4488,4490],{"class":188,"line":292},[186,4487,985],{"class":192},[186,4489,4416],{"class":295},[186,4491,532],{"class":200},[186,4493,4494],{"class":188,"line":307},[186,4495,993],{"class":200},[186,4497,4498],{"class":188,"line":319},[186,4499,249],{"emptyLinePlaceholder":248},[186,4501,4502,4504,4507,4509,4512,4514,4516,4518,4520,4523,4526,4528,4530],{"class":188,"line":331},[186,4503,2640],{"class":192},[186,4505,4506],{"class":208}," str_replace",[186,4508,230],{"class":200},[186,4510,4511],{"class":280},"'%oferta_kategoria%'",[186,4513,1642],{"class":200},[186,4515,4049],{"class":295},[186,4517,2102],{"class":200},[186,4519,1811],{"class":351},[186,4521,4522],{"class":200},"]->",[186,4524,4525],{"class":295},"slug",[186,4527,1642],{"class":200},[186,4529,4416],{"class":295},[186,4531,236],{"class":200},[186,4533,4534],{"class":188,"line":343},[186,4535,366],{"class":200},[11,4537,4538,4541,4542,4545],{},[53,4539,4540],{},"wp_get_object_terms()"," odpytuje bazę przy każdym wywołaniu i nie sprawdza po drodze cache obiektów. Listing dwudziestu produktów to dwadzieścia zapytań na same odnośniki — a ",[53,4543,4544],{},"post_type_link"," odpala się także przy generowaniu mapy witryny i kanałów RSS, gdzie pozycji bywają setki.",[11,4547,4548,4549,4552],{},"Zamiennik to ",[53,4550,4551],{},"get_the_terms()",". Ta funkcja pyta najpierw cache obiektów i dopiero przy pudle schodzi do bazy, a wynik odkłada dla tego wpisu.",[11,4554,4555],{},"Wdrożona poprawka na tym się skończyła: podmiana jednej funkcji na drugą i tyle. To wystarczyło, żeby zapytania zniknęły. Poniżej wersja, którą napisałbym dziś — różni się od wdrożonej trzema rzeczami i żadna z nich nie dotyczy wydajności:",[177,4557,4559],{"className":2251,"code":4558,"language":2253,"meta":182,"style":182},"add_filter('post_type_link', 'motyw_permalink_produktu', 10, 2);\n\nfunction motyw_permalink_produktu(string $odnosnik, WP_Post $post): string {\n  if (!str_contains($odnosnik, '%oferta_kategoria%')) {\n    return $odnosnik;\n  }\n\n  $terminy = get_the_terms($post, 'oferta_kategoria');\n\n  if (empty($terminy) || is_wp_error($terminy)) {\n    return str_replace('%oferta_kategoria%', 'bez-kategorii', $odnosnik);\n  }\n\n  return str_replace('%oferta_kategoria%', reset($terminy)->slug, $odnosnik);\n}\n",[53,4560,4561,4583,4587,4611,4635,4643,4647,4651,4670,4674,4698,4719,4723,4727,4757],{"__ignoreMap":182},[186,4562,4563,4565,4567,4569,4571,4573,4575,4577,4579,4581],{"class":188,"line":189},[186,4564,2485],{"class":196},[186,4566,230],{"class":200},[186,4568,4383],{"class":280},[186,4570,1642],{"class":200},[186,4572,4388],{"class":280},[186,4574,1642],{"class":200},[186,4576,4102],{"class":351},[186,4578,1642],{"class":200},[186,4580,4273],{"class":351},[186,4582,236],{"class":200},[186,4584,4585],{"class":188,"line":218},[186,4586,249],{"emptyLinePlaceholder":248},[186,4588,4589,4591,4593,4595,4597,4599,4601,4603,4605,4607,4609],{"class":188,"line":239},[186,4590,2506],{"class":192},[186,4592,4409],{"class":196},[186,4594,230],{"class":200},[186,4596,1209],{"class":204},[186,4598,4416],{"class":295},[186,4600,56],{"class":200},[186,4602,4421],{"class":204},[186,4604,4141],{"class":295},[186,4606,420],{"class":200},[186,4608,1209],{"class":204},[186,4610,269],{"class":200},[186,4612,4613,4615,4617,4620,4623,4625,4628,4630,4633],{"class":188,"line":245},[186,4614,932],{"class":192},[186,4616,935],{"class":200},[186,4618,4619],{"class":208},"!",[186,4621,4622],{"class":208},"str_contains",[186,4624,230],{"class":200},[186,4626,4627],{"class":295},"$odnosnik",[186,4629,1642],{"class":200},[186,4631,4632],{"class":280}," '%oferta_kategoria%'",[186,4634,3231],{"class":200},[186,4636,4637,4639,4641],{"class":188,"line":252},[186,4638,985],{"class":192},[186,4640,4416],{"class":295},[186,4642,532],{"class":200},[186,4644,4645],{"class":188,"line":272},[186,4646,993],{"class":200},[186,4648,4649],{"class":188,"line":292},[186,4650,249],{"emptyLinePlaceholder":248},[186,4652,4653,4655,4657,4660,4662,4664,4666,4668],{"class":188,"line":307},[186,4654,3169],{"class":295},[186,4656,209],{"class":208},[186,4658,4659],{"class":196}," get_the_terms",[186,4661,230],{"class":200},[186,4663,4240],{"class":295},[186,4665,1642],{"class":200},[186,4667,2562],{"class":280},[186,4669,236],{"class":200},[186,4671,4672],{"class":188,"line":319},[186,4673,249],{"emptyLinePlaceholder":248},[186,4675,4676,4678,4680,4682,4684,4686,4688,4690,4692,4694,4696],{"class":188,"line":331},[186,4677,932],{"class":192},[186,4679,935],{"class":200},[186,4681,4465],{"class":208},[186,4683,230],{"class":200},[186,4685,3228],{"class":295},[186,4687,1439],{"class":200},[186,4689,4474],{"class":208},[186,4691,4477],{"class":196},[186,4693,230],{"class":200},[186,4695,3228],{"class":295},[186,4697,3231],{"class":200},[186,4699,4700,4702,4704,4706,4708,4710,4713,4715,4717],{"class":188,"line":343},[186,4701,985],{"class":192},[186,4703,4506],{"class":208},[186,4705,230],{"class":200},[186,4707,4511],{"class":280},[186,4709,1642],{"class":200},[186,4711,4712],{"class":280}," 'bez-kategorii'",[186,4714,1642],{"class":200},[186,4716,4416],{"class":295},[186,4718,236],{"class":200},[186,4720,4721],{"class":188,"line":357},[186,4722,993],{"class":200},[186,4724,4725],{"class":188,"line":363},[186,4726,249],{"emptyLinePlaceholder":248},[186,4728,4729,4731,4733,4735,4737,4739,4742,4744,4746,4749,4751,4753,4755],{"class":188,"line":990},[186,4730,2640],{"class":192},[186,4732,4506],{"class":208},[186,4734,230],{"class":200},[186,4736,4511],{"class":280},[186,4738,1642],{"class":200},[186,4740,4741],{"class":208}," reset",[186,4743,230],{"class":200},[186,4745,3228],{"class":295},[186,4747,4748],{"class":200},")->",[186,4750,4525],{"class":295},[186,4752,1642],{"class":200},[186,4754,4416],{"class":295},[186,4756,236],{"class":200},[186,4758,4759],{"class":188,"line":996},[186,4760,366],{"class":200},[11,4762,4763,4769,4770,4772,4773,4776,4777,4780],{},[15,4764,4765,4766,71],{},"Wczesny ",[53,4767,4768],{},"return"," Filtr ",[53,4771,4544],{}," dostaje odnośniki wszystkich typów treści, także tych, które żadnego placeholdera nie mają. Sprawdzenie ",[53,4774,4775],{},"str_contains()"," kosztuje ułamek mikrosekundy i wycina większość wywołań przed dotknięciem taksonomii. W oryginale każdy odnośnik strony i wpisu przechodził przez pełne zapytanie, którego wynik i tak trafiał do ",[53,4778,4779],{},"str_replace()"," bez efektu.",[11,4782,4783,4786,4787,4790],{},[15,4784,4785],{},"Zapasowy slug."," Wersja z oryginału zwracała przy braku terminu adres z niepodmienionym ",[53,4788,4789],{},"%oferta_kategoria%"," w środku. Taki odnośnik trafia do HTML-u i do mapy witryny.",[11,4792,4793,3042,4802,4804,4805,4808,4809,4812],{},[15,4794,4795,4798,4799,71],{},[53,4796,4797],{},"reset()"," zamiast ",[53,4800,4801],{},"[0]",[53,4803,4551],{}," zwraca tablicę, której klucze nie muszą zaczynać się od zera — przy odczycie z cache bywają zachowane identyfikatory terminów. ",[53,4806,4807],{},"$terminy[0]"," daje wtedy ",[53,4810,4811],{},"null"," i ostrzeżenie.",[11,4814,4815],{},"Ten ostatni punkt ma wersję, która wygląda na naprawioną, a nie jest. Kod, w którym potrzebny jest drugi termin, a pierwszy służy za zapas, kusi żeby napisać:",[177,4817,4819],{"className":2251,"code":4818,"language":2253,"meta":182,"style":182},"$termin = isset($terminy[1]) ? $terminy[1] : $terminy[0];\n",[53,4820,4821],{"__ignoreMap":182},[186,4822,4823,4825,4827,4830,4832,4834,4836,4838,4841,4843,4845,4847,4849,4852,4854,4856,4858,4860],{"class":188,"line":189},[186,4824,3270],{"class":295},[186,4826,209],{"class":208},[186,4828,4829],{"class":208}," isset",[186,4831,230],{"class":200},[186,4833,3228],{"class":295},[186,4835,2102],{"class":200},[186,4837,1927],{"class":351},[186,4839,4840],{"class":200},"]) ",[186,4842,877],{"class":192},[186,4844,4049],{"class":295},[186,4846,2102],{"class":200},[186,4848,1927],{"class":351},[186,4850,4851],{"class":200},"] ",[186,4853,1636],{"class":192},[186,4855,4049],{"class":295},[186,4857,2102],{"class":200},[186,4859,1811],{"class":351},[186,4861,4862],{"class":200},"];\n",[11,4864,4865,4868,4869,4872,4873,4875,4876,4878,4879,4882,4883,4885],{},[53,4866,4867],{},"isset()"," na ",[53,4870,4871],{},"[1]"," zabezpiecza jeden indeks i zostawia drugi gołym dostępem. Gdy klucze nie będą listą od zera, ",[53,4874,4871],{}," nie istnieje, warunek grzecznie przechodzi do ",[53,4877,4801],{}," — którego też nie ma — i ostrzeżenie leci mimo wszystko. Założenie o kształcie kluczy zostało, tylko przesunięte o jedną linię. ",[53,4880,4881],{},"array_values()"," na wejściu albo ",[53,4884,4797],{}," rozwiązuje to u źródła.",[26,4887,4889],{"id":4888},"jak-sprawdzić-że-poprawka-zadziałała","Jak sprawdzić, że poprawka zadziałała",[11,4891,4892],{},"Zapal licznik z pierwszej sekcji drugi raz i porównaj wpisy z tymi sprzed zmiany. Pomiar przed i po nie wymaga ciągłego działania, tylko dwóch krótkich okien na tej samej podstronie i przy tej samej liczbie kategorii. Spodziewasz się trzech rzeczy:",[11,4894,4895,4898],{},[15,4896,4897],{},"Pierwsze wyświetlenie po wyczyszczeniu transientu"," ma o tyle zapytań mniej, ile węzłów liczyło drzewo razy dwa. Menu zostaje przy kilku.",[11,4900,4901,4904,4905,71],{},[15,4902,4903],{},"Kolejne wyświetlenia"," schodzą do dwóch po stronie menu — timeout i wartość transientu z ",[53,4906,4907],{},"wp_options",[11,4909,4910,4913],{},[15,4911,4912],{},"Liczba przestaje zależeć od liczby kategorii."," To jest właściwy test. Dodaj w panelu dziesięć kategorii i odśwież stronę — wcześniej licznik rósł razem z nimi, po poprawce stoi w miejscu. Dopóki rośnie, gdzieś w kodzie została pętla pytająca bazę.",[26,4915,4917],{"id":4916},"gdzie-jeszcze-szukać-tego-wzorca","Gdzie jeszcze szukać tego wzorca",[11,4919,4920],{},"Trzy miejsca, w których ten sam błąd powtarza się w motywach najczęściej:",[11,4922,4923,4930,4931,4934,4935,4938,4939,4942],{},[15,4924,4925,4926,4929],{},"Pętla po wpisach z ",[53,4927,4928],{},"get_post_meta()"," bez przygotowania cache."," Sama funkcja korzysta z cache, ale zapełnia go dopiero przy pierwszym odczycie danego wpisu, a ",[53,4932,4933],{},"WP_Query"," z ",[53,4936,4937],{},"update_post_meta_cache"," ustawionym na ",[53,4940,4941],{},"false"," odbiera jej to przygotowanie. Sprawdź argumenty zapytania, zanim zaczniesz przepisywać pętlę.",[11,4944,4945,4951,4952,4955],{},[15,4946,4947,4950],{},[53,4948,4949],{},"get_field()"," z ACF w pętli po elementach powtarzalnych."," Każde wywołanie to odczyt metadanych, a przy polach typu relacja także pobranie obiektu wpisu. Wariant ",[53,4953,4954],{},"get_fields()"," dla całego wpisu naraz bywa o rząd wielkości tańszy.",[11,4957,4958,3042,4961,4964,4965,4968],{},[15,4959,4960],{},"Liczniki w listingach.",[53,4962,4963],{},"wp_count_posts()"," albo własne ",[53,4966,4967],{},"COUNT(*)"," wywoływane dla każdej kategorii na liście. To dokładnie ten sam kształt co sprawdzanie dzieci w menu: pytanie zadawane per element, gdy jedno zapytanie grupujące załatwia całość.",[26,4970,4972],{"id":4971},"pytania-które-padają-przy-takiej-poprawce","Pytania, które padają przy takiej poprawce",[11,4974,4975,4978],{},[15,4976,4977],{},"Czy nie prościej wrzucić wtyczkę cache'ującą stronę?","\nCache stron ukryje objaw dla niezalogowanych i nie zmieni nic dla redaktora, dla koszyka ani dla żądań, które omijają cache. Kod wykonujący trzysta zapytań na żądanie dalej je wykonuje, po prostu rzadziej. Wtyczka jest warstwą na wierzchu poprawnego kodu.",[11,4980,4981,4984,4985,4987,4988,4990],{},[15,4982,4983],{},"Czy transient nie zaśmieci bazy?","\nTransient bez persystentnego cache obiektów ląduje w tabeli ",[53,4986,4907],{},". Jeden wpis o rozmiarze kilku kilobajtów to żaden koszt, ale ten rozmiar zależy wyłącznie od tego, co do niego włożysz. Przykład wyżej odkłada sześć pól na termin i przy stu kategoriach mieści się w kilku kilobajtach. Wrzucenie tam pełnych obiektów ",[53,4989,3872],{}," daje przy tej samej liczbie kategorii kilkadziesiąt kilobajtów, a przy tysiącu terminów sama deserializacja przy każdym żądaniu potrafi kosztować więcej niż zapytanie, przed którym transient miał chronić. Trzymaj w nim to, czego naprawdę używa szablon.",[11,4992,4993],{},"Druga pułapka to transienty z unikalnym kluczem na każdą kombinację parametrów — tych potrafią urosnąć tysiące i po wygaśnięciu nikt ich nie sprząta.",[11,4995,4996,4999],{},[15,4997,4998],{},"Zapytania liczne kontra zapytania kosztowne — jak je odróżnić?","\nZ samej liczby się nie da i to jest ważne rozróżnienie. Query Monitor podaje czas każdego zapytania i sumę dla całego żądania — dwieście zapytań po 0,1 ms to 20 ms i przepisywanie filtra się za to nie zwróci. Sprawdź sumę, zanim zaczniesz. Druga rzecz warta sprawdzenia to koszt po stronie PHP: przy setkach pozycji menu samo składanie obiektów potrafi ważyć więcej niż baza, a wtedy cache na poziomie transientu daje więcej niż optymalizacja zapytań.",[11,5001,5002,5008,5009,5011,5012,5014],{},[15,5003,5004,5005,5007],{},"Czy ",[53,5006,3907],{}," nie spowalnia zapytania?","\nSamo ",[53,5010,3907],{}," je upraszcza, bo ",[53,5013,3903],{}," dokłada złączenie z tabelą relacji i warunek na liczbę wpisów. Przy budowaniu drzewa i tak potrzebujesz pełnej listy, żeby poznać relacje rodzic–dziecko.",[11,5016,5017,5018,5020,5021,5023,5024,5027],{},"Koszt wraca przez ",[53,5019,3926],{},", bez którego filtrowanie po ",[53,5022,3290],{}," jest błędne. WordPress odpala wtedy ",[53,5025,5026],{},"_pad_term_counts()",", czyli dodatkowy odczyt tabeli relacji i statusów wpisów. To jedno czy dwa zapytania na całą taksonomię, nie jedno na termin — czyli dokładnie ta zamiana, o którą w tym tekście chodzi.",[5029,5030],"hr",{},[11,5032,5033,5034,5037,5038,71],{},"Pomiar przed poprawką jest tu ważniejszy od samej poprawki. Kod, który wygląda na kosztowny, bywa niewidoczny w profilu, a kilkaset zapytań po 0,1 ms nie jest problemem wartym przepisywania filtra. Zacznij od ",[53,5035,5036],{},"get_num_queries()"," i od czasów w Query Monitorze, a dopiero potem otwieraj ",[53,5039,2277],{},[2181,5041,5042],{},"html pre.shiki code .sjrmR, html code.shiki .sjrmR{--shiki-default:#56B6C2}html pre.shiki code .sn6KH, html code.shiki .sn6KH{--shiki-default:#ABB2BF}html pre.shiki code .subq3, html code.shiki .subq3{--shiki-default:#98C379}html pre.shiki code .sVC51, html code.shiki .sVC51{--shiki-default:#D19A66}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sVbv2, html code.shiki .sVbv2{--shiki-default:#61AFEF}html pre.shiki code .seHd6, html code.shiki .seHd6{--shiki-default:#C678DD}html pre.shiki code .sU0A5, html code.shiki .sU0A5{--shiki-default:#E5C07B}html pre.shiki code .sVyAn, html code.shiki .sVyAn{--shiki-default:#E06C75}",{"title":182,"searchDepth":218,"depth":218,"links":5044},[5045,5046,5047,5048,5055,5056,5057,5058],{"id":2238,"depth":218,"text":2239},{"id":2471,"depth":218,"text":2472},{"id":3049,"depth":218,"text":3050},{"id":3069,"depth":218,"text":3070,"children":5049},[5050,5051,5053,5054],{"id":3733,"depth":239,"text":3734},{"id":3894,"depth":239,"text":5052},"pad_counts to jedna linia, bez której logika się wywraca",{"id":3936,"depth":239,"text":3937},{"id":3967,"depth":239,"text":3968},{"id":4365,"depth":218,"text":4366},{"id":4888,"depth":218,"text":4889},{"id":4916,"depth":218,"text":4917},{"id":4971,"depth":218,"text":4972},"2026-09-19","Filtr menu z taksonomii potrafi kosztować kilkaset zapytań SQL na wyświetlenie. Pomiar, przyczyna i przepisany filtr, który nie rośnie z liczbą kategorii.","WordPress",{},"/img/blog/zapytania-n-plus-1-filtry-wordpress.webp","Laptop na biurku z otwartym plikiem functions.php w edytorze kodu","/zapytania-n-plus-1-filtry-wordpress",{"title":2217,"description":5060},{"loc":5065},"zapytania-n-plus-1-filtry-wordpress","x5CuV2ZOpWLXxO4QlbBKgAJbIcfmAp4dR4QDTh206v8",{"id":5071,"title":5072,"body":5073,"date":8315,"description":8316,"extension":2203,"kategoria":8317,"meta":8318,"navigation":248,"obrazek":8319,"obrazekAlt":8320,"path":8321,"seo":8322,"sitemap":8323,"stem":8324,"szkic":2212,"zaktualizowano":8325,"__hash__":8326},"wpisy/limit-cpu-hosting-wspoldzielony-wordpress.md","Przekroczony limit CPU na hostingu — jak znaleźć prawdziwą przyczynę",{"type":8,"value":5074,"toc":8284},[5075,5099,5105,5108,5114,5118,5121,5129,5132,5135,5141,5147,5154,5158,5161,5170,5177,5191,5197,5203,5209,5216,5220,5223,5226,5232,5235,5241,5244,5248,5251,5254,5432,5438,5445,5451,5455,5458,5593,5596,5599,5662,5665,5668,5672,5675,5733,5740,5743,5795,5798,5804,5807,5810,5814,5817,5890,5896,5899,5912,5928,5934,5937,5943,5949,5952,5956,5959,6091,6101,6105,6119,6122,6128,6134,6140,6146,6163,6170,6173,6182,6191,6202,6205,6208,6211,6218,6221,6225,6237,6539,6545,6548,6561,6613,6617,6623,6640,6656,6718,6725,6728,6735,6738,6741,6754,6769,6780,6783,6862,6865,6869,6875,6882,6888,6895,6905,6912,6918,6929,6933,6936,6943,6946,6950,6956,6959,7004,7007,7014,7017,7036,7040,7044,7050,7101,7104,7110,7113,7126,7130,7136,7142,7145,7148,7152,7155,7170,7177,7180,7203,7213,7217,7220,7226,7262,7269,7279,7301,7310,7321,7327,7381,7384,7409,7422,7432,7435,7462,7472,7483,7489,7526,7539,7542,7568,7571,7588,7594,7640,7664,7670,7766,7773,7776,7785,7799,7813,7819,7828,7842,7845,7911,7921,7925,7936,7939,7945,7948,7952,7964,7972,7994,7997,8047,8050,8054,8057,8063,8072,8078,8086,8094,8100,8104,8110,8116,8122,8128,8131,8140,8236,8240,8246,8252,8264,8270,8276,8278,8281],[11,5076,5077,5078,5081,5082,5085,5086,56,5088,5091,5092,5095,5096,71],{},"Winowajcą okazały się skanery podatności: kilkaset żądań skumulowanych w jednej minucie, pod nieistniejące ścieżki — od ",[53,5079,5080],{},"/.env"," po ",[53,5083,5084],{},"/wp-admin/phpinfo.php",". Drogie okazały się te bez kropki na początku nazwy, w rodzaju ",[53,5087,5084],{},[53,5089,5090],{},"/config/mail.php"," czy ",[53,5093,5094],{},"/actuator/loggers",": każda z nich uruchamiała pełny bootstrap WordPressa. Odcina je blok reguł na początku ",[53,5097,5098],{},".htaccess",[11,5100,5101,5102,5104],{},"Najgłośniejsza kategoria w logu, czyli warianty ",[53,5103,5080],{},", nie kosztowała przy tym nic i nie kosztowała nigdy — odcinał je sam serwer, zanim jakakolwiek reguła zdążyła się do nich odnieść. Zmierzyłem to dopiero przy sprawdzaniu skuteczności blokad i opisuję niżej, bo to ta sama pomyłka co przy trzeciej pułapce: sygnał najbardziej rzucający się w oczy rzadko jest tym najdroższym.",[11,5106,5107],{},"Dojście do tego zajęło dwa dni, bo alert z hostingu nie mówi ani która witryna zawiniła, ani o której godzinie. Po drodze trzy tropy wyglądały przekonująco i wszystkie trzy okazały się nietrafione. Opisuję je razem z komendami, bo to właśnie one zjadają czas.",[11,5109,5110,5111,5113],{},"Samo zdanie o skanerach prowadzi jednak na manowce, jeśli zostawić je bez doprecyzowania. O koszcie decyduje liczba żądań, które doszły do interpretera PHP; wielkość serii nie mówi o nim nic. Sześćdziesiąt sześć żądań odrzuconych przez ",[53,5112,5098],{}," przeszło u mnie bez śladu w statystykach, a dwadzieścia dwa odrzucone przy siedmiu uruchomionych i ubitych procesach PHP wysyciły cztery rdzenie. Po domknięciu bloku jedenaście kolejnych przelotów tego samego automatu nie uruchomiło ani jednego procesu PHP.",[26,5115,5117],{"id":5116},"co-właściwie-znaczy-ten-alert","Co właściwie znaczy ten alert",[11,5119,5120],{},"Typowa treść wygląda tak:",[177,5122,5127],{"className":5123,"code":5125,"language":5126,"meta":182},[5124],"language-text","You reached limit of 400 of total server CPU usage 6 times.\nYour website was forced to load slower to reduce its CPU usage.\n","text",[53,5128,5125],{"__ignoreMap":182},[11,5130,5131],{},"To komunikat CloudLinux, systemu izolującego konta na hostingu współdzielonym. Każde konto dostaje własny kontener LVE z limitami: CPU, pamięci, operacji dyskowych i liczby procesów.",[11,5133,5134],{},"Dwie rzeczy warto od razu wiedzieć.",[11,5136,5137,5140],{},[15,5138,5139],{},"400% to nie „czterokrotne przekroczenie\"."," To równowartość czterech rdzeni. Limit 100% oznaczałby jeden rdzeń.",[11,5142,5143,5146],{},[15,5144,5145],{},"To nie jest blokada."," Po przekroczeniu limitu procesy są spowalniane, aż zużycie spadnie. Strona działa, tylko wolniej. Dlatego problem potrafi trwać tygodniami, zanim ktokolwiek zareaguje.",[11,5148,5149,5150,5153],{},"Limit dotyczy ",[15,5151,5152],{},"całego konta",", nie pojedynczej domeny. Przy kilkunastu witrynach na jednym koncie alert nie wskazuje winowajcy.",[26,5155,5157],{"id":5156},"zacznij-od-panelu-nie-od-konsoli","Zacznij od panelu, nie od konsoli",[11,5159,5160],{},"Pierwszy odruch to zalogować się przez SSH i sprawdzić zużycie:",[177,5162,5164],{"className":1591,"code":5163,"language":1593,"meta":182,"style":182},"lveinfo\n",[53,5165,5166],{"__ignoreMap":182},[186,5167,5168],{"class":188,"line":189},[186,5169,5163],{"class":196},[11,5171,5172,5173,5176],{},"Na koncie współdzielonym to nie zadziała. ",[53,5174,5175],{},"lveinfo"," odpytuje bazę statystyk LVE i wymaga uprawnień administratora, więc nie pomoże też na hostingu bez CageFS-a — a tam, gdzie CageFS jest, konto i tak nie widzi narzędzi systemowych. Nie warto na tym tracić czasu ani budować własnego monitoringu w zastępstwie. Administrator hostingu ma to polecenie pod ręką i widzi w nim wprost, który proces i która domena wywołały spowolnienie.",[11,5178,5179,5180,56,5183,5186,5187,5190],{},"W panelu (DirectAdmin, cPanel, panel własny hostingu) szukaj sekcji ",[15,5181,5182],{},"Resource Usage",[15,5184,5185],{},"Użycie"," albo ",[15,5188,5189],{},"Statystyki obciążenia",". Znajdziesz tam trzy rzeczy, których nie da się odtworzyć z poziomu shella.",[11,5192,5193,5196],{},[15,5194,5195],{},"Tabelę godzinową"," z kolumnami A (średnia), L (limit) i F (faults). Kolumna F to liczba przekroczeń w danej godzinie, czyli gotowa lista godzin do sprawdzenia.",[11,5198,5199,5202],{},[15,5200,5201],{},"Przełącznik jednostki czasu."," To jest pozycja, którą przeoczyłem przy pierwszym podejściu i która kosztowała mnie dzień. Ta sama tabela potrafi pokazać dobę minuta po minucie zamiast godzina po godzinie — u mnie 610 wierszy zamiast jedenastu. Przy zdarzeniach trwających kilkanaście sekund różnica między jedną rozdzielczością a drugą decyduje o tym, czy w ogóle zobaczysz, czego szukasz.",[11,5204,5205,5208],{},[15,5206,5207],{},"Snapshot procesów"," z chwili przeciążenia, razem ze ścieżką do skryptu. To odpowiedź wprost na pytanie „co to generuje\".",[11,5210,5211,5212,5215],{},"W opisywanym przypadku snapshot pokazał kilkanaście równoległych procesów ",[53,5213,5214],{},"index.php"," z jednej witryny, każdy zajmujący 25% CPU. Razem ponad 400%.",[26,5217,5219],{"id":5218},"pułapka-pierwsza-średnie-ukrywają-szczyty","Pułapka pierwsza: średnie ukrywają szczyty",[11,5221,5222],{},"Tabela godzinowa pokazywała zużycie na poziomie 1–16% przy limicie 400%. Wyglądało to niewinnie, a mimo to w trzech godzinach doby odnotowano przekroczenia.",[11,5224,5225],{},"To normalne. Szczyt trwający czterdzieści sekund rozpuszcza się w średniej godzinowej prawie bez śladu. Przy diagnostyce interesują Cię minuty, nie godziny.",[11,5227,5228,5231],{},[15,5229,5230],{},"Przy minutach ta sama pułapka wraca o poziom niżej i tam już mnie złapała."," W tabeli minutowej zdarzenie widać wyraźnie: średnia skacze z 2% na 86%, a liczba procesów z pięciu na dziewięć. Ale licznik procesów wejściowych, czyli żądań HTTP obsługiwanych równolegle, stoi przez całą dobę na czterech albo pięciu i nie drga nawet w minucie przekroczenia.",[11,5233,5234],{},"Wyciągnąłem z tego wniosek, że skoro żądania HTTP nie przybyło, to obciążenie przychodzi spoza serwera WWW, i zacząłem szukać w cronie, w poczcie i w zadaniach systemowych. Wniosek był fałszywy — logi z tych samych minut pokazały serie żądań HTTP.",[11,5236,5237,5240],{},[15,5238,5239],{},"Czemu ta kolumna nie drgnęła, nie ustaliłem."," Pierwsze wyjaśnienie, jakie mi przyszło do głowy, czyli uśrednianie na sześćdziesiąt sekund przy zdarzeniu trwającym kilkanaście, nie broni się: w tej samej minucie i w tym samym oknie uśredniania liczba procesów wzrosła z pięciu na dziewięć. Nie broni się też dlatego, że ta kolumna przez całą dobę trzyma się przedziału od trzech do pięciu — także w minutach, w których konto nie robi nic, gdzie średnia liczba równoległych żądań powinna dążyć do zera. Cokolwiek ona mierzy, nie jest to chwilowa liczba żądań w obsłudze.",[11,5242,5243],{},"Zostaje z tego wniosek ogólniejszy i pewniejszy od mojej pierwszej hipotezy: licznik, który się nie rusza, nie jest dowodem, że nic się nie stało. Zanim zbudujesz na takim liczniku kierunek poszukiwań, sprawdź w logu, czy on w ogóle mierzy to, co sugeruje jego nazwa.",[26,5245,5247],{"id":5246},"pułapka-druga-ruch-to-nie-obciążenie","Pułapka druga: ruch to nie obciążenie",[11,5249,5250],{},"Naturalne założenie brzmi: winna jest witryna z największym ruchem. Policzmy więc żądania na dobę dla każdej domeny.",[11,5252,5253],{},"Logi na hostingach współdzielonych bywają rotowane do archiwów dobowych, więc czyta się je strumieniem:",[177,5255,5257],{"className":1591,"code":5256,"language":1593,"meta":182,"style":182},"for d in ~/domains/*/; do\n  n=0\n  for f in \"$d\"logs/*.tar.gz; do\n    [ -f \"$f\" ] || continue\n    n=$((n + $(tar -xzOf \"$f\" 2>/dev/null | grep -c '\" [0-9]\\{3\\} ')))\n  done\n  printf '%8d  %s\\n' \"$n\" \"$(basename \"$d\")\"\ndone | sort -rn\n",[53,5258,5259,5279,5289,5312,5334,5386,5391,5419],{"__ignoreMap":182},[186,5260,5261,5264,5267,5270,5273,5276],{"class":188,"line":189},[186,5262,5263],{"class":192},"for",[186,5265,5266],{"class":295}," d",[186,5268,5269],{"class":192}," in",[186,5271,5272],{"class":280}," ~/domains/*/",[186,5274,5275],{"class":200},"; ",[186,5277,5278],{"class":192},"do\n",[186,5280,5281,5284,5286],{"class":188,"line":218},[186,5282,5283],{"class":295},"  n",[186,5285,635],{"class":208},[186,5287,5288],{"class":280},"0\n",[186,5290,5291,5294,5297,5299,5302,5305,5308,5310],{"class":188,"line":239},[186,5292,5293],{"class":192},"  for",[186,5295,5296],{"class":295}," f",[186,5298,5269],{"class":192},[186,5300,5301],{"class":280}," \"",[186,5303,5304],{"class":295},"$d",[186,5306,5307],{"class":280},"\"logs/*.tar.gz",[186,5309,5275],{"class":200},[186,5311,5278],{"class":192},[186,5313,5314,5317,5320,5322,5325,5328,5331],{"class":188,"line":245},[186,5315,5316],{"class":200},"    [ ",[186,5318,5319],{"class":208},"-f",[186,5321,5301],{"class":280},[186,5323,5324],{"class":295},"$f",[186,5326,5327],{"class":280},"\"",[186,5329,5330],{"class":200}," ] || ",[186,5332,5333],{"class":192},"continue\n",[186,5335,5336,5339,5341,5344,5347,5350,5353,5356,5359,5361,5363,5365,5368,5371,5374,5377,5380,5383],{"class":188,"line":252},[186,5337,5338],{"class":295},"    n",[186,5340,635],{"class":208},[186,5342,5343],{"class":200},"$((",[186,5345,5346],{"class":196},"n",[186,5348,5349],{"class":280}," +",[186,5351,5352],{"class":200}," $(",[186,5354,5355],{"class":196},"tar",[186,5357,5358],{"class":351}," -xzOf",[186,5360,5301],{"class":280},[186,5362,5324],{"class":295},[186,5364,5327],{"class":280},[186,5366,5367],{"class":200}," 2>",[186,5369,5370],{"class":280},"/dev/null",[186,5372,5373],{"class":200}," | ",[186,5375,5376],{"class":196},"grep",[186,5378,5379],{"class":351}," -c",[186,5381,5382],{"class":280}," '\" [0-9]\\{3\\} '",[186,5384,5385],{"class":200},")))\n",[186,5387,5388],{"class":188,"line":272},[186,5389,5390],{"class":192},"  done\n",[186,5392,5393,5396,5399,5401,5404,5406,5409,5412,5414,5416],{"class":188,"line":292},[186,5394,5395],{"class":208},"  printf",[186,5397,5398],{"class":280}," '%8d  %s\\n'",[186,5400,5301],{"class":280},[186,5402,5403],{"class":295},"$n",[186,5405,5327],{"class":280},[186,5407,5408],{"class":280}," \"$(",[186,5410,5411],{"class":196},"basename",[186,5413,5301],{"class":280},[186,5415,5304],{"class":295},[186,5417,5418],{"class":280},"\")\"\n",[186,5420,5421,5424,5426,5429],{"class":188,"line":307},[186,5422,5423],{"class":192},"done",[186,5425,5373],{"class":200},[186,5427,5428],{"class":196},"sort",[186,5430,5431],{"class":351}," -rn\n",[11,5433,2222,5434,5437],{},[53,5435,5436],{},"'\" [0-9]\\{3\\} '"," jest tu istotny: archiwum zawiera log dostępowy razem z logiem błędów, a bez niego witryna sypiąca tysiącami ostrzeżeń PHP wychodzi na lidera ruchu. Pętla sumuje też archiwa w obrębie domeny, bo bywa ich kilka.",[11,5439,5440,5441,5444],{},"Na opisywanym koncie lider miał 21 tysięcy żądań na dobę. Witryna, która faktycznie powodowała przekroczenia, była na ",[15,5442,5443],{},"dziewiętnastym miejscu"," z tysiącem żądań.",[11,5446,5447,5448,71],{},"Całe konto obsługiwało 77 tysięcy żądań dziennie, czyli poniżej jednego na sekundę. Przy takim ruchu limit czterech rdzeni jest nie do wyczerpania w normalnych warunkach. Problemem nie był wolumen, tylko ",[15,5449,5450],{},"koncentracja",[26,5452,5454],{"id":5453},"pułapka-trzecia-błędy-w-logach-to-objaw-nie-zawsze-przyczyna","Pułapka trzecia: błędy w logach to objaw, nie zawsze przyczyna",[11,5456,5457],{},"Kolejny kuszący trop. Policzmy, która witryna generuje najwięcej wpisów błędów:",[177,5459,5461],{"className":1591,"code":5460,"language":1593,"meta":182,"style":182},"for f in ~/domains/*/logs/*.tar.gz; do\n  d=$(basename \"$(dirname \"$(dirname \"$f\")\")\")\n  e=$(tar -xzOf \"$f\" 2>/dev/null | grep -cE 'PHP (Fatal|Warning|Notice|Deprecated)')\n  [ \"$e\" -gt 50 ] && printf '%6d  %s\\n' \"$e\" \"$d\"\ndone | sort -rn\n",[53,5462,5463,5478,5508,5543,5583],{"__ignoreMap":182},[186,5464,5465,5467,5469,5471,5474,5476],{"class":188,"line":189},[186,5466,5263],{"class":192},[186,5468,5296],{"class":295},[186,5470,5269],{"class":192},[186,5472,5473],{"class":280}," ~/domains/*/logs/*.tar.gz",[186,5475,5275],{"class":200},[186,5477,5278],{"class":192},[186,5479,5480,5483,5485,5488,5490,5492,5495,5497,5499,5501,5503,5506],{"class":188,"line":218},[186,5481,5482],{"class":295},"  d",[186,5484,635],{"class":208},[186,5486,5487],{"class":200},"$(",[186,5489,5411],{"class":196},[186,5491,5408],{"class":280},[186,5493,5494],{"class":196},"dirname",[186,5496,5408],{"class":280},[186,5498,5494],{"class":196},[186,5500,5301],{"class":280},[186,5502,5324],{"class":295},[186,5504,5505],{"class":280},"\")\")\"",[186,5507,2021],{"class":200},[186,5509,5510,5513,5515,5517,5519,5521,5523,5525,5527,5529,5531,5533,5535,5538,5541],{"class":188,"line":239},[186,5511,5512],{"class":295},"  e",[186,5514,635],{"class":208},[186,5516,5487],{"class":200},[186,5518,5355],{"class":196},[186,5520,5358],{"class":351},[186,5522,5301],{"class":280},[186,5524,5324],{"class":295},[186,5526,5327],{"class":280},[186,5528,5367],{"class":200},[186,5530,5370],{"class":280},[186,5532,5373],{"class":200},[186,5534,5376],{"class":196},[186,5536,5537],{"class":351}," -cE",[186,5539,5540],{"class":280}," 'PHP (Fatal|Warning|Notice|Deprecated)'",[186,5542,2021],{"class":200},[186,5544,5545,5548,5550,5553,5555,5558,5561,5564,5567,5570,5572,5574,5576,5578,5580],{"class":188,"line":245},[186,5546,5547],{"class":200},"  [ ",[186,5549,5327],{"class":280},[186,5551,5552],{"class":295},"$e",[186,5554,5327],{"class":280},[186,5556,5557],{"class":208}," -gt",[186,5559,5560],{"class":351}," 50",[186,5562,5563],{"class":200}," ] && ",[186,5565,5566],{"class":208},"printf",[186,5568,5569],{"class":280}," '%6d  %s\\n'",[186,5571,5301],{"class":280},[186,5573,5552],{"class":295},[186,5575,5327],{"class":280},[186,5577,5301],{"class":280},[186,5579,5304],{"class":295},[186,5581,5582],{"class":280},"\"\n",[186,5584,5585,5587,5589,5591],{"class":188,"line":252},[186,5586,5423],{"class":192},[186,5588,5373],{"class":200},[186,5590,5428],{"class":196},[186,5592,5431],{"class":351},[11,5594,5595],{},"Wynik był jednoznaczny: jedna witryna z sześcioma tysiącami wpisów na dobę, kolejna w rankingu miała trzysta. Wygląda na trafienie w dziesiątkę.",[11,5597,5598],{},"Treść błędów też prowadziła do konkretu:",[177,5600,5602],{"className":1591,"code":5601,"language":1593,"meta":182,"style":182},"tar -xzOf ~/domains/twojadomena.pl/logs/*.tar.gz \\\n  | grep -oE 'in /home[^ ]+ on line [0-9]+' \\\n  | sort | uniq -c | sort -rn | head\n",[53,5603,5604,5622,5637],{"__ignoreMap":182},[186,5605,5606,5608,5610,5613,5616,5619],{"class":188,"line":189},[186,5607,5355],{"class":196},[186,5609,5358],{"class":351},[186,5611,5612],{"class":280}," ~/domains/twojadomena.pl/logs/",[186,5614,5615],{"class":204},"*",[186,5617,5618],{"class":280},".tar.gz",[186,5620,5621],{"class":208}," \\\n",[186,5623,5624,5627,5629,5632,5635],{"class":188,"line":218},[186,5625,5626],{"class":200},"  | ",[186,5628,5376],{"class":196},[186,5630,5631],{"class":351}," -oE",[186,5633,5634],{"class":280}," 'in /home[^ ]+ on line [0-9]+'",[186,5636,5621],{"class":208},[186,5638,5639,5641,5643,5645,5648,5650,5652,5654,5657,5659],{"class":188,"line":239},[186,5640,5626],{"class":200},[186,5642,5428],{"class":196},[186,5644,5373],{"class":200},[186,5646,5647],{"class":196},"uniq",[186,5649,5379],{"class":351},[186,5651,5373],{"class":200},[186,5653,5428],{"class":196},[186,5655,5656],{"class":351}," -rn",[186,5658,5373],{"class":200},[186,5660,5661],{"class":196},"head\n",[11,5663,5664],{},"Wyszły dwa miejsca w kodzie motywu, oba warte naprawy, i wymieniam je na końcu tekstu. Ale sześć tysięcy warningów dziennie to koszt zapisu do pliku, nie obciążenie procesora. Tabela z panelu pokazywała operacje dyskowe na poziomie 200 KB/s przy limicie 150 MB/s, czyli margines siedmiusetkrotny.",[11,5666,5667],{},"Ta witryna nie miała z przekroczeniami nic wspólnego. Zgubił mnie tu zwykły błąd wnioskowania: najgłośniejszy sygnał w logach niekoniecznie jest najdroższym.",[26,5669,5671],{"id":5670},"skanery-podatności-wyzwalacz","Skanery podatności: wyzwalacz",[11,5673,5674],{},"Rozkład żądań co do minuty wygląda tak:",[177,5676,5678],{"className":1591,"code":5677,"language":1593,"meta":182,"style":182},"tar -xzOf ~/domains/twojadomena.pl/logs/*.tar.gz \\\n  | grep -oE '[0-9]{2}/[A-Za-z]{3}/[0-9]{4}:[0-9]{2}:[0-9]{2}' \\\n  | sort | uniq -c | sort -rn | head -15\n",[53,5679,5680,5694,5707],{"__ignoreMap":182},[186,5681,5682,5684,5686,5688,5690,5692],{"class":188,"line":189},[186,5683,5355],{"class":196},[186,5685,5358],{"class":351},[186,5687,5612],{"class":280},[186,5689,5615],{"class":204},[186,5691,5618],{"class":280},[186,5693,5621],{"class":208},[186,5695,5696,5698,5700,5702,5705],{"class":188,"line":218},[186,5697,5626],{"class":200},[186,5699,5376],{"class":196},[186,5701,5631],{"class":351},[186,5703,5704],{"class":280}," '[0-9]{2}/[A-Za-z]{3}/[0-9]{4}:[0-9]{2}:[0-9]{2}'",[186,5706,5621],{"class":208},[186,5708,5709,5711,5713,5715,5717,5719,5721,5723,5725,5727,5730],{"class":188,"line":239},[186,5710,5626],{"class":200},[186,5712,5428],{"class":196},[186,5714,5373],{"class":200},[186,5716,5647],{"class":196},[186,5718,5379],{"class":351},[186,5720,5373],{"class":200},[186,5722,5428],{"class":196},[186,5724,5656],{"class":351},[186,5726,5373],{"class":200},[186,5728,5729],{"class":196},"head",[186,5731,5732],{"class":351}," -15\n",[11,5734,5735,5736,5739],{},"Wynik był ten sam na ",[15,5737,5738],{},"każdej"," domenie w koncie: około 300 żądań skumulowanych w jednej minucie, kilka razy na dobę, przy tle na poziomie kilku żądań na godzinę.",[11,5741,5742],{},"Za każdą serią stał jeden adres IP:",[177,5744,5746],{"className":1591,"code":5745,"language":1593,"meta":182,"style":182},"tar -xzOf ~/domains/twojadomena.pl/logs/*.tar.gz \\\n  | grep '17/Sep/2026:16:47:' | awk '{print $1}' | sort | uniq -c | sort -rn\n",[53,5747,5748,5762],{"__ignoreMap":182},[186,5749,5750,5752,5754,5756,5758,5760],{"class":188,"line":189},[186,5751,5355],{"class":196},[186,5753,5358],{"class":351},[186,5755,5612],{"class":280},[186,5757,5615],{"class":204},[186,5759,5618],{"class":280},[186,5761,5621],{"class":208},[186,5763,5764,5766,5768,5771,5773,5776,5779,5781,5783,5785,5787,5789,5791,5793],{"class":188,"line":218},[186,5765,5626],{"class":200},[186,5767,5376],{"class":196},[186,5769,5770],{"class":280}," '17/Sep/2026:16:47:'",[186,5772,5373],{"class":200},[186,5774,5775],{"class":196},"awk",[186,5777,5778],{"class":280}," '{print $1}'",[186,5780,5373],{"class":200},[186,5782,5428],{"class":196},[186,5784,5373],{"class":200},[186,5786,5647],{"class":196},[186,5788,5379],{"class":351},[186,5790,5373],{"class":200},[186,5792,5428],{"class":196},[186,5794,5431],{"class":351},[11,5796,5797],{},"A ścieżki, o które pytał, nie pozostawiały wątpliwości:",[177,5799,5802],{"className":5800,"code":5801,"language":5126,"meta":182},[5124],"/.env\n/wp/.env\n/api/.env\n/laravel/.env\n/wp-admin/phpinfo.php\n/graphql\n/console\n",[53,5803,5801],{"__ignoreMap":182},[11,5805,5806],{},"To automat szukający wycieków kluczy API i niezabezpieczonych paneli. Enumeruje kilkaset ścieżek, każdą po razie lub dwa. Adresy rotują między przelotami, zwykle w zakresach dużych dostawców chmurowych, a ten sam automat przechodzi kolejno po wszystkich domenach serwera z tej samej listy.",[11,5808,5809],{},"Tak wygląda wyzwalacz. Dlaczego bywa drogi, wyjaśnia się dopiero przy porównaniu dwóch witryn.",[26,5811,5813],{"id":5812},"co-naprawdę-kosztuje-wzorzec-wszystko-do-jednego-wejścia","Co naprawdę kosztuje: wzorzec „wszystko do jednego wejścia\"",[11,5815,5816],{},"Tego samego dnia ten sam automat przejechał tą samą listą ścieżek po dwóch witrynach z tego samego konta, w odstępie kilku godzin. Na pierwszej wysycił cztery rdzenie i wywołał przekroczenie limitu. Na drugiej nie zostawił w statystykach żadnego śladu.",[5818,5819,5820,5847],"table",{},[5821,5822,5823],"thead",{},[5824,5825,5826,5830,5833,5838,5841,5844],"tr",{},[5827,5828,5829],"th",{},"Witryna",[5827,5831,5832],{},"Okno",[5827,5834,5835,5836],{},"Odrzuconych przez ",[53,5837,5098],{},[5827,5839,5840],{},"Ciężkich odpowiedzi 404 z PHP",[5827,5842,5843],{},"Ubitych procesów PHP",[5827,5845,5846],{},"Średnia CPU",[5848,5849,5850,5871],"tbody",{},[5824,5851,5852,5856,5859,5862,5865,5868],{},[5853,5854,5855],"td",{},"sklep na WooCommerce",[5853,5857,5858],{},"02:28–02:29",[5853,5860,5861],{},"139",[5853,5863,5864],{},"138",[5853,5866,5867],{},"10",[5853,5869,5870],{},"86% w drugiej minucie, przekroczenie limitu",[5824,5872,5873,5876,5879,5882,5885,5887],{},[5853,5874,5875],{},"autorski sklep w PHP",[5853,5877,5878],{},"09:18",[5853,5880,5881],{},"66",[5853,5883,5884],{},"log dostępowy niepobrany",[5853,5886,1811],{},[5853,5888,5889],{},"poniżej 11%, bez śladu",[11,5891,5892,5893,5895],{},"Dwie granice tego zestawienia trzeba postawić od razu. Okna mają różną długość: dla sklepu na WooCommerce to dwie minuty odczytane z logu dostępowego, dla drugiej witryny jedna minuta z logu błędów. Stąd dziesięć ubitych procesów w tym wierszu, a siedem w tabeli minutowej niżej — trzy pierwsze wypadły w minucie 02:28, siedem pozostałych w 02:29. Ważniejsza jest druga granica — na drugiej witrynie blokady złapały wszystko, co w logu widać, więc te liczby nie mówią, ile kosztowałoby ją żądanie, które przez blokady przeszło. Zgadza się z nimi równie dobrze prostsze wyjaśnienie: jej ",[53,5894,5098],{}," pokrył całą listę skanera, a plik pierwszej witryny pokrywał połowę.",[11,5897,5898],{},"Wyjaśnienie, które opisuję niżej, odczytałem więc z konfiguracji obu witryn. Ma to jedną zaletę: czytelnik sprawdzi je u siebie w jednym pliku, bez czekania na skaner.",[11,5900,5901,5904,5905,5908,5909,5911],{},[15,5902,5903],{},"WordPress kieruje wszystko do jednego pliku wejściowego."," Blok ",[53,5906,5907],{},"# BEGIN WordPress"," kończy się regułą: jeśli żądany plik nie istnieje i nie istnieje taki katalog, przepisz żądanie na ",[53,5910,5214],{},". PHP uruchamia wtedy pełny bootstrap, ładuje wtyczki i motyw, odpytuje bazę i dopiero wtedy zwraca 404. Tak samo działa Laravel, Symfony i praktycznie każdy współczesny framework PHP.",[11,5913,5914,5917,5918,56,5921,56,5924,5927],{},[15,5915,5916],{},"Druga witryna takiej reguły nie ma."," To autorski sklep z jawnymi regułami mapującymi ładne adresy na konkretne pliki: ",[53,5919,5920],{},"listing.php",[53,5922,5923],{},"produkt.php",[53,5925,5926],{},"koszyk.php",". Adres, który nie pasuje do żadnego wzorca, kończy się na systemie plików i nie uruchamia aplikacji.",[11,5929,5930,5931,5933],{},"Stąd wniosek, który odwraca kolejność myślenia o tym problemie. Skaner sam z siebie nie kosztuje prawie nic — to kilkaset żądań rozłożonych na kilkanaście sekund, czyli tyle, ile zwykła strona obsługuje przy jednym wejściu użytkownika. Kosztuje dopiero zderzenie skanera z wzorcem „wszystko, czego nie ma, idzie do jednego pliku wejściowego\". Blokada w ",[53,5932,5098],{}," działa, bo rozłącza te dwie rzeczy.",[11,5935,5936],{},"W logu błędów widać to wprost. Żądaniom, które doszły do PHP, odpowiadają wpisy:",[177,5938,5941],{"className":5939,"code":5940,"language":5126,"meta":182},[5124],"Abort request processing by PID:3159787, kill: 1, begin time: 3, sent time: 3, req processed: 0\n",[53,5942,5940],{"__ignoreMap":182},[11,5944,5945,5948],{},[53,5946,5947],{},"req processed: 0"," znaczy, że proces wystartował i nie obsłużył ani jednego żądania. Serwer aplikacyjny odpala kolejne procesy, żeby nadążyć, ubija je po czasie, i limit konta wysyca się w kilkanaście sekund.",[11,5950,5951],{},"Żeby domknąć porównanie dwóch witryn do końca, brakuje jednego pomiaru: żądania na tej drugiej, które blokady przepuściły, z kodem odpowiedzi, rozmiarem i sprawdzeniem, czy w logu błędów stoi przy nim wpis o ubitym procesie. Jej logu dostępowego tego dnia nie pobrałem. Zostaje więc mechanizm odczytany z konfiguracji i pomiar z witryn, które ten wzorzec mają — ten drugi wypada jednoznacznie.",[1329,5953,5955],{"id":5954},"co-odróżnia-serię-kosztowną-od-darmowej","Co odróżnia serię kosztowną od darmowej",[11,5957,5958],{},"Doba zmierzona minuta po minucie, zestawiona z logiem błędów, pokazuje to na sześciu seriach jednego automatu:",[5818,5960,5961,5983],{},[5821,5962,5963],{},[5824,5964,5965,5968,5970,5975,5977,5980],{},[5827,5966,5967],{},"Seria",[5827,5969,5829],{},[5827,5971,5972,5973],{},"Zablokowanych przez ",[53,5974,5098],{},[5827,5976,5843],{},[5827,5978,5979],{},"Średnia CPU w tej minucie",[5827,5981,5982],{},"Przekroczenie",[5848,5984,5985,6004,6023,6039,6059,6073],{},[5824,5986,5987,5990,5993,5996,5998,6001],{},[5853,5988,5989],{},"01:40",[5853,5991,5992],{},"autorski sklep",[5853,5994,5995],{},"36",[5853,5997,1811],{},[5853,5999,6000],{},"poniżej 11%",[5853,6002,6003],{},"nie",[5824,6005,6006,6009,6011,6014,6017,6020],{},[5853,6007,6008],{},"02:29",[5853,6010,5855],{},[5853,6012,6013],{},"22",[5853,6015,6016],{},"7",[5853,6018,6019],{},"86%",[5853,6021,6022],{},"tak",[5824,6024,6025,6028,6030,6033,6035,6037],{},[5853,6026,6027],{},"04:42",[5853,6029,5992],{},[5853,6031,6032],{},"46",[5853,6034,1811],{},[5853,6036,6000],{},[5853,6038,6003],{},[5824,6040,6041,6044,6047,6050,6053,6056],{},[5853,6042,6043],{},"06:12",[5853,6045,6046],{},"witryna A",[5853,6048,6049],{},"35",[5853,6051,6052],{},"5",[5853,6054,6055],{},"2%",[5853,6057,6058],{},"tak, minutę później",[5824,6060,6061,6063,6065,6067,6069,6071],{},[5853,6062,5878],{},[5853,6064,5992],{},[5853,6066,5881],{},[5853,6068,1811],{},[5853,6070,6000],{},[5853,6072,6003],{},[5824,6074,6075,6078,6081,6084,6086,6088],{},[5853,6076,6077],{},"09:25",[5853,6079,6080],{},"witryna B",[5853,6082,6083],{},"32",[5853,6085,6052],{},[5853,6087,6000],{},[5853,6089,6090],{},"nie, 50% minutę później",[11,6092,6093,6094,6097,6098,71],{},"To jest sześć serii ",[15,6095,6096],{},"jednego automatu na sześciu różnych domenach",", nie sześć przelotów po jednej witrynie — dlatego kolumna z witryną jest tu potrzebna. Trzy kolumny z liczbami pochodzą przy tym z trzech różnych źródeł: zablokowane żądania i ubite procesy z logu błędów danej domeny, średnia CPU i przekroczenia z panelu, który mierzy ",[15,6099,6100],{},"całe konto",[1329,6102,6104],{"id":6103},"dlaczego-oba-logi-podają-inne-liczby-odrzuceń","Dlaczego oba logi podają inne liczby odrzuceń",[11,6106,6107,6108,6111,6112,3042,6115,6118],{},"Ta sama seria, ta sama minuta, dwa logi tej samej domeny: dostępowy ma ",[15,6109,6110],{},"272 odpowiedzi 403",", a log błędów ",[15,6113,6114],{},"62 wiersze",[53,6116,6117],{},"Access is denied by context rewrite."," Rozbieżność wygląda na gubienie wierszy i przez pół dnia tak ją czytałem. Nie jest nim.",[11,6120,6121],{},"Przez ile wierszy log milczy, widać dopiero po ponumerowaniu żądań w kolejności, w jakiej przyszły. Milczenie kończy się dokładnie w tym miejscu, w którym skaner przechodzi z jednej grupy ścieżek na drugą:",[177,6123,6126],{"className":6124,"code":6125,"language":5126,"meta":182},[5124],"210  /development/.env       bez wiersza w logu błędów\n211  /production/.env        bez wiersza\n212  /config/app/.env        bez wiersza\n213  /phpinfo.php            od tego miejsca log pisze wszystko\n214  /info.php\n215  /php.php\n",[53,6127,6125],{"__ignoreMap":182},[11,6129,6130,6131,71],{},"Od żądania 213 do końca serii log błędów ma komplet, a jedyne dwie dziury w numeracji to żądania, które dostały 301, nie 403. Granica nie leży więc w czasie ani w tempie, tylko w ",[15,6132,6133],{},"ścieżce",[11,6135,6136,6137,6139],{},"Sprawdzenie zajmuje jedno żądanie. Wybrałem domenę na tym samym koncie, która nie ma ",[53,6138,5098],{}," w ogóle:",[177,6141,6144],{"className":6142,"code":6143,"language":5126,"meta":182},[5124],"/.env         403\n/.git/config  403\n/phpinfo.php  404\n/info.php     404\n",[53,6145,6143],{"__ignoreMap":182},[11,6147,6148,6149,6151,6152,6155,6156,6158,6159,6162],{},"Ścieżki kropkowe są odcinane przez sam serwer, zanim jakakolwiek reguła z ",[53,6150,5098],{}," zdąży się do nich odnieść. Dlatego nie zostawiają wiersza o odrzuceniu przez przepisanie — bo żadne przepisanie się nie odbyło. Blokada serwera ma przy tym wyjątek na ",[53,6153,6154],{},".well-known",": na tej samej domenie bez ",[53,6157,5098],{}," adres ",[53,6160,6161],{},"/.well-known/acme-challenge/test"," oddaje 404, nie 403, więc odnawianie certyfikatów przez nią przechodzi.",[11,6164,6165,6166,6169],{},"W tej serii 208 z 272 odrzuceń było dziełem serwera, a 64 dziełem bloku reguł. Log błędów zapisał 62 wiersze i te dwa brakujące też się tłumaczą: trzy odrzucenia to ",[53,6167,6168],{},"POST /"," z pierwszych sekund serii, sprzed zakresu, który log numeruje. Wewnątrz zakresu nie brakuje niczego.",[11,6171,6172],{},"Dwa wnioski, oba praktyczne:",[11,6174,6175,6181],{},[15,6176,6177,6178,6180],{},"Log błędów liczy odrzucenia z Twojego ",[53,6179,5098],{},", log dostępowy wszystkie odpowiedzi 403."," To są dwie różne wielkości i tak trzeba je czytać. Kolumna w tabeli wyżej pochodzi z logu błędów, więc opisuje pracę bloku, nie całość odmów.",[11,6183,6184,6187,6188,6190],{},[15,6185,6186],{},"Reguła na pliki kropkowe może być na Twoim serwerze zbędna."," Sprawdź to u siebie tak samo: zapytaj o ",[53,6189,5080],{}," domenę, która nie ma żadnych reguł. Jeśli dostaniesz 403, LiteSpeed robi to sam i pierwsza reguła bloku nic nie dokłada poza kosztem parsowania. Zostawiam ją w bloku wyżej, bo na Apache bez tej ochrony jest niezbędna, a nie każdy czyta ten tekst z LiteSpeedem pod spodem.",[11,6192,6193,6194,6197,6198,6201],{},"Licznik ",[53,6195,6196],{},"hit 11 times per second",", który pojawia się w tym tekście przy stronie błędu, dotyczy przy okazji komunikatu ",[53,6199,6200],{},"File not found",", a nie odrzuceń. Oba siedzą w tym samym pliku i tylko ten pierwszy bywa zwijany — dlatego w minutach z serią nie ma po nim śladu.",[11,6203,6204],{},"Liczba zablokowanych żądań nie przewiduje niczego: sześćdziesiąt sześć przeszło bez śladu, dwadzieścia dwa wysyciły cztery rdzenie. Serie kosztowne od darmowych odróżnia kolumna z ubitymi procesami.",[11,6206,6207],{},"Z jednym zastrzeżeniem, którego ta tabela sama nie pokazuje: ubity proces jest po części skutkiem, nie tylko przyczyną. Serwer aplikacyjny ubija workera po przekroczeniu czasu obsługi żądania, a ten czas rośnie, gdy konto zaczyna być dławione za przekroczenie limitu. Powstaje sprzężenie: dławienie wydłuża żądania, dłuższe żądania są ubijane, każde ubite to kolejny start. Kolumna z ubitymi procesami jest więc dobrym znacznikiem serii, która weszła w ten tryb, i złym miernikiem jej przyczyny.",[11,6209,6210],{},"Przesunięcie o minutę w dwóch wierszach bierze się stąd, że w logu dostępowym stoi czas przyjęcia żądania, a zużycie procesora księguje się wtedy, gdy praca faktycznie idzie. Przy seriach trwających kilkanaście sekund i przełomie minuty w środku serii to normalne.",[11,6212,6213,6214,6217],{},"Jednej rzeczy te dane nie pokazują i warto powiedzieć to wprost: ",[15,6215,6216],{},"zależności ilościowej tu nie ma."," Pięć ubitych procesów o 06:12 skończyło się przekroczeniem limitu w następnej minucie, tyle samo o 09:25 dało w następnej minucie pięćdziesiąt procent i żadnego przekroczenia. Rekordowa seria całej doby, jedenaście ubitych procesów o 02:42 na jeszcze innej domenie tego konta, skończyła się na pięćdziesięciu procentach i też nie dotknęła limitu, a siedem procesów o 02:29 dało osiemdziesiąt sześć i przekroczenie.",[11,6219,6220],{},"Średnia minutowa i przekroczenie to zresztą dwie różne miary. Licznik przekroczeń reaguje na chwilowe dotknięcie limitu, a średnia rozkłada to samo zdarzenie na sześćdziesiąt sekund — w tej samej dobie minuta o średniej 35% ma przekroczenie, a minuta o średniej 50% go nie ma. Sześć punktów pomiarowych wystarcza, żeby odróżnić serię kosztowną od darmowej, i nie wystarcza, żeby przewidywać wysokość szczytu.",[26,6222,6224],{"id":6223},"rozwiązanie-odciąć-przed-php","Rozwiązanie: odciąć przed PHP",[11,6226,6227,6228,6230,6231,6234,6235,1636],{},"Te żądania trzeba obsłużyć regułą w ",[53,6229,5098],{},", zanim uruchomi się jakikolwiek kod PHP. Blok wstawiony ",[15,6232,6233],{},"na początku pliku",", przed sekcją ",[53,6236,5907],{},[177,6238,6242],{"className":6239,"code":6240,"language":6241,"meta":182,"style":182},"language-apache shiki shiki-themes one-dark-pro","\u003CIfModule mod_rewrite.c>\nRewriteEngine On\n\nRewriteRule (^|/)\\.(env|git|svn|aws|config|ssh|npmrc|DS_Store) - [F,L]\nRewriteRule (^|/)phpinfo\\.php$ - [F,L]\nRewriteRule (^|/)wp-config\\.php\\. - [F,L]\nRewriteRule (^|/)(graphql|console|actuator|telescope)(/|$) - [F,L]\nRewriteRule \\.(bak|old|save|swp|sql|log)$ - [F,L]\nRewriteRule ~$ - [F,L]\nRewriteRule ^(info|i|p|pi|php|pinfo|phpinfo|php-info|php_info|phpversion|server-info|server-status|test|debug|_phpinfo|old_phpinfo|admin_phpinfo|infophp|infos|config|configuration|settings|setup|bigdump|adminer|shell)\\.php$ - [F,L]\nRewriteRule ^_?(environment|profiler)(/|$) - [F,L]\n\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_URI} !^/\\.well-known/\nRewriteRule (^|/)\\. - [F,L]\n\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_URI} !^/\\.well-known/\nRewriteRule \\.(js|mjs|cjs|ts|css|map|json|yml|yaml|toml|ini|env|conf|cfg|properties|key|pem|secret|sql|gz|bak|old|orig|save|swp|log|tfstate|tfvars|hcl|rb|py|sh|cgi|csv|lock|settings|backup)$ - [F,L]\n\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteRule ^(config|configs|conf|settings|secrets)/[^/]+\\.php$ - [F,L]\n\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteRule ^(Dockerfile|Rakefile|credentials|env|phpinfo|id_rsa|wp-config)$ - [F,L]\n\nRewriteCond %{QUERY_STRING} (^|&)phpinfo= [NC]\nRewriteRule ^ - [F,L]\n\u003C/IfModule>\n\nErrorDocument 403 /403.shtml\nErrorDocument 404 /404.shtml\n","apache",[53,6243,6244,6255,6263,6267,6281,6292,6303,6314,6325,6336,6347,6358,6362,6373,6383,6394,6398,6406,6414,6425,6429,6437,6448,6452,6460,6471,6475,6488,6499,6504,6508,6525],{"__ignoreMap":182},[186,6245,6246,6249,6252],{"class":188,"line":189},[186,6247,6248],{"class":200},"\u003CIfModule ",[186,6250,6251],{"class":280},"mod_rewrite.c",[186,6253,6254],{"class":200},">\n",[186,6256,6257,6260],{"class":188,"line":218},[186,6258,6259],{"class":192},"RewriteEngine",[186,6261,6262],{"class":200}," On\n",[186,6264,6265],{"class":188,"line":239},[186,6266,249],{"emptyLinePlaceholder":248},[186,6268,6269,6272,6275,6278],{"class":188,"line":245},[186,6270,6271],{"class":192},"RewriteRule",[186,6273,6274],{"class":295}," (^|/)\\.(env|git|svn|aws|config|ssh|npmrc|DS_Store)",[186,6276,6277],{"class":280}," -",[186,6279,6280],{"class":200}," [F,L]\n",[186,6282,6283,6285,6288,6290],{"class":188,"line":252},[186,6284,6271],{"class":192},[186,6286,6287],{"class":295}," (^|/)phpinfo\\.php$",[186,6289,6277],{"class":280},[186,6291,6280],{"class":200},[186,6293,6294,6296,6299,6301],{"class":188,"line":272},[186,6295,6271],{"class":192},[186,6297,6298],{"class":295}," (^|/)wp-config\\.php\\.",[186,6300,6277],{"class":280},[186,6302,6280],{"class":200},[186,6304,6305,6307,6310,6312],{"class":188,"line":292},[186,6306,6271],{"class":192},[186,6308,6309],{"class":295}," (^|/)(graphql|console|actuator|telescope)(/|$)",[186,6311,6277],{"class":280},[186,6313,6280],{"class":200},[186,6315,6316,6318,6321,6323],{"class":188,"line":307},[186,6317,6271],{"class":192},[186,6319,6320],{"class":295}," \\.(bak|old|save|swp|sql|log)$",[186,6322,6277],{"class":280},[186,6324,6280],{"class":200},[186,6326,6327,6329,6332,6334],{"class":188,"line":319},[186,6328,6271],{"class":192},[186,6330,6331],{"class":295}," ~$",[186,6333,6277],{"class":280},[186,6335,6280],{"class":200},[186,6337,6338,6340,6343,6345],{"class":188,"line":331},[186,6339,6271],{"class":192},[186,6341,6342],{"class":295}," ^(info|i|p|pi|php|pinfo|phpinfo|php-info|php_info|phpversion|server-info|server-status|test|debug|_phpinfo|old_phpinfo|admin_phpinfo|infophp|infos|config|configuration|settings|setup|bigdump|adminer|shell)\\.php$",[186,6344,6277],{"class":280},[186,6346,6280],{"class":200},[186,6348,6349,6351,6354,6356],{"class":188,"line":343},[186,6350,6271],{"class":192},[186,6352,6353],{"class":295}," ^_?(environment|profiler)(/|$)",[186,6355,6277],{"class":280},[186,6357,6280],{"class":200},[186,6359,6360],{"class":188,"line":357},[186,6361,249],{"emptyLinePlaceholder":248},[186,6363,6364,6367,6370],{"class":188,"line":363},[186,6365,6366],{"class":192},"RewriteCond",[186,6368,6369],{"class":295}," %{REQUEST_FILENAME}",[186,6371,6372],{"class":280}," !-f\n",[186,6374,6375,6377,6380],{"class":188,"line":990},[186,6376,6366],{"class":192},[186,6378,6379],{"class":295}," %{REQUEST_URI}",[186,6381,6382],{"class":280}," !^/\\.well-known/\n",[186,6384,6385,6387,6390,6392],{"class":188,"line":996},[186,6386,6271],{"class":192},[186,6388,6389],{"class":295}," (^|/)\\.",[186,6391,6277],{"class":280},[186,6393,6280],{"class":200},[186,6395,6396],{"class":188,"line":1001},[186,6397,249],{"emptyLinePlaceholder":248},[186,6399,6400,6402,6404],{"class":188,"line":1018},[186,6401,6366],{"class":192},[186,6403,6369],{"class":295},[186,6405,6372],{"class":280},[186,6407,6408,6410,6412],{"class":188,"line":1023},[186,6409,6366],{"class":192},[186,6411,6379],{"class":295},[186,6413,6382],{"class":280},[186,6415,6416,6418,6421,6423],{"class":188,"line":1028},[186,6417,6271],{"class":192},[186,6419,6420],{"class":295}," \\.(js|mjs|cjs|ts|css|map|json|yml|yaml|toml|ini|env|conf|cfg|properties|key|pem|secret|sql|gz|bak|old|orig|save|swp|log|tfstate|tfvars|hcl|rb|py|sh|cgi|csv|lock|settings|backup)$",[186,6422,6277],{"class":280},[186,6424,6280],{"class":200},[186,6426,6427],{"class":188,"line":1051},[186,6428,249],{"emptyLinePlaceholder":248},[186,6430,6431,6433,6435],{"class":188,"line":1059},[186,6432,6366],{"class":192},[186,6434,6369],{"class":295},[186,6436,6372],{"class":280},[186,6438,6439,6441,6444,6446],{"class":188,"line":1070},[186,6440,6271],{"class":192},[186,6442,6443],{"class":295}," ^(config|configs|conf|settings|secrets)/[^/]+\\.php$",[186,6445,6277],{"class":280},[186,6447,6280],{"class":200},[186,6449,6450],{"class":188,"line":1103},[186,6451,249],{"emptyLinePlaceholder":248},[186,6453,6454,6456,6458],{"class":188,"line":1108},[186,6455,6366],{"class":192},[186,6457,6369],{"class":295},[186,6459,6372],{"class":280},[186,6461,6462,6464,6467,6469],{"class":188,"line":1113},[186,6463,6271],{"class":192},[186,6465,6466],{"class":295}," ^(Dockerfile|Rakefile|credentials|env|phpinfo|id_rsa|wp-config)$",[186,6468,6277],{"class":280},[186,6470,6280],{"class":200},[186,6472,6473],{"class":188,"line":1151},[186,6474,249],{"emptyLinePlaceholder":248},[186,6476,6477,6479,6482,6485],{"class":188,"line":2790},[186,6478,6366],{"class":192},[186,6480,6481],{"class":295}," %{QUERY_STRING}",[186,6483,6484],{"class":280}," (^|&)phpinfo=",[186,6486,6487],{"class":200}," [NC]\n",[186,6489,6490,6492,6495,6497],{"class":188,"line":2808},[186,6491,6271],{"class":192},[186,6493,6494],{"class":295}," ^",[186,6496,6277],{"class":280},[186,6498,6280],{"class":200},[186,6500,6501],{"class":188,"line":2820},[186,6502,6503],{"class":200},"\u003C/IfModule>\n",[186,6505,6506],{"class":188,"line":2833},[186,6507,249],{"emptyLinePlaceholder":248},[186,6509,6510,6513,6516,6519,6522],{"class":188,"line":2839},[186,6511,6512],{"class":192},"ErrorDocument",[186,6514,6515],{"class":351}," 403",[186,6517,6518],{"class":200}," /",[186,6520,6521],{"class":351},"403",[186,6523,6524],{"class":200},".shtml\n",[186,6526,6527,6529,6532,6534,6537],{"class":188,"line":2844},[186,6528,6512],{"class":192},[186,6530,6531],{"class":351}," 404",[186,6533,6518],{"class":200},[186,6535,6536],{"class":351},"404",[186,6538,6524],{"class":200},[11,6540,6541,6542,6544],{},"Kolejność jest tu wszystkim. Reguły WordPressa kończą się przepisaniem wszystkiego na ",[53,6543,5214],{},", więc cokolwiek dopiszesz poniżej, nie zadziała.",[11,6546,6547],{},"Koszt obsługi takiego żądania spada z pełnego bootstrapu WordPressa do dopasowania wyrażenia regularnego.",[11,6549,6550,6551,6554,6555,116,6557,6560],{},"Jedna reguła wymaga decyzji. Jeśli korzystasz z WPGraphQL albo headless WordPressa, usuń ",[53,6552,6553],{},"graphql"," z listy w regule o ",[53,6556,6553],{},[53,6558,6559],{},"console"," — inaczej odetniesz własne API.",[11,6562,6563,6564,6566,6567,6570,6571,6574,6575,6577,6578,6580,6581,6584,6585,6588,6589,56,6592,116,6595,6598,6599,6602,6603,3042,6605,6608,6609,6612],{},"Druga rzecz, która wygląda na niekonsekwencję i nią nie jest: długa lista nazw plików PHP zaczyna się od ",[53,6565,2105],{},", czyli łapie wyłącznie korzeń domeny, a ",[53,6568,6569],{},"phpinfo"," dostał osobną regułę z ",[53,6572,6573],{},"(^|/)"," i łapie każdy katalog. W ",[53,6576,5098],{}," wzorzec ",[53,6579,6271],{}," widzi ścieżkę bez wiodącego ukośnika, więc ",[53,6582,6583],{},"^info\\.php$"," nie dopasuje ",[53,6586,6587],{},"/wp-admin/info.php",". Tak ma być: ",[53,6590,6591],{},"test.php",[53,6593,6594],{},"config.php",[53,6596,6597],{},"settings.php"," bywają prawdziwymi plikami wtyczek, a ",[53,6600,6601],{},"phpinfo.php"," w katalogu WordPressa nie ma prawa istnieć. Jeśli chcesz rozszerzyć tę listę na podkatalogi, przepisz ją na ",[53,6604,6573],{},[15,6606,6607],{},"razem"," z warunkiem ",[53,6610,6611],{},"RewriteCond %{REQUEST_FILENAME} !-f",", dokładnie jak w dolnej części bloku.",[1329,6614,6616],{"id":6615},"warunek-na-którym-stoi-cała-druga-połowa-bloku","Warunek, na którym stoi cała druga połowa bloku",[11,6618,6619,6620,6622],{},"Reguły z dolnej części mają przed sobą ",[53,6621,6611],{},". Ten warunek znaczy: zadziałaj wyłącznie wtedy, gdy żądany plik nie istnieje na dysku.",[11,6624,6625,6626,6628,6629,6632,6633,6635,6636,6639],{},"To jest różnica między blokadą, którą da się bezpiecznie rozszerzać, a blokadą, którą prędzej czy później odetniesz sobie własną witrynę. Bez tego warunku wpisanie ",[53,6627,1620],{}," na listę rozszerzeń zamyka ",[53,6630,6631],{},"manifest.json",", z którego korzystają aplikacje progresywne i ikony na ekranie startowym. Z tym warunkiem ",[53,6634,6631],{}," przechodzi, bo leży na dysku, a ",[53,6637,6638],{},"/gcp-credentials.json"," dostaje 403, bo nie leży.",[11,6641,6642,6643,56,6646,56,6649,5091,6652,6655],{},"Górna grupa reguł tego warunku celowo nie ma i część rozszerzeń powtarza się w obu miejscach. Dla ",[53,6644,6645],{},".bak",[53,6647,6648],{},".sql",[53,6650,6651],{},".swp",[53,6653,6654],{},".old"," istnienie pliku jest samo w sobie problemem — zrzut bazy leżący w katalogu domeny ma zostać niedostępny właśnie dlatego, że tam leży. Gdyby te rozszerzenia stały wyłącznie w dolnej liście, jedynym chronionym przypadkiem byłby plik, którego nie ma. Podział przebiega więc tak: na górze rozszerzenia, których witryna nie serwuje nigdy, na dole te, które mogą być zwykłą treścią.",[11,6657,6658,6659,56,6662,56,6665,56,6668,6671,6672,56,6675,56,6678,56,6681,6684,6685,56,6688,56,6691,56,6694,6697,6698,56,6701,56,6704,6707,6708,56,6710,6713,6714,6717],{},"Naturalny odruch przy pisaniu takiego bloku to wymienić nazwy plików, których szuka automat: ",[53,6660,6661],{},"service-account",[53,6663,6664],{},"credentials",[53,6666,6667],{},"keyfile",[53,6669,6670],{},"firebase-*"," i tak dalej. Takie podejście ma krótki termin przydatności. Między jednym przelotem a drugim, w odstępie dwóch dób, ten sam skaner dołożył pięć nowych kategorii ścieżek: pliki kropkowe konfiguracji chmurowych (",[53,6673,6674],{},"/.boto",[53,6676,6677],{},"/.kube/config",[53,6679,6680],{},"/.s3cfg",[53,6682,6683],{},"/.netrc","), stan infrastruktury (",[53,6686,6687],{},"/terraform.tfstate",[53,6689,6690],{},"/serverless.yml",[53,6692,6693],{},"/app.yaml",[53,6695,6696],{},"/Dockerfile","), klucze płatności i poczty (",[53,6699,6700],{},"/stripe.json",[53,6702,6703],{},"/stripe/webhook_secret.env",[53,6705,6706],{},"/sendgrid.env","), konfiguracje w podkatalogu (",[53,6709,5090],{},[53,6711,6712],{},"/config/stripe.php",") oraz ",[53,6715,6716],{},"/?phpinfo=1"," w parametrze adresu.",[11,6719,6720,6721,6724],{},"Lista nazw przegrywa z każdą aktualizacją słownika po drugiej stronie. Lista rozszerzeń z warunkiem ",[53,6722,6723],{},"!-f"," nie przegrywa, bo nie musi znać nazwy — wystarczy jej, że plik o takim rozszerzeniu na tej witrynie nie istnieje.",[11,6726,6727],{},"Na przelocie zmierzonym co do żądania 137 z 280 żądań skończyło się odmową, a 143 doszły do PHP. Liczba 137 obejmuje jednak odmowy serwerowe na ścieżkach kropkowych, opisane w sekcji o dwóch logach, więc udziału samego bloku z niej nie odczytam — rozbicia dla tej serii nie mam. Znaczenie ma druga liczba, mierzona wprost. Te 143 doszły do PHP i zostały w logu dostępowym razem ze ścieżkami — przepuściłem tę listę przez nowy blok przed wgraniem go gdziekolwiek i wyszło 133 dopasowania na 143. To jest więc symulacja na zapisanym ruchu, nie pomiar z kolejnego przelotu; pomiar przyszedł później i zgodził się z nią, bo po wdrożeniu do PHP nie doszło nic.",[1329,6729,6731,6732,6734],{"id":6730},"czego-f-nie-chroni","Czego ",[53,6733,6723],{}," nie chroni",[11,6736,6737],{},"Warunek sprawdza obecność pliku na dysku, więc nie widzi rzeczy, które istnieją tylko wirtualnie — adresów obsługiwanych przez PHP, a wyglądających jak pliki.",[11,6739,6740],{},"Trzy przypadki, na które warto uważać na WordPressie:",[11,6742,6743,3042,6746,6749,6750,6753],{},[15,6744,6745],{},"Mapy witryny z wtyczek SEO.",[53,6747,6748],{},"/sitemap_index.xml"," i cała reszta map z Yoasta czy Rank Matha nie istnieją jako pliki. Dlatego na liście rozszerzeń wyżej nie ma ",[53,6751,6752],{},".xml"," i nie należy go tam dopisywać.",[11,6755,6756,6764,6765,6768],{},[15,6757,6758,116,6760,6763],{},[53,6759,6631],{},[53,6761,6762],{},"sw.js"," z wtyczek PWA."," Część z nich serwuje te pliki przez ",[53,6766,6767],{},"template_redirect"," zamiast kłaść je w katalogu. Sprawdź je na każdej witrynie osobno, bo decyduje o tym zestaw wtyczek, więc wynik bywa inny na każdej instalacji.",[11,6770,6771,6776,6777,6779],{},[15,6772,6773,71],{},[53,6774,6775],{},"robots.txt"," Na liście go nie ma i nie chodzi wyłącznie o to, że bywa wirtualny. Odpowiedź 403 na ",[53,6778,6775],{}," część robotów czyta jako zakaz indeksowania całej witryny.",[11,6781,6782],{},"Sprawdzenie przed wdrożeniem na kolejnych domenach zajmuje kilkanaście sekund:",[177,6784,6786],{"className":1591,"code":6785,"language":1593,"meta":182,"style":182},"for a in / /robots.txt /sitemap_index.xml /manifest.json /sw.js /.boto /?phpinfo=1; do\n  printf '%s  %s\\n' \"$(curl -s -o /dev/null -w '%{http_code}' \"https://twojadomena.pl$a\")\" \"$a\"\ndone\n",[53,6787,6788,6821,6857],{"__ignoreMap":182},[186,6789,6790,6792,6795,6797,6799,6802,6805,6808,6811,6814,6817,6819],{"class":188,"line":189},[186,6791,5263],{"class":192},[186,6793,6794],{"class":295}," a",[186,6796,5269],{"class":192},[186,6798,6518],{"class":280},[186,6800,6801],{"class":280}," /robots.txt",[186,6803,6804],{"class":280}," /sitemap_index.xml",[186,6806,6807],{"class":280}," /manifest.json",[186,6809,6810],{"class":280}," /sw.js",[186,6812,6813],{"class":280}," /.boto",[186,6815,6816],{"class":280}," /?phpinfo=1",[186,6818,5275],{"class":200},[186,6820,5278],{"class":192},[186,6822,6823,6825,6828,6830,6832,6834,6836,6839,6842,6845,6848,6851,6853,6855],{"class":188,"line":218},[186,6824,5395],{"class":208},[186,6826,6827],{"class":280}," '%s  %s\\n'",[186,6829,5408],{"class":280},[186,6831,1600],{"class":196},[186,6833,1603],{"class":351},[186,6835,1609],{"class":351},[186,6837,6838],{"class":280}," /dev/null ",[186,6840,6841],{"class":351},"-w",[186,6843,6844],{"class":280}," '%{http_code}' \"https://twojadomena.pl",[186,6846,6847],{"class":295},"$a",[186,6849,6850],{"class":280},"\")\"",[186,6852,5301],{"class":280},[186,6854,6847],{"class":295},[186,6856,5582],{"class":280},[186,6858,6859],{"class":188,"line":239},[186,6860,6861],{"class":192},"done\n",[11,6863,6864],{},"Pięć pierwszych adresów ma oddać to samo co przed zmianą. Dwa ostatnie mają oddać 403.",[1329,6866,6868],{"id":6867},"strona-błędu-bez-której-oszczędność-jest-o-połowę-mniejsza","Strona błędu, bez której oszczędność jest o połowę mniejsza",[11,6870,6871,6872,6874],{},"Dwie linie ",[53,6873,6512],{}," w bloku wyżej nie są ozdobą i długo ich tam nie było. To jest najbardziej zaskakująca rzecz, jaką znalazłem przy sprawdzaniu skuteczności tych blokad.",[11,6876,6877,6878,6881],{},"Bez nich każde odrzucone żądanie zostawiało w logu ",[15,6879,6880],{},"dwa"," wiersze zamiast jednego:",[177,6883,6886],{"className":6884,"code":6885,"language":5126,"meta":182},[5124],"Access is denied by context rewrite.\nFile not found [/home/.../public_html/403.shtml]\n",[53,6887,6885],{"__ignoreMap":182},[11,6889,6890,6891,6894],{},"DirectAdmin konfiguruje stronę błędu 403 na plik ",[53,6892,6893],{},"403.shtml",", którego w katalogach po prostu nie ma. Serwer odrzuca żądanie regułą, a potem idzie szukać strony, którą miałby pokazać, i jej nie znajduje.",[11,6896,6897,6898,6900,6901,6904],{},"Skala w szczycie serii: LiteSpeed sam raportuje narastające tempo, od ",[53,6899,6196],{}," aż do ",[15,6902,6903],{},"122",". To 122 nieudane wyszukania ścieżki i 122 zapisy do logu na sekundę.",[11,6906,6907,6908,6911],{},"Zastrzeżenie, żeby nie przesadzić z tą liczbą: to nie są operacje na dysku. Powtarzane sprawdzanie tej samej nieistniejącej ścieżki trafia w cache jądra i po pierwszym razie nie schodzi niżej, a zapis do logu jest buforowany. Mechanizm sam się zresztą wygasza — po kilkudziesięciu próbach serwer dopisuje ",[53,6909,6910],{},"shortcut to 404"," i przestaje szukać. Kosztem są wywołania systemowe i rosnący log, nie ruch głowicy.",[11,6913,6914,6915,6917],{},"Naprawa to plik ",[53,6916,6893],{}," w katalogu domeny, kilkaset bajtów statycznego HTML-a, plus te dwie linie. Dowód, że działa, widać w logu od razu: zamiast pary wierszy zostaje jeden, a rozmiar odpowiedzi spada z 1242 bajtów domyślnej strony serwera na tyle, ile waży własna.",[11,6919,6920,6921,6924,6925,6928],{},"Przy okazji warto zrobić to samo z ",[53,6922,6923],{},"404.shtml",". Jeśli w logu widzisz ",[53,6926,6927],{},"File not found [404.shtml]",", każde nietrafione żądanie płaci ten sam podatek.",[1329,6930,6932],{"id":6931},"lekka-strona-404-oszczędza-łącze","Lekka strona 404 oszczędza łącze",[11,6934,6935],{},"Blokady odcinają to, co przewidzisz. Reszta trafia do WordPressa i dostaje stronę 404 wygenerowaną przez motyw — w moim przypadku 37 905 bajtów. Jeden przelot skanera to przez to ponad pięć megabajtów HTML-a wysłanego do automatu, który go nawet nie czyta.",[11,6937,6938,6939,6942],{},"Kuszące jest policzyć to jako drugą połowę oszczędności na procesorze. ",[15,6940,6941],{},"Pomiar tego nie potwierdza."," Zmierzyłem trzy witryny pod tą samą serią tego samego automatu: ich strony 404 ważyły 5, 17,5 i 37,4 kilobajta, liczba uruchomień aplikacji była w każdym przypadku zbliżona do stu czterdziestu, a szczyty CPU wyszły porównywalne. O koszcie procesora decyduje sam bootstrap, a rozmiar odpowiedzi wpływa na transfer i na czas wysyłki.",[11,6944,6945],{},"Lekki szablon 404 nadal warto zrobić, bo pięć megabajtów na przelot przy kilku przelotach dziennie to realny transfer. Ale jeśli szukasz oszczędności na procesorze, ta zmiana jej nie da — daje ją tylko niedopuszczenie żądania do PHP.",[1329,6947,6949],{"id":6948},"co-pokazał-pomiar-po-wdrożeniu","Co pokazał pomiar po wdrożeniu",[11,6951,6952,6953,6955],{},"Blok z warunkiem ",[53,6954,6723],{}," stanął na osiemnastu domenach jednego popołudnia. Ten sam automat wrócił jeszcze tego samego dnia jedenaście razy.",[11,6957,6958],{},"Najczystsze porównanie daje witryna, którą ta sama lista odwiedziła rano i po południu:",[5818,6960,6961,6977],{},[5821,6962,6963],{},[5824,6964,6965,6968,6971,6974],{},[5827,6966,6967],{},"Godzina",[5827,6969,6970],{},"Odrzuconych, razem z odmowami serwera",[5827,6972,6973],{},"Doszło do PHP",[5827,6975,6976],{},"Ubitych procesów",[5848,6978,6979,6992],{},[5824,6980,6981,6984,6987,6990],{},[5853,6982,6983],{},"09:25 przed wdrożeniem",[5853,6985,6986],{},"137",[5853,6988,6989],{},"143",[5853,6991,6016],{},[5824,6993,6994,6997,7000,7002],{},[5853,6995,6996],{},"16:09 po wdrożeniu",[5853,6998,6999],{},"272",[5853,7001,1811],{},[5853,7003,1811],{},[11,7005,7006],{},"Obie liczby pochodzą z logu dostępowego, czyli z tego źródła, które widzi każde żądanie — i z tego powodu obejmują też odmowy serwerowe na ścieżkach kropkowych, opisane wyżej. W serii popołudniowej blok odpowiada za 64 z tych 272 odrzuceń. Liczy się kolumna obok: rano do PHP doszła równo połowa listy, po południu nie doszło nic.",[11,7008,7009,7010,7013],{},"Przelotów po wdrożeniu było jedenaście, na siedmiu różnych domenach, i ",[15,7011,7012],{},"ubitych procesów PHP było w sumie zero",". Log dostępowy mam dla trzech z nich i tam serie liczyły od 272 do 280 żądań; w pozostałych ośmiu log błędów notuje od trzydziestu do stu czterdziestu pięciu odrzuceń, co jest liczbą zaniżoną z powodu opisanego wyżej. Przed wdrożeniem każda seria, która trafiała w lukę w blokadach, zostawiała od jednego do jedenastu ubitych procesów.",[11,7015,7016],{},"Licznik przekroczeń z panelu potwierdza to słabiej, niż mogłoby się wydawać, i tak trzeba go czytać: przekroczenia ustały tego dnia o 07:44, a blok stanął dopiero cztery i pół godziny później. Między jednym a drugim wypadła seria ze stu czterdziestoma uruchomieniami WordPressa, która dała pięćdziesiąt procent i żadnego faulta. Sam licznik mówi więc tylko tyle, że po wdrożeniu nic się nie pogorszyło. Dowód siedzi w logach, w kolumnie z żądaniami, które doszły do PHP.",[11,7018,7019,7020,56,7023,5091,7025,7028,7029,7032,7033,7035],{},"Jedna kategoria przez blok nadal przechodzi: ścieżki bez rozszerzenia, w rodzaju ",[53,7021,7022],{},"/api/config",[53,7024,5094],{},[53,7026,7027],{},"/_debugbar/open",". W najdłuższej z dzisiejszych list było ich czterdzieści sześć na 326 żądań i żadne nie ubiło procesu. Wyliczanie ich z nazwy wraca do problemu z listą nazw, a blokowanie prefiksu ",[53,7030,7031],{},"/api/"," na WordPressie bywa ryzykowne, bo warunek ",[53,7034,6723],{}," nie chroni adresów, które obsługuje wtyczka.",[26,7037,7039],{"id":7038},"czego-robić-nie-warto","Czego robić nie warto",[1329,7041,7043],{"id":7042},"blokowanie-po-nazwie-przeglądarki","Blokowanie po nazwie przeglądarki",[11,7045,7046,7047,7049],{},"W krążących po sieci „pakietach zabezpieczeń\" do ",[53,7048,5098],{}," znajdziesz listę stu kilkudziesięciu nazw botów:",[177,7051,7053],{"className":6239,"code":7052,"language":6241,"meta":182,"style":182},"RewriteCond %{HTTP_USER_AGENT} curl [NC,OR]\nRewriteCond %{HTTP_USER_AGENT} python [NC,OR]\nRewriteCond %{HTTP_USER_AGENT} java [NC,OR]\nRewriteCond %{HTTP_USER_AGENT} scan [NC,OR]\n",[53,7054,7055,7068,7079,7090],{"__ignoreMap":182},[186,7056,7057,7059,7062,7065],{"class":188,"line":189},[186,7058,6366],{"class":192},[186,7060,7061],{"class":295}," %{HTTP_USER_AGENT}",[186,7063,7064],{"class":280}," curl",[186,7066,7067],{"class":200}," [NC,OR]\n",[186,7069,7070,7072,7074,7077],{"class":188,"line":218},[186,7071,6366],{"class":192},[186,7073,7061],{"class":295},[186,7075,7076],{"class":280}," python",[186,7078,7067],{"class":200},[186,7080,7081,7083,7085,7088],{"class":188,"line":239},[186,7082,6366],{"class":192},[186,7084,7061],{"class":295},[186,7086,7087],{"class":280}," java",[186,7089,7067],{"class":200},[186,7091,7092,7094,7096,7099],{"class":188,"line":245},[186,7093,6366],{"class":192},[186,7095,7061],{"class":295},[186,7097,7098],{"class":280}," scan",[186,7100,7067],{"class":200},[11,7102,7103],{},"Skaner z opisywanego przypadku przedstawiał się tak:",[177,7105,7108],{"className":7106,"code":7107,"language":5126,"meta":182},[5124],"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0\n",[53,7109,7107],{"__ignoreMap":182},[11,7111,7112],{},"Żaden wpis z tej listy go nie łapał. Blokowanie po nazwie działa wyłącznie na boty, które uczciwie się przedstawiają, a te akurat nie są problemem.",[11,7114,7115,7116,116,7118,7121,7122,7125],{},"Za to ",[53,7117,1600],{},[53,7119,7120],{},"python"," na liście zablokują Twój własny monitoring i integracje, a ",[53,7123,7124],{},"java"," potrafi odciąć część klientów mobilnych.",[1329,7127,7129],{"id":7128},"rozdmuchane-pliki-htaccess","Rozdmuchane pliki .htaccess",[11,7131,7132,7133,7135],{},"Te same pakiety dokładają około 160 dyrektyw ",[53,7134,6366],{},", w większości wyrażeń regularnych sprawdzanych na parametrach adresu. Plik urasta do 15 kilobajtów i 400 linii.",[11,7137,7138,7139,7141],{},"Serwer przetwarza go przy ",[15,7140,3041],{}," żądaniu, także przy obrazkach, arkuszach stylów i fontach. Strona ładująca 50 zasobów to 8000 ewaluacji wyrażeń regularnych na jedno wejście użytkownika.",[11,7143,7144],{},"Plik reklamowany jako ochrona przed obciążeniem sam to obciążenie generuje.",[11,7146,7147],{},"Po odchudzeniu do 55 linii zostają rzeczy, które faktycznie działają: blokady ścieżek, blokady plików, nagłówki cache i gzip.",[1329,7149,7151],{"id":7150},"reguły-które-nigdy-nie-działały","Reguły, które nigdy nie działały",[11,7153,7154],{},"W trzech plikach z tego konta znalazłem taki zapis:",[177,7156,7158],{"className":6239,"code":7157,"language":6241,"meta":182,"style":182},"RewriteCond %{QUERY*STRING} (GLOBALS|_REQUEST)\n",[53,7159,7160],{"__ignoreMap":182},[186,7161,7162,7164,7167],{"class":188,"line":189},[186,7163,6366],{"class":192},[186,7165,7166],{"class":295}," %{QUERY*STRING}",[186,7168,7169],{"class":280}," (GLOBALS|_REQUEST)\n",[11,7171,7172,7173,7176],{},"Zmienna nazywa się ",[53,7174,7175],{},"QUERY_STRING",". Zapis z gwiazdką wygląda na artefakt kopiowania z Markdowna, w którym podkreślenie zostało zinterpretowane jako znacznik kursywy — źródła nie znam, bo plik przyszedł z konfiguracją zastaną na koncie. Niezależnie od tego, jak powstał: cała sekcja antyinjekcyjna była martwa, dawała koszt parsowania i zero ochrony.",[11,7178,7179],{},"Sprawdź u siebie:",[177,7181,7183],{"className":1591,"code":7182,"language":1593,"meta":182,"style":182},"grep -l 'QUERY\\*STRING' ~/domains/*/public_html/.htaccess\n",[53,7184,7185],{"__ignoreMap":182},[186,7186,7187,7189,7192,7195,7198,7200],{"class":188,"line":189},[186,7188,5376],{"class":196},[186,7190,7191],{"class":351}," -l",[186,7193,7194],{"class":280}," 'QUERY\\*STRING'",[186,7196,7197],{"class":280}," ~/domains/",[186,7199,5615],{"class":204},[186,7201,7202],{"class":280},"/public_html/.htaccess\n",[11,7204,7205,7206,7209,7210,7212],{},"W innym pliku z tego samego konta stała reguła blokująca robota Yandeksa zapisana jako ",[53,7207,7208],{},"RewriteCond %{HTTP_USER_AGENT} YandexBot^",". Znak ",[53,7211,2105],{}," w wyrażeniu regularnym oznacza początek tekstu, więc wzorzec „YandexBot, po którym zaczyna się tekst\" nie dopasuje się nigdy. Reguła stała tam latami i nie zrobiła nic.",[26,7214,7216],{"id":7215},"pięć-sposobów-w-jakie-htaccess-cicho-psuje-stronę","Pięć sposobów, w jakie .htaccess cicho psuje stronę",[11,7218,7219],{},"Przy okazji porządkowania wyszło, że kilka witryn na koncie zwracało 404 na stronie głównej. Od kiedy, nie wiadomo, bo nikt tego nie zgłosił.",[11,7221,7222,7225],{},[15,7223,7224],{},"Niedomknięta dyrektywa."," Plik urwany w połowie bloku:",[177,7227,7229],{"className":6239,"code":7228,"language":6241,"meta":182,"style":182},"\u003CFiles xmlrpc.php>\n  Order Allow,Deny\n  Deny from all\n",[53,7230,7231,7241,7254],{"__ignoreMap":182},[186,7232,7233,7236,7239],{"class":188,"line":189},[186,7234,7235],{"class":200},"\u003CFiles ",[186,7237,7238],{"class":280},"xmlrpc.php",[186,7240,6254],{"class":200},[186,7242,7243,7246,7249,7251],{"class":188,"line":218},[186,7244,7245],{"class":192},"  Order",[186,7247,7248],{"class":192}," Allow",[186,7250,1642],{"class":200},[186,7252,7253],{"class":192},"Deny\n",[186,7255,7256,7259],{"class":188,"line":239},[186,7257,7258],{"class":192},"  Deny",[186,7260,7261],{"class":200}," from all\n",[11,7263,7264,7265,7268],{},"Brak ",[53,7266,7267],{},"\u003C/Files>"," na końcu. Objaw: 404 na wszystkim, co idzie przez PHP, przy działających plikach statycznych.",[11,7270,7271,7278],{},[15,7272,7273,7274,7277],{},"Flaga ",[186,7275,7276],{},"S=N"," przenosząca się między blokami."," Popularna sekcja „Block the include-only files\" zawiera:",[177,7280,7282],{"className":6239,"code":7281,"language":6241,"meta":182,"style":182},"RewriteRule !^wp-includes/ - [S=3]\n",[53,7283,7284],{"__ignoreMap":182},[186,7285,7286,7288,7291,7293,7296,7298],{"class":188,"line":189},[186,7287,6271],{"class":192},[186,7289,7290],{"class":295}," !^wp-includes/",[186,7292,6277],{"class":280},[186,7294,7295],{"class":200}," [S=",[186,7297,1683],{"class":351},[186,7299,7300],{"class":200},"]\n",[11,7302,7303,7306,7307,7309],{},[53,7304,7305],{},"[S=3]"," znaczy „pomiń następne trzy reguły\". Skopiowana do pliku o innej strukturze przeskakuje poza swój blok i pomija reguły przepisujące na ",[53,7308,5214],{},". Efekt: 404 na całej witrynie poza plikami istniejącymi fizycznie.",[11,7311,7312,7313,7316,7317,7320],{},"Sama blokada bezpośredniego dostępu do plików w ",[53,7314,7315],{},"wp-includes"," bywa nadal rekomendowana jako warstwa obrony w głąb i nie o nią mam pretensję. Problemem jest wyłącznie ",[53,7318,7319],{},"[S=N]",", bo ta flaga liczy reguły, a nie bloki — przepisz sekcję na formę, która na niej nie polega.",[11,7322,7323,7326],{},[15,7324,7325],{},"Składnia nieobsługiwana przez serwer."," Konstrukcja działająca na Apache:",[177,7328,7330],{"className":6239,"code":7329,"language":6241,"meta":182,"style":182},"\u003CFiles wp-login.php>\n  \u003CLimit GET POST>\n    Order Deny,Allow\n    Deny from all\n  \u003C/Limit>\n\u003C/Files>\n",[53,7331,7332,7341,7351,7364,7371,7376],{"__ignoreMap":182},[186,7333,7334,7336,7339],{"class":188,"line":189},[186,7335,7235],{"class":200},[186,7337,7338],{"class":280},"wp-login.php",[186,7340,6254],{"class":200},[186,7342,7343,7346,7349],{"class":188,"line":218},[186,7344,7345],{"class":200},"  \u003CLimit ",[186,7347,7348],{"class":280},"GET POST",[186,7350,6254],{"class":200},[186,7352,7353,7356,7359,7361],{"class":188,"line":239},[186,7354,7355],{"class":192},"    Order",[186,7357,7358],{"class":192}," Deny",[186,7360,1642],{"class":200},[186,7362,7363],{"class":192},"Allow\n",[186,7365,7366,7369],{"class":188,"line":245},[186,7367,7368],{"class":192},"    Deny",[186,7370,7261],{"class":200},[186,7372,7373],{"class":188,"line":252},[186,7374,7375],{"class":200},"  \u003C/Limit>\n",[186,7377,7378],{"class":188,"line":272},[186,7379,7380],{"class":200},"\u003C/Files>\n",[11,7382,7383],{},"LiteSpeed potrafi odrzucić na niej cały plik. Zamiennik:",[177,7385,7387],{"className":6239,"code":7386,"language":6241,"meta":182,"style":182},"\u003CFiles wp-login.php>\nRequire all denied\n\u003C/Files>\n",[53,7388,7389,7397,7405],{"__ignoreMap":182},[186,7390,7391,7393,7395],{"class":188,"line":189},[186,7392,7235],{"class":200},[186,7394,7338],{"class":280},[186,7396,6254],{"class":200},[186,7398,7399,7402],{"class":188,"line":218},[186,7400,7401],{"class":192},"Require",[186,7403,7404],{"class":200}," all denied\n",[186,7406,7407],{"class":188,"line":239},[186,7408,7380],{"class":200},[11,7410,7411,7412,2436,7415,7418,7419,7421],{},"Odcina to logowanie również Tobie, więc w praktyce idzie z tym lista dozwolonych adresów — ",[53,7413,7414],{},"Require ip 203.0.113.7",[53,7416,7417],{},"Require all denied"," — albo uwierzytelnianie Basic na tym jednym pliku. Wklejenie samego ",[53,7420,7417],{}," kończy się telefonem od klienta, który nie może wejść do panelu.",[11,7423,7424,7431],{},[15,7425,7426,7427,7430],{},"Reguła z wzorcem ",[53,7428,7429],{},".*",", której jedynym ogranicznikiem jest warunek."," Ten przypadek popełniłem sam i kosztował dwadzieścia godzin.",[11,7433,7434],{},"Do bloku blokad dołożyłem regułę odcinającą enumerację użytkowników przez REST API:",[177,7436,7438],{"className":6239,"code":7437,"language":6241,"meta":182,"style":182},"RewriteCond %{REQUEST_URI} ^.*wp-json/wp/v2/users(?!/me) [NC]\nRewriteRule .* - [F,L]\n",[53,7439,7440,7451],{"__ignoreMap":182},[186,7441,7442,7444,7446,7449],{"class":188,"line":189},[186,7443,6366],{"class":192},[186,7445,6379],{"class":295},[186,7447,7448],{"class":280}," ^.*wp-json/wp/v2/users(?!/me)",[186,7450,6487],{"class":200},[186,7452,7453,7455,7458,7460],{"class":188,"line":218},[186,7454,6271],{"class":192},[186,7456,7457],{"class":295}," .*",[186,7459,6277],{"class":280},[186,7461,6280],{"class":200},[11,7463,7464,7465,7467,7468,7471],{},"Wzorzec w ",[53,7466,6271],{}," dopasowuje ",[15,7469,7470],{},"każdy"," adres. Od zablokowania całej witryny dzieli tę regułę wyłącznie to, czy warunek nad nią zadziała. Nie zadziałał — pięć witryn oddawało 403 na stronie głównej, dopóki nie zauważyłem tego przypadkiem, przy zupełnie innej diagnostyce. Nikt nie zgłosił.",[11,7473,7474,7475,7478,7479,7482],{},"Której dokładnie konstrukcji silnik nie strawił, nie ustaliłem. Podmieniłem całą regułę naraz i strony wróciły, więc winowajca jest potwierdzony na poziomie tych dwóch linii, a nie pojedynczego zapisu. Kandydaci: negatywny lookahead ",[53,7476,7477],{},"(?!/me)",", zbędne ",[53,7480,7481],{},"^.*"," na początku wzorca, albo sam układ warunku z regułą łapiącą wszystko. Rozdzielenie tego wymagałoby wstawiania wariantów po kolei na żywych witrynach klientów i nie uważam, żeby było warte ceny.",[11,7484,7485,7486,7488],{},"Wersja, którą wgrałem tego samego dnia — bez ",[53,7487,7429],{}," i bez lookaheada, dwa zwykłe warunki:",[177,7490,7492],{"className":6239,"code":7491,"language":6241,"meta":182,"style":182},"RewriteCond %{REQUEST_URI} wp-json/wp/v2/users [NC]\nRewriteCond %{REQUEST_URI} !wp-json/wp/v2/users/me [NC]\nRewriteRule ^ - [F,L]\n",[53,7493,7494,7505,7516],{"__ignoreMap":182},[186,7495,7496,7498,7500,7503],{"class":188,"line":189},[186,7497,6366],{"class":192},[186,7499,6379],{"class":295},[186,7501,7502],{"class":280}," wp-json/wp/v2/users",[186,7504,6487],{"class":200},[186,7506,7507,7509,7511,7514],{"class":188,"line":218},[186,7508,6366],{"class":192},[186,7510,6379],{"class":295},[186,7512,7513],{"class":280}," !wp-json/wp/v2/users/me",[186,7515,6487],{"class":200},[186,7517,7518,7520,7522,7524],{"class":188,"line":239},[186,7519,6271],{"class":192},[186,7521,6494],{"class":295},[186,7523,6277],{"class":280},[186,7525,6280],{"class":200},[11,7527,7528,7529,7532,7533,7535,7536,7538],{},"Działa i stoi na koncie, ale ",[15,7530,7531],{},"łamie zasadę, którą za chwilę z tej awarii wyprowadzam",", i warto to powiedzieć wprost zamiast liczyć, że nikt nie zauważy. Wzorzec ",[53,7534,2105],{}," dopasowuje każdy adres tak samo jak ",[53,7537,7429],{},", więc jeśli oba warunki zawiodą tak, jak zawiódł lookahead, skutek jest ten sam: 403 na całej witrynie. Zawężenie siedzi wyłącznie w warunkach.",[11,7540,7541],{},"Wersja zgodna z zasadą przenosi zawężenie do wzorca reguły i zostawia w warunku samo odstępstwo:",[177,7543,7545],{"className":6239,"code":7544,"language":6241,"meta":182,"style":182},"RewriteCond %{REQUEST_URI} !wp-json/wp/v2/users/me [NC]\nRewriteRule ^wp-json/wp/v2/users - [F,L]\n",[53,7546,7547,7557],{"__ignoreMap":182},[186,7548,7549,7551,7553,7555],{"class":188,"line":189},[186,7550,6366],{"class":192},[186,7552,6379],{"class":295},[186,7554,7513],{"class":280},[186,7556,6487],{"class":200},[186,7558,7559,7561,7564,7566],{"class":188,"line":218},[186,7560,6271],{"class":192},[186,7562,7563],{"class":295}," ^wp-json/wp/v2/users",[186,7565,6277],{"class":280},[186,7567,6280],{"class":200},[11,7569,7570],{},"Teraz pomyłka w warunku kosztuje jedną ścieżkę zamiast wszystkich.",[11,7572,7573,7574,7577,7578,7580,7581,7584,7585,7587],{},"Jedna poprawka do tego wzorca, zanim go skopiujesz. Wersja wgrana dopasowywała podciąg w dowolnym miejscu adresu, więc łapała też ",[53,7575,7576],{},"/index.php/wp-json/wp/v2/users","; wersja zakotwiczona na ",[53,7579,2105],{}," już nie. Kotwica ma więc brzmieć ",[53,7582,7583],{},"(^|/)wp-json/wp/v2/users"," — zawężenie zostaje w regule, a segment ",[53,7586,5214],{}," przestaje być obejściem.",[11,7589,7590,7593],{},[15,7591,7592],{},"I tak żadna z tych wersji nie wystarcza, co sprawdziłem, zanim to napisałem."," WordPress wystawia REST API także przez parametr zapytania, niezależnie od ustawień bezpośrednich odnośników. Trzy witryny z wgranym blokiem, trzy zapytania:",[5818,7595,7596,7606],{},[5821,7597,7598],{},[5824,7599,7600,7603],{},[5827,7601,7602],{},"Adres",[5827,7604,7605],{},"Odpowiedź",[5848,7607,7608,7617,7630],{},[5824,7609,7610,7615],{},[5853,7611,7612],{},[53,7613,7614],{},"/wp-json/wp/v2/users",[5853,7616,6521],{},[5824,7618,7619,7624],{},[5853,7620,7621],{},[53,7622,7623],{},"/?rest_route=/wp/v2/users",[5853,7625,7626,7629],{},[15,7627,7628],{},"200 i pełna lista użytkowników",", od 1,2 do 11,7 kB JSON-a",[5824,7631,7632,7637],{},[5853,7633,7634],{},[53,7635,7636],{},"/?rest_route=%2Fwp%2Fv2%2Fusers",[5853,7638,7639],{},"200 na jednej z witryn, z imionami i nazwiskami w odpowiedzi",[11,7641,7642,7643,7646,7647,7649,7650,7652,7653,7656,7657,6584,7660,7663],{},"Reguła patrzy na ",[53,7644,7645],{},"REQUEST_URI",", a przy tej formie wywołania ścieżką jest samo ",[53,7648,2093],{},". Warunek na ",[53,7651,7175],{}," domyka pierwszy wariant, ale nie drugi, bo ",[53,7654,7655],{},"%{QUERY_STRING}"," widzi ciąg surowy, a ",[53,7658,7659],{},"rest_route=/wp/v2/users",[53,7661,7662],{},"rest_route=%2Fwp%2Fv2%2Fusers",". Pokrycie obu form wymaga wzorca z alternatywą na każdym ukośniku i nadal nie obejmuje form, których nie sprawdziłem.",[11,7665,7666,7669],{},[15,7667,7668],{},"Tu kończy się warstwa, w której warto to robić."," Blokada enumeracji użytkowników jest kontrolą bezpieczeństwa, nie kontrolą obciążenia: chodzi o kilka żądań dziennie, więc argument o oszczędzaniu bootstrapu nie działa. Dwie linijki w mu-pluginie pokrywają wszystkie warianty naraz, bo działają po znormalizowaniu trasy przez WordPressa:",[177,7671,7673],{"className":2251,"code":7672,"language":2253,"meta":182,"style":182},"add_filter('rest_endpoints', function ($endpoints) {\n  if (current_user_can('list_users')) {\n    return $endpoints;\n  }\n  unset($endpoints['/wp/v2/users'], $endpoints['/wp/v2/users/(?P\u003Cid>[\\d]+)']);\n  return $endpoints;\n});\n",[53,7674,7675,7697,7716,7725,7729,7754,7762],{"__ignoreMap":182},[186,7676,7677,7679,7681,7684,7686,7688,7690,7693,7695],{"class":188,"line":189},[186,7678,2485],{"class":196},[186,7680,230],{"class":200},[186,7682,7683],{"class":280},"'rest_endpoints'",[186,7685,1642],{"class":200},[186,7687,212],{"class":192},[186,7689,935],{"class":200},[186,7691,7692],{"class":295},"$endpoints",[186,7694,2319],{"class":200},[186,7696,269],{"class":200},[186,7698,7699,7701,7703,7706,7708,7711,7714],{"class":188,"line":218},[186,7700,932],{"class":192},[186,7702,935],{"class":200},[186,7704,7705],{"class":196},"current_user_can",[186,7707,230],{"class":200},[186,7709,7710],{"class":280},"'list_users'",[186,7712,7713],{"class":200},"))",[186,7715,269],{"class":200},[186,7717,7718,7720,7723],{"class":188,"line":239},[186,7719,985],{"class":192},[186,7721,7722],{"class":295}," $endpoints",[186,7724,532],{"class":200},[186,7726,7727],{"class":188,"line":245},[186,7728,993],{"class":200},[186,7730,7731,7734,7736,7738,7740,7743,7745,7747,7749,7752],{"class":188,"line":252},[186,7732,7733],{"class":208},"  unset",[186,7735,230],{"class":200},[186,7737,7692],{"class":295},[186,7739,2102],{"class":200},[186,7741,7742],{"class":280},"'/wp/v2/users'",[186,7744,1814],{"class":200},[186,7746,7722],{"class":295},[186,7748,2102],{"class":200},[186,7750,7751],{"class":280},"'/wp/v2/users/(?P\u003Cid>[\\d]+)'",[186,7753,2119],{"class":200},[186,7755,7756,7758,7760],{"class":188,"line":272},[186,7757,2640],{"class":192},[186,7759,7722],{"class":295},[186,7761,532],{"class":200},[186,7763,7764],{"class":188,"line":292},[186,7765,2429],{"class":200},[11,7767,7768,7769,7772],{},"Warunek na uprawnienia nie jest ozdobą. Bez niego znika też lista autorów w edytorze bloków, bo panel dokumentu pobiera ją tym samym adresem ",[53,7770,7771],{},"/wp/v2/users",". Na witrynie z jednym administratorem nikt tego nie zauważy, na witrynie z redakcją zauważy redaktor — i zgłosi to tak samo jak poprzednie awarie z tego tekstu, czyli wcale.",[11,7774,7775],{},"Sprawdzenie na jednej instalacji zajmuje minutę: otwórz edytor wpisu jako administrator i rozwiń listę autorów, potem wywołaj oba adresy w oknie prywatnym.",[11,7777,7778,7779,7781,7782,7784],{},"Reguła na ",[53,7780,7645],{}," zostaje w ",[53,7783,5098],{}," jako druga warstwa — jest darmowa i odcina najczęstszą formę wywołania przed PHP. Filtr w mu-pluginie zamyka resztę. Reszta bloku zostaje tam, gdzie była, bo tam stawką jest niedopuszczenie żądania do interpretera.",[11,7786,7787,7788,7798],{},"Zasada do zapamiętania jest prostsza niż diagnoza: ",[15,7789,7790,7791,7794,7795,7797],{},"jeśli piszesz ",[53,7792,7793],{},"RewriteRule .*",", a o tym, czy reguła odpali, decyduje ",[53,7796,6366],{}," nad nią, to dowolny błąd w tym warunku wyłącza całą witrynę."," Wzorzec reguły powinien sam zawężać dopasowanie zawsze, gdy rozróżnia ścieżka.",[11,7800,7801,7802,7804,7805,7807,7808,7810,7811,71],{},"Gdy rozróżnia parametr zapytania, nie możesz tego zrobić i trzeba to powiedzieć wprost, zamiast udawać, że zasada jest bez wyjątków. Wzorzec ",[53,7803,6271],{}," nie widzi query stringu, więc zostaje ",[53,7806,2105],{}," i całe zawężenie siedzi w warunkach — tak wygląda blokada ",[53,7809,6716],{}," w bloku z początku tekstu. Takie reguły wymagają osobnego sprawdzenia strony głównej po każdej zmianie, bo to one wywracają witrynę, gdy warunek zawiedzie. To jest zresztą drugi powód, dla którego enumeracja użytkowników wylądowała wyżej w mu-pluginie zamiast w kolejnej regule na ",[53,7812,7175],{},[11,7814,7815,7818],{},[15,7816,7817],{},"Blokada rozszerzenia bez sprawdzenia, czy plik istnieje."," Ten przypadek o włos popełniłem sam.",[11,7820,7821,7822,7824,7825,7827],{},"Pisząc listę rozszerzeń do zablokowania, miałem na niej ",[53,7823,6752],{},". Wyleciała stamtąd na kilka minut przed wgraniem, kiedy sprawdziłem w logu, co pod tym rozszerzeniem odpowiada kodem 200. Okazało się, że cztery mapy witryny z Yoasta — a żadna z nich nie istnieje jako plik. Reguła bez warunku ",[53,7826,6723],{}," wycięłaby je wszystkie naraz, a zobaczyłbym to dopiero po tygodniach w Search Console.",[11,7829,7830,7831,56,7833,7835,7836,7838,7839,7841],{},"Ta sama pułapka dotyczy ",[53,7832,6631],{},[53,7834,6762],{}," i wszystkiego, co wtyczka serwuje przez ",[53,7837,6767],{},". Adres wyglądający jak plik nie musi być plikiem. Warunek ",[53,7840,6611],{}," przed regułą zamyka całą tę klasę błędów, bo plik, który na dysku jest, przechodzi zawsze.",[11,7843,7844],{},"Szybki test, czy problem leży w pliku:",[177,7846,7848],{"className":1591,"code":7847,"language":1593,"meta":182,"style":182},"cp .htaccess ~/htaccess-backup-$(date +%s)\nmv .htaccess .htaccess.off\ncurl -s -o /dev/null -w '%{http_code}\\n' https://twojadomena.pl/\nmv .htaccess.off .htaccess\n",[53,7849,7850,7871,7881,7901],{"__ignoreMap":182},[186,7851,7852,7855,7858,7861,7863,7866,7869],{"class":188,"line":189},[186,7853,7854],{"class":196},"cp",[186,7856,7857],{"class":280}," .htaccess",[186,7859,7860],{"class":280}," ~/htaccess-backup-",[186,7862,5487],{"class":200},[186,7864,7865],{"class":196},"date",[186,7867,7868],{"class":280}," +%s",[186,7870,2021],{"class":200},[186,7872,7873,7876,7878],{"class":188,"line":218},[186,7874,7875],{"class":196},"mv",[186,7877,7857],{"class":280},[186,7879,7880],{"class":280}," .htaccess.off\n",[186,7882,7883,7885,7887,7889,7892,7895,7898],{"class":188,"line":239},[186,7884,1600],{"class":196},[186,7886,1603],{"class":351},[186,7888,1609],{"class":351},[186,7890,7891],{"class":280}," /dev/null",[186,7893,7894],{"class":351}," -w",[186,7896,7897],{"class":280}," '%{http_code}\\n'",[186,7899,7900],{"class":280}," https://twojadomena.pl/\n",[186,7902,7903,7905,7908],{"class":188,"line":245},[186,7904,7875],{"class":196},[186,7906,7907],{"class":280}," .htaccess.off",[186,7909,7910],{"class":280}," .htaccess\n",[11,7912,7913,7914,7917,7918,7920],{},"Kopia idzie pierwsza, a plik na czas testu zostaje w tym samym katalogu pod inną nazwą. Przeniesienie go do ",[53,7915,7916],{},"/tmp"," wygląda niewinnie, dopóki sekwencji coś nie przerwie — wtedy witryna zostaje bez ",[53,7919,5098],{},", a plik leży w katalogu, który hosting potrafi wyczyścić. Przywróć nazwę od razu po sprawdzeniu, niezależnie od wyniku.",[26,7922,7924],{"id":7923},"gdy-logów-nie-widać-z-konsoli","Gdy logów nie widać z konsoli",[11,7926,7927,7928,7931,7932,7935],{},"Na koncie z CageFS bieżące logi bywają poza zasięgiem użytkownika. ",[53,7929,7930],{},"/var/log/httpd/domains/"," nie istnieje, katalog podstawiony przez CageFS jest pusty, a w ",[53,7933,7934],{},"~/domains/*/logs/"," leżą wyłącznie dobowe archiwa. Rotacja idzie w środku nocy, więc wszystko, co wydarzyło się po niej, jest z powłoki niewidoczne — czyli dokładnie to, czego szukasz, gdy alert przyszedł rano.",[11,7937,7938],{},"Wyjście jest w panelu. DirectAdmin udostępnia log dostępowy i log błędów osobno dla każdej domeny:",[177,7940,7943],{"className":7941,"code":7942,"language":5126,"meta":182},[5124],"https://\u003Ckonto>.hostido.net.pl/CMD_SHOW_LOG?domain=\u003Cdomena>&type=error\nhttps://\u003Ckonto>.hostido.net.pl/CMD_SHOW_LOG?domain=\u003Cdomena>&type=log\n",[53,7944,7942],{"__ignoreMap":182},[11,7946,7947],{},"Widok sięga do ostatniej rotacji i obejmuje kilkaset do kilku tysięcy wierszy na domenę. U mnie to wystarczyło, żeby rozstrzygnąć sprawę bez czekania do następnego dnia. Logi starsze niż bieżąca doba bierze się już normalnie z archiwów.",[26,7949,7951],{"id":7950},"kod-został-na-koniec","Kod został na koniec",[11,7953,7954,7955,7957,7958,7960,7961,7963],{},"Warningi z logów wskazały przy okazji dwa realne błędy w motywie: filtr ",[53,7956,2225],{}," budujący menu rekurencyjnie, po kilkaset zapytań SQL na jedno wyświetlenie strony, i ",[53,7959,4544],{}," wołający przy każdym permalinku ",[53,7962,4540],{},", czyli funkcję bez cache. Oba warto naprawić.",[11,7965,7966,7967,7971],{},"Żaden nie miał związku z przekroczeniami limitu i to jest tu najważniejsze zdanie. Oba filtry rozbieram w osobnym tekście: ",[2231,7968,7970],{"href":7969},"/zapytania-n-plus-1-filtry-wordpress/","zapytania N+1 w filtrach WordPressa",". Przy nagłym przekroczeniu limitu zaczynanie od kodu kosztuje dni i zwykle nie trafia w przyczynę.",[11,7973,7974,7975,7978,7979,7982,7983,7986,7987,7989,7990,7993],{},"Jest natomiast osobna kategoria błędów w kodzie, która z tym tematem ma wszystko wspólne: ",[15,7976,7977],{},"martwe odwołania w motywie",". Na witrynie z największym ruchem na tym koncie motyw wskazywał na ",[53,7980,7981],{},"/styles.css"," w korzeniu domeny i na skrypt z własnego katalogu ",[53,7984,7985],{},"assets",", a żaden z tych plików nie istniał. Oba adresy szły więc do ",[53,7988,5214],{}," i oddawały pełną stronę 404 z motywu, 37 kilobajtów, ",[15,7991,7992],{},"przy każdej odsłonie",". W niecałe dziesięć godzin dało to 206 dodatkowych uruchomień WordPressa i 7,7 megabajta transferu, a przeglądarka nie pokazywała nic, bo brakujący arkusz stylów ignoruje po cichu.",[11,7995,7996],{},"To jest ten sam mechanizm co przy skanerze, tylko wyzwalany przez własny kod. Szukaj go tak:",[177,7998,8000],{"className":1591,"code":7999,"language":1593,"meta":182,"style":182},"tar -xzOf ~/domains/twojadomena.pl/logs/*.tar.gz \\\n  | grep -oE '\"GET [^\"]+\" 404 [0-9]{5,}' | sort | uniq -c | sort -rn | head\n",[53,8001,8002,8016],{"__ignoreMap":182},[186,8003,8004,8006,8008,8010,8012,8014],{"class":188,"line":189},[186,8005,5355],{"class":196},[186,8007,5358],{"class":351},[186,8009,5612],{"class":280},[186,8011,5615],{"class":204},[186,8013,5618],{"class":280},[186,8015,5621],{"class":208},[186,8017,8018,8020,8022,8024,8027,8029,8031,8033,8035,8037,8039,8041,8043,8045],{"class":188,"line":218},[186,8019,5626],{"class":200},[186,8021,5376],{"class":196},[186,8023,5631],{"class":351},[186,8025,8026],{"class":280}," '\"GET [^\"]+\" 404 [0-9]{5,}'",[186,8028,5373],{"class":200},[186,8030,5428],{"class":196},[186,8032,5373],{"class":200},[186,8034,5647],{"class":196},[186,8036,5379],{"class":351},[186,8038,5373],{"class":200},[186,8040,5428],{"class":196},[186,8042,5656],{"class":351},[186,8044,5373],{"class":200},[186,8046,5661],{"class":196},[11,8048,8049],{},"Filtr na pięciocyfrowy rozmiar odpowiedzi jest tu istotny: odsiewa zwykłe czterysta czwórki i zostawia te, które oddały pełną stronę z motywu.",[26,8051,8053],{"id":8052},"kolejność-która-oszczędza-czas","Kolejność, która oszczędza czas",[11,8055,8056],{},"Gdybym miał przejść tę drogę jeszcze raz:",[11,8058,8059,8062],{},[15,8060,8061],{},"Panel hostingu, od razu w rozdzielczości minutowej."," Godziny przekroczeń, przełącznik jednostki czasu na minuty i snapshot procesów. Widok godzinowy podsunął mi trzy różne błędne wnioski, zanim znalazłem ten przełącznik.",[11,8064,8065,8068,8069,8071],{},[15,8066,8067],{},"Support, jeśli panel nic nie pokazuje."," Administratorzy mają dostęp do ",[53,8070,5175],{}," i widzą wprost, który proces i która domena wywołały spowolnienie. To jedno pytanie zamiast dwóch dni pracy.",[11,8073,8074,8077],{},[15,8075,8076],{},"Logi co do minuty",", nie co do godziny. Szukasz koncentracji, nie wolumenu. Jeśli powłoka ich nie widzi, sięgnij po podgląd z panelu.",[11,8079,8080,8085],{},[15,8081,8082,8083],{},"Blokady ścieżek w ",[53,8084,5098],{}," na wszystkich witrynach, niezależnie od wyniku diagnostyki. To tanie i działa prewencyjnie.",[11,8087,8088,8093],{},[15,8089,8090,8091],{},"Przegląd ",[53,8092,5098],{}," pod kątem martwych i szkodliwych reguł. Przy okazji wyjdą strony, które nie działają.",[11,8095,8096,8099],{},[15,8097,8098],{},"Dopiero na końcu kod."," Optymalizacja zapytań ma sens, ale rzadko bywa przyczyną nagłych przekroczeń. Te zwykle przychodzą z zewnątrz.",[26,8101,8103],{"id":8102},"co-zrobić-żeby-nie-wracać-do-tematu","Co zrobić, żeby nie wracać do tematu",[11,8105,8106,8109],{},[15,8107,8108],{},"Cloudflare przed witrynami."," Darmowy plan nie odetnie wszystkich skanerów, bo zarządzane reguły WAF są w nim ograniczone, ale zdejmie z serwera ruch botów z listy znanych źródeł i cały ruch statyczny. Przy kilkunastu stronach na jednym koncie to największy zysk przy najmniejszym nakładzie.",[11,8111,8112,8115],{},[15,8113,8114],{},"Prosty monitoring kodów odpowiedzi."," Zadanie cron raz dziennie, sprawdzające listę adresów i wysyłające wiadomość tylko przy problemie. Trzy strony z 404 chodziły tak nie wiadomo jak długo.",[11,8117,8118,8119,8121],{},"Wartość tej pozycji zmierzyłem przy pisaniu tego tekstu. Sklep z tego samego konta zaczął oddawać 500 na każdym adresie idącym przez PHP, bo interpreter przestał mieć prawo odczytu własnego ",[53,8120,2248],{}," — plik miał prawa 600 od pół roku i nikt go nie dotykał, zmieniło się coś po stronie serwera. Zauważyłem to przypadkiem, przy sprawdzaniu zupełnie innej reguły, kilkanaście minut po starcie awarii. Bez tego przypadku sklep stałby do rana.",[11,8123,8124,8127],{},[15,8125,8126],{},"Usunięcie z konta domen, które nie mają witryny."," Nocna rotacja logów razem z przeliczaniem statystyk weszła u mnie do pierwszej dziesiątki najdroższych minut doby: średnia 45% CPU w tej jednej minucie, przy dobowej średniej w okolicach 2%. Ten koszt skaluje się z liczbą domen na koncie, a nie z ich ruchem — domena wygaszona, przekierowana albo z samą pocztą płaci w nim tyle samo co witryna z ruchem, a do tego zostaje na liście celów skanera.",[11,8129,8130],{},"Na obietnicę zdjęcia przekroczeń to za mało i tak trzeba tę pozycję czytać: rotacja ani razu nie wywołała faulta, więc oszczędza koszt, który nie był wiążący. Argument zostaje, bo kilkanaście domen bez witryny na jednym koncie podnosi tło, a przy następnej serii skanera liczy się właśnie to, od jakiego poziomu ona startuje.",[11,8132,8133,8139],{},[15,8134,8135,8136,8138],{},"Kopia ",[53,8137,5098],{}," przed każdą zmianą."," Jedna komenda, a oszczędza cały wieczór:",[177,8141,8143],{"className":1591,"code":8142,"language":1593,"meta":182,"style":182},"mkdir -p ~/htaccess-backup\nfor f in ~/domains/*/public_html/.htaccess; do\n  [ -f \"$f\" ] || continue\n  d=$(basename \"$(dirname \"$(dirname \"$f\")\")\")\n  cp \"$f\" ~/htaccess-backup/\"$d\".htaccess\ndone\n",[53,8144,8145,8156,8171,8187,8213,8232],{"__ignoreMap":182},[186,8146,8147,8150,8153],{"class":188,"line":189},[186,8148,8149],{"class":196},"mkdir",[186,8151,8152],{"class":351}," -p",[186,8154,8155],{"class":280}," ~/htaccess-backup\n",[186,8157,8158,8160,8162,8164,8167,8169],{"class":188,"line":218},[186,8159,5263],{"class":192},[186,8161,5296],{"class":295},[186,8163,5269],{"class":192},[186,8165,8166],{"class":280}," ~/domains/*/public_html/.htaccess",[186,8168,5275],{"class":200},[186,8170,5278],{"class":192},[186,8172,8173,8175,8177,8179,8181,8183,8185],{"class":188,"line":239},[186,8174,5547],{"class":200},[186,8176,5319],{"class":208},[186,8178,5301],{"class":280},[186,8180,5324],{"class":295},[186,8182,5327],{"class":280},[186,8184,5330],{"class":200},[186,8186,5333],{"class":192},[186,8188,8189,8191,8193,8195,8197,8199,8201,8203,8205,8207,8209,8211],{"class":188,"line":245},[186,8190,5482],{"class":295},[186,8192,635],{"class":208},[186,8194,5487],{"class":200},[186,8196,5411],{"class":196},[186,8198,5408],{"class":280},[186,8200,5494],{"class":196},[186,8202,5408],{"class":280},[186,8204,5494],{"class":196},[186,8206,5301],{"class":280},[186,8208,5324],{"class":295},[186,8210,5505],{"class":280},[186,8212,2021],{"class":200},[186,8214,8215,8218,8220,8222,8224,8227,8229],{"class":188,"line":252},[186,8216,8217],{"class":196},"  cp",[186,8219,5301],{"class":280},[186,8221,5324],{"class":295},[186,8223,5327],{"class":280},[186,8225,8226],{"class":280}," ~/htaccess-backup/\"",[186,8228,5304],{"class":295},[186,8230,8231],{"class":280},"\".htaccess\n",[186,8233,8234],{"class":188,"line":272},[186,8235,6861],{"class":192},[26,8237,8239],{"id":8238},"pięć-pytań-które-i-tak-padną","Pięć pytań, które i tak padną",[11,8241,8242,8245],{},[15,8243,8244],{},"Czy przekroczenie limitu CPU oznacza, że strona była zaatakowana?","\nNie w sensie włamania. Skanowanie podatności to szum tła internetu, dostaje je każda witryna publiczna. Problemem jest koszt obsługi tych żądań, nie samo ich istnienie.",[11,8247,8248,8251],{},[15,8249,8250],{},"Czy wystarczy zablokować adres IP skanera?","\nNie. Adresy rotują przy każdym przelocie, zwykle z zakresów dużych dostawców chmurowych. Blokować trzeba ścieżki, nie źródła.",[11,8253,8254,8257,8258,8260,8261,8263],{},[15,8255,8256],{},"Czy to problem WordPressa?","\nTylko w takim stopniu, w jakim WordPress kieruje wszystko, czego nie ma na dysku, do jednego pliku wejściowego. Tak samo robi Laravel, Symfony i większość współczesnych aplikacji PHP, więc kosztem obciążone są one wszystkie. Sprawdzisz to u siebie w ",[53,8259,5098],{},", bez czekania na skaner: szukaj reguły przepisującej na ",[53,8262,5214],{}," wszystko, co nie jest istniejącym plikiem ani katalogiem.",[11,8265,8266,8269],{},[15,8267,8268],{},"Czy przejście na droższy pakiet rozwiąże problem?","\nPodniesie limit, ale przy takim profilu obciążenia raczej odsunie objawy niż usunie przyczynę. Najpierw sprawdź, czy limit wyczerpuje ruch użytkowników, czy automaty.",[11,8271,8272,8275],{},[15,8273,8274],{},"Ile witryn można trzymać na jednym koncie?","\nNie ma reguły, ale przy kilkunastu warto pamiętać, że limit jest wspólny. Jedna zaniedbana instalacja spowalnia wszystkie pozostałe, więc diagnostykę zaczynam od przeglądu całego konta.",[5029,8277],{},[11,8279,8280],{},"Alert o przekroczeniu limitu prawie nigdy nie wskazuje winowajcy, a pierwszy przekonujący trop bywa fałszywy. Zacznij od panelu w rozdzielczości minutowej, w logach szukaj żądań, które doszły do PHP, a kod zostaw na koniec.",[2181,8282,8283],{},"html pre.shiki code .sVbv2, html code.shiki .sVbv2{--shiki-default:#61AFEF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .seHd6, html code.shiki .seHd6{--shiki-default:#C678DD}html pre.shiki code .sVyAn, html code.shiki .sVyAn{--shiki-default:#E06C75}html pre.shiki code .subq3, html code.shiki .subq3{--shiki-default:#98C379}html pre.shiki code .sn6KH, html code.shiki .sn6KH{--shiki-default:#ABB2BF}html pre.shiki code .sjrmR, html code.shiki .sjrmR{--shiki-default:#56B6C2}html pre.shiki code .sVC51, html code.shiki .sVC51{--shiki-default:#D19A66}html pre.shiki code .sU0A5, html code.shiki .sU0A5{--shiki-default:#E5C07B}",{"title":182,"searchDepth":218,"depth":218,"links":8285},[8286,8287,8288,8289,8290,8291,8292,8296,8304,8309,8310,8311,8312,8313,8314],{"id":5116,"depth":218,"text":5117},{"id":5156,"depth":218,"text":5157},{"id":5218,"depth":218,"text":5219},{"id":5246,"depth":218,"text":5247},{"id":5453,"depth":218,"text":5454},{"id":5670,"depth":218,"text":5671},{"id":5812,"depth":218,"text":5813,"children":8293},[8294,8295],{"id":5954,"depth":239,"text":5955},{"id":6103,"depth":239,"text":6104},{"id":6223,"depth":218,"text":6224,"children":8297},[8298,8299,8301,8302,8303],{"id":6615,"depth":239,"text":6616},{"id":6730,"depth":239,"text":8300},"Czego !-f nie chroni",{"id":6867,"depth":239,"text":6868},{"id":6931,"depth":239,"text":6932},{"id":6948,"depth":239,"text":6949},{"id":7038,"depth":218,"text":7039,"children":8305},[8306,8307,8308],{"id":7042,"depth":239,"text":7043},{"id":7128,"depth":239,"text":7129},{"id":7150,"depth":239,"text":7151},{"id":7215,"depth":218,"text":7216},{"id":7923,"depth":218,"text":7924},{"id":7950,"depth":218,"text":7951},{"id":8052,"depth":218,"text":8053},{"id":8102,"depth":218,"text":8103},{"id":8238,"depth":218,"text":8239},"2026-09-18","Hosting spowalnia konto, a alert nie mówi, która strona zawiniła. Komendy do logów, trzy fałszywe tropy i przyczyna, która zwykle nie leży w kodzie.","Hosting",{},"/img/blog/limit-cpu-hosting-wspoldzielony-wordpress.webp","Szafa serwerowa z czerwonym alarmem oznaczonym LIMIT i tabliczkami przekreślonych fałszywych tropów","/limit-cpu-hosting-wspoldzielony-wordpress",{"title":5072,"description":8316},{"loc":8321},"limit-cpu-hosting-wspoldzielony-wordpress","2026-09-20","7rXz2FUyyvUVOdYvZOxbMacg7W4oGdhTmhxlsZJRTFk",{"id":8328,"title":8329,"body":8330,"date":8981,"description":8982,"extension":2203,"kategoria":5061,"meta":8983,"navigation":248,"obrazek":8984,"obrazekAlt":8985,"path":8986,"seo":8987,"sitemap":8988,"stem":8989,"szkic":2212,"zaktualizowano":2213,"__hash__":8990},"wpisy/zmiana-domeny-wordpress-aktualizacja-bazy-danych.md","Zmiana domeny w WordPress — jak zaktualizować bazę danych",{"type":8,"value":8331,"toc":8969},[8332,8339,8342,8346,8375,8383,8386,8390,8393,8431,8434,8438,8441,8444,8466,8472,8488,8494,8511,8515,8518,8680,8694,8697,8731,8735,8738,8772,8775,8781,8787,8790,8796,8800,8810,8816,8822,8826,8832,8838,8856,8892,8898,8902,8916,8920,8923,8933,8937,8943,8949,8961,8963,8966],[11,8333,8334,8335,8338],{},"Po przeniesieniu WordPressa na nową domenę adresy w bazie zostają stare. Najszybciej naprawia to jedna komenda WP-CLI: ",[53,8336,8337],{},"wp search-replace 'https://stara-domena.pl' 'https://nowa-domena.pl'",". Jeśli nie masz dostępu do konsoli, niżej znajdziesz wersję przez phpMyAdmin.",[11,8340,8341],{},"Zanim jednak skopiujesz pierwsze lepsze zapytanie z internetu: dwa z popularnie powielanych poleceń potrafią rozbić ustawienia motywu i wtyczek tak, że objawy wyjdą dopiero po kilku dniach. Opisuję je w dalszej części.",[26,8343,8345],{"id":8344},"dlaczego-zmiana-adresu-w-kokpicie-nie-wystarcza","Dlaczego zmiana adresu w kokpicie nie wystarcza",[11,8347,8348,8349,8351,8352,116,8355,8358,8359,8362,8363,8366,8367,8370,8371,8374],{},"WordPress trzyma adres strony w kilku miejscach naraz. W tabeli ",[53,8350,4907],{}," siedzą pola ",[53,8353,8354],{},"home",[53,8356,8357],{},"siteurl",". W ",[53,8360,8361],{},"wp_posts"," każdy wstawiony obrazek, link wewnętrzny i osadzony materiał zapisany jest jako adres bezwzględny, czyli ",[53,8364,8365],{},"https://stara-domena.pl/wp-content/uploads/2024/03/zdjecie.jpg"," zamiast samego ",[53,8368,8369],{},"/zdjecie.jpg",". Własne adresy zapisują też motywy i wtyczki, w ",[53,8372,8373],{},"wp_postmeta"," i we własnych tabelach.",[11,8376,8377,8378,116,8380,8382],{},"Zmiana ",[53,8379,8354],{},[53,8381,8357],{}," przestawia tylko to, pod jakim adresem WordPress widzi sam siebie. Treść wpisów zostaje nietknięta i dalej wskazuje starą domenę.",[11,8384,8385],{},"Ta sama zasada dotyczy sklepów. W WooCommerce dochodzą jeszcze adresy w danych produktów i konfiguracji bramek płatności, więc migracja sklepu wymaga sprawdzenia większej liczby miejsc niż zwykła witryna.",[26,8387,8389],{"id":8388},"zacznij-od-kopii-bazy","Zacznij od kopii bazy",[11,8391,8392],{},"Każda operacja z tego tekstu zmienia dane nieodwracalnie:",[177,8394,8396],{"className":1591,"code":8395,"language":1593,"meta":182,"style":182},"mysqldump -u uzytkownik -p nazwa_bazy > kopia-przed-migracja.sql\nls -lh kopia-przed-migracja.sql\n",[53,8397,8398,8420],{"__ignoreMap":182},[186,8399,8400,8403,8406,8409,8411,8414,8417],{"class":188,"line":189},[186,8401,8402],{"class":196},"mysqldump",[186,8404,8405],{"class":351}," -u",[186,8407,8408],{"class":280}," uzytkownik",[186,8410,8152],{"class":351},[186,8412,8413],{"class":280}," nazwa_bazy",[186,8415,8416],{"class":200}," > ",[186,8418,8419],{"class":280},"kopia-przed-migracja.sql\n",[186,8421,8422,8425,8428],{"class":188,"line":218},[186,8423,8424],{"class":196},"ls",[186,8426,8427],{"class":351}," -lh",[186,8429,8430],{"class":280}," kopia-przed-migracja.sql\n",[11,8432,8433],{},"Drugie polecenie nie jest ozdobnikiem. Kopia o zerowym rozmiarze wygląda w panelu hostingu identycznie jak poprawna i przekonujesz się o tym w najgorszym momencie.",[26,8435,8437],{"id":8436},"sposób-pierwszy-wp-cli","Sposób pierwszy: WP-CLI",[11,8439,8440],{},"Mając dostęp do konsoli, używaj wyłącznie tej metody. WP-CLI rozumie dane serializowane i przelicza je przy zamianie, czego zwykłe zapytanie SQL nie potrafi.",[11,8442,8443],{},"Najpierw przebieg próbny, który niczego nie zmienia, a pokazuje liczbę trafień:",[177,8445,8447],{"className":1591,"code":8446,"language":1593,"meta":182,"style":182},"wp search-replace 'https://stara-domena.pl' 'https://nowa-domena.pl' --dry-run\n",[53,8448,8449],{"__ignoreMap":182},[186,8450,8451,8454,8457,8460,8463],{"class":188,"line":189},[186,8452,8453],{"class":196},"wp",[186,8455,8456],{"class":280}," search-replace",[186,8458,8459],{"class":280}," 'https://stara-domena.pl'",[186,8461,8462],{"class":280}," 'https://nowa-domena.pl'",[186,8464,8465],{"class":351}," --dry-run\n",[11,8467,8468,8469,1636],{},"Gdy liczby się zgadzają, to samo bez ",[53,8470,8471],{},"--dry-run",[177,8473,8475],{"className":1591,"code":8474,"language":1593,"meta":182,"style":182},"wp search-replace 'https://stara-domena.pl' 'https://nowa-domena.pl'\n",[53,8476,8477],{"__ignoreMap":182},[186,8478,8479,8481,8483,8485],{"class":188,"line":189},[186,8480,8453],{"class":196},[186,8482,8456],{"class":280},[186,8484,8459],{"class":280},[186,8486,8487],{"class":280}," 'https://nowa-domena.pl'\n",[11,8489,8490,8491,71],{},"Domyślnie WP-CLI przechodzi po tabelach z prefiksem instalacji. Jeśli wtyczki założyły tabele poza tym prefiksem, dołóż ",[53,8492,8493],{},"--all-tables",[11,8495,8496,8497,56,8500,8503,8504,8507,8508,71],{},"Adres podawaj bez ukośnika na końcu i dokładnie w takiej postaci, w jakiej występuje w bazie. Zamianę powtórz osobno dla każdego wariantu: ",[53,8498,8499],{},"https://",[53,8501,8502],{},"http://"," oraz wersji z ",[53,8505,8506],{},"www",", jeśli była używana. Próba załatwienia wszystkiego jednym wyrażeniem kończy się adresami w rodzaju ",[53,8509,8510],{},"https://www.https://nowa-domena.pl",[26,8512,8514],{"id":8513},"sposób-drugi-zapytania-sql","Sposób drugi: zapytania SQL",[11,8516,8517],{},"Kiedy zostaje tylko phpMyAdmin albo Adminer, wykonaj poniższe zapytania. Dotykają wyłącznie pól, w których dane nie są serializowane, więc są bezpieczne.",[177,8519,8523],{"className":8520,"code":8521,"language":8522,"meta":182,"style":182},"language-sql shiki shiki-themes one-dark-pro","UPDATE wp_options\nSET option_value = REPLACE(option_value, 'https://stara-domena.pl', 'https://nowa-domena.pl')\nWHERE option_name IN ('home', 'siteurl');\n\nUPDATE wp_posts\nSET post_content = REPLACE(post_content, 'https://stara-domena.pl', 'https://nowa-domena.pl');\n\nUPDATE wp_posts\nSET post_excerpt = REPLACE(post_excerpt, 'https://stara-domena.pl', 'https://nowa-domena.pl');\n\nUPDATE wp_comments\nSET comment_content = REPLACE(comment_content, 'https://stara-domena.pl', 'https://nowa-domena.pl');\n","sql",[53,8524,8525,8533,8559,8582,8586,8593,8615,8619,8625,8647,8651,8658],{"__ignoreMap":182},[186,8526,8527,8530],{"class":188,"line":189},[186,8528,8529],{"class":192},"UPDATE",[186,8531,8532],{"class":200}," wp_options\n",[186,8534,8535,8538,8541,8543,8546,8549,8552,8554,8557],{"class":188,"line":218},[186,8536,8537],{"class":192},"SET",[186,8539,8540],{"class":200}," option_value ",[186,8542,635],{"class":208},[186,8544,8545],{"class":208}," REPLACE",[186,8547,8548],{"class":200},"(option_value, ",[186,8550,8551],{"class":280},"'https://stara-domena.pl'",[186,8553,56],{"class":200},[186,8555,8556],{"class":280},"'https://nowa-domena.pl'",[186,8558,2021],{"class":200},[186,8560,8561,8564,8567,8570,8572,8575,8577,8580],{"class":188,"line":239},[186,8562,8563],{"class":192},"WHERE",[186,8565,8566],{"class":200}," option_name ",[186,8568,8569],{"class":192},"IN",[186,8571,935],{"class":200},[186,8573,8574],{"class":280},"'home'",[186,8576,56],{"class":200},[186,8578,8579],{"class":280},"'siteurl'",[186,8581,236],{"class":200},[186,8583,8584],{"class":188,"line":245},[186,8585,249],{"emptyLinePlaceholder":248},[186,8587,8588,8590],{"class":188,"line":252},[186,8589,8529],{"class":192},[186,8591,8592],{"class":200}," wp_posts\n",[186,8594,8595,8597,8600,8602,8604,8607,8609,8611,8613],{"class":188,"line":272},[186,8596,8537],{"class":192},[186,8598,8599],{"class":200}," post_content ",[186,8601,635],{"class":208},[186,8603,8545],{"class":208},[186,8605,8606],{"class":200},"(post_content, ",[186,8608,8551],{"class":280},[186,8610,56],{"class":200},[186,8612,8556],{"class":280},[186,8614,236],{"class":200},[186,8616,8617],{"class":188,"line":292},[186,8618,249],{"emptyLinePlaceholder":248},[186,8620,8621,8623],{"class":188,"line":307},[186,8622,8529],{"class":192},[186,8624,8592],{"class":200},[186,8626,8627,8629,8632,8634,8636,8639,8641,8643,8645],{"class":188,"line":319},[186,8628,8537],{"class":192},[186,8630,8631],{"class":200}," post_excerpt ",[186,8633,635],{"class":208},[186,8635,8545],{"class":208},[186,8637,8638],{"class":200},"(post_excerpt, ",[186,8640,8551],{"class":280},[186,8642,56],{"class":200},[186,8644,8556],{"class":280},[186,8646,236],{"class":200},[186,8648,8649],{"class":188,"line":331},[186,8650,249],{"emptyLinePlaceholder":248},[186,8652,8653,8655],{"class":188,"line":343},[186,8654,8529],{"class":192},[186,8656,8657],{"class":200}," wp_comments\n",[186,8659,8660,8662,8665,8667,8669,8672,8674,8676,8678],{"class":188,"line":357},[186,8661,8537],{"class":192},[186,8663,8664],{"class":200}," comment_content ",[186,8666,635],{"class":208},[186,8668,8545],{"class":208},[186,8670,8671],{"class":200},"(comment_content, ",[186,8673,8551],{"class":280},[186,8675,56],{"class":200},[186,8677,8556],{"class":280},[186,8679,236],{"class":200},[11,8681,8682,8683,8686,8687,8689,8690,8693],{},"Prefiks ",[53,8684,8685],{},"wp_"," bywa inny. Sprawdź go w ",[53,8688,2248],{}," w zmiennej ",[53,8691,8692],{},"$table_prefix"," i podmień w zapytaniach.",[11,8695,8696],{},"Po wykonaniu policz, czy coś zostało:",[177,8698,8700],{"className":8520,"code":8699,"language":8522,"meta":182,"style":182},"SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%stara-domena.pl%';\n",[53,8701,8702],{"__ignoreMap":182},[186,8703,8704,8707,8710,8713,8716,8719,8721,8723,8726,8729],{"class":188,"line":189},[186,8705,8706],{"class":192},"SELECT",[186,8708,8709],{"class":208}," COUNT",[186,8711,8712],{"class":200},"(*) ",[186,8714,8715],{"class":192},"FROM",[186,8717,8718],{"class":200}," wp_posts ",[186,8720,8563],{"class":192},[186,8722,8599],{"class":200},[186,8724,8725],{"class":192},"LIKE",[186,8727,8728],{"class":280}," '%stara-domena.pl%'",[186,8730,532],{"class":200},[26,8732,8734],{"id":8733},"pułapka-pierwsza-dane-serializowane","Pułapka pierwsza: dane serializowane",[11,8736,8737],{},"W sieci krąży wersja tej instrukcji z dodatkowym zapytaniem na metadanych:",[177,8739,8741],{"className":8520,"code":8740,"language":8522,"meta":182,"style":182},"UPDATE wp_postmeta SET meta_value = REPLACE(meta_value, 'stara', 'nowa');\n",[53,8742,8743],{"__ignoreMap":182},[186,8744,8745,8747,8750,8752,8755,8757,8759,8762,8765,8767,8770],{"class":188,"line":189},[186,8746,8529],{"class":192},[186,8748,8749],{"class":200}," wp_postmeta ",[186,8751,8537],{"class":192},[186,8753,8754],{"class":200}," meta_value ",[186,8756,635],{"class":208},[186,8758,8545],{"class":208},[186,8760,8761],{"class":200},"(meta_value, ",[186,8763,8764],{"class":280},"'stara'",[186,8766,56],{"class":200},[186,8768,8769],{"class":280},"'nowa'",[186,8771,236],{"class":200},[11,8773,8774],{},"To właśnie ono psuje strony po migracji. PHP zapisuje tam tablice w formacie serializowanym, w którym przed każdym tekstem stoi jego długość:",[177,8776,8779],{"className":8777,"code":8778,"language":5126,"meta":182},[5124],"a:1:{s:3:\"url\";s:32:\"https://stara-domena.pl/logo.png\";}\n",[53,8780,8778],{"__ignoreMap":182},[11,8782,8783,8784,8786],{},"Liczba ",[53,8785,6083],{}," to liczba znaków adresu. Po zamianie na krótszą lub dłuższą domenę adres się zmienia, a liczba zostaje. PHP próbuje odczytać 32 znaki tam, gdzie jest ich 30, i porzuca odczyt całej tablicy.",[11,8788,8789],{},"Objawy wychodzą z opóźnieniem: znikają ustawienia motywu, przestaje działać suwak na stronie głównej, konfiguracja formularza kontaktowego wraca do stanu domyślnego, pola dodatkowe przy wpisach są puste. Baza wygląda na kompletną, bo dane fizycznie w niej są. Po prostu nie da się ich odczytać.",[11,8791,8792,8793,71],{},"Pola serializowane zostaw narzędziom, które przeliczają długości od nowa: WP-CLI, wtyczce Better Search Replace albo skryptowi Search Replace DB. Nigdy zwykłemu ",[53,8794,8795],{},"REPLACE",[26,8797,8799],{"id":8798},"pułapka-druga-kolumna-guid","Pułapka druga: kolumna guid",[11,8801,8802,8803,8806,8807,8809],{},"Ta sama popularna instrukcja każe zaktualizować ",[53,8804,8805],{},"guid"," w tabeli ",[53,8808,8361],{},". Nie rób tego.",[11,8811,8812,8813,8815],{},"Mimo że ",[53,8814,8805],{}," wygląda jak adres, pełni rolę trwałego identyfikatora wpisu. Czytniki RSS rozpoznają po nim, które wpisy już pokazały. Po zmianie wartości wszystkie archiwalne wpisy zostaną uznane za nowe i subskrybenci dostaną powiadomienia o kilkuset publikacjach naraz. Dokumentacja WordPressa wprost odradza modyfikowanie tej kolumny.",[11,8817,8818,8819,8821],{},"Stara domena w ",[53,8820,8805],{}," niczego nie psuje, bo ta wartość nie służy do budowania odnośników na stronie.",[26,8823,8825],{"id":8824},"cztery-rzeczy-do-sprawdzenia-po-zamianie","Cztery rzeczy do sprawdzenia po zamianie",[11,8827,8828,8831],{},[15,8829,8830],{},"Wyloguj się i zaloguj ponownie."," Ciasteczko sesji jest przypisane do starej domeny, więc kokpit potrafi odsyłać do ekranu logowania w kółko.",[11,8833,8834,8837],{},[15,8835,8836],{},"Zapisz ustawienia odnośników."," Kokpit, Ustawienia, Bezpośrednie odnośniki, przycisk Zapisz bez zmieniania czegokolwiek. To przebudowuje reguły przepisywania adresów i usuwa błędy 404 na podstronach.",[11,8839,8840,8845,8846,116,8849,8852,8853,8855],{},[15,8841,8842,8843,71],{},"Zajrzyj do ",[53,8844,2248],{}," Stałe ",[53,8847,8848],{},"WP_HOME",[53,8850,8851],{},"WP_SITEURL"," nadpisują wartości z bazy, więc dopóki zostaje w nich stary adres, zmiana w ",[53,8854,4907],{}," niczego nie da:",[177,8857,8859],{"className":2251,"code":8858,"language":2253,"meta":182,"style":182},"define( 'WP_HOME', 'https://nowa-domena.pl' );\ndefine( 'WP_SITEURL', 'https://nowa-domena.pl' );\n",[53,8860,8861,8877],{"__ignoreMap":182},[186,8862,8863,8865,8867,8870,8872,8874],{"class":188,"line":189},[186,8864,2260],{"class":208},[186,8866,230],{"class":200},[186,8868,8869],{"class":280}," 'WP_HOME'",[186,8871,1642],{"class":200},[186,8873,8462],{"class":280},[186,8875,8876],{"class":200}," );\n",[186,8878,8879,8881,8883,8886,8888,8890],{"class":188,"line":218},[186,8880,2260],{"class":208},[186,8882,230],{"class":200},[186,8884,8885],{"class":280}," 'WP_SITEURL'",[186,8887,1642],{"class":200},[186,8889,8462],{"class":280},[186,8891,8876],{"class":200},[11,8893,8894,8897],{},[15,8895,8896],{},"Ustaw przekierowania ze starej domeny."," Przekierowanie stałe 301 przenosi na nową domenę wypracowaną pozycję w wyszukiwarce. Tymczasowe 302 tego nie zrobi.",[26,8899,8901],{"id":8900},"multisite-to-osobna-historia","Multisite to osobna historia",[11,8903,8904,8905,116,8908,8911,8912,8915],{},"W instalacji wielowitrynowej adresy siedzą dodatkowo w ",[53,8906,8907],{},"wp_blogs",[53,8909,8910],{},"wp_site",", a każda witryna ma własny zestaw tabel z numerem w prefiksie. Zapytania z tego tekstu obejmą wyłącznie witrynę główną. Przy multisite użyj WP-CLI z parametrem ",[53,8913,8914],{},"--url"," wskazującym konkretną witrynę.",[26,8917,8919],{"id":8918},"w-jakiej-kolejności-to-robić","W jakiej kolejności to robić",[11,8921,8922],{},"Przy przenoszeniu stron klientów trzymam się tej samej kolejności. Najpierw kopia bazy i plików. Potem import na nowy serwer bez dotykania adresów. Dopiero gdy strona odpowiada pod nowym adresem, choćby krzywo, robię zamianę przebiegiem próbnym i sprawdzam liczbę trafień.",[11,8924,8925,8926,4798,8928,8930,8931,71],{},"Zero trafień znaczy, że adres w bazie zapisany jest inaczej, niż zakładam — najczęściej z ",[53,8927,8502],{},[53,8929,8499],{}," albo z przedrostkiem ",[53,8932,8506],{},[26,8934,8936],{"id":8935},"trzy-pytania-które-i-tak-padną","Trzy pytania, które i tak padną",[11,8938,8939,8942],{},[15,8940,8941],{},"Czy da się to zrobić bez dostępu do bazy?","\nTak, wtyczką Better Search Replace z poziomu kokpitu. Ma tryb podglądu, który pokazuje liczbę zmian przed ich wykonaniem, i poprawnie obsługuje dane serializowane.",[11,8944,8945,8948],{},[15,8946,8947],{},"Dlaczego po zmianie adresu widzę błąd 404 na podstronach?","\nReguły przepisywania adresów zostały zapisane dla starej domeny. Wejdź w Ustawienia, Bezpośrednie odnośniki i kliknij Zapisz — to je przebuduje.",[11,8950,8951,8954,8955,116,8958,8960],{},[15,8952,8953],{},"Czy trzeba zmieniać adresy w plikach motywu?","\nZwykle nie, o ile motyw buduje odnośniki funkcjami WordPressa. Sprawdź ",[53,8956,8957],{},"header.php",[53,8959,2277],{}," pod kątem adresów wpisanych na sztywno, bo zamiana w bazie ich nie obejmie. W motywach pisanych od zera tego problemu nie ma, bo adresy budowane są funkcjami od początku.",[5029,8962],{},[11,8964,8965],{},"Migracja to moment, w którym najłatwiej coś przeoczyć, a skutki wychodzą tygodniami. Zrób kopię bazy przed zamianą, użyj WP-CLI zamiast surowego SQL-a i przejdź listę z góry, zanim uznasz przeprowadzkę za zakończoną.",[2181,8967,8968],{},"html pre.shiki code .sVbv2, html code.shiki .sVbv2{--shiki-default:#61AFEF}html pre.shiki code .sVC51, html code.shiki .sVC51{--shiki-default:#D19A66}html pre.shiki code .subq3, html code.shiki .subq3{--shiki-default:#98C379}html pre.shiki code .sn6KH, html code.shiki .sn6KH{--shiki-default:#ABB2BF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .seHd6, html code.shiki .seHd6{--shiki-default:#C678DD}html pre.shiki code .sjrmR, html code.shiki .sjrmR{--shiki-default:#56B6C2}",{"title":182,"searchDepth":218,"depth":218,"links":8970},[8971,8972,8973,8974,8975,8976,8977,8978,8979,8980],{"id":8344,"depth":218,"text":8345},{"id":8388,"depth":218,"text":8389},{"id":8436,"depth":218,"text":8437},{"id":8513,"depth":218,"text":8514},{"id":8733,"depth":218,"text":8734},{"id":8798,"depth":218,"text":8799},{"id":8824,"depth":218,"text":8825},{"id":8900,"depth":218,"text":8901},{"id":8918,"depth":218,"text":8919},{"id":8935,"depth":218,"text":8936},"2026-09-12","Po zmianie adresu obrazki i linki wciąż prowadzą do starej domeny. Komenda WP-CLI, gotowe zapytania SQL i dwie pułapki, które po cichu psują bazę.",{},"/img/blog/zmiana-domeny-wordpress-aktualizacja-bazy-danych.jpg","Praca przy kodzie na ekranie komputera","/zmiana-domeny-wordpress-aktualizacja-bazy-danych",{"title":8329,"description":8982},{"loc":8986},"zmiana-domeny-wordpress-aktualizacja-bazy-danych","fBiJeRhXvUj8sMYuqnRD-xL8ULtHY86902vptceRbpA",1790435811956]