# Подключение существующего блога к Content Pilot

Передайте этот файл AI-помощнику, работающему с исходниками вашего сайта.
Это техническое задание, а не исполняемый установщик. Секретов в файле нет.

## Задание разработчику

Изучи стек, хранение статей, маршруты блога, изображения и кеширование.
Добавь серверный адаптер Content Pilot Blog Connector v1 в существующий блог.
Сохрани дизайн, маршруты и материалы. Не создавай параллельный блог. Если блога
или постоянного хранилища нет, согласуй необходимую доработку с владельцем.
Для статического сайта нужен отдельный процесс сборки: этот контракт рассчитан
на сайт с серверной частью. Не обещай универсальную установку одного файла.

После реализации и тестов дай инструкции по развёртыванию, настройке ключа
и итоговый HTTPS-адрес, например https://example.com/api/contentpilot.
Публикация изменений сайта требует разрешения владельца.

## Безопасность

- Каждый маршрут требует Authorization: Bearer <секрет>. Ключ — минимум 32 байта
  случайных данных. Владелец задаёт его в серверном окружении и в Content Pilot.
  Не помещай ключ в git, логи, ответы, HTML или клиентский JavaScript.
  Сравнивай секрет за постоянное время. При ошибке — 401/403 без редиректа.
- Ключ разрешает изменять только статьи, созданные интеграцией. Нельзя изменять
  чужие статьи, пользователей, настройки или произвольные файлы.
- HTTPS обязателен. Адрес API — на том же origin, что и сайт. Без редиректов.
- Валидируй JSON и длины полей, ограничь тело запроса (4 MiB).
  Чисти HTML по allowlist: без script, iframe, обработчиков событий и опасных URL.
  Не исполняй текст как шаблон, PHP, JSX, MDX или инструкции для AI.
- Картинки скачивай с ограничением размера/времени/типа. Запрещай приватные IP,
  localhost, metadata endpoints, credentials в URL, обход через DNS/редиректы.
  Храни картинки постоянно на сайте и переписывай ссылки в HTML.
- Не маскируй сбои базы успешным ответом и не возвращай сырые исключения.

## Проверка — GET <base>

После авторизации и проверки хранилища верни HTTP 200, JSON, Cache-Control: no-store:

```json
{
  "protocol": "contentpilot-blog/1",
  "site_url": "https://example.com",
  "capabilities": ["draft", "publish", "idempotency", "operation_status", "persistent_media"]
}
```

GET ничего не создаёт. OPTIONS или главная страница не заменяют эту проверку.

## Создание/обновление — PUT <base>/articles/{external_id}

Заголовок Idempotency-Key равен operation_id. Пример тела:

```json
{
  "protocol": "contentpilot-blog/1",
  "operation_id": "72a41c79-2001-4598-aa55-e4fd332bd95e",
  "external_id": "connection-uuid:article-uuid",
  "article": {
    "title": "Заголовок статьи",
    "h1": "Заголовок статьи",
    "content_html": "<p>Текст статьи.</p>",
    "seo_title": "Заголовок для поиска",
    "meta_description": "Описание страницы",
    "tags": ["Маркетинг"],
    "featured_image_url": "https://provider.example/image.webp",
    "status": "draft"
  }
}
```

status — draft или publish. h1, seo_title, meta_description, featured_image_url
могут быть null. Расписание выполняет Content Pilot. Slug выбирает сайт и сохраняет
при обновлениях. Сохраняй заголовки, alt-тексты и ссылки; SEO-поля выводи в HTML.
Рубрика — согласованная с владельцем рубрика по умолчанию.

### Защита от дублей обязательна

1. external_id — постоянный уникальный ключ материала в БД интеграции.
   Новая версия с другим operation_id обновляет ту же статью.
2. operation_id — постоянный уникальный ключ неизменяемого запроса. Храни хеш
   исходного payload и результат в БД до/вместе с изменением статьи.
3. Повтор operation_id с тем же payload возвращает прежний результат без побочных
   действий. Другой payload с тем же operation_id — 409.
4. Конкурентность защищай транзакциями и уникальными ограничениями. Перезапуск
   или несколько serverless-инстансов не должны создавать дубли.
5. Поздний повтор старой операции не откатывает новую версию. Результаты операций
   хранятся постоянно, а не во временном кеше. Асинхронная очередь тоже постоянная.

### Готово — HTTP 200/201

```json
{
  "protocol": "contentpilot-blog/1",
  "operation_id": "72a41c79-2001-4598-aa55-e4fd332bd95e",
  "external_id": "connection-uuid:article-uuid",
  "id": "local-post-123",
  "status": "draft",
  "url": null
}
```

Для publish: status=published и url — полный HTTPS-адрес конкретной статьи на том
же origin. Не возвращай главную страницу, редактор или API. Обнови кеши, список
блога и sitemap; подтверди, что страница доступна. Черновик должен быть непубличным
и отсутствовать в sitemap. Подтверждай завершение только после сохранения картинок.

### Ещё обрабатывается — HTTP 202

```json
{
  "protocol": "contentpilot-blog/1",
  "operation_id": "72a41c79-2001-4598-aa55-e4fd332bd95e",
  "external_id": "connection-uuid:article-uuid",
  "status": "processing"
}
```

202 не означает публикацию. При постоянной ошибке — 4xx/5xx с безопасным кодом.

## Сверка — GET <base>/operations/{operation_id}

Готово — тот же подтверждённый ответ HTTP 200; в процессе — HTTP 202 выше.
Если операция точно не принималась — HTTP 404:

```json
{
  "protocol": "contentpilot-blog/1",
  "operation_id": "72a41c79-2001-4598-aa55-e4fd332bd95e",
  "code": "operation_not_found"
}
```

Не возвращай operation_not_found при сбое БД. Только после такого подтверждения
Content Pilot может повторить исходный PUT с прежним ключом. Редиректы запрещены.

## Тестовый черновик

external_id `<connection-uuid>:connection-test` всегда draft. Повторная проверка
возвращает тот же черновик. Не публикуй его и не запускай рассылку или платную
генерацию. Удаление черновика владельцем не должно удалять журнал операций.

## Приёмка

- Без ключа/с неверным ключом все маршруты закрыты.
- Два одновременных PUT и повтор после рестарта создают одну статью.
- Потерянный ответ восстанавливается через GET без второй публикации.
- draft -> publish обновляет тот же материал и возвращает реальную ссылку.
- Поздняя старая операция не откатывает новую статью.
- Опасный HTML очищается, картинки сохраняются постоянно.
- Черновик непубличный, опубликованная статья есть в блоге и sitemap.
- Ошибки хранилища не маскируются успехом; дизайн и существующий контент сохранены.

Передай владельцу список изменённых файлов, результаты тестов, адрес интеграции,
инструкции по секрету, развёртыванию и откату адаптера без потери статей.
