Jak funguje tento blog

Tento blog je server-rendered webová aplikace napsaná v Rustu. Žádný WordPress, žádný statický generátor — čistý Rust server, který renderuje stránky na požádání z PostgreSQL databáze.

Zdrojový kód je dostupný na github.com/xmiksay/blog.

CoJak
BackendRust, Axum 0.8, Tokio
DatabázePostgreSQL přes SeaORM
ŠablonyMiniJinja
Markdownpulldown-cmark s custom rozšířeními
AuthArgon2 + Bearer tokeny
FrontendStatické HTML/CSS/JS, Tailwind
NasazeníDocker + docker-compose
AI integraceMCP server pro Claude

Architektura a tech stack

Tech stack

  • Backend: Rust (edition 2024), Axum 0.8, Tokio async runtime
  • Databáze: PostgreSQL přes SeaORM 1.x
  • Šablony: MiniJinja — Jinja2-like templating v Rustu, soubory v templates/
  • Markdown: pulldown-cmark s vlastními rozšířeními (transclusion <!--BLOG_PLACEHOLDER_0-->, aj.)
  • Auth: Argon2 hashování hesel, Bearer token sessions, service tokeny pro MCP
  • Frontend: Statické soubory v static/, Tailwind CSS
  • Logging: tracing + tracing-subscriber s env filtrem

Struktura projektu

src/
  bin/
    blog_server.rs      # Hlavní web server (port 3000)
    blog_migration.rs   # Migration CLI (up/down/fresh/status)
    blog_cli.rs         # Admin CLI (create-user)
  entity/               # SeaORM entity modely
    user.rs, token.rs, tag.rs, menu.rs, page.rs,
    page_revision.rs, image.rs, gallery.rs, gallery_image.rs
  migration/            # SeaORM migrace
  routes/
    public/             # Veřejné routes (stránky, obrázky, galerie)
    admin/              # Admin CMS routes (chráněné autentizací)
    mod.rs              # Sdílené view typy, menu builder
  auth.rs               # Hashování hesel, token middleware
  markdown.rs           # Renderování Markdownu s custom rozšířeními
  state.rs              # AppState (db + template env)
templates/              # MiniJinja HTML šablony
static/                 # CSS, JS, obrázky

Jak server funguje

Při spuštění blog_server automaticky proběhnou databázové migrace, načte se template engine a Axum spustí HTTP server na portu 3000.

Catch-all route /{*path} funguje takto:

  1. Hledá shodu v menu (statický markdown obsah)
  2. Hledá shodu v pages (dynamický obsah ze stránky v DB)
  3. Vrátí 404

Revize stránek se ukládají jako diff patche — ne jako plné snapshoty. Šetří místo, zachovává historii.


Datový model

Databáze je PostgreSQL, přístup přes SeaORM. Migrace běží automaticky při startu serveru.

Tabulky

users

SloupecTypPopis
idPK
usernameuniquePřihlašovací jméno
password_hashtextArgon2 hash

tokens

SloupecTypPopis
idPK
nonceuniqueToken hodnota
user_idFK → usersVlastník
expires_attimestamp?NULL = trvalý (service tokeny)

Tokeny slouží jak pro session (po přihlášení), tak pro service tokeny (MCP integrace).

tags

SloupecTypPopis
idPK
nameuniqueNázev tagu
descriptiontextPopis

menus

SloupecTypPopis
idPK
pathuniqueURL cesta
markdowntextObsah (renderovaný jako stránka)

pages

SloupecTypPopis
idPK
pathuniqueURL cesta (např. about/blog)
summarytextKrátký popis
markdowntextObsah stránky
tag_idsINT[]Pole tagů
privateboolSkrytá stránka
created_at/byAudit
modified_at/byAudit

page_revisions

SloupecTypPopis
idPK
page_idFK → pages
patchtextDiff oproti předchozí verzi
created_at/byAudit

images a galleries

Obrázky se ukládají jako binární data (bytea) přímo v PostgreSQL — včetně automaticky generovaného náhledu (thumbnail). Galerie jsou uspořádané kolekce obrázků přes image_ids INT[].


Routes — veřejné i admin

Veřejné routes

PathMetodaPopis
/obrazky/{id}GETPlný obrázek
/obrazky/{id}/nahledGETThumbnail
/{*path}GETCatch-all (viz níže)

Catch-all logika

GET /{*path}
  1. match v menus  → render menu markdown
  2. match v pages  → render stránky
  3. → 404

Admin routes (chráněno Bearer tokenem)

PathMetodaPopis
/admin/loginGET, POSTPřihlašovací formulář
/admin/logoutGETOdhlášení
/adminGETDashboard
/admin/strankyCRUDSpráva stránek
/admin/menuCRUDSpráva menu
/admin/tagyCRUDSpráva tagů
/admin/obrazkyCRUDSpráva obrázků
/admin/galerieCRUDSpráva galerií
/admin/tokenyCRUDSpráva service tokenů (pro MCP)

Auth flow

  1. POST /admin/login → ověří heslo Argon2, vytvoří session token, vrátí jako Bearer
  2. Všechny /admin/* routes čtou Authorization: Bearer <token> header
  3. Middleware token ověří v DB, přiřadí user_id do request extension
  4. Service tokeny (pro MCP) nemají expiraci — spravují se ručně přes /admin/tokeny

MCP integrace (Claude)

Blog vystavuje MCP server (Model Context Protocol) na /mcp/* routes. To umožňuje Claudovi číst a editovat stránky přímo z konverzace — přesně tak, jak vznikla tato stránka.

Dostupné nástroje

NástrojKdy použítPopis
search_pagesJako první — při průzkumu obsahuFiltruje stránky podle prefixu cesty a/nebo tagu; vrátí path + summary
read_pagePřed editací, nebo když potřebuji plný obsahPřečte stránku podle přesné cesty — vrátí metadata + markdown
edit_pageVytvoření nebo aktualizace stránkyMění jen předaná pole; automaticky ukládá diff revizi
list_tagsPřed přiřazením tagůVrátí všechny dostupné tagy — jména jsou case-sensitive

SERVER_INSTRUCTIONS — stránka CLAUDE

Namísto konstanty v kódu jsou instrukce pro Clauda uloženy jako privátní stránka blogu na cestě CLAUDE. Server ji načte a předá jako SERVER_INSTRUCTIONS při každém MCP handshaku.

Výhoda: instrukce lze editovat přes admin rozhraní (nebo přes edit_page) bez recompilu.

Transclusion a read_page

Markdown extenze <!--BLOG_PLACEHOLDER_0--> slouží k renderování — server při výstupu do HTML vloží obsah odkazované stránky inline. Z pohledu Clauda jde o pouhý textový token; obsah odkazované stránky v markdownu není automaticky přítomen.

Pokud tedy Claude narazí na <!--BLOG_PLACEHOLDER_1--> a potřebuje znát její obsah, musí si sám zavolat read_page("some/page").

Auth

MCP routes používají service tokeny — vytváří se přes /admin/tokeny. Token se posílá jako Authorization: Bearer <token>. MCP handler z tokenu odvodí user_id pro audit pole (created_by, modified_by).

Service tokeny nemají defaultně expiraci — životní cyklus spravuje administrátor.

Markdown rozšíření

SyntaxeVýsledek
<!--BLOG_PLACEHOLDER_2-->Transclusion — vloží obsah jiné stránky (při renderování)
``Obrázek s odkazem na plnou velikost
``Mřížka náhledů galerie
<!--BLOG_PLACEHOLDER_3-->Statická šachová pozice
<!--BLOG_PLACEHOLDER_4-->Přehrávatelná šachová partie

Implementováno v src/markdown.rs.


Provoz a nasazení

Požadavky

  • Rust (edition 2024)
  • PostgreSQL
  • Docker + Docker Compose (pro kontejnerizované nasazení)

Proměnné prostředí

ProměnnáPopisDefault
DATABASE_URLPostgreSQL connection stringpostgres://blog:blog@localhost:5432/blog
RUST_LOGLog level filtrblog=debug,tower_http=debug,info

Spuštění s Docker Compose

# Build release binárek
cargo build --release

# Spuštění
docker compose up --build -d

Aplikace běží na http://localhost:3000.

Vytvoření uživatele

# S Docker Compose
docker compose exec app ./blog_cli create-user <username> <password>

# Bez Dockeru (DATABASE_URL musí být v .env nebo env)
cargo run --bin blog_cli -- create-user <username> <password>

Migrace

Migrace běží automaticky při startu serveru. Ruční správa:

cargo run --bin blog_migration              # apply all pending
cargo run --bin blog_migration -- down      # rollback last
cargo run --bin blog_migration -- fresh     # reset & reapply vše
cargo run --bin blog_migration -- status    # zobraz stav

# S Docker Compose
docker compose exec app ./blog_migration

Vývoj lokálně

# Vyžaduje DATABASE_URL v .env
cargo run --bin blog_server

# Ověření kompilace bez spuštění
cargo check

Docker image

cargo build --release
docker build -t blog .
docker run -e DATABASE_URL=... -p 3000:3000 blog

CI/CD

Repozitář obsahuje .github/workflows/ — GitHub Actions pipeline pro automatický build a nasazení.