Мой первый блог

Постановка задачи:
На платформе отсутствует канал контент‑маркетинга. Блог позволяет каждому тенанту публиковать SEO‑оптимизированные многоязычные статьи, создаваемые в Strapi CMS и отображаемые в приложении SvelteKit — это приводит органический трафик, укрепляет экспертный статус и поддерживает существующий i18n‑конвейер для 13 локалей.
Критерии успеха
- Посты блога, созданные в Strapi, отображаются на фронтенде в брендинге арендатора
- Посты автоматически переводятся на все 13 локалей через существующий webhook-конвейер (без изменений в инфраструктуре перевода)
- Страницы блога ранжируются в поисковых системах (структурированные данные, sitemap, hreflang, og:article)
- Арендаторы без фича-флага
blogне видят маршруты блога (404)
User Stories
- Как автор контента, я хочу писать в Strapi блог‑посты с расширенным markdown, изображениями и видео‑вставками, чтобы публиковать материалы без необходимости трогать код
- Как посетитель сайта, я хочу просматривать блог‑посты с фильтром по категориям, чтобы находить релевантный контент
- Как посетитель сайта, я хочу видеть оглавление в длинных постах, чтобы быстро по ним перемещаться
- Как посетитель сайта, я хочу читать посты на своём языке, с автоматическим переводом с английского
- Как посетитель сайта, я хочу делиться постами в социальных сетях
- Как посетитель сайта, я хочу находить связанные посты и узнавать больше об авторе
- Как SEO‑менеджер, я хочу, чтобы у каждого поста были корректные метатеги, структурированные данные и записи в sitemap
Функциональные требования
Обязательные (MVP)
- Типы контента Strapi:
blog-posts,blog-authors,blog-categories— все с полями tenant, locale, inherit - Страница списка блога (
/blog) — постраничная сетка постов, фильтр по категории через query‑параметр - Страница поста (
/blog/[slug]) — полный рендеринг markdown с оглавлением (ToC), карточкой автора, связанными постами, кнопками шаринга в соцсетях - Страница категории (
/blog/category/[slug]) — отфильтрованный список с переиспользованием тех же компонентов списка - Страница автора (
/blog/author/[slug]) — био автора + его/её посты - Оглавление (Table of Contents) — авто‑генерация из заголовков h2/h3, «липкий» сайдбар на десктопе, сворачиваемый блок на мобильных, подсветка активного заголовка через Intersection Observer
- Встраивание YouTube/Vimeo — «голые» URL в markdown автоматически конвертируются в адаптивные iframes (youtube-nocookie.com для приватности)
- Время чтения — рассчитывается на сервере по количеству слов (Math.ceil(words / 200))
- Кнопки шаринга — X, Facebook, LinkedIn, email, копирование ссылки (Web Share API, где доступно)
- Похожие посты — та же категория, исключая текущий, максимум 3, сортировка по publishedAt по убыванию
- RSS‑лента (
/blog/rss.xml) — RSS 2.0, локаль через query‑параметр - SEO — SeoComponent для каждого поста, теги og:article, схема BlogPosting JSON‑LD, канонические URL
- Карта сайта — динамическое добавление URL постов/категорий/авторов блога в существующую sitemap с hreflang
- Фича‑флаг —
blog: booleanв TenantConfig.features, guard в layout возвращает 404 при выключенном флаге - i18n — ключи сообщений Paraglide для всех UI‑строк (навигация, подписи, пагинация), контент Strapi авто‑переводится через существующий webhook
- Пагинация — постраничная, с использованием нативной пагинации Strapi (9 постов на страницу)
- Избранный пост — булевый флаг
featuredна blog-posts; избранный пост отображается как hero‑карточка вверху страницы списка/blog(увеличенная карточка, на всю ширину, над основной сеткой)
Желательно (после MVP)
- Полнотекстовый поиск по постам блога
- Серии постов / многочастные статьи
- Индикатор прогресса чтения
- Комментарии (скорее всего через сторонний виджет)
- CTA подписки на рассылку внутри постов
- Счётчик просмотров / интеграция с аналитикой
Технические требования
Модель данных (типы контента Strapi)
blog-posts (api::blog-post.blog-post):
| Поле | Тип | Локализуется | Примечания |
|---|---|---|---|
| title | Text (required) | Yes | Соответствует конфигу переводов |
| slug | UID (from title) | No | Исключено из перевода |
| excerpt | Text | Yes | Для карточек + мета-описания |
| body | Rich Text (Markdown) | Yes | Рендерится через marked |
| featuredImage | Media (single) | No | URL + alternativeText |
| category | Relation (manyToOne -> blog-category) | No | |
| author | Relation (manyToOne -> blog-author) | No | |
| featured | Boolean (default false) | No | Избранный пост, показан как hero-карта в списке |
| seo | Component (shared.seo) | Yes | Повторное использование существующего SeoComponent |
| tenant | Text | No | Стандартный фильтр tenant |
| inherit | Boolean (default true) | No | Флаг авто-перевода |
blog-authors (api::blog-author.blog-author):
| Поле | Тип | Локализуется | Примечания |
|---|---|---|---|
| name | Text (required) | Yes | |
| slug | UID (from name) | No | |
| bio | Rich Text (Markdown) | Yes | |
| avatar | Media (single) | No | |
| socialLinks | JSON | No | { twitter?, linkedin?, website? } |
| tenant | Text | No | |
| inherit | Boolean (default true) | No |
blog-categories (api::blog-category.blog-category):
| Поле | Тип | Локализуется | Примечания |
|---|---|---|---|
| name | Text (required) | Yes | |
| slug | UID (from name) | No | |
| description | Text | Yes | |
| color | Text | No | Hex-цвет для UI-бейджей |
| tenant | Text | No | |
| inherit | Boolean (default true) | No |
Интерфейсы TypeScript
См. src/lib/services/strapi-types.ts для BlogPost, BlogAuthor, BlogCategory.
Маршруты
Все под src/routes/(marketing)/blog/:
| Маршрут | Server Load | Назначение |
|---|---|---|
blog/+layout.server.ts |
Feature guard + загрузка категорий | Гейт + навигационные данные |
blog/+page.server.ts |
Постраничные записи, необязательный фильтр ?category= |
Список |
blog/[slug]/+page.server.ts |
Пост по slug + связанные посты + расчёт времени чтения | Детальная страница |
blog/category/[slug]/+page.server.ts |
Посты, отфильтрованные по категории | Список по категории |
blog/author/[slug]/+page.server.ts |
Автор + его/её посты | Страница автора |
blog/rss.xml/+server.ts |
RSS 2.0 XML, параметр ?locale= |
Лента |
UI‑компоненты
Все в src/lib/components/blog/:
| Компонент | Props | Примечания |
|---|---|---|
BlogPostCard.svelte |
post: BlogPost |
Карточка с изображением, бейджем категории, заголовком, описанием, автором, датой, временем чтения |
BlogPostContent.svelte |
body: string |
Markdown в HTML через marked с расширением для встраивания видео + ID для заголовков |
TableOfContents.svelte |
entries: TocEntry[] |
Прикреплённый сайдбар, Intersection Observer для активного состояния, сворачиваемый на мобильных |
AuthorCard.svelte |
author: BlogAuthor |
Аватар, имя, био, ссылки на соцсети |
SocialShare.svelte |
url: string, title: string |
Кнопки шаринга с запасным вариантом на Web Share API |
RelatedPosts.svelte |
posts: BlogPost[] |
Сетка из BlogPostCard |
BlogPagination.svelte |
pagination: StrapiPagination, basePath: string |
Предыдущая/следующая + номера страниц |
Безопасность и аутентификация
- Блог публичный (аутентификация не требуется) — маркетинговый контент
- Доступ на чтение к Strapi API через существующий токен
STRAPI_JWT - Видеовстраивания используют
youtube-nocookie.comдля соблюдения требований GDPR
Частные случаи и обработка ошибок
- Пост не найден: 404 через SvelteKit
error() - Нет постов в категории: пустое состояние с сообщением
blog_no_posts - Отсутствует избранное изображение: карточка отображается без изображения
- Перевод недоступен:
loadContentWithFallback()откатывается на английский - RTL‑языки: макет учитывает
dir="rtl"(ar, he, ur)
План E2E‑тестирования
Тестовые файлы находятся в tests/blog/. Следуйте существующим паттернам: параметризация по вьюпортам, Axe для a11y, networkidle ожидает контент из Strapi.
Тесты критического пути
- Загрузка списка блога —
/blogрендерит сетку карточек постов с заголовком, выдержкой, бейджем категории, автором, датой и временем чтения - Работа пагинации —
/blog?page=2показывает вторую страницу результатов; кнопки «предыдущая/следующая» корректно переходят между страницами; на странице 1 нет кнопки «Previous» - Фильтр по категории — клик по чипу категории фильтрует посты; URL обновляется до
?category=<slug>; чип «All» сбрасывает фильтр - Отрисовка страницы поста —
/blog/[slug]показывает заголовок, обложку (featured image), форматированное тело, карточку автора, время чтения и дату публикации - Оглавление (ToC) — на странице поста генерируется ToC из заголовков h2/h3; клик по пункту ToC скроллит к соответствующему заголовку
- Похожие посты — на странице поста отображается до 3 похожих постов из той же категории (без текущего поста)
- Страница категории —
/blog/category/[slug]показывает название категории, описание и только посты из этой категории - Страница автора —
/blog/author/[slug]показывает био автора, аватар, ссылки на соцсети и его/её посты - Кнопки шаринга в соцсетях — на странице поста видны кнопки шаринга; «Copy link» копирует URL и показывает подтверждающее сообщение
- RSS‑лента —
GET /blog/rss.xmlвозвращает валидный XML сContent-Type: application/rss+xmlи содержит записи постов - Навигация — ссылка на блог в меню сайта ведёт на
/blog; хлебные крошки отображают корректную иерархию - Избранный пост —
/blogотображает избранный пост как крупную hero‑карточку над основной сеткой
Тесты краевых случаев и ошибок
- Пост не найден —
/blog/nonexistent-slugвозвращает страницу 404 - Пустая категория — категория без постов показывает сообщение пустого состояния
blog_no_posts - Отсутствует обложка — карточка поста корректно рендерится без изображения (нет битого
<img>) - Отсутствует автор/категория — пост без автора или категории рендерится без этих элементов (без падения)
- Фича‑флаг выключен — когда
blog: false,/blogвозвращает 404
Сквозные тесты
- i18n: переключение локали — перейти на
/fr/blog, проверить, что UI‑строки (заголовок страницы, «Read more», подписи пагинации) на французском; контент поста по умолчанию на английском, если нет перевода - i18n: RTL‑верстка — перейти на
/ar/blogи/ar/blog/[slug], проверить, что установленdir="rtl"и верстка отображается корректно (ToC на правильной стороне) - SEO: мета‑теги — страница поста содержит
og:type=article,og:title,og:description,article:published_timeи canonical‑ссылку - SEO: JSON‑LD — страница поста содержит
<script type="application/ld+json">с@type: BlogPosting, headline, author, datePublished - SEO: sitemap —
/sitemap.xmlвключает URL‑ы постов блога с hreflang‑альтернативами - Доступность — запустить Axe‑аудит на
/blog,/blog/[slug],/blog/author/[slug]для мобильного и десктопного вьюпорта (следует существующему паттерну a11y‑тестов) - Клавиатурная навигация — пункты ToC и кнопки пагинации доступны в фокусе и активируются с клавиатуры; индикаторы фокуса видимы
Вне области проекта
- Комментарии к записям блога
- Полнотекстовый поиск
- Интеграция рассылки внутри записей
- Аналитика / счётчики просмотров
- Индивидуальные шаблоны записей для каждого арендатора
Ludo — преподаватель и предприниматель в сфере технологий, которым движет простая идея: качественное образование должно быть доступно каждому.