Документ фиксирует принятые решения и их причины. Все спорные развилки обсуждены в проектной сессии, выбранные ветки помечены ✓.
Источник правды — git-репозиторий. Все ссылки видны как файлы, история редактирования — это история коммитов, бэкап — git clone.
/index.json ← журнал, JSON-массив
/l/{slug}.html ← страница редиректа, один файл на ссылку
/admin/index.html ← статичная админка (генерируется при сборке)
index.json{
"version": 1,
"links": [
{
"slug": "abc1",
"target": "https://example.com/very/long/path",
"created_at": "2026-06-21T10:30:00Z",
"created_by": "tg:123456",
"og": {
"title": "...",
"description": "...",
"image": "https://...",
"site_name": "...",
"fetched_at": "2026-06-21T10:30:00Z"
},
"stats": {
"metrika_counter": "1234567"
}
}
]
}
OG — снимок на момент создания. Не обновляется автоматически — только командой /refresh {slug} через бот или POST /api/refresh-og?slug=....
Конфигурируемо:
SLUG_MODE=random (по умолчанию) — base62 длины SLUG_LEN_INIT (4), при коллизии увеличиваем до SLUG_LEN_MAX (8).SLUG_MODE=counter — последовательно a, b, ..., z, aa, ab, .... Короче, но предсказуемо.Коллизии проверяются всегда, против актуального index.json. При создании пачкой (1–5) проверка делается атомарно перед коммитом — внутри одной транзакции не может появиться два одинаковых slug.
Решение: eager на создании + ручной refresh.
Причина: Telegram-краулер приходит за превью через секунды после публикации в канале и жёстко кэширует результат. Если в HTML нет OG-тегов в первый показ — превью пустое надолго.
Поток:
GET и парсит <meta property="og:*"> (плюс fallback на <title> и <meta name="description">).title из URL, description пустой), помечаем флагом og.partial=true. Юзер увидит в админке предупреждение и сможет дозаполнить.index.json и зашивается в HTML /l/{slug}.html.Ручное обновление: /refresh {slug} боту или кнопка в админке — повторяет шаг 2 и пересобирает HTML.
/l/{slug}.htmlШаблон один (template/link.html.tmpl). Логика:
<head> — OG-теги назначения, Яндекс.Метрика (если METRIKA_ID задан).ym('hit') отстреливается, затем location.replace(target) через малый setTimeout (~150 мс) для гарантии доставки.<meta http-equiv="refresh" content="2;url=..."> на случай JS-выключенного браузера.UA-определение можно делать как клиентским JS (по navigator.userAgent), так и серверно — но GH Pages не умеет, поэтому клиентский путь в шаблоне. Краулеры обычно не выполняют JS, и для них «нет редиректа» = они остаются на странице с OG. Это и нужно.
| Метод | Путь | Назначение |
|---|---|---|
POST |
/api/shorten |
Создать 1–5 ссылок. Принимает initData/launchParams/MAX-аналог от Mini App. |
POST |
/api/refresh-og |
Перетянуть OG для существующего slug. |
GET |
/api/og |
Предпросмотр OG по URL (для Mini App, чтобы пользователь видел, что зашьётся). |
POST |
/webhook/telegram |
Команды бота: /shorten <url>, /refresh <slug>, /list. |
POST |
/webhook/vk |
(задел) |
POST |
/webhook/max |
(задел) |
Mini App шлёт initData (TG) / launch_params (VK) / аналог (MAX). Worker проверяет подпись секретом бота — это даёт userId и гарантию, что запрос пришёл из мессенджера, а не подделан.
Дополнительно: список разрешённых userId в конфиге (ALLOWED_USERS=123,456). Незваные гости получают 403, даже с валидной подписью.
Не 5 отдельных коммитов, а один батч через Git Trees API (или GraphQL createCommitOnBranch). Алгоритм:
main.index.json.link.html.tmpl → /l/{slug}.html.index.json.main на новый commit.Один HTTP-туда-сюда на каждый шаг; всё это в пределах одного запроса Worker’а (< 1 сек обычно).
После коммита Pages пересобирается ~30–60 сек. Mini App показывает статус pending и опрашивает /api/build-status либо просто ждёт 60 сек и показывает «готово».
Платформа определяется по window и параметрам запуска:
window.Telegram?.WebApp → TG
window.vkBridge / VK params → VK
window.maxApi (или аналог) → MAX
Каждая платформа имеет адаптер, который:
initData/launchParams для бэкенда.Контракт адаптера:
interface PlatformAdapter {
name: 'telegram' | 'vk' | 'max';
getAuth(): string; // raw init/launch data для бэкенда
getUser(): { id: string };
applyTheme(): void;
hapticImpact(): void;
close(): void;
}
Одна форма с 1–5 полями для URL. Под каждым полем — превью OG (тянется через GET /api/og?url=... по blur). Кнопка «Сократить» отправляет батч.
После отправки:
navigator.clipboard.writeText).См. docs/BOOTSTRAP.md. Два режима — локальный CLI и GitHub Action workflow_dispatch. Общий движок — scripts/bootstrap.mjs.
Восемь идемпотентных стадий:
[1/8] verify tokens
[2/8] write secrets to GitHub Secrets
[3/8] render wrangler.toml from template
[4/8] deploy worker
[5/8] create + bind KV namespace
[6/8] configure telegram bot (setWebhook, setMenuButton)
[7/8] enable GitHub Pages
[8/8] seed repo (initial commit with empty index.json)
Любую стадию можно перезапустить отдельно. Состояние пишется в .bootstrap-state.json (в git не коммитится).
konspekt.md)Главные: