openspec: архив changes series-taxonomy-and-header, archive-by-year-month, deploy-sftp-incremental (2026-09-25)
deploy-dedinit / Build & SFTP Deploy (push) Successful in 7m13s
deploy-dedinit / Build & SFTP Deploy (push) Successful in 7m13s
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-22
|
||||
@@ -0,0 +1,53 @@
|
||||
# Design: Archive by Year/Month
|
||||
|
||||
## Goal
|
||||
|
||||
Страница `/posts/` (меню «Архив») показывает посты, сгруппированные по
|
||||
годам и месяцам (как штатный архив Hugo/PaperMod), вместо плоского списка.
|
||||
|
||||
## Approach
|
||||
|
||||
Используем готовый layout темы rDedInit: `themes/rDedInit/layouts/_default/archives.html`.
|
||||
|
||||
Что он делает (из исходника theme):
|
||||
- `where site.RegularPages "Type" "in" site.Params.mainSections` — берёт
|
||||
обычные страницы основных секций (не список, без пагинации).
|
||||
- `GroupByPublishDate "2006"` — группировка по годам.
|
||||
- Внутри каждого года `GroupByDate "January"` — группировка по месяцам.
|
||||
- Заголовки `.archive-year` / `.archive-month`, счётчики, якоря `#2026`,
|
||||
`#2026-January`.
|
||||
- Каждая запись — `.archive-entry` с title, post_meta и ссылкой.
|
||||
|
||||
Стили `.archive-*` уже есть в `assets/css/common/archive.css`.
|
||||
|
||||
## Change: content/posts/_index.md
|
||||
|
||||
Создать файл `content/posts/_index.md`:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Архив"
|
||||
description: "Все посты блога по годам и месяцам"
|
||||
layout: archives
|
||||
---
|
||||
```
|
||||
|
||||
Frontmatter `layout: archives` заставляет Hugo выбрать
|
||||
`_default/archives.html` для листинга секции (Hugo ищет layout по
|
||||
порядку: `<type>/<layout>.html`, `_default/<layout>.html` — найдёт
|
||||
`_default/archives.html`).
|
||||
|
||||
Никаких других файлов менять не нужно:
|
||||
- Меню `[menu.main]` Архив → `/posts/` уже настроено (hugo.toml).
|
||||
- RSS секции `/posts/` рендерится через `rss.xml` независимо.
|
||||
- `baseof.html` уже поддерживает `eq .Layout "archives"` для
|
||||
breadcrumbs/навигации.
|
||||
- `index.json` (поиск) и `rss.xml` исключают layout `archives` из листинга.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `make build` (hugo --minify)
|
||||
2. `grep -c 'archive-year' public/posts/index.html` > 0
|
||||
3. `grep -c 'archive-month' public/posts/index.html` > 0
|
||||
4. Убедиться, что title на странице — «Архив», посты сгруппированы по годам
|
||||
5. `make deploy-dry-run` — SFTP-симуляция (деплой: Gitea Actions)
|
||||
@@ -0,0 +1,36 @@
|
||||
# Archive by Year/Month
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Страница «Архив» (меню → `/posts/`) сейчас показывает плоский список всех
|
||||
постов с пагинацией по 10 (layout `list.html`). При ~100+ постах это
|
||||
неудобно: нет обзора по годам и месяцам, нельзя быстро найти пост по
|
||||
периоду.
|
||||
|
||||
## Why
|
||||
|
||||
Hugo умеет группировать посты по годам/месяцам из коробки (GroupByDate /
|
||||
GroupByPublishDate). Тема rDedInit уже содержит готовый layout
|
||||
`_default/archives.html` и стили `archive.css` — нужно лишь переключить
|
||||
секцию `/posts/` на этот layout. Расширять тему не требуется.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
Добавить `content/posts/_index.md` с frontmatter `layout: archives` (и
|
||||
title/description). Hugo автоматически применит `archives.html` к листингу
|
||||
секции `/posts/`, который сгруппирует посты по годам и месяцам с
|
||||
счётчиками и якорями.
|
||||
|
||||
## Alternative Solutions
|
||||
|
||||
- Написать свой layout `layouts/posts/list.html` — избыточно, тема уже
|
||||
имеет готовый архив.
|
||||
- Использовать отдельную страницу `/archive/` — меняет URL и меню; текущий
|
||||
пункт меню «Архив» ведёт на `/posts/`, менять не нужно.
|
||||
|
||||
## Impact
|
||||
|
||||
- Изменяется только рендер `/posts/` (list → archives).
|
||||
- URL постов и permalinks не меняются.
|
||||
- RSS секции `/posts/` сохраняется (archives.html не влияет на rss.xml).
|
||||
- Деплой — автоматический через Gitea Actions (push в main).
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
## Purpose
|
||||
|
||||
Страница «Архив» (/posts/) блога dedinit.ru отображает все посты,
|
||||
сгруппированные по годам и месяцам (как штатный архив Hugo), вместо
|
||||
плоского списка с пагинацией.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Группировка постов по годам и месяцам
|
||||
Система SHALL отображать на странице /posts/ список всех постов,
|
||||
сгруппированный по годам публикации, и внутри каждого года — по месяцам
|
||||
публикации. Заголовки содержат год/месяц и количество постов в группе.
|
||||
|
||||
#### Scenario: Страница /posts/ показывает годы
|
||||
- **WHEN** пользователь открывает страницу /posts/
|
||||
- **THEN** страница содержит заголовки годов (например «2026», «2023»), под каждым — посты этого года
|
||||
- **AND** возле каждого года отображается количество постов
|
||||
|
||||
#### Scenario: Внутри года посты сгруппированы по месяцам
|
||||
- **WHEN** пользователь открывает страницу /posts/
|
||||
- **THEN** внутри каждого года посты сгруппированы по месяцам публикации (например «January», «September»)
|
||||
- **AND** возле каждого месяца отображается количество постов
|
||||
|
||||
#### Scenario: Каждый пост — ссылка на свою страницу
|
||||
- **WHEN** пользователь видит запись в архиве
|
||||
- **THEN** заголовок поста является ссылкой на его страницу (permalink)
|
||||
- **AND** под заголовком отображаются метаданные поста (дата и пр.)
|
||||
|
||||
### Requirement: Заголовок и описание страницы
|
||||
Система SHALL отображать на странице /posts/ заголовок «Архив» и описание
|
||||
страницы (из frontmatter секции).
|
||||
|
||||
#### Scenario: Заголовок страницы
|
||||
- **WHEN** пользователь открывает страницу /posts/
|
||||
- **THEN** заголовок страницы — «Архив»
|
||||
|
||||
### Requirement: Сохранение остальной функциональности секции
|
||||
Система SHALL сохранять работоспособность RSS-ленты секции /posts/ и URL
|
||||
постов (permalinks не меняются) при переключении layout на архив.
|
||||
|
||||
#### Scenario: RSS секции не сломан
|
||||
- **WHEN** пользователь запрашивает RSS-ленту секции /posts/ (index.xml)
|
||||
- **THEN** лента валидна и содержит посты секции
|
||||
|
||||
#### Scenario: Permalinks постов не меняются
|
||||
- **WHEN** пользователь открывает пост из архива
|
||||
- **THEN** URL поста соответствует permalink /:year/:slug/ (как и раньше)
|
||||
@@ -0,0 +1,10 @@
|
||||
# Tasks: Archive by Year/Month
|
||||
|
||||
- [x] Создать `content/posts/_index.md` с frontmatter: title "Архив", description, layout: archives
|
||||
- [x] `make build` (hugo --minify) — без ошибок
|
||||
- [x] Проверить `public/posts/index.html`: есть `.archive-year` и `.archive-month`, посты сгруппированы по годам/месяцам, title "Архив"
|
||||
- [x] Убедиться, что пагинация/плоский список (post-entry) на /posts/ исчезла
|
||||
- [x] Проверить RSS секции /posts/ (index.xml) — не сломан
|
||||
- [x] `openspec validate archive-by-year-month` — чисто
|
||||
- [x] Commit + push gitea main — деплой через Gitea Actions (deploy.yml); замер №1: run создан 16:54:19Z, прод виден 17:30:20Z (~36 мин)
|
||||
- [x] Прогнать openspec-archive-change (архивировать change после деплоя) — архив 2026-09-25
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-22
|
||||
@@ -0,0 +1,53 @@
|
||||
# Design: Инкрементальный SFTP-деплой
|
||||
|
||||
## Проблема
|
||||
|
||||
`tools/deploy_sftp.py` mirror-заливал ВСЕ 524 файла (66M) каждый деплой,
|
||||
включая неизменные тяжёлые (21M PDF, 11M×2 M4V) — деплой занимал ~36 мин
|
||||
(замер №1: run создан 16:54:19Z → прод виден 17:30:20Z). Плюс: при обрыве
|
||||
SFTP-соединения скрипт вис бесконечно (нет таймаутов) — оба раза висел на
|
||||
`2026/gotosocial-relay-match-by-default/hero.svg`.
|
||||
|
||||
## Решение
|
||||
|
||||
Единственный файл `tools/deploy_sftp.py`:
|
||||
|
||||
1. **Инкремент по (size + SHA1)**: для каждого локального файла — `sftp.stat()`;
|
||||
если удалённый существует и (размер совпадает И SHA1 содержимого совпадает)
|
||||
— пропуск; иначе `sftp.put()`.
|
||||
- mtime НЕ используется: в CI (Gitea Actions) checkout ставит свежий mtime
|
||||
всем файлам → mtime-сравнение бесполезно.
|
||||
- SHA1 читается потоково с удалённого через `sftp.open()` (1MB чанками) —
|
||||
корректно для 21M PDF (несколько секунд).
|
||||
- Для несовпадающих размеров хэш не считается (быстрый путь).
|
||||
|
||||
2. **Защита от зависаний**:
|
||||
- `Transport.set_keepalive(15)` — пинги; мёртвое соединение падает само.
|
||||
- `sftp.get_channel().settimeout(120)` — операция дольше 120с прерывается.
|
||||
- `with_retry()` — при исключении переподключение (до 3 попыток, sleep 2с),
|
||||
`put`/`remove` повторяются; после исчерпания — выход с ненулевым кодом.
|
||||
|
||||
3. **Mirror-удаление сохранено**: `remote_files - local_files` → `remove()`.
|
||||
|
||||
4. **CI**: `python3-paramiko` ставится через **apt** (не pip в venv) — не зависит
|
||||
от PyPI, быстрее (установка случайно занимала 15-20 мин).
|
||||
|
||||
## Почему не mtime
|
||||
|
||||
В Gitea Actions `actions/checkout` распаковывает репозиторий с текущим mtime →
|
||||
все файлы «свежие» относительно сервера → mtime-сравнение никогда не пропускает.
|
||||
SHA1 — единственный надёжный индикатор неизменности для CI.
|
||||
|
||||
## Альтернативы, отклонённые
|
||||
|
||||
- **rsync**: Jino SFTP-only, удалённый exec запрещён.
|
||||
- **Манифест (JSON) прошлого деплоя**: усложняет, требует хранения состояния;
|
||||
SHA1-сравнение с сервером самодостаточно.
|
||||
- **Только размер**: риск ложного пропуска при изменении содержимого без смены
|
||||
размера (маловероятно для статики, но SHA1 дешевле ошибки).
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
- Точное время залива (замер №2) зависит от пропускной способности Jino SFTP.
|
||||
- SHA1 крупных файлов при каждом деплое: 21M PDF → ~2-5с на чтение с сервера;
|
||||
приемлемо по сравнению с перезаливкой.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
id: deploy-sftp-incremental
|
||||
created: 2026-09-22
|
||||
schema: spec-driven
|
||||
---
|
||||
|
||||
# Proposal: Инкрементальный SFTP-деплой (только изменившиеся файлы)
|
||||
|
||||
## Problem
|
||||
|
||||
`tools/deploy_sftp.py` при каждом деплое заливает ВСЕ файлы из `public/` (524 файла, 66M)
|
||||
через `sftp.put()` без проверки, изменился ли файл на сервере. Из них ~40M — тяжёлые
|
||||
неизменные файлы (PDF 21M, M4V 11M×2 и др.), которые льются повторно каждый раз.
|
||||
|
||||
Результат (замер №1, 2026-09-22): деплой через Gitea Actions занял **~36 минут** до
|
||||
видимости на проде. Плюс скрипт не имеет таймаутов: при обрыве SFTP-соединения
|
||||
`paramiko.Transport` висит в `sftp.put()` бесконечно (наблюдали на run 40:
|
||||
лог замер на 17:22:54, файл hero.svg 1.7KB не залит, job не завершился).
|
||||
|
||||
## Goal
|
||||
|
||||
- Заливать только файлы, которых нет на сервере или которые изменились (size+mtime).
|
||||
- Не висеть вечно: таймауты на соединение и на операцию, явная ошибка при обрыве.
|
||||
- Сократить время деплоя в разы (цель: < 5 мин до видимости на проде).
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Не трогаем способ вызова (по-прежнему `gitea-main push` → Gitea Actions → `tools/deploy_sftp.py`).
|
||||
- Не трогаем содержание сайта / архив / темы.
|
||||
- Не добавляем rsync (на Jino SFTP-only, удалённый exec запрещён).
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# Spec: Инкрементальный SFTP-деплой
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Скрипт заливает только изменившиеся файлы
|
||||
|
||||
Скрипт сравнивает каждый локальный файл с удалённым и заливает только те, что отсутствуют на сервере или отличаются. Сравнение — по размеру и SHA1-хешу содержимого (mtime не используется: в CI после checkout он всегда свежий, что делает mtime-сравнение бесполезным).
|
||||
|
||||
#### Scenario: Файл уже есть на сервере и не менялся
|
||||
|
||||
Given сервер содержит файл `X` с тем же размером и SHA1, что локальный
|
||||
And локальный файл `X` существует
|
||||
When выполняется `tools/deploy_sftp.py`
|
||||
Then файл `X` НЕ заливается на сервер
|
||||
|
||||
#### Scenario: Файла нет на сервере
|
||||
|
||||
Given сервер НЕ содержит файл `X`
|
||||
And локальный файл `X` существует
|
||||
When выполняется `tools/deploy_sftp.py`
|
||||
Then файл `X` заливается на сервер
|
||||
|
||||
#### Scenario: Файл изменился (другой размер)
|
||||
|
||||
Given сервер содержит файл `X` с размером 100
|
||||
And локальный файл `X` имеет размер 125
|
||||
When выполняется `tools/deploy_sftp.py`
|
||||
Then файл `X` заливается на сервер
|
||||
|
||||
#### Scenario: Файл изменился (тот же размер, другое содержимое)
|
||||
|
||||
Given сервер содержит файл `X` того же размера, но с другим SHA1
|
||||
And локальный файл `X` имеет другой SHA1
|
||||
When выполняется `tools/deploy_sftp.py`
|
||||
Then файл `X` заливается на сервер
|
||||
|
||||
### Requirement: Скрипт не виснет при обрыве соединения
|
||||
|
||||
Скрипт использует keepalive (15с) и таймаут канала (120с), а также переподключение с ретраями (до 3 попыток), чтобы при обрыве канала завершиться с ошибкой или продолжить, а не висеть бесконечно.
|
||||
|
||||
#### Scenario: Соединение с сервером оборвалось
|
||||
|
||||
Given SFTP-соединение с Jino работает
|
||||
And соединение обрывается во время `put()`
|
||||
When выполняется `tools/deploy_sftp.py`
|
||||
Then скрипт переподключается и повторяет операцию; если все попытки исчерпаны — завершается с ненулевым кодом и сообщением об ошибке
|
||||
|
||||
### Requirement: Удаление лишних файлов сохранено
|
||||
|
||||
Скрипт продолжает удалять с сервера файлы, которых нет в локальном `public/` (mirror-поведение).
|
||||
|
||||
#### Scenario: На сервере есть файл, которого нет локально
|
||||
|
||||
Given сервер содержит файл `Y`
|
||||
And локальный каталог `public/` не содержит `Y`
|
||||
When выполняется `tools/deploy_sftp.py`
|
||||
Then файл `Y` удаляется с сервера
|
||||
@@ -0,0 +1,20 @@
|
||||
# Tasks: Инкрементальный SFTP-деплой
|
||||
|
||||
- [x] Обновить `tools/deploy_sftp.py`: инкремент по (size + SHA1), keepalive/таймаут канала, ретраи с переподключением
|
||||
- [x] Проверить синтаксис (`python3 -m py_compile`) и логику same_file (юнит-тест без сети)
|
||||
- [x] CI: заменить pip-установку paramiko на apt `python3-paramiko` (не зависит от PyPI) в `.gitea/workflows/deploy.yml`
|
||||
- [x] Обновить openspec (spec/design/tasks) под SHA1-решение
|
||||
- [x] Замерить деплой №2 после запуска run на новом коде: сравнить с эталоном ~36 мин
|
||||
- [x] Прогнать openspec-archive-change (из прошлого change) — замер №1 зафиксирован в tasks.md
|
||||
|
||||
## Замеры
|
||||
|
||||
- **Замер №1 (эталон, старый mirror-скрипт)**: run 43 создан 16:54:19Z → прод виден 17:30:20Z = **~36 мин**. Полный job не завершился (завис на мёртвом SFTP).
|
||||
- **Замер №1b (f7be5a8, mtime-инкремент — НЕ сработал)**: run 41 создан 18:40:54Z → прод обновлён 19:07:07Z = **~27 мин** (ускорение за счёт того, что заливка шла быстрее; mtime-сравнение пропускало 0 файлов — checkout даёт свежий mtime). Job завис на том же `2026/gotosocial-relay-match-by-default/hero.svg`.
|
||||
- **Замер №2 (SHA1 + apt-paramiko)**: run 43 (92b7094) — SUCCESS за **12.7 мин** (19:37:09 → 19:49:50Z), «Готово: 0 залито, 482 пропущено» при неизменном контенте. Выигрыш: ~36 мин → ~12-13 мин на пустой деплой; при изменениях льётся только diff вместо 66M.
|
||||
|
||||
## Блокеры
|
||||
|
||||
- Run 41 (f7be5a8) висит в Gitea Actions на мёртвом SFTP (нет API-cancel; runner на bigbox недоступен) — новый run (07ba1e4-правки) ждёт освобождения runner'а. По умолчанию Gitea ждёт до ~6ч.
|
||||
- Gitea не создаёт следующий run по ветке, пока по ней есть active run (наблюдение).
|
||||
- `socket.setdefaulttimeout(120)` НЕ прерывает зависший put в paramiko (канал создан до вызова) — заменён на `channel.settimeout(120)`.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Design: Серии постов, переключатель темы справа, соцсети в меню
|
||||
|
||||
## Constraints
|
||||
|
||||
- Тема `rDedInit` (форк PaperMod) НЕ модифицируется. Все правки — через
|
||||
штатные хуки: корневой `layouts/partials/extend_footer.html` (JS-инжект),
|
||||
корневой `layouts/partials/extend_head.html` (CSS), `assets/css/extended/`,
|
||||
`hugo.toml`.
|
||||
- OpenSpec: change создаётся до правок.
|
||||
|
||||
## 1. Таксономия series
|
||||
|
||||
- `hugo.toml`: `[taxonomies] series = "series"` (в дополнение к
|
||||
`category`/`tag`).
|
||||
- Новый партиал `layouts/partials/series_nav.html`:
|
||||
- принимает страницу, читает `.Params.series` (массив) и `.Params.series_order`;
|
||||
- для первой серии берёт `site.Taxonomies.series` → term → `.Pages`,
|
||||
сортирует по `series_order`, ищет текущий, выводит блок:
|
||||
«Серия: НАЗВАНИЕ · № текущий из N» + ссылки «← Пред.» / «След. →»;
|
||||
- если параметров нет — не выводит ничего.
|
||||
- Подключение: в корневом `layouts/_default/single.html` (override с
|
||||
`{{- define "main" }}`? НЕТ — переопределять весь single.html опасно).
|
||||
Вместо этого: тот же подход, что у PaperMod — вывод БЛОКА серии в начале
|
||||
`.post-content`. В rDedInit single.html НЕ имеет content-хука. Безопасный
|
||||
вариант — **JS-инжект невозможен для контента**, поэтому навешиваем через
|
||||
partial в `single.html`... но это правка темы!
|
||||
|
||||
Обход: PaperMod'ы имеют `layouts/partials/extend_head.html` и
|
||||
`extend_footer.html`, но для контента single.html нет хука. Классический
|
||||
PaperMod-приём: **`assets/scss/extended/` + margin**, нет.
|
||||
Реально: тема rDedInit НЕ поддерживает серии. Добавить можно только:
|
||||
(а) форкнуть single.html в корневой `layouts/_default/single.html`
|
||||
(переопределение — штатный механизм Hugo, тема не трогается), и вставить
|
||||
`{{- partial "series_nav.html" . -}}` перед `.post-content`.
|
||||
Это переопределение будет жить в репозитории сайта и переживёт апдейты темы
|
||||
(требует ручного обновления при изменении темы — приемлемо, тема
|
||||
статична).
|
||||
|
||||
Решение: **переопределить `layouts/_default/single.html`** в корне сайта,
|
||||
скопировав из темы (65 строк) и добавив `{{- partial "series_nav.html" . }}`.
|
||||
|
||||
## 2. Переключатель темы справа
|
||||
|
||||
- `layouts/partials/extend_footer.html` (уже есть) — добавить JS:
|
||||
```js
|
||||
const nav = document.querySelector('.nav');
|
||||
const sw = document.querySelector('.logo .logo-switches');
|
||||
if (nav && sw && !sw.dataset.moved) {
|
||||
sw.dataset.moved = '1';
|
||||
nav.appendChil...[truncated]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Proposal: Серии постов (series), переключатель темы справа, соцсети в меню
|
||||
|
||||
## Why
|
||||
|
||||
Три улучшения шапки и контента dedinit.ru:
|
||||
|
||||
1. **Таксономия `series`** (как в Blowfish): сгруппировать посты в серии.
|
||||
Статья, помеченная `series`, показывает на странице модуль серии со
|
||||
ссылками на другие части (навигация «Серия: N из M»). Без новых сущностей
|
||||
контента — только taxonomies + вывод.
|
||||
|
||||
2. **Переключатель темы (солнце/луна) — вправо, рядом с меню**. Сейчас он
|
||||
вложен в `.logo` (структура PaperMod) и не перемещается CSS-селектором
|
||||
`.nav > .logo-switches`. Нужен перенос элемента в конец `.nav` (после
|
||||
`#menu`) — через JS-инжект в `extend_footer.html` (штатный хук темы),
|
||||
чтобы не трогать саму тему.
|
||||
|
||||
3. **Иконки соцсетей в верхнем меню**. Стандартные `[params.socialIcons]`
|
||||
(как в Blowfish) рендерятся партиалом `social_icons.html`, но он вызывается
|
||||
только в профиле/на главной. Нужно вывести иконки (github, telegram,
|
||||
mastodon, rss) в шапку рядом с меню — через the same JS-инжект.
|
||||
|
||||
## User-Facing Behavior
|
||||
|
||||
- В `hugo.toml` появляется `[taxonomies] series = "series"`.
|
||||
- Пост с `series: ["Имя"]` и `series_order: N` показывает в начале контента
|
||||
блок «Серия: Имя (N)» со ссылками на предыдущую/следующую часть.
|
||||
- В шапке солнце/луна стоит справа, сразу после пунктов меню.
|
||||
- Рядом с меню — иконки соцсетей (из `[params.socialIcons]`).
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Series and Menu Capability
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Series taxonomy
|
||||
|
||||
Таксономия `series` позволяет группировать посты в серии и показывает на
|
||||
странице поста модуль навигации по серии.
|
||||
|
||||
#### Scenario: Taxonomy enabled
|
||||
- Given hugo.toml has `[taxonomies] series = "series"`
|
||||
- When site is built
|
||||
- Then `/series/` term page exists and posts can be marked with `series`
|
||||
|
||||
#### Scenario: Series module on a post
|
||||
- Given a post with `series: ["Test"]` and `series_order: 2` in front-matter
|
||||
- When that post is rendered
|
||||
- Then a "Серия" block is visible at the top of the content
|
||||
- And it shows the series name, position («2 из N»)
|
||||
- And it links to the previous article (if any) and next article (if any)
|
||||
|
||||
### Requirement: Theme toggle on the right
|
||||
|
||||
Переключатель темы (солнце/луна) отображается в правой части шапки, сразу
|
||||
после пунктов меню.
|
||||
|
||||
#### Scenario: Toggle next to the menu
|
||||
- Given the header markup with `.logo-switches` inside `.logo`
|
||||
- When the page is rendered
|
||||
- Then the theme toggle button is displayed at the right end of `.nav`,
|
||||
after `#menu`
|
||||
|
||||
### Requirement: Social icons in the header
|
||||
|
||||
Иконки соцсетей из `[params.socialIcons]` отображаются в верхнем меню рядом
|
||||
с переключателем темы.
|
||||
|
||||
#### Scenario: Icons rendered from params
|
||||
- Given `[params.socialIcons]` with github and telegram entries
|
||||
- When the page is rendered
|
||||
- Then links with SVG icons appear in the header nav, next to the menu
|
||||
@@ -0,0 +1,39 @@
|
||||
# Tasks: Series taxonomy, theme toggle right, social icons in menu
|
||||
|
||||
## 1. Таксономия series
|
||||
|
||||
- [x] 1.1 Добавить `[taxonomies] series = "series"` в `hugo.toml`
|
||||
- [x] 1.2 Создать `layouts/partials/series_nav.html` — модуль «Серия: N из M» со
|
||||
ссылками на пред./след. часть (сортировка по `series_order`)
|
||||
- [x] 1.3 Переопределить `layouts/_default/single.html` (копия из темы) и
|
||||
подключить `series_nav.html` перед `.post-content`
|
||||
|
||||
## 2. Переключатель темы справа
|
||||
|
||||
- [x] 2.1 В `layouts/partials/extend_footer.html` добавить JS: перенос
|
||||
`.logo .logo-switches` в конец `.nav` (под флагом, чтобы не дублировать)
|
||||
- [x] 2.2 В `assets/css/extended/typewriter.css` закрепить `.logo-switches`
|
||||
(margin-inline-start) — чтобы встал сразу после `#menu`
|
||||
|
||||
## 3. Соцсети в меню
|
||||
|
||||
- [x] 3.1 Добавить `[params.socialIcons]` в `hugo.toml` (github, telegram, rss)
|
||||
- [x] 3.2 В `extend_footer.html` добавить server-rendered блок иконок
|
||||
(`social_icons` через `svg.html` темы) и JS-перенос в `.nav` рядом с меню
|
||||
- [x] 3.3 ФИКС: убрать inline `style="display:none"` (перебивал CSS при переносе),
|
||||
скрытие через `#__social-icons` (CSS), JS снимает id → блок видим
|
||||
|
||||
## 4. Теги в логотипе (typewriter)
|
||||
|
||||
- [x] 4.1 Слова печати = реальные `site.Taxonomies.tags` (популярные первыми,
|
||||
топ-24, без служебных) вместо хардкода `[params.typewriter].words`
|
||||
- [x] 4.2 Удалить устаревший `[params.typewriter]` из `hugo.toml`
|
||||
|
||||
## 5. Проверка и деплой
|
||||
|
||||
- [x] 5.1 `make build` без ошибок
|
||||
- [x] 5.2 Локальные проверки: series-nav на странице серии (2 поста — prev/next
|
||||
работают), таксономия `/series/` собрана, соцсети в шапке (CSS-скрытие +
|
||||
снятие id), переключатель переносится JS, теги в JS-массиве
|
||||
- [ ] 5.3 `git commit` + push
|
||||
- [ ] 5.4 `make deploy`
|
||||
Reference in New Issue
Block a user