feat(subscriptions): add personal AI subscriptions page #9

Merged
slaid098 merged 1 commit from feat/subscriptions-page into main 2026-08-07 15:57:52 +03:00
Owner

Что сделано

Реализована статичная страница /subscriptions — личная AI-подписки (issue #7).

Новые файлы

  • src/lib/subscriptions.ts — конфиг 4 сервисов (имя + URL)
  • src/app/[locale]/subscriptions/page.tsx — статичная страница (SSG), 4 карточки + сноска + JSON-LD
  • src/__tests__/subscriptions-metadata.test.ts — 6 тестов metadata
  • src/__tests__/subscriptions.test.ts — 7 тестов конфига + словарей
  • src/__tests__/subscriptions-page.test.tsx — 2 теста footer-ссылки

Изменённые файлы

  • src/lib/i18n.ts — секция subscriptions в типе Dict + ruDict/enDict, поле footer.subscriptions
  • src/components/site-footer.tsx — текстовая ссылка next/link (между madeBy и heart)
  • src/app/sitemap.ts — роут /subscriptions (priority 0.6)
  • biome.json — override noDangerouslySetInnerHtml off для JSON-LD

Карточки

Имена брендов не локализуются (только в конфиге), описания — в i18n (ru/en). Внешние ссылки target="_blank" rel="noopener noreferrer". Footer-ссылка через next/link (client-side nav).

Почему

SEO-трафик + реферальные ссылки. Контент стабильный (только личный опыт и механика лимитов, без конкретных цифр/сумм). Изменчивость покрыта сноской со звёздочкой. Страница полностью статическая (SSG), как /contacts — никаких cookies()/headers()/fetch к внешним API.

Watch out

  • Biome override paths: при миграции роутов (rename папки) пер-файл overrides ломаются без ошибки (lint перестаёт применять override). Добавлен src/app/[locale]/subscriptions/page.tsx в overrides[0].include.
  • Footer SVG-тест: contacts.test.tsx проверяет svgs[0] (heart) и svgs[1] (forgejo). Новая ссылка текстовая (без SVG) — индексы не сбились, тест проходит.
  • exactOptionalPropertyTypes: карточки описаний через cards[i]?.description ?? "" (noUncheckedIndexedAccess guard).

Pending

  • Редизайн шапки (#8) — после влития этого PR (меню должно содержать «Подписки»).
  • EN-переводы — адаптированные (не дословные).

Closes #7

## Что сделано Реализована статичная страница `/subscriptions` — личная AI-подписки (issue #7). ### Новые файлы - `src/lib/subscriptions.ts` — конфиг 4 сервисов (имя + URL) - `src/app/[locale]/subscriptions/page.tsx` — статичная страница (SSG), 4 карточки + сноска + JSON-LD - `src/__tests__/subscriptions-metadata.test.ts` — 6 тестов metadata - `src/__tests__/subscriptions.test.ts` — 7 тестов конфига + словарей - `src/__tests__/subscriptions-page.test.tsx` — 2 теста footer-ссылки ### Изменённые файлы - `src/lib/i18n.ts` — секция `subscriptions` в типе `Dict` + `ruDict`/`enDict`, поле `footer.subscriptions` - `src/components/site-footer.tsx` — текстовая ссылка `next/link` (между `madeBy` и heart) - `src/app/sitemap.ts` — роут `/subscriptions` (priority 0.6) - `biome.json` — override `noDangerouslySetInnerHtml` off для JSON-LD ### Карточки - OpenCode Go — https://opencode.ai/go - Synthetic.new — https://synthetic.new/?referral=KKPRqRfw - Ollama — https://ollama.com - Umans — https://app.umans.ai/register?ref=YZ78E3D7 Имена брендов не локализуются (только в конфиге), описания — в i18n (ru/en). Внешние ссылки `target="_blank" rel="noopener noreferrer"`. Footer-ссылка через `next/link` (client-side nav). ## Почему SEO-трафик + реферальные ссылки. Контент стабильный (только личный опыт и механика лимитов, без конкретных цифр/сумм). Изменчивость покрыта сноской со звёздочкой. Страница полностью статическая (SSG), как `/contacts` — никаких `cookies()`/`headers()`/fetch к внешним API. ## Watch out - **Biome override paths**: при миграции роутов (rename папки) пер-файл overrides ломаются без ошибки (lint перестаёт применять override). Добавлен `src/app/[locale]/subscriptions/page.tsx` в `overrides[0].include`. - **Footer SVG-тест**: `contacts.test.tsx` проверяет `svgs[0]` (heart) и `svgs[1]` (forgejo). Новая ссылка текстовая (без SVG) — индексы не сбились, тест проходит. - **`exactOptionalPropertyTypes`**: карточки описаний через `cards[i]?.description ?? ""` (noUncheckedIndexedAccess guard). ## Pending - Редизайн шапки (#8) — после влития этого PR (меню должно содержать «Подписки»). - EN-переводы — адаптированные (не дословные). Closes #7
4 сервиса (OpenCode Go, Synthetic.new, Ollama, Umans)
статичная страница /subscriptions (SSG как /contacts)
секция i18n subscriptions (ru/en) + footer link
sitemap + biome override
тесты: metadata, config, footer

Closes #7
Author
Owner

Code Review Summary

Реализована статичная страница /subscriptions (issue #7) — 4 карточки AI-подписок + сноска + JSON-LD + footer-ссылка + sitemap-роут. Код следует паттерну /contacts (SSG, тот же стиль metadata/JSON-LD), тесты покрывают конфиг/словари/metadata/footer. Все контракты ТЗ выполнены verbatim.

Positives

  • Тексты карточек RU — VERBATIM из ТЗ (проверены побуквенно все 4 описания в src/lib/i18n.ts:170,174,178,182). EN — адаптированный перевод (как и требует ТЗ: «EN — адаптированный перевод»).
  • URL сервисов — точное совпадение с ТЗ, включая query-параметры рефералов: ?referral=KKPRqRfw (Synthetic.new), ?ref=YZ78E3D7 (Umans) — src/lib/subscriptions.ts:8,10.
  • Сноска RU — VERBATIM из ТЗ со звёздочкой (src/lib/i18n.ts:166).
  • Footer-ссылка через next/link (НЕ <a>), внутренняя (без target), класс text-muted transition hover:text-accent, текст «Подписки»/«Subscriptions» через i18n, расположена рядом с madeBysrc/components/site-footer.tsx:11-13. Шапку НЕ трогали.
  • Внешние ссылкиtarget="_blank" rel="noopener noreferrer" на обеих ссылках карточки (заголовок + кнопка) — src/app/[locale]/subscriptions/page.tsx:60-61,69-70.
  • JSON-LD@id/url = ${baseUrl}/${loc}/subscriptions, inLanguage через jsonLdLanguage(loc)page.tsx:40,43,44.
  • Metadata — canonical /${loc}/subscriptions, OG title с суффиксом // slaid098.dev, OG locale через ogLocale(loc)page.tsx:18,20,23.
  • Статичность — нет cookies()/headers()/fetch к внешним API/export const revalidate. Страница SSG (layout имеет generateStaticParams для locale).
  • Biome override корректно добавлен для src/app/[locale]/subscriptions/page.tsx (noDangerouslySetInnerHtml: off для JSON-LD) — biome.json:42.
  • TypeScriptnoUncheckedIndexedAccess guard card?.description ?? "" (page.tsx:55), exactOptionalPropertyTypes соблюдён.
  • Тесты покрывают: 4 сервиса, валидные https URL, 4 описания ru/en, presence footnote/heading/openButton/title/description, footer-ссылка ru/en, metadata (title/description/canonical/OG locale ru/en) — 15 тестов в 3 файлах.
  • Существующие тесты не сломаныcontacts.test.tsx:142-143 проверяет svgs[0] (heart) и svgs[1] (forgejo). Новая subscriptions-ссылка текстовая (через next/link, без SVG) → SVG-индексы не сбились.
  • KnipSUBSCRIPTIONS используется в page + tests; секция subscriptions i18n используется в page + tests. Мёртвого кода нет.
  • Архитектура — конфиг в src/lib/subscriptions.ts (отдельный файл, readonly массив), страница следует существующей структуре роутов, тесты в src/__tests__/. Файлы < 300 строк, функции < 30 строк (компонент 57 строк — JSX render, нормально).
  • PR body## Что сделано / ## Почему / ## Watch out / ## Pending заполнены осмысленно, Closes #7.

Suggestions (info, not blocking)

  • src/app/[locale]/subscriptions/page.tsx:54const card = d.subscriptions.cards[i] прямой индексный доступ. Guard card?.description ?? "" на строке 55 корректно обрабатывает T | undefined (noUncheckedIndexedAccess). Можно было бы использовать SUBSCRIPTIONS[i] + cards.find, но текущий подход соответствует паттерну и работает — менять не нужно.
  • src/app/[locale]/subscriptions/page.tsx:57key={sub.url} — URL как React key. Уникальны и стабильны — корректный выбор.

Контракты ТЗ выполнены. CI на PR не запустился (проблема триггера ci.yml — отдельная проблема, не блокер review). Локально biome/tsc/vitest/build зелёные (153 теста).

Verdict: APPROVE

## Code Review Summary Реализована статичная страница `/subscriptions` (issue #7) — 4 карточки AI-подписок + сноска + JSON-LD + footer-ссылка + sitemap-роут. Код следует паттерну `/contacts` (SSG, тот же стиль metadata/JSON-LD), тесты покрывают конфиг/словари/metadata/footer. Все контракты ТЗ выполнены verbatim. ### Positives - **Тексты карточек RU — VERBATIM из ТЗ** (проверены побуквенно все 4 описания в `src/lib/i18n.ts:170,174,178,182`). EN — адаптированный перевод (как и требует ТЗ: «EN — адаптированный перевод»). - **URL сервисов — точное совпадение** с ТЗ, включая query-параметры рефералов: `?referral=KKPRqRfw` (Synthetic.new), `?ref=YZ78E3D7` (Umans) — `src/lib/subscriptions.ts:8,10`. - **Сноска RU — VERBATIM из ТЗ** со звёздочкой (`src/lib/i18n.ts:166`). - **Footer-ссылка** через `next/link` (НЕ `<a>`), внутренняя (без `target`), класс `text-muted transition hover:text-accent`, текст «Подписки»/«Subscriptions» через i18n, расположена рядом с `madeBy` — `src/components/site-footer.tsx:11-13`. Шапку НЕ трогали. - **Внешние ссылки** — `target="_blank" rel="noopener noreferrer"` на обеих ссылках карточки (заголовок + кнопка) — `src/app/[locale]/subscriptions/page.tsx:60-61,69-70`. - **JSON-LD** — `@id`/`url` = `${baseUrl}/${loc}/subscriptions`, `inLanguage` через `jsonLdLanguage(loc)` — `page.tsx:40,43,44`. - **Metadata** — canonical `/${loc}/subscriptions`, OG title с суффиксом `// slaid098.dev`, OG locale через `ogLocale(loc)` — `page.tsx:18,20,23`. - **Статичность** — нет `cookies()`/`headers()`/fetch к внешним API/`export const revalidate`. Страница SSG (layout имеет `generateStaticParams` для locale). - **Biome override** корректно добавлен для `src/app/[locale]/subscriptions/page.tsx` (`noDangerouslySetInnerHtml: off` для JSON-LD) — `biome.json:42`. - **TypeScript** — `noUncheckedIndexedAccess` guard `card?.description ?? ""` (`page.tsx:55`), `exactOptionalPropertyTypes` соблюдён. - **Тесты** покрывают: 4 сервиса, валидные https URL, 4 описания ru/en, presence footnote/heading/openButton/title/description, footer-ссылка ru/en, metadata (title/description/canonical/OG locale ru/en) — 15 тестов в 3 файлах. - **Существующие тесты не сломаны** — `contacts.test.tsx:142-143` проверяет `svgs[0]` (heart) и `svgs[1]` (forgejo). Новая subscriptions-ссылка текстовая (через `next/link`, без SVG) → SVG-индексы не сбились. - **Knip** — `SUBSCRIPTIONS` используется в page + tests; секция `subscriptions` i18n используется в page + tests. Мёртвого кода нет. - **Архитектура** — конфиг в `src/lib/subscriptions.ts` (отдельный файл, `readonly` массив), страница следует существующей структуре роутов, тесты в `src/__tests__/`. Файлы < 300 строк, функции < 30 строк (компонент 57 строк — JSX render, нормально). - **PR body** — `## Что сделано` / `## Почему` / `## Watch out` / `## Pending` заполнены осмысленно, `Closes #7`. ### Suggestions (info, not blocking) - **src/app/[locale]/subscriptions/page.tsx:54** — `const card = d.subscriptions.cards[i]` прямой индексный доступ. Guard `card?.description ?? ""` на строке 55 корректно обрабатывает `T | undefined` (noUncheckedIndexedAccess). Можно было бы использовать `SUBSCRIPTIONS[i]` + `cards.find`, но текущий подход соответствует паттерну и работает — менять не нужно. - **src/app/[locale]/subscriptions/page.tsx:57** — `key={sub.url}` — URL как React key. Уникальны и стабильны — корректный выбор. Контракты ТЗ выполнены. CI на PR не запустился (проблема триггера ci.yml — отдельная проблема, не блокер review). Локально biome/tsc/vitest/build зелёные (153 теста). ### Verdict: APPROVE
slaid098 deleted branch feat/subscriptions-page 2026-08-07 15:57:52 +03:00
Sign in to join this conversation.
No reviewers
No labels
bug
performance
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/slaid098-dev!9
No description provided.