Există o categorie de defect pe care niciun build n-o prinde. Trece de tipuri. Linterul e mulțumit. Testele sunt verzi. Pagina se randează. Și lucrul e tot stricat, fiindcă ce s-a rupt nu e codul — e relația dintre două bucăți de cod pe care nicio unealtă n-a fost pusă să le compare.
Trei dintre ele au ieșit dintr-un singur proiect, într-o singură săptămână. Niciunul nu e exotic. Pe toate trei le vei mai întâlni.
1. A doua listă
Site-ul e în trei limbi. Engleza la rădăcină, româna sub /ro, rusa sub /ru.
O funcție decide dacă o adresă există în toate trei, iar constructorul de linkuri
o folosește ca să țină vizitatorul în limba lui:
export const TRANSLATED_PATHS = [
"/", "/work", "/services", "/faq", "/contact",
"/work/erp", "/work/vision", "/work/commerce", "/work/trading",
] as const;
export function useLangPath() {
const lang = useLang();
return (path: string) =>
hasTranslation(path) ? withLang(lang, path) : path;
}
Citește rezerva din ultima linie. O adresă pe care funcția n-o recunoaște e întoarsă neschimbată — ceea ce e corect pentru cele două pagini legale, care sunt deliberat doar în engleză, și tăcut greșit pentru tot restul.
Lista aia era scrisă de mână și a rămas în urmă de trei ori: când s-au adăugat trei studii de caz, când s-au adăugat șapte pagini de serviciu, și când s-a adăugat pagina „Despre". Nu s-a plâns nimic. Rutele existau, erau prerandate, erau în sitemap, întorceau 200. Un vizitator care citea în română apăsa pe un serviciu și ajungea pur și simplu în engleză.
Nouăzeci și opt de linkuri pe tot site-ul. Numărate după aceea, parcurgând rezultatul prerandat și căutând adrese fără prefix de limbă pe pagini care aveau unul:
for (const file of prerenderedPages) {
if (!/^(ro|ru)_/.test(basename(file))) continue;
for (const href of hrefsIn(file)) {
if (!href.startsWith('/ro/') && !href.startsWith('/ru/')) suspicious.push(href);
}
}
Aceeași funcție decide și dacă o pagină primește alternative hreflang, și unde
te duce comutatorul de limbă. Deci aceeași intrare lipsă a produs trei defecte
separate: vizitatori scoși din limba lor, pagini publicate fără alternative, și
un comutator care te lăsa pe pagina principală în loc de pagina pe care o citeai.
Soluția nu e o listă mai lungă. E să nu existe o a doua:
export const TRANSLATED_PATHS = [
"/", "/work", "/services", "/faq", "/contact", "/about",
...CASE_SLUGS.map((s) => `/work/${s}`),
...SERVICE_SLUGS.map((s) => `/services/${s}`),
] as const;
Listele alea de slug-uri hrănesc deja routerul și pasul de prerandare. Derivând din ele, o rută nouă nu mai poate exista într-un loc și lipsi din altul.
Forma generală: de fiecare dată când scrii o listă care trebuie să fie de acord cu altă listă, ai creat un defect cu fitil. Întrebarea nu e dacă deviază, ci când.
2. Butonul care nu se putea apăsa
Secțiunea de hero desenează un material animat în spatele textului, pe un canvas. Canvasul are nevoie de pointer, deci stratul de deasupra i-l cedează:
.hero .herostage { pointer-events: none; }
.hero .herostage a,
.hero .figs { pointer-events: auto; }
Citește a doua regulă. Reactivează ancorele. Acțiunea principală a site-ului —
cea care deschide formularul de proiect — e un <button>, fiindcă deschide ceva
în pagină, nu duce la o adresă.
Așa că a moștenit pointer-events: none și nu se putea apăsa. Nici cu mouse-ul,
nici cu degetul. Se randa corect, primea contur vizibil la focus din tastatură,
avea un onClick funcțional, iar la click nu se întâmpla nimic. Linkul de
alături, „See the work", mergea — fiindcă e ancoră.
A supraviețuit unui audit complet de design mobil — măsurători de zone de atingere, contrast, ordinea titlurilor — fiindcă fiecare dintre verificările alea citește geometrie și stil calculat, iar geometria era în regulă. L-a găsit un script care a încercat să apese lucrul:
- attempting tap action
- <section class="hero">…</section> intercepts pointer events
- retrying tap action
TimeoutError: tap: Timeout 30000ms exceeded.
Asta e toată metoda de detectare: nu „există butonul", ci „apăsarea lui face ceva".
.hero .herostage a,
.hero .herostage button,
.hero .figs { pointer-events: auto; }
Forma generală: pointer-events: none pe un container e o promisiune că-ți
vei aminti fiecare tip de descendent interactiv, pentru totdeauna. Selectorii de
element codifică tăcut o presupunere despre ce conține marcajul tău. Când
marcajul capătă un <button>, <summary>, <input> sau <label>, promisiunea
se rupe și nu te anunță nimeni.
3. Regula care a pierdut în fața uneia scrise mai devreme
O bară fixă stă în josul paginilor de caz și arată faza scenei 3D. Pe telefon scena nu se mai încarcă deloc — se desena în spatele textului la contrast foarte mic, deci consuma baterie ca să deseneze ceva ce nu vede nimeni — iar bara n-are ce să mai descrie:
@media (max-width: 900px) {
#gl, #glfallback, .phasebar { display: none; }
}
Bara a rămas vizibilă. Peste text.
Cu nouăzeci de linii mai devreme, în aceeași foaie de stil:
.phasebar { position: fixed; /* … */ }
.phasebar.on { display: block; }
.phasebar.on are specificitatea (0,2,0). .phasebar are (0,1,0). Ordinea din
fișier decide doar între reguli de specificitate egală — iar un media query nu
adaugă absolut nimic la specificitate. Regula de mai jos a pierdut în fața celei
de mai sus, exact cum spune cascada.
Soluția e să egalezi specificitatea pe care vrei s-o învingi:
@media (max-width: 900px) {
#gl, #glfallback, .phasebar, .phasebar.on { display: none; }
}
Nu !important. Ăla câștigă cearta și pierde fișierul: fiecare suprascriere de
după are nevoie de propriul !important, iar într-un an foaia de stil e o stivă
de ele, din care nu mai poți deduce ce se aplică.
Forma generală: e ușor să crezi că un media query e mai puternic decât o regulă simplă. Nu e. E o condiție, nu o greutate. Orice regulă pe care vrei s-o suprascrii dinăuntrul unuia trebuie învinsă pe terenul ei.
Ce au în comun
Niciunul nu e o greșeală în sensul unei scăpări de tastatură. Fiecare e o decizie corectă care a încetat să fie corectă când s-a schimbat altceva în jurul ei.
O listă scrisă de mână e corectă până crește lucrul pe care îl oglindește. Un selector de element e corect până când marcajul capătă un element pe care nu-l numește. O specificitate e corectă până încerci s-o suprascrii dintr-un loc care ți s-a părut mai puternic.
Ce le prinde cu adevărat e plictisitor de concret:
- Derivă listele, nu le scrie de două ori. Dacă două liste trebuie să fie de acord, una din ele ar trebui calculată din cealaltă.
- Testează interacțiunea, nu randarea. „Butonul există și are 44 de pixeli" și „butonul face ceva când e apăsat" sunt afirmații diferite, iar doar a doua ar fi prins defectul 2.
- Citește stilul calculat, nu foaia de stil. La defectul 3, foaia spunea
display: noneiar elementul spuneadisplay: block. Un singurgetComputedStyleîncheie discuția într-o secundă.
Tema de sub toate trei: build-ul verifică fiecare fișier. Nu verifică niciodată că două fișiere mai sunt de acord unul cu altul. În golul ăla trăiește categoria asta.