Закрытый предпросмотр черновика
Предпросмотр позволяет увидеть черновик в шаблоне сайта до публикации. Обычный параметр preview=true ещё не даёт права читать закрытые данные: его может написать любой посетитель. Нам нужен отдельный вход, который проверит разрешение, выберет конкретный документ и не сохранит ответ в общем cache. Для «Редакционного дома» сделаем короткую подписанную ссылку.
Исходное состояние — публичные маршруты читают только published через серверный token. После урока редакторская панель сможет сформировать ссылку на /preview для Article. Ссылка предназначена для локальной лаборатории и действует пять минут. Пример не запускался; это подготовленный авторский механизм поверх документированной функции Strapi Preview.
Панель составляет ссылку
В config/admin.js включён preview. Allowed origin совпадает с адресом нашего сайта. Handler получает uid, documentId и locale; для типов, которым не нужна страница, возвращает null. У нас preview есть только у api::article.article: справочник Category не открывает случайный шаблон статьи.
Handler не вкладывает Content API token в URL. Вместо этого он подписывает сочетание documentId, locale и времени окончания действия через HMAC SHA-256. CMS и сервер сайта знают общий PREVIEW_SECRET. Браузер получает подпись, но не secret. Изменить ID или срок действия без повторного вычисления корректной подписи он не может.
Полный config/admin.js включает и остальные административные настройки. Не заменяйте существующий файл одним фрагментом preview, потеряв auth или salts.
const { createHmac } = require('node:crypto');
module.exports = ({ env }) => ({
auth: { secret: env('ADMIN_JWT_SECRET') },
apiToken: { salt: env('API_TOKEN_SALT') },
transfer: { token: { salt: env('TRANSFER_TOKEN_SALT') } },
secrets: { encryptionKey: env('ENCRYPTION_KEY') },
preview: {
enabled: true,
config: {
allowedOrigins: env('CLIENT_URL', 'http://127.0.0.1:4178'),
async handler(uid, { documentId, locale }) {
if (uid !== 'api::article.article') return null;
const language = locale || 'ru';
if (!['ru', 'en'].includes(language)) return null;
const expires = String(Math.floor(Date.now() / 1000) + 300);
const message = `${documentId}.${language}.${expires}`;
const signature = createHmac('sha256', env('PREVIEW_SECRET'))
.update(message).digest('hex');
const query = new URLSearchParams({
documentId, locale: language, expires, signature,
});
return `${env('CLIENT_URL')}/preview?${query}`;
},
},
},
});
Значение expires получается из текущего времени плюс триста секунд. Срок является частью подписываемой строки. Locale ограничена ru/en. Такое ограничение важно: preview не должен становиться скрытым универсальным запросом любого типа и любой локали. AllowedOrigins помогает настройке панели, но основная проверка доступа находится на принимающем сервере.
Функция Preview доступна в бесплатной линии, а Live Preview имеет отдельные условия лицензии. Мы используем обычный переход к странице, не обещаем встроенное синхронное редактирование. Устройство handler и ограничения опубликованы в Preview.
Сервер проверяет подпись до чтения
Модуль site/preview.mjs сначала проверяет форму documentId, locale, expires и signature. Затем убеждается, что срок ещё не закончился и не находится слишком далеко в будущем. Только после этого вычисляет ожидаемую подпись и сравнивает bytes. Проверка длины и формата нужна также для корректной работы timingSafeEqual.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function validPreview(parameters) {
const documentId = parameters.get('documentId') || '';
const locale = parameters.get('locale') || '';
const expires = parameters.get('expires') || '';
const signature = parameters.get('signature') || '';
if (!process.env.PREVIEW_SECRET) return false;
if (!/^[a-z0-9]{10,40}$/.test(documentId)) return false;
if (!['ru', 'en'].includes(locale)) return false;
if (!/^\d{10}$/.test(expires) || !/^[a-f0-9]{64}$/.test(signature)) return false;
const now = Math.floor(Date.now() / 1000);
if (Number(expires) < now || Number(expires) > now + 300) return false;
const expected = createHmac('sha256', process.env.PREVIEW_SECRET)
.update(`${documentId}.${locale}.${expires}`).digest('hex');
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));
}
В сервере одиннадцатого урока validPreview вызывается до readDraft. Неверная ссылка получает 401, не обращаясь за черновиком. При успешной проверке readDraft устанавливает status draft и использует обычный закрытый CMS_TOKEN из серверного окружения. Подпись разрешает действие на сайте; token разрешает серверу прочитать CMS. Это два независимых слоя с разными назначениями.
Черновая страница получает Cache-Control: private, no-store, X-Robots-Tag: noindex, nofollow и Referrer-Policy: no-referrer. Первое препятствует обычному сохранению ответа в совместном cache, второе обозначает нежелательность индексации, третье уменьшает передачу адреса как referrer. Ни один из этих заголовков сам по себе не заменяет проверку подписи. Скрытая от поиска страница может быть доступна тому, кто знает URL.
Шаблон показывает видимую фразу «Закрытый предпросмотр черновика». Она помогает редактору не принять новый текст за уже опубликованный. Используются те же title, summary и body, что у обычной страницы. Если preview строится совсем другим renderer, его внешний вид может ничего не сказать о реальном выпуске; поэтому общая articlePage получает лишь дополнительный признак preview.
Ссылка является временным доступом
Подписанный URL остаётся bearer-ссылкой: любой, кто получил её до окончания срока, может открыть соответствующий черновик. Не вставляйте такие адреса в публичные документы или issue. Наш механизм не содержит отдельного входа редактора на сайте, списка разрешённых пользователей или одноразового погашения ссылки. Это явные ограничения короткой локальной демонстрации.
Срок действия ограничивает время использования, но не отозвает уже скопированный текст. Если черновик содержит сведения, которые нельзя передавать другим, нужен более строгий редакторский вход и правила аудитирования. Не следует считать пятиминутную подпись достаточной для любой конфиденциальной редакции. В библиотеке курса все материалы вымышлены и пригодны для публичного содержания после выпуска.
Подпись также не фиксирует snapshot текста. Редактор может сохранить ещё одно изменение, и та же действующая ссылка прочитает актуальный draft этого документа. Если согласование должно относиться к определённой версии, понадобится идентификатор ревизии или отдельный неизменяемый снимок. Наш handler даёт доступ к текущему черновику, а не заверяет неизменность содержания.
Не передавайте произвольный redirect URL в preview. Здесь маршрут всегда открывает articlePage по подписанному documentId. Клиент не задаёт чужой домен или путь для перенаправления. Такое устройство сохраняет маленькую область действия и позволяет объяснить каждое входное значение.
Сравните нашу подпись с передачей secret напрямую в query. В обоих случаях URL требует бережного обращения, но HMAC не раскрывает долгоживущий общий ключ. Утечка одной короткой ссылки ограничена её documentId, locale и сроком; утечка самого PREVIEW_SECRET позволяет составлять новые ссылки. Поэтому секрет хранится только в окружениях CMS и сайта. CMS_TOKEN при этом вообще не участвует в построении URL и не должен попасть в frontend bundle.
Проверяемая граница относится также к типу ресурса. Handler подписывает только Article, а readDraft использует фиксированный /api/articles/. Нельзя подменить путь на Authors, Users или служебный endpoint через дополнительный параметр. Чем меньше входных значений, тем проще объяснить получаемое полномочие. При расширении preview на другой тип создайте явный разрешённый маршрут и отдельную ветвь renderer, сохранив проверку принадлежности.
Наконец, подпись не является журналом согласования. Открытие страницы не записывает, кто её одобрил и какую версию увидел. Для полноценного выпуска нужны отдельные авторство решения и история изменений. В данном уроке результат ограничен временным чтением текущего черновика.
Для будущей локальной проверки сначала сохраните изменённый summary в draft и откройте Preview из панели. Ожидается новый текст с отметкой предпросмотра. Обычная статья должна сохранить published-summary. Затем измените один символ подписи или documentId и ожидайте отказ. После пяти минут исходная ссылка также должна перестать разрешать чтение. Эти состояния описаны как ожидания, не как полученные ответы. Они показывают результат урока: редактор видит черновик в привычном шаблоне, а обычные маршруты сохраняют прежнюю публичную границу.