Przejdź do treści

Publikacja: dokumentacja i referencja API pod jednym adresem

Dokumentacja stoi pod dokumentacja.olski.pl, a wydaje ją GitHub Pages z tego repozytorium. Na stronę idzie proza, którą repozytorium już ma — README, ten katalog, todo/ i CLAUDE.md — oraz referencja API wypisana z docstringów pakietu. Demo parsera stoi osobno, pod olski.pl, i opisuje je witryna.md.

Buduje to python3 -m dokumentacja, jednym poleceniem i lokalnie. Workflow woła to samo polecenie, więc przepis jest jeden, a nie dwa. Konfiguracja nosi adres jeden raz, w mkdocs.yml: plik CNAME, którego żąda GitHub Pages, wychodzi z tamtego wiersza przebiegiem. Poza konfiguracją adres jest linkiem dla czytelnika, w README i w stopce demo (witryna/strona.html). Kopie adresu utrzymuje równe tests/test_docs.py, więc adres zmieniony w konfiguracji wywraca suitę, dopóki nie wejdzie do nich.

Samą prozę GitHub renderuje już dziś, w tej konwencji kotwic, w której pisane są linki. Strona dokłada do tego adres dla czytelnika, który o projekcie nie słyszał (roles.md), jedną wyszukiwarkę po prozie i po referencji naraz oraz samą referencję API, której GitHub nie pokaże w ogóle, bo docstring jest w pliku .py.

Dokument mówi, czemu strona nie stoi na domyślnym Jekyllu, co sprawdza jej budowanie i ile kosztuje referencja API. Wylicza na koniec to, na czym decyzja nie zapadła.

Domyślny Jekyll kasuje diakrytyki z kotwic

GitHub Pages renderuje Markdown kramdownem, a ten zostawia w kotwicy same znaki ASCII: z nagłówka Skreślenie bywa całą naprawą wychodzi mu skrelenie-bywa-ca-napraw. Sprawdza się to bez wdrożenia, na kramdownie 2.5.2: gem install kramdown, a potem Kramdown::Document.new("## …").to_html.

Większość kotwic w tym repozytorium niesie diakrytyk, więc na domyślnym Jekyllu większość linków między dokumentami prowadzi w pustkę. Awaria jest przy tym cicha z dwóch stron naraz. Kramdown kotwicy nie zgłasza, bo wystawia własną. Suita jej nie zgłasza, bo anchor_of w tests/test_docs.py pilnuje konwencji GitHuba, a nie tego, co wystawi strona.

Korzeniem strony jest korzeń repozytorium

Z docs/ i z todo/ wychodzi kilkaset linków ponad swój katalog: do README, do CLAUDE.md i do rejestru otwartej roboty. Katalog docs/ wydany sam urywa je wszystkie, więc na stronę idzie proza w tym układzie katalogów, w którym stoi w repozytorium.

Układ ten składa dokumentacja.py w katalogu _dokumentacja/, a stronę wypisuje z niego mkdocs do _strona/. Oba katalogi są wynikiem przebiegu i stoją w .gitignore. Proza linkuje też do modułu, do pyproject.toml i do workflowu, więc skrypt dokłada każdy plik, na który wskazuje link. Listy tych plików nie ma nigdzie wypisanej: właścicielem jest sama proza, a lista obok niej rozjeżdżałaby się z nią po cichu.

Kotwica na stronie zgadza się z kotwicą na GitHubie

Kotwice liczy slugify z pymdown-extensions, wołany z mkdocs.yml. Na nagłówkach tego repozytorium daje on to samo, co github-slugger, czyli ta sama kotwica działa w obu miejscach i linku nie trzeba pisać dwa razy. Tak samo liczy ją suita (anchor_of w tests/test_docs.py).

Sprawdza to mkdocs build --strict: mkdocs porównuje każdą kotwicę z tym, co naprawdę wystawił, a --strict czyni ostrzeżenie błędem. Konwencji pilnują przez to dwa checki nad dwoma silnikami: suita nad GitHubem, a budowanie nad stroną.

Referencja API powstaje z docstringów

Wypisuje ją mkdocstrings z pakietu olski, i nie jest to druga kopia faktu: kod jest właścicielem tego, co zaimplementowane (CLAUDE.md), a referencja powstaje z niego przy każdym budowaniu. Docstringi są tu prozą pod łamaniem wierszy, a pojedynczy nowy wiersz zwija się w spację, więc renderują się jak akapit.

Referencja jest częścią tej strony, a nie drzewem obok niej. Każdy moduł dostaje stronę Markdown — nagłówek i wiersz ::: olski.check — więc mkdocs zna te strony tak samo jak dokumenty: wciąga je do nawigacji, indeksuje jedną wyszukiwarką i renderuje tym samym motywem, a symbol cytowany w docstringu prowadzi linkiem tam, gdzie go zadeklarowano. Generator wypisujący własne drzewo HTML tego nie umie i dlatego tu go nie ma: pdoc wystawiał referencję z własnym motywem i własnym oknem szukania, a mkdocs nie wiedział o niej nic poza tym, że przenosi jej pliki. Odwraca tę decyzję wydruk, którego mkdocstrings nie umie: referencja wraca wtedy obok strony, a razem z nią drugie okno szukania.

Docstringi cytują symbol rolą reStructuredText, a mkdocstrings zna tylko własną składnię: przekłada ją dokumentacja.py, a nie autor docstringu, i tam też stoi powód, dla którego konwencja w kodzie zostaje ta sama. Że przekład się wykonał, żąda samo budowanie, bo rola przepuszczona na stronę nie wywraca przebiegu sama z siebie.

Ceną jest cytat dokumentu i widać ją na stronie: docstring cytuje go gołym napisem, a nie linkiem, i robi to kilkadziesiąt modułów pakietu, więc w referencji te cytaty są martwym tekstem. Wpis o tym trzyma todo/dokumenty.md.

Nawigacji nie piszemy ręką

Nawigację układa mkdocs z drzewa katalogów. Wypisana w mkdocs.yml byłaby drugą kopią spisu z README, a spis ten mówi o każdym dokumencie zdanie i wskazuje właściciela faktu. Dwie listy tych samych dokumentów rozjeżdżają się przy pierwszym dopisanym pliku, a suita żąda wiersza tylko w tej pierwszej.

Referencja wchodzi do tej nawigacji tą samą drogą, bo strony modułów wypisuje dokumentacja.py z drzewa modułów pakietu. Moduł dopisany jutro dostaje przez to stronę bez wiersza dopisanego gdziekolwiek, a spis modułów na wejściu do referencji liczy z pakietu mkdocstrings.

Podpisy strony, nazwa motywu i przełącznik motywu należą do mkdocs.yml, tak jak podpisy demo należą do jego strony (witryna.md).

Pull request buduje stronę, a wydaje ją tylko main

Robi to .github/workflows/dokumentacja.yml. Buduje na każdym pull requeście, bo --strict jest sprawdzeniem: martwy link ma wywrócić przebieg przed scaleniem, a nie po nim. Push na gałąź bez pull requesta budowania nie uruchamia, bo minuty runnera liczą się za każdy przebieg (CLAUDE.md). Wydanie jest osobnym zadaniem i czeka na main, bo adres jest jeden.

Do bloku checków budowanie nie wchodzi. mkdocs i mkdocstrings instaluje dodatek dokumentacja z pyproject.toml, a suita go nie instaluje — tak samo jak nie instaluje gunicorna dla demo. Ceną jest to, że o martwym linku mówi dopiero pull request, a nie lokalny przebieg bloku checków.

Nie zapadło

Czy DNS-y wskazują, czy GitHub Pages przyjmuje adres własny z pliku CNAME i czy artefakt tej wielkości przechodzi, mówi dopiero pierwszy przebieg na main.

Sources