yarlykon

Архитектура Ярлыкона

Документ фиксирует принятые решения и их причины. Все спорные развилки обсуждены в проектной сессии, выбранные ветки помечены .

1. Модель данных

1.1 Источник правды

Источник правды — git-репозиторий. Все ссылки видны как файлы, история редактирования — это история коммитов, бэкап — git clone.

/index.json            ← журнал, JSON-массив
/l/{slug}.html         ← страница редиректа, один файл на ссылку
/admin/index.html      ← статичная админка (генерируется при сборке)

1.2 Запись в 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=....

2. Slug

Конфигурируемо:

Коллизии проверяются всегда, против актуального index.json. При создании пачкой (1–5) проверка делается атомарно перед коммитом — внутри одной транзакции не может появиться два одинаковых slug.

3. Превью (OG) — гибрид

Решение: eager на создании + ручной refresh.

Причина: Telegram-краулер приходит за превью через секунды после публикации в канале и жёстко кэширует результат. Если в HTML нет OG-тегов в первый показ — превью пустое надолго.

Поток:

  1. Mini App шлёт батч URL в Worker.
  2. Worker для каждого URL делает GET и парсит <meta property="og:*"> (плюс fallback на <title> и <meta name="description">).
  3. Если у источника нет OG или фетч упал — зашиваем минимум (title из URL, description пустой), помечаем флагом og.partial=true. Юзер увидит в админке предупреждение и сможет дозаполнить.
  4. OG-снимок сохраняется в index.json и зашивается в HTML /l/{slug}.html.

Ручное обновление: /refresh {slug} боту или кнопка в админке — повторяет шаг 2 и пересобирает HTML.

4. Страница редиректа /l/{slug}.html

Шаблон один (template/link.html.tmpl). Логика:

UA-определение можно делать как клиентским JS (по navigator.userAgent), так и серверно — но GH Pages не умеет, поэтому клиентский путь в шаблоне. Краулеры обычно не выполняют JS, и для них «нет редиректа» = они остаются на странице с OG. Это и нужно.

5. Cloudflare Worker

5.1 Эндпоинты

Метод Путь Назначение
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 (задел)

5.2 Авторизация

Mini App шлёт initData (TG) / launch_params (VK) / аналог (MAX). Worker проверяет подпись секретом бота — это даёт userId и гарантию, что запрос пришёл из мессенджера, а не подделан.

Дополнительно: список разрешённых userId в конфиге (ALLOWED_USERS=123,456). Незваные гости получают 403, даже с валидной подписью.

5.3 Коммит в GitHub

Не 5 отдельных коммитов, а один батч через Git Trees API (или GraphQL createCommitOnBranch). Алгоритм:

  1. Получить SHA текущей main.
  2. Прочитать index.json.
  3. Сгенерировать slug’и, проверить на коллизии (в файле и в самом батче).
  4. Для каждой ссылки отрендерить link.html.tmpl/l/{slug}.html.
  5. Обновить index.json.
  6. Создать tree с N+1 blob’ами (N страниц + index).
  7. Создать commit с этим tree.
  8. Обновить main на новый commit.

Один HTTP-туда-сюда на каждый шаг; всё это в пределах одного запроса Worker’а (< 1 сек обычно).

После коммита Pages пересобирается ~30–60 сек. Mini App показывает статус pending и опрашивает /api/build-status либо просто ждёт 60 сек и показывает «готово».

6. Mini App

6.1 Универсальность

Платформа определяется по window и параметрам запуска:

window.Telegram?.WebApp     → TG
window.vkBridge / VK params → VK
window.maxApi (или аналог)  → MAX

Каждая платформа имеет адаптер, который:

Контракт адаптера:

interface PlatformAdapter {
  name: 'telegram' | 'vk' | 'max';
  getAuth(): string;            // raw init/launch data для бэкенда
  getUser(): { id: string };
  applyTheme(): void;
  hapticImpact(): void;
  close(): void;
}

6.2 UX

Одна форма с 1–5 полями для URL. Под каждым полем — превью OG (тянется через GET /api/og?url=... по blur). Кнопка «Сократить» отправляет батч.

После отправки:

7. Bootstrap (развёртывание)

См. 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 не коммитится).

8. Инварианты (см. konspekt.md)

Главные:

  1. Каждая ссылка — это файл в git. Не строка в БД, не запись в KV.
  2. OG зашит, не подтянут. Стабильность превью важнее свежести.
  3. Slug проверяется на коллизии всегда, даже в режиме counter (на случай кривого состояния).
  4. Один батч — один коммит. Минимум пересборок Pages.
  5. Авторизация по подписи Mini App + allowlist. Никаких анонимных создателей.
  6. Платформа Mini App абстрагирована. Ядро ничего не знает про TG/VK/MAX.

9. Что выходит за рамки v1