Jak je tenhle blog postavený
Když jsem se rozhodl opustit Tumblr a mít svůj obsah konečně pod vlastní kontrolou, věděl jsem jedno: nechci žádný Wordpress, Ghost ani jiný hotový CMS s databází, pluginy a věčnými aktualizacemi. Chtěl jsem něco tak jednoduchého, aby to šlo i za deset let přečíst a opravit, i kdybych už sám zapomněl jak na to.
Tohle zároveň z tohohle web/logu dělá geekovskou, ne-li nerdovskou záležitost, protože nikdo normální (ať už to znamená cokoliv) nepíše posty v Markdownu na serverovém terminálu...d8-D
Žádná databáze, jen soubory
Každý příspěvek je jeden JSON soubor. Žádné MySQL, SQLite, ORM. Obsah příspěvku je pole bloků — text, obrázek, video, odkaz, seznam — ve stejném formátu, jaký používá Tumblr pro svoje NPF (Neue Post Format). Díky tomu mají historické importy z Tumblru a Twitteru úplně stejnou strukturu jako příspěvky, které píšu ručně dnes. Žádná zvláštní výjimka, žádný "legacy formát".
Fotky a videa leží vedle postu — nikdy nic neodkazuju na cizí server (žádný hotlinking), všechno je stažené a uložené lokálně už při importu.
Build: statický web z JSONů
`build_blog.rb` je jeden Ruby skript, který projde všechny JSONy a vyrenderuje z nich statické HTML. Žádný Jekyll, Hugo ani Next.js — jen ERB šablony a hrst funkcí, které skládají HTML ručně. Build mimo jiné:
- generuje stránkování po desítkách příspěvků, archivy podle štítků a podle typu obsahu (text/obrázky/video/odkazy),
- staví JSON index pro vyhledávání a RSS/sitemap,
- řeší formátování textu (tučně, kurzíva, odkazy, přeškrtnutí) podle offsetů v poli `formatting` — stejný princip jako u Tumblr NPF,
- automaticky seskupuje po sobě jdoucí obrázkové bloky do fotogalerie.
Výsledek je čistá hromada statických souborů, kterou umí obsloužit úplně cokoliv.
Psaní: CLI místo administrace v prohlížeči
Zvažoval jsem administrační rozhraní v prohlížeči, ale místní HTTP server, který umí zapisovat soubory, otevírá zbytečná rizika (CSRF z jiné otevřené záložky, path traversal a podobně). Místo toho mám `manage_post.rb`, který otevře `$EDITOR` na markdown souboru s YAML frontmatterem (přesně takhle vypadá i tenhle článek, než se zpracuje). `tučně`, `kurzíva`, `odkaz` a `!popisek` se převedou na stejné content bloky, jaké produkují importy z Tumblru a Twitteru — píšu tedy stejným "jazykem", jakým jsou uložené i příspěvky staré přes deset let.
Skript umí `add`, `edit <slug>`, `delete <slug>` i `list` s filtrem podle typu nebo štítku, a po uložení příspěvku rovnou nabídne rebuild webu.
Publikování z mobilu
Když píšu z iPadu nebo z telefonu přes SSH, nemám po ruce lokální cestu k fotce. Vyřešil jsem to složkou `incoming/` na serveru, kam nahraju fotku přes SFTP, a v Markdownu pak stačí napsat jen `!popisek` — holé jméno souboru se automaticky přeloží na `incoming/foto.jpg`. Pokud soubor v tu chvíli ještě neexistuje, skript počká a opakovaně kontroluje, dokud fotku nenahraju nebo uložení nezruším. Díky tomu můžu nejdřív napsat celý text a fotky dohnat později.
Mastodon jako vrstva pro komentáře
Nemám (a ani nechci) vlastní systém komentářů. Každý nový příspěvek se automaticky tootne na Mastodon (`mastodon_poster.rb`), a odkaz na ten toot se uloží k příspěvku jako `mastodon_url`. Na stránce příspěvku pak JavaScript (`comments.js`) stáhne přes veřejné Mastodon API odpovědi na ten toot a zobrazí je jako komentáře — se jménem, avatarem a časem. Žádná vlastní databáze komentářů, žádná moderace navíc — to všechno dělá Mastodon. Stejný widget zobrazuje i počet favů, boostů a odpovědí přímo pod textem příspěvku.
Widgety v postranním panelu
V patičce/postranním panelu žijou tři malé JS widgety, každý s jiným zdrojem dat a jiným způsobem načtení:
- Poslední tooty — natahují se za běhu v prohlížeči přímo z Mastodon API.
- Poslední commity — totéž, ale z GitHub API (poslední push eventy mého účtu).
- Poslední Pixelfedy — na rozdíl od těch dvou se stahují už při buildu na serveru (`build_blog.rb`), protože Pixelfedí Atom feed nemá CORS hlavičku a z prohlížeče by nešel načíst.
Vyhledávání a další drobnosti
Vyhledávání je čistě klientské: build vygeneruje `search-index.json` se všemi příspěvky (bez diakritiky, malými písmeny), prohlížeč si ho stáhne jednou a pak nad ním vyhodnocuje dotazy s podporou frází v uvozovkách a vylučování slova pomocí `-slovo`. Žádný Algolia, žádný server-side search.
Zbytek je už jen běžná výbava: přepínač světlého/tmavého motivu (CSS proměnné + `prefers-color-scheme`), lightbox na obrázky, sbalovací navigace na mobilu — všechno bez jakéhokoli frameworku, čistý vanilla JS v malých samostatných souborech.
Nasazení: chytrý sync na Surfer
Build maže a znovu vytváří celé `public/`, takže se nedá spoléhat na čas změny souborů — každý build má nové mtime úplně na všem. `deploy-web.sh` proto drží mimo `public/` manifest SHA-256 hashů všech souborů a při nasazení nahraje na úložiště (Cloudron Surfer, přes jeho Files API) jen to, co se opravdu změnilo. Výsledek: rebuild tří tisíc příspěvků neznamená při každém nasazení upload tří tisíc souborů, ale jen těch pár, co se skutečně změnily.
Odkud se to sem všechno dostalo
Blog neběží od nuly — naimportoval jsem do něj historii ze čtyř Tumblr blogů a z archivu vlastních tweetů z Twitteru/X (2008–2022, jen samostatné příspěvky, žádné odpovědi ani retweety). Oba importy (`migrate_tumblr.rb`, `migrate_twitter.rb`) publikovaly posty přes `post_writer.rb`, který používá i ruční psaní, takže žádný příspěvek nemá "výsadní" postavení podle původu.
Proč takhle
Celé je to sázka na to, že obyčejné soubory, obyčejný Ruby skript a hrstka JavaScriptu bez frameworku vydrží déle a budou se snáz opravovat než cokoliv postavené na cizí platformě nebo cizí databázi. Nic tu neběží jako služba, kterou by mi někdo mohl vypnout nebo zpoplatnit — jen soubory, které mám u sebe a rozumím jim.
A přesně tak jsem to chtěl...d8-D
