From cb4decb672864426cfdb8f6212f26e2cee62b086 Mon Sep 17 00:00:00 2001 From: Storozhenko Evgeny Date: Tue, 8 Sep 2026 20:04:44 +0000 Subject: [PATCH] =?UTF-8?q?feat(rss):=20=D0=BF=D0=BE=D0=BB=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D0=B8=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BB=D0=BE=D0=B6=D0=BA=D0=B8=20=D0=B2=20RSS-=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B5,=20=D0=BB=D0=B8=D0=BC=D0=B8=D1=82=205=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hugo.toml: ShowFullTextinRSS=true, [services.RSS] limit=5 - тема rDedInit rss.xml: prepend cover (figure.entry-cover, absURL) в content:encoded - openspec change rss-full-feed (план A реализован) --- hugo.toml | 6 ++ openspec/changes/archive/.gitkeep | 0 openspec/changes/rss-full-feed/.openspec.yaml | 2 + openspec/changes/rss-full-feed/design.md | 74 +++++++++++++++++++ openspec/changes/rss-full-feed/proposal.md | 46 ++++++++++++ .../rss-full-feed/specs/site-rss/spec.md | 73 ++++++++++++++++++ openspec/changes/rss-full-feed/tasks.md | 27 +++++++ openspec/config.yaml | 25 +++++++ openspec/specs/.gitkeep | 0 themes/rDedInit/layouts/_default/rss.xml | 9 ++- 10 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 openspec/changes/archive/.gitkeep create mode 100644 openspec/changes/rss-full-feed/.openspec.yaml create mode 100644 openspec/changes/rss-full-feed/design.md create mode 100644 openspec/changes/rss-full-feed/proposal.md create mode 100644 openspec/changes/rss-full-feed/specs/site-rss/spec.md create mode 100644 openspec/changes/rss-full-feed/tasks.md create mode 100644 openspec/config.yaml create mode 100644 openspec/specs/.gitkeep diff --git a/hugo.toml b/hugo.toml index eec1f2b..66420ae 100644 --- a/hugo.toml +++ b/hugo.toml @@ -34,6 +34,9 @@ draft = false languageAlt = "ru" slugify_title = true + # Полный текст постов в RSS-ленте (content:encoded) + ShowFullTextinRSS = true + # Настройки PaperMod defaultTheme = "auto" disableThemeToggle = false @@ -120,5 +123,8 @@ draft = false [services.googleAnalytics] ID = "" # Укажите ваш ID Google Analytics, например "G-XXXXXXXXXX" + [services.RSS] + limit = 5 # Количество постов в RSS-ленте + [permalinks] posts = "/:year/:slug/" \ No newline at end of file diff --git a/openspec/changes/archive/.gitkeep b/openspec/changes/archive/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/openspec/changes/rss-full-feed/.openspec.yaml b/openspec/changes/rss-full-feed/.openspec.yaml new file mode 100644 index 0000000..7a8e2be --- /dev/null +++ b/openspec/changes/rss-full-feed/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-08 diff --git a/openspec/changes/rss-full-feed/design.md b/openspec/changes/rss-full-feed/design.md new file mode 100644 index 0000000..b79cb8b --- /dev/null +++ b/openspec/changes/rss-full-feed/design.md @@ -0,0 +1,74 @@ +# Design: RSS Full Feed + +## Overview + +Включение полного текста и лимита RSS чисто конфигурационное (тема уже +поддерживает `content:encoded`). Меняем только `hugo.toml`. + +## Changes + +### 1. `hugo.toml` — добавить в `[params]` + +```toml + # Полный текст постов в RSS (content:encoded) + ShowFullTextinRSS = true +``` + +### 2. `hugo.toml` — добавить секцию `[services.RSS]` + +```toml +[services] + [services.RSS] + limit = 5 +``` + +Примечание: в `hugo.toml` уже есть секция `[services]` с +`[services.googleAnalytics]` — добавим `[services.RSS]` рядом. + +### 3. `themes/rDedInit/layouts/_default/rss.xml` — обложка в content:encoded + +Перед `.Content` в `` добавляется `figure.entry-cover` с +`` обложки (абсолютный URL), если `cover.image` задан: + +```go-html-template +{{- $coverHtml := "" }} +{{- with .Params.cover.image }} + {{- $coverAbs := (path.Join $.RelPermalink .) | absURL }} + {{- $coverHtml = printf "
\"%s\"
" $coverAbs ($.Params.cover.alt | default $.Title | htmlEscape) }} +{{- end }} +{{ (printf "" $coverHtml .Content) | safeHTML }} +``` + +Примечание: тема — форк (rDedInit), правим её шаблон напрямую, т.к. +медиа-обложки рендерятся отдельным партиалом `cover.html` и в `.Content` не +попадают. + +## Verification + +```bash +cd /opt/dedinit.ru +hugo --minify # собрать +grep -c '' public/index.xml # ожидаем 5 +grep -c 'content:encoded' public/index.xml # ожидаем 5 (полный текст) +# проверить абсолютные медиа-ссылки в content:encoded: +python3 - <<'EOF' +import re +xml = open('public/index.xml', encoding='utf-8').read() +blocks = re.findall(r'', xml, re.S) +print('blocks:', len(blocks)) +for i, b in enumerate(blocks[:2], 1): + urls = re.findall(r'https://dedinit\.ru/[^"\')\s]+', b) + print(i, 'media urls:', urls[:4]) +EOF +``` + +## Rollback + +Удалить из `hugo.toml`: +- строку `ShowFullTextinRSS = true` +- секцию `[services.RSS] limit = 5` + +И откатить `themes/rDedInit/layouts/_default/rss.xml` (git checkout -- файл). + +Пересобрать `hugo --minify`. Поведение вернётся к прежнему (все посты, без +полного текста). \ No newline at end of file diff --git a/openspec/changes/rss-full-feed/proposal.md b/openspec/changes/rss-full-feed/proposal.md new file mode 100644 index 0000000..d664bd7 --- /dev/null +++ b/openspec/changes/rss-full-feed/proposal.md @@ -0,0 +1,46 @@ +# RSS Full Feed for dedinit.ru + +## Why + +Блог dedinit.ru (Hugo 0.165, тема rDedInit/PaperMod) уже генерирует RSS-ленту +(`public/index.xml`), но она неполноценна: + +- **Лимит не задан** — в ленту попадают ВСЕ посты (сейчас 17), хотя типовой + читатель RSS ожидает короткую ленту последних публикаций. +- **Нет полного текста** — тема умеет выводить `` (полный HTML + контент поста) при параметре `site.Params.ShowFullTextinRSS`, но он не включён. + Сейчас в `` только summary, у части постов обрезанный HTML. +- **Нет явных ссылок на медиа** — изображения (hero.svg и др.) рендерятся в + абсолютные URL внутри HTML-контента, поэтому при включении полного текста + медиа-ссылки появятся автоматически. Но если полный текст по какой-то причине + недоступен, лента должна хотя бы содержать корректное описание. + +Цель: сделать RSS-ленту пригодной для полного забирания контента постов +(full-content syndication) — 5 последних постов с полным текстом и медиа-ссылками. + +## What + +1. Включить полный текст в RSS: `[params] ShowFullTextinRSS = true`. +2. Задать лимит 5 постов: `[services.RSS] limit = 5`. +3. Убедиться, что в `` попадают: ``, `<link>`, `<pubDate>`, + `<description>` (описание или summary), `<content:encoded>` (полный HTML, + включая абсолютные ссылки на медиа), `<guid>`. +4. Fallback (план B, если полный текст окажется недоступен): добавить явные + `description` в frontmatter постов и увеличить лимит до 10. + +## Non-goals + +- Не меняем дизайн/вёрстку сайта. +- Не трогаем taxonomy RSS (категории/теги)-ленты. +- Не добавляем новые форматы (Atom только как самоссылка, уже есть). + +## Affected files + +- `hugo.toml` — `[params] ShowFullTextinRSS`, `[services.RSS] limit` +- (план B) `content/posts/*/index.md` — `description` в frontmatter + +## Rollback + +Откат = удалить две строки из `hugo.toml` (параметр `ShowFullTextinRSS` и +`[services.RSS] limit`), пересобрать `hugo --minify` — вернётся прежнее поведение +ленты без полного текста и без лимита. \ No newline at end of file diff --git a/openspec/changes/rss-full-feed/specs/site-rss/spec.md b/openspec/changes/rss-full-feed/specs/site-rss/spec.md new file mode 100644 index 0000000..1f1ac86 --- /dev/null +++ b/openspec/changes/rss-full-feed/specs/site-rss/spec.md @@ -0,0 +1,73 @@ +# RSS Feed Capability + +## ADDED Requirements + +### Requirement: R1 — Лимит постов в RSS-ленте + +Главная RSS-лента (`/index.xml`) MUST содержать не более 5 последних постов +(по `publishDate`), отсортированных по дате публикации по убыванию. + +#### Scenario: Сборка сайта даёт ровно 5 элементов item + +GIVEN сайт собран командой `hugo --minify` +WHEN запрашивается `https://dedinit.ru/index.xml` (файл `public/index.xml`) +THEN число элементов `<item>` в `<channel>` равно 5 (или меньше, если постов < 5), +и это 5 самых свежих постов по дате. + +### Requirement: R2 — Полный текст поста в RSS + +Каждый `<item>` главной RSS-ленты MUST содержать `<content:encoded>` с полным +HTML-контентом поста (поле `.Content` темы Hugo), обёрнутым в CDATA, включая +разметку, блоки кода и абсолютные ссылки на медиафайлы поста. + +#### Scenario: content:encoded присутствует и содержит медиа-URL + +GIVEN сайт собран командой `hugo --minify` +WHEN в `public/index.xml` смотреть элемент `<item>` любого из 5 последних постов +THEN присутствует непустой `<content:encoded>`, содержащий HTML поста и ссылки +вида `https://dedinit.ru/<год>/<slug>/hero.svg` (или аналогичные абсолютные URL +медиа). + +### Requirement: R3 — Описание поста в RSS + +`<description>` каждого `<item>` MUST быть непустым: используется frontmatter +`description` если задан, иначе авто-summary `.Summary`. + +#### Scenario: description непустое + +GIVEN сайт собран командой `hugo --minify` +WHEN в `public/index.xml` смотреть `<description>` каждого `<item>` +THEN оно непустое и не содержит обрезанных HTML-тегов. + +### Requirement: R4 — Метаданные элемента RSS + +Каждый `<item>` MUST содержать корректные `<title>`, `<link>`, `<pubDate>`, +`<guid>` (permalink), согласно текущему шаблону темы +`themes/rDedInit/layouts/_default/rss.xml`. + +#### Scenario: Обязательные поля элемента + +GIVEN сайт собран командой `hugo --minify` +WHEN парсить `public/index.xml` как XML +THEN каждый `<item>` содержит все 4 обязательных поля, `<link>` и `<guid>` +равны полному URL поста (`https://dedinit.ru/<год>/<slug>/`). + +### Requirement: R5 — Медиа-обложка в RSS + +Каждый `<item>` MUST включать в `<content:encoded>` обложку поста +(`cover.image`, hero.svg) как `<figure class="entry-cover"><img ...></figure>` +перед полным текстом, со ссылкой на абсолютный URL медиафайла +(`https://dedinit.ru/<год>/<slug>/hero.svg`), если `cover.image` задан. + +#### Scenario: Обложка в content:encoded + +GIVEN сайт собран командой `hugo --minify` +WHEN в `public/index.xml` смотреть `<content:encoded>` поста с cover.image +THEN он начинается с `<figure class="entry-cover">` и содержит +`<img src="https://dedinit.ru/<год>/<slug>/hero.svg"`. + +#### Scenario: Нет cover — нет figure + +GIVEN сайт собран командой `hugo --minify` +WHEN в `<content:encoded>` поста без cover.image +THEN отсутствует `<figure class="entry-cover">`, контент начинается с текста поста. \ No newline at end of file diff --git a/openspec/changes/rss-full-feed/tasks.md b/openspec/changes/rss-full-feed/tasks.md new file mode 100644 index 0000000..2b529f0 --- /dev/null +++ b/openspec/changes/rss-full-feed/tasks.md @@ -0,0 +1,27 @@ +# Tasks: RSS Full Feed + +## 1. Конфигурация + +- [x] 1.1 В `hugo.toml`, в секции `[params]`, добавить `ShowFullTextinRSS = true` +- [x] 1.2 В `hugo.toml`, в секции `[services]`, добавить `[services.RSS]` с `limit = 5` +- [x] 1.3 В `themes/rDedInit/layouts/_default/rss.xml` добавить prepend обложки + (figure.entry-cover с absURL) в `<content:encoded>` + +## 2. Сборка и проверка + +- [x] 2.1 Выполнить `hugo --minify` — сборка без ошибок +- [x] 2.2 Проверить `grep -c '<item>' public/index.xml` == 5 +- [x] 2.3 Проверить `grep -c 'content:encoded' public/index.xml` == 5 +- [x] 2.4 Проверить, что `<content:encoded>` начинается с `<figure class="entry-cover">` + и содержит абсолютную ссылку на hero.svg (скриптом из design.md) + +## 3. Git + +- [x] 3.1 Закоммитить изменения (`hugo.toml`, openspec-артефакты) +- [ ] 3.2 Запушить в origin (gitverse.ru) — после согласования с пользователем + +## 4. Деплой (по согласованию с пользователем) + +- [ ] 4.1 Выполнить `make deploy` (rsync на kpa39l.myjino.ru:2222) +- [ ] 4.2 Проверить `curl -s https://dedinit.ru/index.xml | grep -c '<item>'` == 5 + и наличие `content:encoded` \ No newline at end of file diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..6ff88ac --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,25 @@ +schema: spec-driven + +context: | + Проект: dedinit.ru — статический блог на Hugo (версия 0.165.0, extended), тема rDedInit (форк PaperMod). + Контент: bundle-хранение статей (content/posts/<name>/index.md), фронтматтер YAML. + Тема rDedInit поддерживает полный текст в RSS: шаблон themes/rDedInit/layouts/_default/rss.xml + выводит <content:encoded> при параметре site.Params.ShowFullTextinRSS. + Лимит постов в RSS задаётся [services.RSS].limit в hugo.toml (0/отсутствие = без лимита). + Деплой: make build (hugo --minify) → make deploy (rsync на kpa39l.myjino.ru:2222), git origin = gitverse.ru (истина). + Конвенции: пользователь требует подтверждения перед удалением/перезаписью чужих данных; + краткие статус-отчёты на русском; тихие watchdog'и. + +rules: + proposal: + - Указывать затронутые файлы и параметры конфига + - Включать план отката (rollback) + - Не более 500 слов + specs: + - Требования в формате MUST/SHOULD/MAY + - Сценарии проверки GIVEN/WHEN/THEN с конкретными командами (hugo, grep, curl) + design: + - Указывать конкретные файлы и команды применения/проверки + - Включать rollback + tasks: + - Каждая задача — проверяемый шаг \ No newline at end of file diff --git a/openspec/specs/.gitkeep b/openspec/specs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/themes/rDedInit/layouts/_default/rss.xml b/themes/rDedInit/layouts/_default/rss.xml index fed8ae9..460e51d 100644 --- a/themes/rDedInit/layouts/_default/rss.xml +++ b/themes/rDedInit/layouts/_default/rss.xml @@ -74,7 +74,14 @@ <guid>{{ .Permalink }}</guid> <description>{{ with .Description | html }}{{ . }}{{ else }}{{ .Summary | html }}{{ end -}}</description> {{- if and site.Params.ShowFullTextinRSS .Content }} - <content:encoded>{{ (printf "<![CDATA[%s]]>" .Content) | safeHTML }}</content:encoded> + {{- $page := . }} + {{- $coverHtml := "" }} + {{- with .Params.cover.image }} + {{- $coverAbs := (path.Join $page.RelPermalink .) | absURL }} + {{- $coverAlt := ($page.Params.cover.alt | default $page.Title) | htmlEscape }} + {{- $coverHtml = printf "<figure class=\"entry-cover\"><img src=\"%s\" alt=\"%s\"></figure>" $coverAbs $coverAlt }} + {{- end }} + <content:encoded>{{ (printf "<![CDATA[%s%s]]>" $coverHtml .Content) | safeHTML }}</content:encoded> {{- end }} </item> {{- end }}