mandrock-files / ai

lock-public-pipeline

Власний сайт-довідник на 200 рядків Python: markdown як джерело правди, один прапорець ділить статті на закриті й публічні, nginx basic auth робить решту. Без бази, без CMS, без білду.

Довідник із замком: markdown → nginx, без CMS

Задача проста на словах: тримати власні статті, частину віддавати всім, частину лишати за паролем, і щоб нова стаття коштувала рівно один файл. Канонічна відповідь на це — статичний генератор плюс хостинг. Коштує вона більше, ніж на око: конфіг, теми, плагіни, білд, деплой — і кожне з п'яти треба спершу вивчити, а вже потім воно почне економити.

Замість цього під цим самим сайтом живе приблизно 200 рядків Python і десяток рядків nginx. Розписую не бо воно унікальне, а бо повторюване: схема тримається на будь-якому наборі текстів, де є межа «своє / публічне».


Принцип

Джерело правди — markdown на диску. Згенерований HTML не несе нічого свого: його можна знести цілком і зібрати наново коли завгодно. Це те рішення, з якого випливає решта, — і єдине, яке тут справді треба ухвалити свідомо.

Кожен файл тягне фронтметр:

---
title: slug-статті
desc: Одне речення — іде в meta description і в картку на індексі.
locked: true       # true → /ai/<секція>/, false → /<секція>/
order: 3           # позиція в списку
section: claude    # у який розділ
---

Один прапорець locked вирішує все. Генератор розкидає статті по двох коренях, і кожен корінь дістає власний індекс, який знає тільки про свої статті. Закритий індекс у публічному дереві не існує взагалі — це не приховані посилання, це фізично інша тека. Різниця тут критична: приховане посилання можна вгадати, теки, якої нема, — ні.


Генератор

Три функції тягнуть усю роботу:

def parse(path):
    """markdown + фронтметр → dict з мета і тілом"""

def build_root(arts, out, base, ...):
    """список статей → тека з index.html і підтеками статей"""

def main():
    """розкласти по секціях × (locked / public), зібрати кожен корінь"""

Два місця, де підставитись найлегше:

build_root робить rmtree перед збіркою. Навмисно: інакше видалена стаття лишається жити в дереві вічно. Ціна — усе, що покладено в цільову теку руками, зникає на наступному рендері. Звідси правило: кастомний файл у вихідній теці — це баг, а не фіча; будь-яка потрібна поведінка мусить жити в генераторі.

Права на файли. Після збірки все дерево перепризначається на користувача веб-сервера. Забув chown — і дістаєш 403 на випадкових сторінках, рівно там, куди щойно приїхали нові файли. Перший раз це читається як містика, поки не згадуєш про власника.


Секції без коду

Розділи описані словником, а не гілками в коді:

SECTIONS = {
    "claude": {"title": "Claude", "lead": "…"},
    "tools":  {"title": "Tools",  "lead": "…"},
}

Далі генератор просто ітерується по ньому. Новий розділ — чотири рядки конфігу і один location у nginx, без жодного рефакторингу. Той рідкісний випадок, коли «винести в конфіг» окупається того ж дня: розділи я додаю частіше, ніж чіпаю сам рендер.


Замок

Уся приватність тримається на двох речах: basic auth і те, що закриті статті лежать в іншому префіксі:

location /ai {
    root /root/cdn;
    auth_basic "restricted";
    auth_basic_user_file /etc/nginx/.htpasswd-ai;
    try_files $uri $uri/ =404;
}

location /claude/ {
    root /root/cdn;
    try_files $uri $uri/ =404;
}

Що дає і чого не дає:

Дає Не дає
відсікає випадкового відвідувача й пошуковики захисту від того, кому дали пароль
нульову вартість підтримки пер-юзерних прав
нуль коду в застосунку аудиту, хто що читав, поза логами nginx

Для особистого довідника цього рівно досить. Для чогось, де витік має реальну ціну, — ні; і вдавати, що досить, було б найгіршим із варіантів. Це замок від чесної людини, а не від зловмисника, — і я не продаю його як більше.


Таблиця рішень

Ситуація Дія
Нова стаття файл у articles/, прогнати генератор
Перекинути статтю з закритих у публічні змінити locked:, прогнати генератор
Новий розділ запис у SECTIONS + location у nginx
Треба змінити вигляд усіх сторінок правити тему, не HTML
Стаття зникла з сайту перевірити section: — невідома секція валить генератор
403 на новій сторінці пропущений chown на дереві
Треба редирект зі старої адреси location = з return 301, вище за загальний блок

Антипатерни


Чекліст

P.S. — про 200 рядків

Найдорожче в статичних генераторах — не сам генератор, а те, що його треба тримати в голові. Конфіг, конвенції тек, синтаксис шаблонів, набір плагінів і три способи зробити одну й ту саму річ (обов'язково три, ніколи один).

Двісті рядків власного скрипта не вміють майже нічого — і саме тому їх читаєш цілком за десять хвилин, а потім міняєш будь-що за п'ять. Через півроку виявляється, що це була єдина метрика, яка щось важила.

Правило звідси звучить неприємно для інженерського его: якщо задача влазить у скрипт, який читається за один присід, — фреймворк тут не економія, а борг у розстрочку. Я його теж не люблю. Він теж правий.