Landing Service API

Сервис принимает текст в простом блочном формате и возвращает ссылку на опубликованную страницу.

Машиночитаемые версии: /llms.txt · /docs.md · /openapi.json

Готовый скилл для Claude Code: /skill — положить в .claude/skills/landing-pages/SKILL.md


1. Быстрый старт

curl -X POST https://lp.somnarium.ru/api/v1/sites \
  -H "Authorization: Bearer $LP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Отчёт по тренировке",
    "content": ":::hero\n# Пётр Иванов\nВольная борьба · 6 августа\n:::\n\n:::text\nОтличное занятие: отработали проходы в ноги.\n:::"
  }'

Ответ:

{
  "id": "k3f9dl2m8x1q",
  "slug": "otchet-po-trenirovke",
  "url": "https://lp.somnarium.ru/s/demo/otchet-po-trenirovke",
  "status": "published",
  "warnings": []
}

Поле url — готовая ссылка, её можно сразу отдавать человеку.


2. Формат контента

Контент — это plain text. Блоки размечаются «заборчиками» :::, внутри блока — обычный Markdown.

:::тип атрибут=значение другой="значение с пробелами"
markdown-содержимое
:::

Правила, на которые можно положиться:

Правило Поведение
Незнакомый тип блока Отрисуется как обычный текст, запрос не упадёт
Забыл закрыть ::: Блок закроется сам на следующем открывающем или в конце
Текст вне блоков Станет блоком text
Ошибка внутри блока Блок деградирует до текста, остальная страница не пострадает

Всё, что не отрисовалось как ожидалось, попадает в массив warnings ответа — но запрос всё равно успешен.

Атрибуты

Пишутся в открывающей строке: key=value, key="значение с пробелами" или просто key (равно true).

Картинки в атрибутах

Где ожидается изображение, принимается три формы:


3. Справочник блоков

hero — шапка страницы

Атрибуты: image, alt, eyebrow, align (left | center | right, по умолчанию center).

:::hero image=k7f2m9x1 eyebrow="Отчёт за 6 августа"
# Пётр Иванов
Вольная борьба · группа 2
:::

text — обычный текст

Атрибуты: align. Внутри — любой markdown: заголовки, списки, ссылки, жирный, курсив.

:::text
## Что получилось
- Уверенный **тайминг** на проходах
- Хорошая работа в партере
:::

stats — числовые показатели

Строки вида - значение | подпись | пояснение. Третья колонка необязательна.

:::stats
- 90 мин | на ковре
- 12 | схваток | из них 8 выиграно
- 85% | посещаемость
:::

cards — карточки сеткой

Секции ## Заголовок + текст под ними. Альтернативно строки - Заголовок | Описание.

:::cards
## Техника
Проходы в ноги стали заметно чище.

## Дисциплина
Не пропустил ни одного занятия за месяц.
:::

gallery — сетка фотографий

По одной картинке на строку: id ассета, URL, ![alt](url) или url | подпись.

:::gallery
k7f2m9x1
k7f2m9x2
https://example.com/photo.jpg | На разминке
:::

image — одна картинка

Атрибуты: src, alt, caption. Если атрибутов нет, ссылка берётся из тела блока.

:::image caption="Финальная схватка"
k7f2m9x1
:::

quote — цитата или отзыв

Атрибуты: author, role, avatar.

:::quote author="тренер Алексей" role="Вольная борьба" avatar=k7f2m9x3
Пётр заметно прибавил в дисциплине за последний месяц.
:::

list — список с галочками

Строки - Пункт | Пояснение. Вторая колонка необязательна.

:::list
- Разминка | 15 минут, полный комплекс
- Отработка проходов
- Схватки
:::

steps — пронумерованные шаги

Секции ## Заголовок + текст, либо строки - Заголовок | Описание. Нумерация проставляется сама.

:::steps
## Разминка
Суставная гимнастика, бег.

## Техника
Проходы в одну и две ноги.

## Схватки
Шесть схваток по две минуты.
:::

progress — шкалы и оценки

Строки - Название | значение | максимум. Максимум необязателен: если его нет и значение ≤ 5, шкала считается пятибалльной, иначе процентной.

:::progress
- Проходы в ноги | 4 | 5
- Защита | 3 | 5
- Посещаемость | 85 | 100
:::

table — таблица

Обычная markdown-таблица. На узком экране получает горизонтальную прокрутку внутри себя.

:::table
| Упражнение | Подходы | Результат |
|---|---|---|
| Приседания | 3×15 | выполнено |
| Мост | 3×30 сек | выполнено |
:::

faq — раскрывающиеся вопросы

Секции ## Вопрос + ответ.

:::faq
## Что взять на следующее занятие?
Форму, борцовки и бутылку воды.

## Когда следующий турнир?
14 сентября, подробности позже.
:::

cta — кнопка действия

Атрибуты: href, label, style (outline для менее заметной кнопки).

:::cta href="https://t.me/coach" label="Написать тренеру"
Остались вопросы?
:::

contacts — строки контактов

Строки - Подпись | Значение | ссылка. Ссылка необязательна.

:::contacts
- Тренер | Алексей Смирнов
- Telegram | @coach | https://t.me/coach
- Зал | ул. Спортивная, 4
:::

badges — теги

Через запятую или по одному на строку.

:::badges
вольная борьба, группа 2, 12 лет
:::

divider — разделитель

:::divider
Итоги месяца
:::

footer — подвал

:::footer
Отчёт сформирован автоматически · club-nika.ru
:::

Синонимы

Эти имена работают как псевдонимы, чтобы формат прощал промахи:

section, paragraph, markdown, md, headingtext ·
kpi, stat, metricsstats ·
card, features, feature, gridcards ·
images, photosgallery ·
img, photo, pictureimage ·
testimonial, blockquotequote ·
checklist, bulletslist ·
timeline, processsteps ·
rating, skills, barsprogress ·
accordion, questionsfaq ·
button, actioncta ·
contactcontacts ·
tags, chipsbadges ·
hr, separatordivider


4. Методы работы с сайтами

Все требуют Authorization: Bearer <api-key>. Каждый ключ видит только сайты своего клиента.

POST /api/v1/sites — создать

Поле Тип Обяз. Описание
title string да Заголовок страницы и <title>
content string да Блочный контент
slug string нет Часть URL. По умолчанию строится из title
description string нет Описание для превью в мессенджере
status string нет published (по умолчанию) или draft
ttl string нет Срок жизни, см. раздел 6. По умолчанию бессрочно

Ответ 201 с полями id, slug, url, warnings.

GET /api/v1/sites — список

Параметры: limit (1–100, по умолчанию 50), offset, status.

curl https://lp.somnarium.ru/api/v1/sites \
  -H "Authorization: Bearer $LP_API_KEY"

GET /api/v1/sites/{id|slug} — получить один

Возвращает объект вместе с полем content — исходным текстом блоков.

GET /api/v1/sites/{id|slug}/html — предпросмотр

Возвращает готовую HTML-страницу, не публикуя её. Работает и для черновиков, поэтому вёрстку можно проверить до того, как ссылка уйдёт человеку.

curl https://lp.somnarium.ru/api/v1/sites/otchet/html \
  -H "Authorization: Bearer $LP_API_KEY" > preview.html

PATCH /api/v1/sites/{id|slug} — изменить

Принимает те же поля, что и создание; передавать нужно только изменяемые. PUT работает так же.

curl -X PATCH https://lp.somnarium.ru/api/v1/sites/otchet-po-trenirovke \
  -H "Authorization: Bearer $LP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": ":::hero\n# Обновлённый отчёт\n:::"}'

DELETE /api/v1/sites/{id|slug} — удалить

curl -X DELETE https://lp.somnarium.ru/api/v1/sites/otchet-po-trenirovke \
  -H "Authorization: Bearer $LP_API_KEY"

Удаление безвозвратно; привязанные к странице картинки удаляются вместе с ней.


5. Картинки

Загрузка файла

curl -X POST https://lp.somnarium.ru/api/v1/assets \
  -H "Authorization: Bearer $LP_API_KEY" \
  -F "file=@photo.jpg" \
  -F "ttl=3w"

Загрузка по ссылке

curl -X POST https://lp.somnarium.ru/api/v1/assets \
  -H "Authorization: Bearer $LP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/photo.jpg", "ttl": "1y"}'

Ответ содержит id и url. id подставляется в блоки: image=<id>.

Что происходит с файлом при загрузке:

Другие методы: GET /api/v1/assets (список), DELETE /api/v1/assets/{id}.


6. Сроки хранения

ttl принимает: 30m, 12h, 30d, 3w, 6M, 1y, forever, page.

Регистр важен только у месяцев: M — месяцы, m — минуты.

Главное правило: картинка без явного ttl живёт столько же, сколько страница, к которой она привязана (-F "site=<slug>"). Логотип, который переиспользуется, заливается один раз с ttl=forever.

# фотка с тренировки — умрёт вместе с отчётом
curl ... -F "file=@training.jpg" -F "site=otchet-6-avgusta"

# логотип клуба — живёт всегда
curl ... -F "file=@logo.png" -F "ttl=forever"

Страница с истёкшим сроком отдаёт 410 и нейтральную заглушку, а не ошибку. Картинка с истёкшим сроком просто исчезает — вёрстка вокруг остаётся целой.


7. Приватность

Ссылка — это и есть доступ: у кого она есть, тот и видит страницу. Для чувствительного содержимого ставьте ttl.


8. Ошибки

Единый формат:

{ "error": "Сайт \"report-42\" не найден", "status": 404 }
Код Когда
400 Некорректное тело запроса, неверный ttl или slug
401 Ключ отсутствует, неизвестен или отозван
404 Объект не найден или принадлежит другому клиенту
409 Запрошенный slug уже занят
413 Файл больше 12 МБ

9. Полный пример

API=https://lp.somnarium.ru
KEY=$LP_API_KEY

# 1. Логотип клуба — заливаем один раз, храним всегда
LOGO=$(curl -s -X POST $API/api/v1/assets -H "Authorization: Bearer $KEY" \
  -F "file=@logo.png" -F "ttl=forever" | jq -r .id)

# 2. Собираем отчёт
curl -s -X POST $API/api/v1/sites \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d @- <<JSON | jq -r .url
{
  "title": "Отчёт · Пётр Иванов · 6 августа",
  "description": "Итоги занятия по вольной борьбе",
  "ttl": "90d",
  "content": ":::hero image=$LOGO eyebrow=\"Занятие 6 августа\"\n# Пётр Иванов\nВольная борьба · группа 2\n:::\n\n:::stats\n- 90 мин | на ковре\n- 12 | схваток\n- 85% | посещаемость\n:::\n\n:::progress\n- Проходы в ноги | 4 | 5\n- Защита | 3 | 5\n:::\n\n:::text\n## Что получилось\n- Уверенный **тайминг** на проходах\n- Хорошая работа в партере\n:::\n\n:::quote author=\"тренер Алексей\"\nПётр заметно прибавил в дисциплине.\n:::\n\n:::cta href=\"https://t.me/coach\" label=\"Написать тренеру\"\nОстались вопросы?\n:::"
}
JSON

Скрипт печатает готовую ссылку, которую тренер отправляет родителю.