Jakub Kubík
Napište mi

Všechny zápisky

zápisek · weby · ~8 min čtení

Jak vznikl web OZ Juliána: redesign bez WordPressu a s vlastní správou aktualit

Proč web malého sdružení běží bez WordPressu a databáze, jak funguje správa aktualit s bezpečným přihlášením z telefonu a jak se ověřoval redesign.

Publikováno
téma
Weby pro malé organizace
stav
hotovo

V administraci hostingu je tlačítko „nastavit DNS“. Kdo na něj klikne, potichu přepíše záznamy, podle kterých prohlížeč hledá server domény: najednou míří přímo na server hostingu, mimo CDN (síť serverů, která web doručuje a chrání). Web běží dál, takže si změny nikdo nevšimne. Na ozjuliana.sk se přesně tohle stalo a změnu neodhalil člověk, ale skript, který po každém nasazení živý web pouze čte. Takových pojistek má web malého sdružení víc, než by člověk čekal, a tenhle zápisek je o tom, proč tam jsou.

Občanské sdružení Juliána (slovenská obdoba spolku) celoročně sbírá oblečení, obuv, hračky a potřeby do domácnosti a rozváží je lidem v nouzi. Web ozjuliana.sk pro něj dobrovolnicky dělám a spravuji od dubna 2026. Zadání zní jednoduše: říct, co sdružení dělá a jak se dá pomoct, a umožnit člověku bez technického zázemí přidat z telefonu zprávu o sbírce i s fotkami. Čísla ve zkratce jsou v případové studii; tady je delší příběh.

Proč ne WordPress a proč soubory JSON bez databáze

Web běží na levném sdíleném webhostingu za CDN. Na serveru je jen PHP: žádné SSH (vzdálená příkazová řádka), žádný Git, nasazuje se přes FTPS (přenos souborů šifrovaným spojením). WordPress by tam běžel, ale přinesl by s sebou pravidelné aktualizace jádra a pluginů, databázi a nekonečné pokusy o přihlášení na všeobecně známé adrese. Sdružení přitom přidá článek s fotkami jednou za pár týdnů. Pro takovou potřebu se nevyplatí udržovat celý redakční systém.

Výsledkem je vlastní PHP kód bez frameworku a bez Composeru. Veřejné stránky jsou šablony, které fungují i bez JavaScriptu. Tři malé skripty jen přidávají animace, lištu pro souhlas s cookies, úvodní clonu s logem a prohlížeč fotek.

Databáze není ani pro články. Každý článek je jeden soubor JSON (textový formát pro strukturovaná data), fotky leží vedle ve složce a při nahrání je zmenší knihovna GD. Dala by se použít i SQLite, jenže při desítkách článků nic nezrychlí a soubory mají dvě praktické výhody: celý obsah webu se zálohuje zkopírováním dvou složek a v nouzi se dá článek opravit přes FTP v obyčejném textovém editoru.

Zápis jde vždy do dočasného souboru, který se teprve potom přejmenuje na cílový, takže přerušené uložení nenechá napůl zapsaný článek.

{
    "slug": "zbierka-satstva-2026",
    "title": "Zbierka šatstva 2026",
    "status": "published",
    "published_at": "2026-09-18T12:00:00+02:00",
    "text": "Ďakujeme všetkým, ktorí prispeli.\n\nZbierka pokračuje každý piatok.",
    "cover": { "big": "uploads/…-big.jpg", "thumb": "uploads/…-thumb.jpg", "alt": "Triedenie darovaného oblečenia" },
    "gallery": []
}

Článek je vymyšlený, skutečné soubory mají navíc čas vytvoření a poslední úpravy. Obsah ze serveru každý večer stáhne záloha, která přes FTPS pouze čte a uchovává 30 denních snímků. Při chybě přijde e‑mail.

Dvoufaktorové přihlášení v čistém PHP

Administrace článků je jediná část webu, kde se dá jeho obsah změnit, proto má povinné dvoufaktorové přihlášení: heslo a šestimístný kód z aplikace Google Authenticator. Bez druhého faktoru účet ani nevznikne.

TOTP, tedy jednorázový kód odvozený od času (norma RFC 6238), je překvapivě malý algoritmus: HMAC (kryptografický otisk) z tajného klíče a pořadového čísla 30sekundového okna, z výsledku se vybere šest číslic. Na hostingu bez Composeru je jednodušší napsat ho v čistém PHP ze standardních funkcí, než ručně nahrávat knihovnu se závislostmi. Implementace se ověřuje proti testovacím hodnotám přímo z normy a testovací sada bez závislostí běží před každým nasazením.

Kolem toho je běžná výbava: hesla uložená jako otisk bcrypt, ochranný token proti podvrženému odeslání (CSRF) v každém formuláři, session s omezenou platností, počítadlo neúspěšných pokusů a první účet, který se nedá založit bez přístupu k serveru. Konkrétní nastavení tu záměrně neuvádím; útočníkovi by pomohla víc než čtenáři.

Administrace pro telefon a člověka bez technického zázemí

Obsah se spravuje téměř výhradně z telefonu. Administrace je proto navržená od šířky 320 px: větší tlačítka a zaškrtávací pole, dotykové cíle alespoň 44 px a vstupní pole s písmem alespoň 16 px, aby iPhone při ťuknutí stránku nepřibližoval. Funguje bez JavaScriptu, takže ji nerozbije ani starší prohlížeč.

Formulář má jen to, co je potřeba: nadpis, datum, text, úvodní fotku, galerii, popisky fotek a přepínač mezi konceptem a zveřejněním. Text je obyčejný text, prázdný řádek znamená nový odstavec. Žádný editor s tlačítky, který by do článku vkládal skryté HTML. Fotky z telefonu se otočí podle údajů EXIF, které do nich zapíše fotoaparát, zmenší a uloží bez metadat. Při auditu se ověřilo všech 84 fotek na živém webu a ani jedna nemá souřadnice GPS.

Jedno rozhodnutí vzešlo přímo z toho, kdo web obsluhuje. Prohlížeč, ve kterém se už někdo úspěšně přihlásil, má vlastní počítadlo pokusů. Cizí pokusy o přihlášení tak mohou zablokovat neznámá zařízení, ale ne telefon, ze kterého se obsah opravdu přidává.

K administraci patří i krátký návod ve slovenštině. Nejužitečnější rada v něm není technická: první věta článku má říct kdy, kde, co a kolik, protože z ní vzniká úryvek na kartách i popis pro vyhledávače.

Lišta pro souhlas podle zákona 452/2021

Slovenský zákon o elektronických komunikacích v § 109 odst. 8 vyžaduje souhlas s ukládáním údajů v zařízení návštěvníka, pokud nejsou nezbytné pro fungování služby. Web je postavený tak, aby bez souhlasu nic takového nepotřeboval: veřejné stránky nenastavují žádnou cookie ani nespouštějí session a písma se načítají z vlastního serveru, ne z Google Fonts.

Google Analytics se načte až po kliknutí na „Prijať“ (Přijmout). „Prijať“ a „Odmietnuť“ jsou na první vrstvě rovnocenná tlačítka, volbu lze kdykoli změnit odkazem v patičce a odvolání smaže analytické cookies. Každá volba se zapíše na server jako doklad o souhlasu: náhodný identifikátor, čas, verze a volba, bez IP adresy. Výjimkou je vložená mapa Google v kontaktu, která se načte hned; stránka o ochraně osobních údajů to otevřeně uvádí.

Redesign, který se dá změřit

V září přišel redesign s pracovním názvem „Pokojná elegancia“ (klidná elegance). Předcházel mu audit staré verze a výsledky byly střízlivé: 29 různých velikostí písma, hranice vstupních polí s kontrastem 1,24 : 1, čtyři nálezy nástroje axe pro automatickou kontrolu přístupnosti a navigace, která na některých šířkách telefonu o pár pixelů přetékala.

Práce měla dvě fáze. První byl refaktoring s jediným pravidlem: žádná viditelná změna. Společná šablona, jeden modul kontaktního formuláře, barvy a velikosti jako proměnné.

Po každém commitu se nová verze porovnala se starou: HTML výstupy 41 scénářů bajt po bajtu na PHP 8.4 i 8.5 a screenshoty pixel po pixelu. Na konci fáze byly výstupy shodné, kromě jedné předem schválené opravy, a 450 screenshotů (15 stránek × 10 šířek × 3 režimy: běžný, bez JavaScriptu a se sníženým pohybem) mělo rozdíl 0 pixelů. Celá sada běžela bez administrátorských práv: PHP ve WebAssembly a Chrome bez okna, ovládaný přes Playwright.

Teprve potom následoval samotný redesign. Každá zamýšlená odchylka se zapsala předem a porovnání ji normalizovalo; všechno ostatní muselo zůstat shodné. Takhle se nedá omylem změnit stránka, která se měnit neměla.

Výsledek: typografická škála s 12 kroky (audit před redesignem napočítal 29 hodnot), hranice polí s kontrastem nad 3 : 1, nula porušení podle axe (pravidla přístupnosti WCAG 2, úrovně A a AA) ve 30 bězích na 10 stránkách a žádné vodorovné přetečení v rozsahu 320–1440 px, v nejrizikovějších pásmech kontrolované po jednom pixelu. Sekce o poslání sdružení se na mobilu zkrátila z přibližně 1 000 na 450 px a logo v hlavičce telefonu narostlo z 20 × 20 na 36 × 32 px, takže je konečně čitelné.

Co se pokazilo a jak se to opravilo

Audit před redesignem našel v původní administraci chyby, které běžné klikání neodhalí. Apostrof v nadpisu rozbil potvrzovací okno při mazání, takže se článek smazal bez zeptání. Neplatný znak UTF-8 ve jméně účtu vedl k zápisu prázdného souboru s účty. Na PHP 8.5 navíc zastaralá funkce pro práci s obrázky vypsala po uložení článku varování a přesměrování se neprovedlo. Všechny tři chyby byly opraveny a ověřeny ještě před spuštěním.

Hned po spuštění přišel bezpečnostní audit živého webu ve dvou kolech, druhé z nich se dvěma nezávislými útočnými revizemi kódu. Přinesl 38 zjištění, ale cestu do administrace bez přihlášení nenašlo ani jedno kolo. Nejpoučnější zjištění: ze 16 souběžných pokusů se špatným heslem se nezablokoval ani jeden, protože pokus se započítal až po kontrole hesla. Dnes se pokus započítá pod zámkem ještě před ní. Opravy kódu vyšly v pěti pull requestech ještě tentýž den.

Poslední chyba, ta s tlačítkem z úvodu, nebyla v kódu. Doména po jednom kliknutí mířila přímo na server hostingu a web fungoval dál, jen bez CDN a její ochrany. Prozradil to skript, který po každém nasazení kontroluje živý web pouze čtením: neprošla mu kontrola certifikátu. Záznamy se vrátily hned následující ráno. Bez toho skriptu mohl web takhle běžet celé měsíce a nikdo by si toho nevšiml.

Co si z toho beru

  • Malé sdružení nepotřebuje redakční systém, potřebuje jeden dobrý formulář. Při desítkách článků mu stačí i soubory bez databáze, pokud je zápis atomický a záloha automatická.
  • Návrh administrace začíná otázkou, kdo ji obsluhuje a na jakém zařízení. Od toho se odvíjí i bezpečnost, například důvěryhodný prohlížeč s vlastním počítadlem.
  • Refaktoring bez měření stojí na dojmu. Bajtová a pixelová parita mění větu „myslím, že se nic nezměnilo“ v důkaz.
  • Audit se vyplatí dvakrát: na starém kódu před změnami a na živém webu po spuštění. Každý z nich našel něco jiného.
  • Kontrola nasazení, která živý web pouze čte, zachytí i chyby mimo kód.

Tlačítko „nastavit DNS“ je v administraci hostingu dodnes. Skript, který na to při příštím nasazení přijde, taky.