From 8cb3f1a8f0f9818b48acdcb1530dc7f038e36052 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sun, 27 Sep 2026 17:04:41 +0500 Subject: [PATCH] docs: update to slug - id changes --- README.md | 11 +- docs/README.md | 8 +- docs/architecture.md | 173 ++++++++++++++++++-------------- docs/backlog.md | 47 ++++----- docs/decisions.md | 27 ++++- docs/plan-fields-audit-fixes.md | 11 +- docs/plan-seo.md | 64 ++++++------ docs/roadmap.md | 6 +- docs/tools-map.md | 21 ++-- 9 files changed, 204 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index 4a08e8d..49ac8db 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,12 @@ pnpm build # static production export → web/build/ ## Architecture -Everything is built around the **tool registry** (`web/src/lib/registry/`): each -tool is a self-contained entry -`{id, title, description, category, schema, run}`. Pages, forms, and pipelines -are generated from it, and all execution goes through a single -`executor.execute` entry point. +Everything is built around the **two-layer registry** (`web/src/lib/registry/`): +a tool is a self-contained implementation `{id, schema, input, result, run}` +(`registry/tools/`), and a page is `{slug, title, description, category, steps}` +(`registry/pages/`) that names the address and the tools to run — one tool can +back several pages. Pages, forms, and pipelines are generated from it, and all +execution goes through a single `executor.execute` entry point. Core image processing lives in `web/src/lib/core/` and operates directly on `ImageData`, with no browser API dependencies. diff --git a/docs/README.md b/docs/README.md index 6d5ee2d..fe6fca7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -56,10 +56,10 @@ Единственный документ в архиве со статусом `active` — `docs/archive/README.md`: он описывает процедуру архивирования, а не архивный материал, поэтому не подлежит 30-дневному удалению. -- Ссылаясь на инструмент по id, сверять id с реестром (`web/src/lib/registry/`): - исторические названия в старых планах не действуют. Сейчас `id` записи — это - одновременно адрес страницы `/tools/`, отдельного поля `slug` нет - (`docs/architecture.md`, раздел 3). +- Ссылаясь на инструмент, сверяться с реестром: внутреннее имя (`id`) — в + `web/src/lib/registry/tools/`, адрес и имя страницы (`slug`) — в + `web/src/lib/registry/pages/`; исторические названия в старых планах не + действуют (`docs/architecture.md`, раздел 3). - Поиск по `docs/` задевает `docs/archive/` (объём больше, чем у активных документов вместе). Для правок production-кода архив не нужен: grep по `docs/*.md` без `archive/`. diff --git a/docs/architecture.md b/docs/architecture.md index 025c6a9..686a9f0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -9,11 +9,13 @@ ```txt web/src/lib/ - registry/ # источник истины: записи ToolEntry + registry/ # источник истины: инструменты и страницы + registry/tools/ # Tool: реализации (id), без адресов и текстов + registry/pages/ # Page: адрес, тексты, категория, список шагов registry-schema.ts # декларативная схема параметров + санитизация дефолтов - catalog.ts # группировка и счётчики для /list-tools + catalog.ts # группировка страниц и счётчики для /list-tools categories.ts # id/названия категорий - tool-icons.ts # id → иконка + tool-icons.ts # slug страницы → иконка core/ # чистый TS: операции над ImageData, без DOM executor/ # единственная точка исполнения инструментов i18n/ # словари ru/en, t(), списки для i18n-правил линтера @@ -23,7 +25,7 @@ web/src/lib/ web/src/routes/ +page.svelte # главная list-tools/ # каталог - tools/[id]/ # страница инструмента (адрес = id записи) + tools/[slug]/ # страница инструмента (адрес = slug страницы) kit/ # витрина компонентов (production, но исключена из i18n-гейта) ``` @@ -34,15 +36,18 @@ web/src/routes/ ### Чего в коде нет -Страница инструмента работает ровно с одним инструментом: загрузил файл → -применил одну запись реестра → скачал результат. Отсутствуют: +Страница исполняет ровно один шаг: загрузил файл → применила инструмент → +скачала результат. Отсутствуют: - **Пайплайн / цепочки шагов** — модуля нет, состояния нет, localStorage для - него нет. `docs/roadmap.md` описывает его как отдельную фазу, а не как - поставленное. Под будущую фазу зарезервированы `components/StepCard.svelte` + него нет. Модель страницы уже хранит список `steps`, но исполняет первый шаг, + а `registry.test.ts` требует, чтобы шаг был ровно один: композиция формы из + полей двух инструментов — отдельная работа, описанная как фаза 11 + `docs/roadmap.md`. Под неё зарезервированы `components/StepCard.svelte` (сейчас импортируется только витриной `/kit`) и ключи `chain.*`, `errors.badPipelineShape`, `errors.noSteps`. -- **Разделения `id` и `slug`** — раздел 3. +- **Разделения страниц, которые делят один инструмент** — сводит 14 + convert-страниц на один инструмент; задача в `docs/backlog.md`. Задачи по обоим пунктам — в `docs/backlog.md`; архитектурные решения по пайплайну — `docs/roadmap.md`. @@ -50,15 +55,15 @@ web/src/routes/ ## 2. Поток данных ```txt -registry/*.ts ──(TOOLS: ToolEntry[]) +registry/{tools,pages}/*.ts │ - ├─► catalog.ts ──► routes/list-tools (каталог, поиск, фильтры) + ├─► catalog.ts ──► routes/list-tools (каталог страниц, поиск, фильтры) │ - └─► routes/tools/[id]/+page.svelte - │ getTool(id) → запись реестра + └─► routes/tools/[slug]/+page.svelte + │ getPageBySlug(slug) → Page, getTool(steps[0].id) → Tool ▼ SchemaToolView.svelte оркестратор страницы - │ schema = tool.schema + │ schema = tool.schema, тексты и имя файла — от page ├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол) ├─► SchemaSourceTile/SchemaTextSource.svelte вход └─► SchemaPreview + schema-preview-model.ts вид превью @@ -71,14 +76,15 @@ registry/*.ts ──(TOOLS: ToolEntry[]) │ ▼ SchemaResultTile / SchemaTextResult + SchemaActions (download/copy) - │ кодирование по ToolEntry.output (mime/ext/quality) + │ кодирование по Tool.output (mime/ext/quality) └─► zip.ts, если result === "files" ``` Ключевые решения: - **Реестр — единственный источник истины.** Страница и форма строятся из - записи; новый инструмент = новая запись + функция `run`. + записей; новый инструмент = новая запись `Tool` + функция `run` и новая + страница `Page` на него. - **Исполнение идёт только через `executor.execute`.** Компоненты не зовут `tool.run` напрямую — иначе нельзя перенести тяжёлое в worker/wasm без правки вызывающего кода. @@ -87,39 +93,50 @@ registry/*.ts ──(TOOLS: ToolEntry[]) - **Модель превью отделена от вёрстки.** `schema-preview-model.ts` — чистая функция, покрыта unit-тестами; `SchemaPreview.svelte` только рисует её вывод. -## 3. Запись реестра +## 3. Инструмент и страница -`ToolEntry` (`web/src/lib/registry/types.ts`): +Реестр двухслойный: реализация и то, что видит пользователь, лежат в разных +файлах. `Tool` (`web/src/lib/registry/types.ts`, записи в +`registry/tools/.ts`): -| Поле | Смысл | -| ------------- | ------------------------------------------------------------------------------------------------ | -| `id` | идентичность инструмента **и** публичный адрес `/tools/` **и** база имени скачиваемого файла | -| `title` | EN-строка реестра, переводится через `tools.*.title` | -| `description` | EN-строка реестра, переводится через `tools.*.description` | -| `category` | id из `categories.ts` | -| `schema` | `toolSchema({ fields, layout?, meta? })` | -| `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе | -| `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт | -| `domOnly` | инструмент требует DOM (canvas); превью-executor запускает его напрямую, минуя worker | -| `output` | mime/ext/quality для download (не у всех инструментов) | -| `icon` | id иконки из `tool-icons.ts` | -| `run(ctx)` | единственный метод: `ctx` → `ToolResult` | +| Поле | Смысл | +| ---------- | ------------------------------------------------------------------------------ | +| `id` | внутреннее имя инструмента, уникально; ключ `tools..*` и `toolId` в worker | +| `schema` | `toolSchema({ fields, layout?, meta? })` | +| `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе | +| `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт | +| `domOnly` | инструмент требует DOM (canvas); превью-executor запускает его напрямую | +| `output` | mime/ext/quality для скачивания (не у всех) | +| `run(ctx)` | единственный метод: `ctx` → `ToolResult` | -Отдельного поля `slug` **нет**: адрес страницы, ключ i18n (`tools..*`) и имя -файла скачивания — это один и тот же `id`. Инструмент работает с любым -поддержанным форматом, но страница держит png-интент, поэтому `-png` живёт -внутри `id` (`id: "crop-png"` → `/tools/crop-png` → `crop-png.png`). Следствие: -переименование `id` меняет адрес страницы, ключи `tools.*` и имя файла. +`Page` (записи в `registry/pages/.ts`): + +| Поле | Смысл | +| ------------- | ---------------------------------------------------------------------- | +| `slug` | уникальный адрес `/tools/`, имя скачиваемого файла и ключ иконки | +| `title` | EN-строка страницы, переводится через `pages..title` | +| `description` | EN-строка страницы, переводится через `pages..description` | +| `category` | id из `categories.ts`; по нему каталог группирует страницы | +| `steps` | упорядоченный список шагов `{ id, params? }` — ссылки на инструменты | + +Кто по чему адресуется: `slug` — маршрут, имя файла, `TOOL_ICONS`, поиск по +каталогу; `id` — ключ `tools..*` (подписи полей, `options`, `results`), +`toolId` worker-протокола и запись пайплайна. Один инструмент может обслуживать +несколько страниц, одна страница — состоять из нескольких инструментов. + +Инструмент работает с любым поддержанным форматом, поэтому png-интент живёт +только в `slug`: `id: "crop"` → страница `slug: "crop-png"` → `crop-png.png`. +Правило имени: снять суффикс `-png` и в середине имени (`png-to-base64` → +`to-base64`, `X-to-png` → `from-X`, `verify-is-png` → `verify-png`); следствие — +переименование `id` не трогает адреса, а переименование `slug` меняет адрес, имя +файла, ключ `pages.*`, иконку и запись в `docs/tools-map.md`. Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS` -собирается в `registry/index.ts` из восьми групп; `getTool(id)` — единственный -способ достать запись, `registry.test.ts` проверяет уникальность `id`. - -Разделение `id` (внутреннее имя) и `slug` (адрес) — решённое, но **не -реализованное** решение: задача и порядок волн в `docs/backlog.md`, раздел -«Мелочи всякие». До её выполнения этот документ описывает текущее состояние, а -id и slug совпадают. +и `PAGES` собираются в `registry/index.ts` из восьми групп; `getTool(id)` и +`getPageBySlug(slug)` — способы достать запись, `registry.test.ts` проверяет +уникальность обоих имён, существование инструмента под каждым шагом и то, что +шаг ровно один. ## 4. Схема параметров @@ -154,52 +171,58 @@ i18n-подписи → тест в `registry-schema.test.ts`. Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с `LOCALES`/`BASE_LOCALE`). Пользовательский текст живёт только в словарях и -читается через `t()`; строки реестра (`title`/`description`) — источник для -EN-ключей `tools.*` и словарь-переводчик. Инварианты паритета ключей, -плейсхолдеров и запрет хардкода проверяются линтером, детали — -`web/eslint-plugins/README.md`. +читается через `t()`. Секции разделены по слоям: `pages..title` и +`pages..description` — тексты страницы (в базовой локали секция пустая, EN +берётся из реестра), `tools..options/params/results` — подписи полей и +тексты вердиктов инструмента, `fields.*`/`groups.*` — общие подписи схемы. +Инварианты паритета ключей, плейсхолдеров и запрет хардкода проверяются +линтером, детали — `web/eslint-plugins/README.md`. ## 7. Точки расширения -| Задача | Где править | -| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Новый инструмент | запись в `registry/.ts` + i18n-ключи + тесты (рецепт ниже) | -| Смена `id` | запись реестра + i18n-ключи `tools.*` + `tool-icons.ts` + хардкод id в тестах, `/kit` и e2e; меняется адрес страницы — операция с SEO-последствиями, не часть обычной задачи | -| Новый вид поля | `registry-schema.ts` + контрол + `FIELDS` (раздел 4) | -| Новый маршрут/страница | `web/src/routes/**` + ссылка в каталоге | -| Новый визуальный элемент | компонент в `components/**` по образцу `ui/` или `layout/` | -| Новый дизайн-токен | `web/src/app.css` (только там) + потребитель сразу | -| Тяжёлая операция → worker/wasm | `executor/**`, без правок компонентов | -| Мультифайловый результат | `result: "files"` + `zip.ts` | +| Задача | Где править | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Новый инструмент | `registry/tools/.ts` + страница в `registry/pages/.ts` + i18n + тесты (рецепт ниже) | +| Новая страница на готовом инструменте | запись `Page` со своим `slug` и текстами; новый код не нужен | +| Смена `id` | `registry/tools/*` + ключи `tools.*` в словарях + хардкод id в тестах; адреса, имена файлов, иконки и карта не меняются | +| Смена `slug` | `registry/pages/*` + `pages.*` в словарях + `tool-icons.ts` + `docs/tools-map.md` + хардкод slug в тестах и e2e; меняется адрес страницы — операция с SEO-последствиями, не часть обычной задачи | +| Новый вид поля | `registry-schema.ts` + контрол + `FIELDS` (раздел 4) | +| Новый маршрут/страница | `web/src/routes/**` + ссылка в каталоге | +| Новый визуальный элемент | компонент в `components/**` по образцу `ui/` или `layout/` | +| Новый дизайн-токен | `web/src/app.css` (только там) + потребитель сразу | +| Тяжёлая операция → worker/wasm | `executor/**`, без правок компонентов | +| Мультифайловый результат | `result: "files"` + `zip.ts` | ## 8. Рецепт: добавить инструмент -1. **Найти соседей.** Поиск по ключу в `registry/.ts` и по всему +1. **Найти соседей.** Поиск по ключу в `registry/tools/.ts` и по всему `web/src/` — не заводить дублирующую константу, тип или утилиту. 2. **Выбрать группу** (`geometry`/`alpha`/`color`/`convert`/`filters`/ - `generate`/`text`/`analyze`) и положить запись в её экспорт; при новой + `generate`/`text`/`analyze`) и положить инструмент в её экспорт; при новой категории — сначала `categories.ts`. -3. **Задать `id`.** Он же адрес страницы, он же ключ i18n, он же база имени - скачиваемого файла. Страница держит png-интент, поэтому суффикс `-png` входит - в `id` (`crop-png`, `resize-png`) — это осознанно, отдельного поля `slug` в - реестре нет (раздел 3). +3. **Задать `id`** по правилу раздела 3: без png-интента (`crop`, `to-jpg`, + `from-base64`, `is-grayscale`). 4. **Описать схему** через `field.*`, задать `layout`, если поля нужно - сгруппировать. + сгруппировать. Качество кодирования (`quality`) — не поле страницы, а + настройка скачивания (`docs/plan-seo.md` §1, S1f). 5. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в `core/`, без DOM. -6. **Проверить обязательные контракты** для новой записи: она попадает в - `TOOLS`, страница `/tools/` prerender-ится, `input`/`result` согласованы - с тем, что делает `run`. -7. **Добавить i18n-ключи во все локали** сразу (ключ — по `id`). -8. **Тесты:** unit — в `registry/*.test.ts` или рядом с `core`-функцией; +6. **Добавить страницу** в `registry/pages/.ts`: `slug` с адресным + png-интентом (`crop-png`), тексты EN, `category`, `steps: [{ id }]`. +7. **Проверить обязательные контракты**: инструмент попадает в `TOOLS`, страница + в `PAGES`, `/tools/` prerender-ится, `input`/`result` согласованы с + тем, что делает `run`. +8. **Добавить i18n-ключи во все локали** сразу: `pages..title` и + `pages..description`, при наличии опций и вердиктов — `tools.`. +9. **Тесты:** unit — в `registry/registry.test.ts` или рядом с `core`-функцией; e2e-smoke — существующий spec по типу `input`/`result` (правила — `docs/testing-strategy.md`). -9. **Обновить `docs/tools-map.md`:** новая страница обязательна в разделе - «Реализовано», формат строки — в шапке раздела. Карта перечисляет **`id`** - реестра, которые одновременно являются адресами страниц. Проверяет - `web/src/lib/registry/tools-map-doc.test.ts`: id без инструмента и инструмент - без записи в карте роняют тест. Обоснование и границы автоматизации — - `docs/decisions.md`, раздел 4. +10. **Обновить `docs/tools-map.md`:** новая страница обязательна в разделе + «Реализовано», формат строки — в шапке раздела. Карта перечисляет **`slug` + страниц** из `registry/pages/`, то есть адреса. Проверяет + `web/src/lib/registry/tools-map-doc.test.ts`: slug без страницы и страница + без записи в карте роняют тест. Обоснование и границы автоматизации — + `docs/decisions.md`, раздел 4. Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для инструмента, меняющего пользовательский поток, — e2e (см. раздел 3 diff --git a/docs/backlog.md b/docs/backlog.md index d7f0d7d..d4bbdea 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -72,28 +72,17 @@ - [ ] для keep Aspect field - пересчитывать второе поле, учитывать какое было изменено - [ ] обновить страницу kit или сделать storybook -- [ ] Слаги инструментов (`*-png`) — **развести `id` и адрес страницы.** - Инструменты работают с любым форматом, но называются `crop-png`, - `resize-png`; имя в коде, пайплайнах и i18n должно быть `crop`, `resize`, - а адрес и имя скачиваемого файла — прежними (`/tools/crop-png` → - `crop-png.png`). Решение и его SEO-последствия: `docs/plan-seo.md` §0; - текущее состояние (поля `slug` нет, роут — `tools/[id]`) — - `docs/architecture.md`, раздел 3. Порядок работ: 1. добавить `slug` в - `ToolEntry` + `toolSlug()` / `getToolBySlug()` в `registry/index.ts`, - проверку уникальности `slug` в `registry.test.ts`; 2. перевести - `routes/tools/[id]` → `[slug]` (`entries()`, `load()`, `+page.svelte`), - `ToolCard`, крошку в `+layout.svelte` и имя файла download в - `SchemaToolView` на `toolSlug(tool)`; старые адреса продолжают работать, - редиректы не нужны; 3. переименовать `id` волнами по группам реестра - (`convert`, `alpha`, `color`, `geometry`, `filters`, `analyze`, - `generate`, `text`); в каждой волне — `registry/.ts`, ключи - `tools.*` в `en.ts`/`ru.ts`, `tool-icons.ts`, захардкоженные id в тестах и - e2e (`smoke-i18n.test.ts`, `kit/+page.svelte`); 4. `docs/tools-map.md` при - этом **не меняется**: карта перечисляет id, а они остаются прежними как - адреса. Обратной совместимости не делаем: формат пайплайна ещё не - устоялся, старые экспорты и записи в localStorage после переименования - просто перестают открываться — это осознанно, миграция усложнила бы код - ради пары пользователей. +- [ ] Свести convert-страницы на один инструмент `convert` — реализация уже + умеет «скопировать пиксели, декодировать вход, отдать в заданном формате», + а страницы различаются только форматом вывода. **Зависит от S1f** + (`docs/plan-seo.md` §1): пока `quality` живёт в схеме страницы, у + `convert-png-to-bmp` и `convert-png-to-jpg` разные наборы полей, и один + инструмент на них не сойдётся. Качество — настройка кнопки Download + («Download as JPEG»), а не поле страницы; после его переезда пресеты схемы + не нужны, страницы различаются только `output` и `params` шага. Модель + страницы к этому готова: `Page.steps` — список инструментов, один + инструмент может обслуживать много страниц (`docs/architecture.md`, раздел + 3). - [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop, удаление, добавление инструментов. переход один инструмент - pipeline без перезагрузки @@ -193,21 +182,21 @@ UI. Оценки (S/M/L) — оценка труда, не приоритет. 5. **Избранные инструменты (fav tools)** — оценка S/M. Тоггл-звёздочка на карточках каталога, результатах поиска и в шапке страницы инструмента; список - id в localStorage. Избранное показывается отдельной секцией сверху каталога и - поднимается в выдаче поиска (бонус к popularity при скоринге). + slug в localStorage. Избранное показывается отдельной секцией сверху каталога + и поднимается в выдаче поиска (бонус к popularity при скоринге). 6. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download** — оценка M/L. Решение и продуктовое обоснование: `docs/plan-seo.md` §0 и задача **S1f**; почему селектор не заменяет существующие convert-страницы — - `docs/plan-seo.md` §5. Механическая часть уже есть: `ToolEntry.output` + `docs/plan-seo.md` §5. Механическая часть уже есть: `Tool.output` (`OutputFormat`: mime/ext/qualityParamId) и кодирование по нему в `SchemaToolView.download()` — `docs/architecture.md`, раздел 3. Что осталось сверх S1f: - - экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png` - (целевой KB, бинарный поиск по k из квантования). Проработать вместе с - достройкой download-флоу. + - экспорт с лимитом размера → закрывает отложенную страницу + `reduce-to-size-png` (целевой KB, бинарный поиск по k из квантования). + Проработать вместе с достройкой download-флоу. -7. **`watermark-image-png` (водяной знак картинкой)** — оценка M. Нужна +7. **Страница `watermark-image-png` (водяной знак картинкой)** — оценка M. Нужна overlay-механика: второй источник изображения (getOverlay/store), параметры scale/opacity/position/margin/aspect. Решается вместе с вопросом, как второй источник картинки вписывается в schema-driven preview (`SchemaToolView`). diff --git a/docs/decisions.md b/docs/decisions.md index 521dc98..bfe1b4e 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -64,16 +64,18 @@ Правило про числа не закрывает более опасный класс расхождений: документ не устарел числом, а **перечисляет то, чего нет в коде**. В разделе «Реализовано» -карты лежали несуществующие id (`png-info`, `color-wheel-generator`, +карты лежали несуществующие страницы (`png-info`, `color-wheel-generator`, `jpg-to-png`, `watermark-image-png`), которые агент принял бы за готовые инструменты. Счёт при этом был верный — число такой класс не ловит. Сверка множеств отвечает на вопрос однозначно и без allowlist, поэтому автоматизирована: `web/src/lib/registry/tools-map-doc.test.ts` парсит раздел -«Реализовано» и сравнивает id с `TOOLS` в обе стороны (id без инструмента и -инструмент без записи), плюс ловит повтор id между буллетами. Формат строки -карты — всё до первого тире, id через `/` или `,` — закреплён в самом документе -и в тесте. +«Реализовано» и сравнивает slug с `PAGES` в обе стороны (slug без страницы и +страница без записи), плюс ловит повтор slug между буллетами. Формат строки +карты — всё до первого тире, slug через `/` или `,` — закреплён в самом +документе и в тесте. Сверяются именно страницы, а не инструменты: карта +описывает то, что видит пользователь, а один инструмент может обслуживать +несколько страниц (см. `docs/architecture.md`, раздел 3). Чтобы сломанный парсер не выглядел как успех, у теста есть отдельная проверка «разбор не пустой»: если формат карты снова поменяется, тест упадёт с сообщением @@ -153,3 +155,18 @@ PR. Актуальные длительности — в логе прогона 5. красный `e2e` только из-за ресурсов раннера фиксируется как ограничение окружения: сначала проверяются один проект и один spec (`docs/agent-workflow.md`, раздел 3), а не увеличиваются таймауты. + +## 11. Тексты страниц: в базовой локали секция `pages` пустая + +Заголовки и описания страниц переводятся (`pages..*`), а EN-строка лежит в +реестре страниц — дублировать её в `en.ts` нельзя, иначе при правке реестра +английский текст разъедется со страницей, а `dict-consistency` этого не увидит. +Поэтому `en.ts` объявляет `pages: {}` — пустую секцию, а линтер правила +`i18n/dict-consistency` не считает это нарушением: в базовой локали отсутствие +`pages.*.title/description` разрешено опцией `baseLocaleFallback`, а полнота +перевода для остальных локалей проверяется строго и юнит-тестом +(`tools-i18n.test.ts`). + +Альтернатива — `allowPaths: ["pages"]` — была бы слабее: она разрешила бы пустую +секцию `pages` в любой локали, то есть пропущенный перевод страницы прошёл бы +линтер молча. Пустая секция в EN и есть то, что делает правило точным. diff --git a/docs/plan-fields-audit-fixes.md b/docs/plan-fields-audit-fixes.md index 89d2791..a65db22 100644 --- a/docs/plan-fields-audit-fixes.md +++ b/docs/plan-fields-audit-fixes.md @@ -16,10 +16,13 @@ > которые в бэклоге, здесь не дублируются: этот документ задаёт **порядок**, > backlog — **учёт** (`docs/backlog.md`). > -> **Ids инструментов сверены с реестром** (`web/src/lib/registry/`) для шагов -> FA-B и волн C–E. Названия из `docs/tools-audit.md` вида `png-to-hsl`, -> `color-wheel-generator`, `webp-to-png` в реестре отсутствуют (реальные — -> `hsl`, `color-wheel-png`): при переносе пункта аудита в шаг id сверять. +> **Инструменты в этом документе названы по адресам страниц (slug).** В коде у +> страницы `slug` (`registry/pages/`), у вызываемого инструмента — `id` без +> png-интента (`crop-png` → `id: "crop"`, `png-to-hsl` → `to-hsl`), модель — +> `docs/architecture.md`, раздел 3. Названия из `docs/tools-audit.md` вида +> `color-wheel-generator`, `webp-to-png` в реестре отсутствуют (реальная +> страница — `color-wheel-png`): при переносе пункта аудита в шаг сверять с +> `PAGES`. ## 0. Ключевое решение: select → buttons diff --git a/docs/plan-seo.md b/docs/plan-seo.md index eea87cb..9b2960c 100644 --- a/docs/plan-seo.md +++ b/docs/plan-seo.md @@ -1,11 +1,11 @@ # План: SEO + GEO — индексация и конкуренция в image tools -> **Статус: active.** Адреса страниц инструментов — `/tools/` (`crop-png` и -> подобные), каталог — `/list-tools`; отдельного поля `slug` в реестре пока нет, -> `id` и есть адрес. +> **Статус: active.** Адреса страниц инструментов — `/tools/` (`crop-png` +> и подобные), каталог — `/list-tools`; реестр двухслойный: страница `slug` +> ссылается на инструмент по `id` (`docs/architecture.md`, раздел 3). > > **`noindex, nofollow` выставлен на каждой странице инструмента** -> (`web/src/routes/tools/[id]/+page.svelte`) — осознанно, до появления +> (`web/src/routes/tools/[slug]/+page.svelte`) — осознанно, до появления > уникального контента (S2). Пока он стоит, сайт не индексируется вообще, и > **S1b–S1e (sitemap, `robots.txt` с `Sitemap:`, canonical, OG) не дают > эффекта**: их имеет смысл делать вместе со снятием `noindex`, а не вместо @@ -40,22 +40,24 @@ - **Форматы: формат = выбор в UI, не отдельные страницы.** Download-кнопка каждого инструмента — с селектором PNG/JPG/WebP/BMP (+quality для lossy). Райз «выбор формата/качества в Download» вытащен из backlog №7 в S1f: механика уже - есть (`ToolEntry.output`), нужна как продуктовая основа «image + есть (`Tool.output`), нужна как продуктовая основа «image tools»-позиционирования. В направлении «из формата в PNG» отдельных инструментов нет (загрузка принимает любой формат) — см. §5. - **Адреса страниц `-png` сохраняются, внутренние имена — без.** Инструмент работает с любым форматом, поэтому его `id` не должен врать (`crop`, а не - `crop-png`); png-интент держит адрес страницы и имя скачиваемого файла. - Требуется поле `slug` в `ToolEntry` + переход `routes/tools/[id]` → `[slug]`. - **Сейчас не реализовано:** в реестре `id: "crop-png"`, поля `slug` нет, роут — - `tools/[id]/`; текущее состояние описано в `docs/architecture.md`, раздел 3. - **Следствие для SEO: редиректы не нужны, canonical и sitemap не меняются, - адреса остаются теми же** — поэтому это безопасная правка, которую можно - делать до индексации, а не после. Побочная выгода: широкие интенты («crop - image online») переносятся теми же страницами контентом — H1 «Crop image», - текст «works with PNG, JPG, WebP»; URL-алиасы без `-png` по-прежнему не - создаются (канонический беспорядок). Порядок работ — `docs/backlog.md`, задача - «Слаги инструментов (`*-png`) — развести `id` и адрес страницы». + `crop-png`); png-интент держат адрес страницы и имя скачиваемого файла. + **Реализовано:** реестр разделён на инструменты (`registry/tools/`, уникальный + `id`) и страницы (`registry/pages/`, уникальный `slug`), роут — + `tools/[slug]/`; модель — `docs/architecture.md`, раздел 3. **Следствие для + SEO: редиректы не понадобились, canonical и sitemap не меняются, адреса те + же** — поэтому это была безопасная правка до индексации, а не после. Побочная + выгода: широкие интенты («crop image online») переносятся теми же страницами + контентом — H1 «Crop image», текст «works with PNG, JPG, WebP»; URL-алиасы без + `-png` по-прежнему не создаются (канонический беспорядок). Побочный эффект + модели: страница может ссылаться на один инструмент, а инструмент — + обслуживать много страниц, поэтому следующие шаги (свести convert-страницы на + один `convert`, селектор формата в Download — S1f) не требуют новой миграции + реестра. Задача на консолидацию — `docs/backlog.md`. - **Индексируемая локаль — EN** (базовая; конкурент англоязычный). RU-версия — отдельно и позже: префикс `/ru/`, prerender обеих локалей, hreflang (сознательно отложено). Пока язык переключается на клиенте (localStorage) — @@ -65,11 +67,11 @@ canonical и «корневость» страниц. Свой домен + корень. Текущий деплой — GitHub Pages (`.github/workflows/deploy.yml`); перенос на свой домен выполняется сменой origin в CI и `PUBLIC_SITE_ORIGIN`. -- **URL-схема инструментов:** короткие плоские адреса — `/tools/` для - инструмента, `/list-tools` для каталога, где `` берётся из записи реестра. - Схему не менять без отдельного SEO-плана: после индексации переименование URL - означает 301 и потерю позиций. Разделение `id`/`slug` (решение выше) адресов - не меняет. +- **URL-схема инструментов:** короткие плоские адреса — `/tools/` для + страницы, `/list-tools` для каталога, где `` берётся из записи + `registry/pages/`. Схему не менять без отдельного SEO-плана: после индексации + переименование URL означает 301 и потерю позиций. Разделение инструмента и + страницы (решение выше) адресов не меняет. - **Без GA и рекламных скриптов:** аналитика — self-hosted Plausible/Umami или Cloudflare Web Analytics. «No tracking» — часть позиционирования, её нельзя подрывать собственными скриптами. @@ -83,9 +85,10 @@ - SvelteKit + `adapter-static`, `prerender = true` на весь сайт (`web/src/routes/+layout.ts`) — весь контент в статичном HTML, ботам не нужен JS. Это главный технический козырь. -- Инструменты в `web/src/lib/registry` с «SEO-шными» id (`crop-png`, - `add-border-png`, …) — URL уже повторяют паттерн конкурента. Актуальный - состав: `docs/tools-map.md`. +- Страницы в `web/src/lib/registry/pages` со «SEO-шными» slug (`crop-png`, + `add-border-png`, …) — URL уже повторяют паттерн конкурента, при этом + внутренние имена инструментов png-интента не держат. Актуальный состав: + `docs/tools-map.md`. - На корневых страницах инструментов есть базовые `` и `robots`; полный SEO-слой (canonical, OG, content и JSON-LD) остаётся S1/S2. - Каталог со ссылками на все инструменты (внутренняя перелинковка) и карта @@ -160,7 +163,7 @@ **domain-required** (после покупки и DNS-настройки). - [ ] **[S1h]** Снять `noindex, nofollow` со страниц инструментов - (`web/src/routes/tools/[id]/+page.svelte`) — заменить на + (`web/src/routes/tools/[slug]/+page.svelte`) — заменить на `<meta name="robots" content="index, follow" />`. **Это первым обязательным шагом S1:** пока тег стоит, остальные задачи S1 готовят инфраструктуру, но не приводят трафик. Порядок: S1h ставится после @@ -172,14 +175,16 @@ - [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд, - файл `static/og/[id].png`, ссылка из `Seo.svelte`. Относительные пути + файл `static/og/[slug].png`, ссылка из `Seo.svelte`. Относительные пути (`/og/crop-png.png`) работают без домена; при деплое на собственном домене OG-картинки будут абсолютными и корректными. - [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality - для lossy) для всех инструментов — механика `ToolEntry.output` уже есть + для lossy) для всех инструментов — механика `Tool.output` уже есть (`docs/backlog.md`, «Идеи» №6, UX-райз «Download»). Продуктовая основа «image tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести - параллельно с текущими quality-задачами. + параллельно с текущими quality-задачами. Побочно снимает `quality` из схем + страниц — после этого 14 convert-страниц сводятся на один инструмент + (`docs/backlog.md`). - [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов формат исходника (disable) — чтобы `convert-png-to-jpg` не мог сохранить в png. Существующие страницы не трогать: `convert-png-to-jpg`, @@ -350,7 +355,8 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl **один будущий инструмент `/convert-image`**: сначала пустой **core-tool** в `core/` — функция ре-энкода («копия с перекодированием»: декодирует любой поддержанный исходник в ImageData и кодирует в выбранный формат; поверх неё - S1f строит селектор формата). `ToolEntry` и страницу добавлять **только вместе + S1f строит селектор формата). Инструмент и страницу добавлять **только вместе + с собственным текстом** («Convert any image to PNG, JPG or WebP in your browser») — core в коде не обязывает к странице. Этот же шаблон потом закрывает форматные волны (jpg→webp и т.п.) без новых страниц. diff --git a/docs/roadmap.md b/docs/roadmap.md index 729978d..0ed57d6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -132,9 +132,11 @@ Manifest + service worker, оффлайн-режим («работает без исполнителе и реестре, поэтому может идти параллельно с SEO. Номер 11 выбран чтобы не сдвигать уже используемые ссылки на фазы 2–10. -- **Формат шага** — `{tool, params}`, список шагов применяется последовательно, +- **Формат шага** — `{id, params}`, список шагов применяется последовательно, результат предыдущего идёт в следующий. Формат зафиксирован в §0 как общий для - браузера, wasm и CLI, поэтому версионируется с первого дня. + браузера, wasm и CLI, поэтому версионируется с первого дня. Он уже совпадает с + полем `Page.steps` (`{ id, params? }`), так что страница может быть готовой + цепочкой без новой модели данных; исполняется пока первый шаг. - **Первый шаг фазы:** модуль состояния цепочки + сохранение в localStorage + экспорт/импорт JSON (ошибки разбора уже описаны ключами `errors.badPipelineShape` / `errors.pipelineVersion` / `errors.noSteps`). diff --git a/docs/tools-map.md b/docs/tools-map.md index 42c9bb7..615710c 100644 --- a/docs/tools-map.md +++ b/docs/tools-map.md @@ -2,13 +2,12 @@ > Статус: **active**. Живой документ для сверки с каталогом. > -> **Карта перечисляет `id` записи реестра (`web/src/lib/registry/`), которые -> одновременно являются адресами страниц:** `/tools/crop-png` — это -> `id: "crop-png"`. Отдельного поля `slug` в `ToolEntry` нет, поэтому id и адрес -> сейчас совпадают; их разделение — решённая, но не реализованная задача -> (`docs/backlog.md`, раздел «Мелочи всякие», и `docs/architecture.md`, раздел -> 3). Источник правды — реестр. Этот список навигационный: сверять перед -> использованием. Формат записи: `id — параметры через запятую`. Сравнение с +> **Карта перечисляет `slug` страниц (`web/src/lib/registry/pages/`) — то есть +> адреса вида `/tools/crop-png`:** это `slug: "crop-png"` у страницы, которая +> вызывает инструмент `id: "crop"`. Внутренние имена инструментов png-интента не +> держат, поэтому они от карты отделены; модель — `docs/architecture.md`, +> раздел 3. Источник правды — реестр. Этот список навигационный: сверять перед +> использованием. Формат записи: `slug — параметры через запятую`. Сравнение с > каталогом источника (объём, нишевые клоны серий Logo*/Icon*/Stamp*Signature*) > — замер в `docs/plan-seo.md`, §2; здесь он не дублируется. > @@ -20,13 +19,13 @@ ## 1. Реализовано -Ids приведены к фактическим в `web/src/lib/registry/`. +Ids приведены к фактическим в `web/src/lib/registry/pages/`. **Формат строки (проверяется тестом `web/src/lib/registry/tools-map-doc.test.ts`):** в буллите всё **до первого -тире** `—` — это id инструментов через `/` или `,`; всё после тире — свободное -описание параметров и не разбирается. Второй id нельзя оставлять после тире, -иначе он не попадёт в разбор и инструмент будет считаться неописанным. +тире** `—` — это slug страниц через `/` или `,`; всё после тире — свободное +описание параметров и не разбирается. Второй slug нельзя оставлять после тире, +иначе он не попадёт в разбор и страница будет считаться неописанной. ### Конвертация (`convert.ts`)