Ten wpis nie opisuje żadnego zlecenia ani konkretnej realizacji.
Opisuje sam ten blog, jako produkt: co potrafi, jakie decyzje stoją za
poszczególnymi mechanizmami i - co ważne dla kogoś oglądającego wersję
demo pod blog-demo.ronim.com.pl - co jest w tej wersji
celowo zablokowane i dlaczego. Jeśli klikasz teraz po panelu admina na
koncie demo, to najlepszy moment na tę lekturę.
Wyszukiwanie: dokładne, z awaryjnym trybem rozmytym
Wyszukiwarka pod /szukaj działa dwuetapowo. Najpierw
zwykłe dopasowanie podciągu w tytule, opisie i tagach wpisu - szybkie
i przewidywalne. Dopiero gdy to nie zwróci żadnego wyniku, uruchamia się
tryb awaryjny: podobieństwo trigramowe (Jaccard na trójznakowych
fragmentach słów), które wybacza literówkę. Zapytanie "wyceniejsz"
zamiast "wycenisz" wciąż trafi do właściwego wpisu, zamiast pokazać
pustą stronę wyników.
Liczone jest to w Pythonie, nie w bazie danych - dla skali tego
bloga (kilkanaście, może kilkadziesiąt wpisów) skan w pamięci jest
wystarczająco szybki i nie wymaga rozszerzenia PostgreSQL-a takiego jak
pg_trgm. Przy realnie dużej bazie wpisów warto by to
przenieść do zapytań SQL, ale to inny próg skali niż mamy teraz.
Do tego dochodzi /szukaj/live - podpowiedzi pojawiające
się pod polem wyszukiwania w trakcie pisania, z niewielkim opóźnieniem
(debounce), żeby nie odpytywać serwera przy każdym naciśniętym klawiszu.
Reużywa dokładnie tej samej funkcji wyszukującej co strona wyników, więc
podpowiedzi i pełne wyniki nigdy się nie rozjeżdżają.
Osobna strona, /tagi, pokazuje chmurę wszystkich tagów
z filtrem na żywo - można zawęzić widoczne tagi wpisując fragment nazwy,
bez przeładowania strony.
Tagi kontra znaczki - dwa niezależne mechanizmy
To częste źródło nieporozumienia, więc osobny nagłówek. Wpis może mieć jednocześnie tagi i znaczki, i są to dwie zupełnie oddzielne rzeczy w bazie danych, zarządzane w dwóch różnych miejscach panelu.
| Tag | Znaczek (Label) |
|---|---|
| Opisuje temat wpisu (np. "flask", "wycena") | Sygnalizuje coś o samym wpisie (np. "w budowie", "do aktualizacji") |
| Dowolna liczba na wpis, bez limitu | Dowolna liczba na wpis, bez limitu |
| Widoczny na liście wpisów i filtruje po kliknięciu | Widoczny jako kolorowa plakietka przy tytule |
Zarządzanie: /admin/tags |
Zarządzanie: /admin/labels |
| Ma własny kolor przypisany ręcznie: nie | Ma, do wyboru z ustalonej palety (niebieski, fioletowy, zielony, bursztynowy, czerwony, szary) |
W obu panelach dostępne są te same trzy operacje: zmiana nazwy, scalanie duplikatów (np. gdy przypadkiem powstały "flask" i "Flask" jako dwa osobne wpisy) i usunięcie. Scalanie przepina wszystkie wpisy z jednego tagu/znaczka na drugi, a potem usuwa ten zbędny - nic nie znika po drodze.
W formularzu edycji wpisu pole tagów ma autouzupełnianie: podczas wpisywania pojawia się lista pasujących, istniejących już tagów, żeby nie tworzyć przypadkiem drugiego "case-study" obok "case study".
Osobno od obu tych mechanizmów istnieje jeszcze pole
is_concept na wpisie realizacji - to nie jest tag ani
znaczek, tylko stała flaga uczciwości: czy opisywany projekt jest
prawdziwym wdrożeniem, czy fikcyjnym przykładem koncepcyjnym (jak
"Kwiaciarnia" czy "Szkoła językowa" w tym blogu). Widoczna wprost
w interfejsie, nie ukryta.
Harmonogram publikacji
Wpis można zaplanować na przyszłość zamiast publikować od razu. W formularzu edycji wystarczy ustawić datę i godzinę w polu harmonogramu - wpis dostaje wtedy status "zaplanowany" i pozostaje niewidoczny publicznie, dopóki ta chwila nie nadejdzie.
Ciekawszy jest mechanizm, który za tym stoi: nie ma tu żadnego osobnego procesu w tle (Celery, cron), który by o określonej godzinie "obudził się" i przełączył status. Zamiast tego wpis dojrzewa leniwie - przy każdym publicznym odczycie listy wpisów aplikacja sprawdza, czy któryś zaplanowany wpis ma już minięty termin, i jeśli tak, przełącza go na opublikowany w tym samym momencie. Dla odwiedzającego różnicy nie widać: wpis po prostu pojawia się na stronie po zaplanowanej dacie, bez żadnej dodatkowej akcji z mojej strony. Dla hostingu, który i tak nie udźwignąłby dodatkowego procesu w tle, to rozwiązanie tańsze niż prawdziwy harmonogram.
Najprostszy harmonogram to taki, który nie wymaga osobnego procesu do pilnowania zegarka - wystarczy, że każde zapytanie o listę wpisów samo sprawdza, czy coś "dojrzało".
Wersjonowanie treści
Przed każdą zmianą treści wpisu (tytułu, zajawki albo samej zawartości) powstaje migawka poprzedniej wersji. Migawki nie tworzą się przy drobnych zmianach klasyfikacji (np. dodanie tagu) bez zmiany treści - inaczej tabela wersji rosłaby przy każdym kliknięciu "Zapisz" niezależnie od tego, co realnie się zmieniło.
Historię wersji danego wpisu widać pod
/admin/post/<id>/historia, razem z logiem działań
(kto i kiedy co zrobił z wpisem - opublikował, zaplanował, usunął,
zduplikował). Każdą starszą wersję można przywrócić jednym kliknięciem.
Przywrócenie samo w sobie staje się nową wersją w historii - jest
więc odwracalne, można cofnąć się o kilka wersji, a potem "cofnąć
cofnięcie", bez utraty żadnego stanu po drodze. Adres wpisu (slug) nie
jest przeliczany przy przywracaniu, więc linki do niego prowadzące
z zewnątrz nie psują się.
Kosz zamiast trwałego usuwania
Usunięcie wpisu z listy w panelu nie kasuje go od razu z bazy danych
- wpis trafia do kosza (/admin/kosz), z którego można go
przywrócić (zawsze jako szkic, żeby nic nie wróciło od razu na żywo bez
świadomej decyzji) albo usunąć trwale. Te dwie akcje są rozdzielone
celowo: trwałe usunięcie jest osobnym, dodatkowym krokiem tylko z widoku
kosza, żeby nie dało się tego zrobić przez pomyłkę jednym kliknięciem
z listy głównej.
Panel admina - reszta
- Dashboard ze statystykami: liczba wpisów w podziale na status, najstarszy nietknięty szkic, szybkie filtrowanie po typie i branży wpisu wprost z listy.
- Szybki toggle statusu - przełączenie szkic/opublikowany bezpośrednio z listy wpisów, bez wchodzenia w pełną edycję.
- Duplikowanie wpisu - kopia zawsze powstaje jako szkic, nigdy nie publikuje się sama z siebie.
- Podgląd mobilny - w widoku podglądu wpisu przełącznik zwężający podgląd do szerokości telefonu, bez fizycznego zmieniania rozmiaru okna przeglądarki.
- Diff "co zmieniłem?" - przy edycji istniejącego wpisu przycisk pokazujący różnicę słowo po słowie między treścią sprzed otwarcia formularza a bieżącym stanem edytora, zanim jeszcze cokolwiek zostanie zapisane. To osobny, lżejszy mechanizm niż pełne wersjonowanie opisane wyżej - działa wyłącznie po stronie przeglądarki i niczego nie zapisuje, jest tylko podglądem przed decyzją o zapisie.
Popover z definicjami pojęć
Niektóre wpisy (na przykład ten o superpozycji) zawierają
specjalistyczne terminy, które nie każdy czytelnik zna. Zamiast
tłumaczyć je w nawiasach w środku zdania, pierwsze wystąpienie takiego
terminu w treści jest owinięte w podpowiedź: najechanie kursorem albo
zaznaczenie klawiaturą (Tab) pokazuje krótką definicję w tooltipie.
Zrobione czystym CSS, bez ani jednej linijki JavaScriptu - działa na
pseudoklasach :hover i :focus. Definicje
są osobnym, deklaratywnym słownikiem w kodzie, nie są częścią treści
wpisu, więc poprawienie definicji nie wymaga edycji samego wpisu przez
panel.
Bezpieczeństwo treści z edytora
Edytor w panelu (Quill) zwraca gotowy HTML, który trzeba bezpiecznie
zapisać i wyświetlić. Cała treść przechodzi przez sanityzację
(biblioteka bleach) z jawną whitelistą dozwolonych tagów -
żadnego <script>, żadnych atrybutów on*,
żadnego javascript: w linkach. Do tego dochodzi
restrykcyjna polityka bezpieczeństwa treści (CSP) w nagłówkach
odpowiedzi. Reguły sanityzacji mają swoją wersję
(sanitizer_version na każdym wpisie) - po zmianie whitelisty
komenda flask resanitize przelicza starą treść nowymi
regułami, bez ręcznego przechodzenia po każdym wpisie z osobna. Więcej
o tym w osobnym wpisie, "Jak zbudowany jest ten serwis".
Czego nie da się przetestować w wersji demo
Wersja demo pod blog-demo.ronim.com.pl daje prawdziwy
dostęp do panelu - logujesz się jako konto pokazowe i naprawdę
edytujesz treść w prawdziwej bazie danych, nie w symulacji. Baza
resetuje się co godzinę do stanu początkowego, więc żadne zmiany nie
zostają na stałe. Kilka akcji jest mimo to celowo zablokowanych
(odpowiedź serwera: 403), niezależnie od tego resetu:
- Upload obrazków - obrazki trafiają na Cloudinary, zewnętrzny serwis, którego reset bazy demo w ogóle nie dotyka. Bez tej blokady konto pokazowe mogłoby w nieskończoność zapychać cudzy, współdzielony limit miejsca.
- Embed YouTube - z tego samego powodu co obrazki: skutek (wywołanie zewnętrznego serwisu) nie znika wraz z resetem bazy.
- Zarządzanie tagami i znaczkami (zmiana nazwy, scalanie, usuwanie) - to są operacje na strukturze wspólnej dla wszystkich wpisów, nie na jednym wpisie. Usunięcie albo scalenie tagu zepsułoby doświadczenie następnej osobie, która zajrzy na demo przed najbliższym resetem, nawet jeśli sam reset i tak by to później naprawił.
- Trwałe usunięcie wpisu (purge z kosza) - zwykłe, odwracalne usunięcie (do kosza) jest dozwolone, bo reset bazy je i tak naprawia. Purge jest zablokowany, bo w połączeniu z niskim priorytetem sensowności tego akurat kliku w środowisku współdzielonym, nie wnosi nic ponad zwykłe usunięcie, a ryzykuje nieporozumienie.
Dodatkowo każda próba zapisania treści zawierającej wulgaryzmy (prosta lista słów, dopasowanie bez rozróżniania wielkości liter) jest odrzucana z komunikatem walidacji - nie po fakcie, przy najbliższym resecie, tylko od razu. To nie jest ogólna moderacja treści bloga (prawdziwe konto administratora nie ma tego ograniczenia), tylko doraźne zabezpieczenie publicznego demo przed oczywistym spamem.
Wszystko inne - tworzenie i edycja wpisów, zwykłe usuwanie do kosza, zmiana statusu, harmonogram publikacji, duplikowanie, przeglądanie historii wersji i logu działań - działa dokładnie tak samo jak na prawdziwym koncie administratora.