mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
Compare commits
2
Commits
5a7f598d50
...
66b23ec3bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66b23ec3bf | ||
|
|
b7ba1f501f |
@@ -49,7 +49,7 @@ jobs:
|
|||||||
working-directory: web
|
working-directory: web
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
env:
|
env:
|
||||||
BASE_PATH: /easy-png-tools
|
PUBLIC_SITE_ORIGIN: "https://easy-image-tools.com"
|
||||||
PUBLIC_GA_ENABLED: "true"
|
PUBLIC_GA_ENABLED: "true"
|
||||||
|
|
||||||
- name: Upload Pages artifact
|
- name: Upload Pages artifact
|
||||||
|
|||||||
@@ -4,6 +4,3 @@ node_modules
|
|||||||
web/audit/cdp-audit*.txt
|
web/audit/cdp-audit*.txt
|
||||||
css-lint.txt
|
css-lint.txt
|
||||||
web/coverage
|
web/coverage
|
||||||
|
|
||||||
# Локальные планы с приватной информацией (выбранное имя домена) — не коммитить
|
|
||||||
docs/plan-domain.local.md
|
|
||||||
|
|||||||
@@ -41,14 +41,10 @@
|
|||||||
| Направления развития, фазы 1–11 | `docs/roadmap.md` |
|
| Направления развития, фазы 1–11 | `docs/roadmap.md` |
|
||||||
| SEO/GEO-план (адреса, `noindex`, sitemap) | `docs/plan-seo.md` |
|
| SEO/GEO-план (адреса, `noindex`, sitemap) | `docs/plan-seo.md` |
|
||||||
| API/PWA/шрифты | `docs/plan-platform.md` |
|
| API/PWA/шрифты | `docs/plan-platform.md` |
|
||||||
| Переименование бренда и покупка домена | `docs/plan-domain.local.md` † |
|
|
||||||
| Сырые UX-находки по инструментам (вход аудита) | `docs/tools-audit.md` |
|
| Сырые UX-находки по инструментам (вход аудита) | `docs/tools-audit.md` |
|
||||||
| Архивирование и удаление старых документов | `docs/archive/README.md` |
|
| Архивирование и удаление старых документов | `docs/archive/README.md` |
|
||||||
| Индекс остальных документов | `docs/README.md` |
|
| Индекс остальных документов | `docs/README.md` |
|
||||||
|
|
||||||
† `docs/plan-domain.local.md` вне git, в клоне отсутствует — в этом случае
|
|
||||||
задачу ищут по `docs/plan-seo.md` §6.
|
|
||||||
|
|
||||||
## Инварианты, которые линтер не проверяет
|
## Инварианты, которые линтер не проверяет
|
||||||
|
|
||||||
- **Сначала искать.** Новая константа, тип или утилита не заводится, пока не
|
- **Сначала искать.** Новая константа, тип или утилита не заводится, пока не
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
|
||||||
|
|
||||||
- - -
|
- - -
|
||||||
|
## [v0.9.0](https://github.com/Ku6epXBOCTuK/easy-image-tools/compare/b7ba1f501fc704b699dec67d46e69d6892432a38..v0.9.0) - 2026-10-12
|
||||||
|
#### Features
|
||||||
|
- update brand name, add domain - ([b7ba1f5](https://github.com/Ku6epXBOCTuK/easy-image-tools/commit/b7ba1f501fc704b699dec67d46e69d6892432a38)) - Ku6epXBOCTuK
|
||||||
|
|
||||||
|
- - -
|
||||||
|
|
||||||
## [v0.8.0](https://github.com/Ku6epXBOCTuK/easy-png-tools/compare/f67ddb3b0a7c69316caa3bed30d976f41d7a1219..v0.8.0) - 2026-10-11
|
## [v0.8.0](https://github.com/Ku6epXBOCTuK/easy-png-tools/compare/f67ddb3b0a7c69316caa3bed30d976f41d7a1219..v0.8.0) - 2026-10-11
|
||||||
#### Features
|
#### Features
|
||||||
- add tooltip for errors\warnings - ([be9ecd0](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/be9ecd0e8edfbe83a6c148cad5c61cce22687ccb)) - Ku6epXBOCTuK
|
- add tooltip for errors\warnings - ([be9ecd0](https://github.com/Ku6epXBOCTuK/easy-png-tools/commit/be9ecd0e8edfbe83a6c148cad5c61cce22687ccb)) - Ku6epXBOCTuK
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# easy-png-tools
|
# easy-image-tools
|
||||||
|
|
||||||
Browser-based image utility toolkit — no server, no data upload, everything runs
|
Browser-based image utility toolkit — no server, no data upload, everything runs
|
||||||
locally in your browser.
|
locally in your browser.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pre_bump_hooks = ["node scripts/sync-version.mjs {{version}}"]
|
|||||||
path = "CHANGELOG.md"
|
path = "CHANGELOG.md"
|
||||||
template = "remote"
|
template = "remote"
|
||||||
remote = "github.com"
|
remote = "github.com"
|
||||||
repository = "easy-png-tools"
|
repository = "easy-image-tools"
|
||||||
owner = "Ku6epXBOCTuK"
|
owner = "Ku6epXBOCTuK"
|
||||||
|
|
||||||
[commit_types]
|
[commit_types]
|
||||||
|
|||||||
+2
-10
@@ -28,17 +28,10 @@
|
|||||||
| `docs/plan-fields-audit-fixes.md` | Разбивка аудита на волны правок | draft |
|
| `docs/plan-fields-audit-fixes.md` | Разбивка аудита на волны правок | draft |
|
||||||
| `docs/plan-seo.md` | SEO/GEO-план | active |
|
| `docs/plan-seo.md` | SEO/GEO-план | active |
|
||||||
| `docs/plan-platform.md` | API/PWA/шрифты-план | active |
|
| `docs/plan-platform.md` | API/PWA/шрифты-план | active |
|
||||||
| `docs/plan-domain.local.md` | Чек-лист переименования бренда и покупки домена | active |
|
|
||||||
| `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active |
|
| `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active |
|
||||||
| `web/eslint-plugins/README.md` | Правка lint-правил, токенов и их конфигов | active |
|
| `web/eslint-plugins/README.md` | Правка lint-правил, токенов и их конфигов | active |
|
||||||
| `docs/archive/README.md` | Архивирование и удаление старых документов | active |
|
| `docs/archive/README.md` | Архивирование и удаление старых документов | active |
|
||||||
|
|
||||||
`docs/plan-domain.local.md` **исключён из git** через `.gitignore`: выбранное
|
|
||||||
имя домена не должно попасть в историю до покупки. В клоне файла нет — это
|
|
||||||
ожидаемо, и ссылки на него в `plan-seo.md` и `backlog.md` не разрешаются. Строка
|
|
||||||
в таблице оставлена, чтобы задачу R1/R2 можно было найти по индексу; сам
|
|
||||||
чек-лист лежит только на машине разработчика.
|
|
||||||
|
|
||||||
## Правила
|
## Правила
|
||||||
|
|
||||||
- У каждой темы ровно один владелец: остальные документы ссылаются на него, а не
|
- У каждой темы ровно один владелец: остальные документы ссылаются на него, а не
|
||||||
@@ -64,8 +57,7 @@
|
|||||||
- Поиск по `docs/` задевает `docs/archive/`: там вынесены завершённые планы и
|
- Поиск по `docs/` задевает `docs/archive/`: там вынесены завершённые планы и
|
||||||
исследования, поэтому grep по `docs/` даёт больше шума, чем пользы. Для правок
|
исследования, поэтому grep по `docs/` даёт больше шума, чем пользы. Для правок
|
||||||
production-кода архив не нужен: grep по `docs/*.md` без `archive/`.
|
production-кода архив не нужен: grep по `docs/*.md` без `archive/`.
|
||||||
- Проверка ссылок в документации не автоматизирована; ожидаемо неразрешимыми
|
- Проверка ссылок в документации не автоматизирована; любая неразрешимая ссылка
|
||||||
считаются ссылки на `docs/plan-domain.local.md` (файл вне git). Всё остальное
|
— ошибка.
|
||||||
неразрешимое — ошибка.
|
|
||||||
- Протокол «остановиться и показать изменения» — `docs/agent-workflow.md`,
|
- Протокол «остановиться и показать изменения» — `docs/agent-workflow.md`,
|
||||||
раздел 6.
|
раздел 6.
|
||||||
|
|||||||
@@ -41,9 +41,7 @@
|
|||||||
5. Найти оставшиеся входящие ссылки:
|
5. Найти оставшиеся входящие ссылки:
|
||||||
`git grep -n "<filename>" -- 'docs/*.md' AGENTS.md README.md web/eslint-plugins/README.md`.
|
`git grep -n "<filename>" -- 'docs/*.md' AGENTS.md README.md web/eslint-plugins/README.md`.
|
||||||
Шаг 4 ломает их, и агент без этого шага напишет в новом документе ссылку на
|
Шаг 4 ломает их, и агент без этого шага напишет в новом документе ссылку на
|
||||||
удалённый файл. Ожидаемо неразрешимыми остаются ссылки на
|
удалённый файл.
|
||||||
`docs/plan-domain.local.md` — он вне git, это не ошибка (`docs/README.md`,
|
|
||||||
раздел «Правила»).
|
|
||||||
6. Не менять архивный документ под новые требования.
|
6. Не менять архивный документ под новые требования.
|
||||||
|
|
||||||
## Удаление
|
## Удаление
|
||||||
|
|||||||
@@ -230,9 +230,6 @@
|
|||||||
|
|
||||||
## SEO/GEO
|
## SEO/GEO
|
||||||
|
|
||||||
- [ ] переименование бренда + домен: чек-лист в локальном файле
|
|
||||||
`plan-domain.local.md` (в git не входит, в клоне отсутствует; подробности
|
|
||||||
и порядок — `docs/plan-seo.md` §6, R1/R2)
|
|
||||||
- [ ] снять `noindex` со страниц инструментов, когда будут sitemap, canonical и
|
- [ ] снять `noindex` со страниц инструментов, когда будут sitemap, canonical и
|
||||||
контент (S1h; блокирует индексацию всего сайта) — `docs/plan-seo.md` §4
|
контент (S1h; блокирует индексацию всего сайта) — `docs/plan-seo.md` §4
|
||||||
- [ ] форматные страницы по конкурентным слагам (`crop-jpeg`, …) — только по
|
- [ ] форматные страницы по конкурентным слагам (`crop-jpeg`, …) — только по
|
||||||
|
|||||||
+37
-37
@@ -30,14 +30,14 @@
|
|||||||
|
|
||||||
## 0. Решения
|
## 0. Решения
|
||||||
|
|
||||||
- **Бренд: переименовать до индексации** (новое имя и домен сознательно не
|
- **Бренд: `easy-image-tools`, домен `easy-image-tools.com`.** Переименование до
|
||||||
записаны в этом репозитории — чек-лист в локальном файле
|
индексации выполнено (R1): хардкод убран, точка истины — константа `brand` в
|
||||||
`plan-domain.local.md`, см. §6, он вне git: выбранное имя не должно утекать в
|
i18n + README (хардкод в компонентах недопустим). Домен зарегистрирован
|
||||||
историю коммитов до покупки домена). Легенда честная: «started as PNG tools,
|
2026-10-11 (R2); остался перевод прода на него (DNS, HTTPS, снятие
|
||||||
now all formats» — это контент-плюс, а не баг. Адреса страниц
|
`BASE_PATH`, `PUBLIC_SITE_ORIGIN`, 301 со старого GitHub Pages) — теперь это
|
||||||
(`/tools/crop-png`) и localStorage-ключи не переименовывать (см. решение про
|
блокер S1-domain (§4). Легенда честная: «started as PNG tools, now all
|
||||||
`slug` ниже). Точка истины по бренду: константа в i18n + README; хардкод в
|
formats» — это контент-плюс, а не баг. Адреса страниц (`/tools/crop-png`) и
|
||||||
компонентах недопустим.
|
localStorage-ключи не переименовывать (localStorage-ключи бренда не содержат).
|
||||||
- **Форматы: формат = выбор в UI, не отдельные страницы.** Download-кнопка
|
- **Форматы: формат = выбор в UI, не отдельные страницы.** Download-кнопка
|
||||||
каждого инструмента — с селектором PNG/JPG/WebP/BMP (+quality для lossy). Райз
|
каждого инструмента — с селектором PNG/JPG/WebP/BMP (+quality для lossy). Райз
|
||||||
«выбор формата/качества в Download» вытащен из backlog №7 в S1f: механика уже
|
«выбор формата/качества в Download» вытащен из backlog №7 в S1f: механика уже
|
||||||
@@ -203,12 +203,14 @@
|
|||||||
`convert-png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами
|
`convert-png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами
|
||||||
со своим контентом (§5).
|
со своим контентом (§5).
|
||||||
|
|
||||||
#### S1-domain: с доменом (после покупки + DNS)
|
#### S1-domain: свой домен (подключён; §6 R2-cut)
|
||||||
|
|
||||||
- [ ] **[S1a]** Перевести прод на собственный домен: убрать `BASE_PATH` из
|
- [x] **[S1a]** Перевести прод на собственный домен — **сделано 2026-10-11**:
|
||||||
`deploy.yml`, задать `PUBLIC_SITE_ORIGIN`, перенаправить
|
`BASE_PATH` убран из `deploy.yml`, задан `PUBLIC_SITE_ORIGIN`, добавлен
|
||||||
`https://<юзер>/<repo>/…` на корневые URL (301). Делается одновременно с
|
`web/static/CNAME`. Редирект `https://<юзер>/<repo>/…` → корневые URL
|
||||||
S1d (canonical), чтобы Google проиндексировал сразу с правильным доменом.
|
(301) делает сам GitHub Pages при включённом кастомном домене. Делается
|
||||||
|
одновременно с S1d (canonical), чтобы Google проиндексировал сразу с
|
||||||
|
правильным доменом.
|
||||||
- [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в
|
- [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в
|
||||||
`build/` из `PAGES` реестра (`web/src/lib/registry`) + статических
|
`build/` из `PAGES` реестра (`web/src/lib/registry`) + статических
|
||||||
страниц. Взять адреса у страниц, а не у инструментов: страниц может быть
|
страниц. Взять адреса у страниц, а не у инструментов: страниц может быть
|
||||||
@@ -314,14 +316,14 @@
|
|||||||
> Группы здесь — по категории страницы в реестре (`registry/pages/`), а не по
|
> Группы здесь — по категории страницы в реестре (`registry/pages/`), а не по
|
||||||
> смыслу: `round-corners-png` лежит в `alpha`, `add-text-png` и
|
> смыслу: `round-corners-png` лежит в `alpha`, `add-text-png` и
|
||||||
> `watermark-tile-png` — в `text`.
|
> `watermark-tile-png` — в `text`.
|
||||||
|
>
|
||||||
> **Открытый вопрос по интенту «сжать до N КБ».** Лимит размера теперь живёт в
|
> **Открытый вопрос по интенту «сжать до N КБ».** Лимит размера теперь живёт в
|
||||||
> кнопке Download (`fitWithinBytes` в `web/src/lib/core/io.ts`); интенты вида
|
> кнопке Download (`fitWithinBytes` в `web/src/lib/core/io.ts`); интенты вида
|
||||||
> «compress png to 500kb / 1mb» — high-volume и лимит-ориентированные, селектор
|
> «compress png to 500kb / 1mb» — high-volume и лимит-ориентированные, селектор
|
||||||
> в Download их не закрывает как посадочная. Решение (отдельная страница / приём
|
> в Download их не закрывает как посадочная. Решение (отдельная страница / приём
|
||||||
> в контенте существующих / ничего) требует вордстата; имя страницы не выбрано.
|
> в контенте существующих / ничего) требует вордстата; имя страницы не выбрано.
|
||||||
> Прорабатывать вместе с вопросом «из формата в PNG» выше.
|
> Прорабатывать вместе с вопросом «из формата в PNG» выше.
|
||||||
|
>
|
||||||
> **Открытый вопрос по интентам «из формата в PNG».** У конкурента есть
|
> **Открытый вопрос по интентам «из формата в PNG».** У конкурента есть
|
||||||
> `jpg-to-png`, `webp-to-png`, `gif-to-png` как отдельные страницы; у нас таких
|
> `jpg-to-png`, `webp-to-png`, `gif-to-png` как отдельные страницы; у нас таких
|
||||||
> инструментов нет и создавать их не хочется — загрузка файла уже принимает
|
> инструментов нет и создавать их не хочется — загрузка файла уже принимает
|
||||||
@@ -398,28 +400,28 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
|
|||||||
интенты, которые подтверждены вордстатом, и сразу с контентом S2 (страница без
|
интенты, которые подтверждены вордстатом, и сразу с контентом S2 (страница без
|
||||||
текста — потраченный URL).
|
текста — потраченный URL).
|
||||||
|
|
||||||
## 6. Переименование бренда и домен (сделать до S1-domain, дёшево сейчас)
|
## 6. Переименование бренда и домен
|
||||||
|
|
||||||
Выбранное имя и домен **в этом репозитории не записаны**: детальный чек-лист
|
Бренд — `easy-image-tools`, домен — `easy-image-tools.com`.
|
||||||
лежит в локальном файле `plan-domain.local.md` рядом с этим планом, он исключён
|
|
||||||
из git через `.gitignore`, чтобы имя не попало в историю коммитов до покупки
|
|
||||||
домена. Агент, работающий из клона, этого файла не увидит — при необходимости
|
|
||||||
попросить чек-лист у пользователя.
|
|
||||||
|
|
||||||
- [ ] **[R1]** Механический переезд бренда на новое имя. Список файлов и строк с
|
- [x] **[R1]** Переезд бренда выполнен (2026-10-11): хардкод в компонентах
|
||||||
хардкодом — в локальном плане (раздел «Где живёт бренд в коде»); здесь
|
убран, бренд — i18n-константа `t("brand")` (точка истины — словари
|
||||||
только SEO-ограничения. **НЕ трогать:** адреса страниц инструментов
|
`brand` + README). Репозиторий переименован в `easy-image-tools`, remote
|
||||||
(`/tools/crop-png`) и localStorage-ключи — `theme` и `locale` лежат без
|
обновлён, привязанные к репо ссылки/пути переключены: `BASE_PATH` в
|
||||||
префикса бренда, поэтому переименование с ними не столкнётся (проверено в
|
`deploy.yml`, GitHub-URL в `TopBar.svelte`, `repository` в `cog.toml`.
|
||||||
`theme.svelte.ts` и `i18n/locale.svelte.ts`); историю `docs/archive`.
|
**НЕ трогать:** адреса страниц инструментов (`/tools/crop-png`),
|
||||||
Бренд должен стать i18n-константой, а не тремя независимыми хардкодами.
|
localStorage-ключи (бренда не содержат), историю `docs/archive`.
|
||||||
- [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и
|
- [x] **[R2]** Домен `easy-image-tools.com` зарегистрирован (2026-10-11);
|
||||||
перевести прод на него без `BASE_PATH` (§0). Домен нужен до S1-domain:
|
`SITE_URL` в `web/src/lib/seo.ts` переключён на него.
|
||||||
canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`.
|
- [x] **[R2-cut]** Прод переведён на домен без `BASE_PATH` (§0) — **сделано
|
||||||
- **Порядок:** R1+R2 можно делать параллельно с текущими quality-задачами (не
|
2026-10-11**: DNS + HTTPS, `PUBLIC_SITE_ORIGIN` =
|
||||||
блокируют). R2 (домен) — блокер для S1-domain (canonical + sitemap), а
|
`https://easy-image-tools.com` в `deploy.yml`, `web/static/CNAME`. 301 со
|
||||||
S1-domain — блокер для S1h (снятие `noindex`). После индексации переименование
|
старого GitHub Pages делает сам GitHub при включённом кастомном домене.
|
||||||
= переезд с 301, потеря позиций и ссылок — не делать.
|
Canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`.
|
||||||
|
- **Порядок:** R1(код), покупка домена и cutover (R2-cut) сделаны. Осталось
|
||||||
|
S1-domain (canonical + sitemap: S1b/S1c/S1d), а S1-domain — блокер для S1h
|
||||||
|
(снятие `noindex`). Переименование делали до индексации (до S1h): после снятия
|
||||||
|
`noindex` это был бы переезд с 301 и потеря позиций — не делать.
|
||||||
|
|
||||||
## 7. Что осознанно НЕ делаем
|
## 7. Что осознанно НЕ делаем
|
||||||
|
|
||||||
@@ -443,5 +445,3 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
|
|||||||
- `docs/roadmap.md` — фазы 2–8 (ядро TS→Rust/wasm, CLI), фаза 11 (пайплайн);
|
- `docs/roadmap.md` — фазы 2–8 (ядро TS→Rust/wasm, CLI), фаза 11 (пайплайн);
|
||||||
фазы 9–10 — API и PWA.
|
фазы 9–10 — API и PWA.
|
||||||
- `docs/plan-platform.md` — планы API и PWA (общее ядро, оффлайн-режим).
|
- `docs/plan-platform.md` — планы API и PWA (общее ядро, оффлайн-режим).
|
||||||
- `plan-domain.local.md` — локальный чек-лист переименования и покупки домена;
|
|
||||||
**в git не входит** (`.gitignore`), в клоне отсутствует.
|
|
||||||
|
|||||||
+5
-4
@@ -1,10 +1,11 @@
|
|||||||
# План развития: easy-png-tools
|
# План развития: easy-image-tools
|
||||||
|
|
||||||
> **Статус: active.** Фаза 1 выполнена — сайт живёт на типизированном реестре,
|
> **Статус: active.** Фаза 1 выполнена — сайт живёт на типизированном реестре,
|
||||||
> production UI — корневые маршруты. Текущий фокус: SEO S1-free
|
> production UI — корневые маршруты. Текущий фокус: SEO S1-free
|
||||||
> (`docs/plan-seo.md`) + открытые задачи (`docs/backlog.md`). Домен нужен для
|
> (`docs/plan-seo.md`) + открытые задачи (`docs/backlog.md`). Домен
|
||||||
> SEO (S1-domain). Фазы 2–8 — будущие; 9–10 спланированы в
|
> `easy-image-tools.com` зарегистрирован; остался cutover для индексации
|
||||||
> `docs/plan-platform.md`; 11 — пайплайн.
|
> (S1-domain). Фазы 2–8 — будущие; 9–10 спланированы в `docs/plan-platform.md`;
|
||||||
|
> 11 — пайплайн.
|
||||||
>
|
>
|
||||||
> **Идентификаторы.** Ссылаться на фазу этого документа. Пространства
|
> **Идентификаторы.** Ссылаться на фазу этого документа. Пространства
|
||||||
> идентификаторов задач: `FA-*` — правки полей и раскладки
|
> идентификаторов задач: `FA-*` — правки полей и раскладки
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "easy-png-tools",
|
"name": "easy-image-tools",
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const ruleTester = new RuleTester({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const OPTIONS = [{ allowWords: ["easy-png-tools"] }];
|
const OPTIONS = [{ allowWords: ["easy-image-tools"] }];
|
||||||
|
|
||||||
ruleTester.run("no-hardcoded-user-text", asRuleModule(rule), {
|
ruleTester.run("no-hardcoded-user-text", asRuleModule(rule), {
|
||||||
valid: [
|
valid: [
|
||||||
@@ -62,12 +62,12 @@ ruleTester.run("no-hardcoded-user-text", asRuleModule(rule), {
|
|||||||
},
|
},
|
||||||
// Brand configured as an allowed word.
|
// Brand configured as an allowed word.
|
||||||
{
|
{
|
||||||
code: "<span>easy-png-tools</span>",
|
code: "<span>easy-image-tools</span>",
|
||||||
filename: "Component.svelte",
|
filename: "Component.svelte",
|
||||||
options: OPTIONS,
|
options: OPTIONS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "<footer><span>easy-png-tools</span><span>v0.1.0</span></footer>",
|
code: "<footer><span>easy-image-tools</span><span>v0.1.0</span></footer>",
|
||||||
filename: "Component.svelte",
|
filename: "Component.svelte",
|
||||||
options: OPTIONS,
|
options: OPTIONS,
|
||||||
},
|
},
|
||||||
@@ -130,7 +130,7 @@ ruleTester.run("no-hardcoded-user-text", asRuleModule(rule), {
|
|||||||
{
|
{
|
||||||
// Without the option the brand word is reported: the exclusion is
|
// Without the option the brand word is reported: the exclusion is
|
||||||
// configuration, not a hardcoded constant inside the rule.
|
// configuration, not a hardcoded constant inside the rule.
|
||||||
code: "<span>easy-png-tools</span>",
|
code: "<span>easy-image-tools</span>",
|
||||||
filename: "Component.svelte",
|
filename: "Component.svelte",
|
||||||
errors: [{ messageId: "hardcodedText" }],
|
errors: [{ messageId: "hardcodedText" }],
|
||||||
},
|
},
|
||||||
@@ -156,8 +156,8 @@ describe("hasUserWords", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("accepts configured brand words", () => {
|
it("accepts configured brand words", () => {
|
||||||
expect(hasUserWords("easy-png-tools", ["easy-png-tools"])).toBe(false);
|
expect(hasUserWords("easy-image-tools", ["easy-image-tools"])).toBe(false);
|
||||||
expect(hasUserWords("easy-png-tools")).toBe(true);
|
expect(hasUserWords("easy-image-tools")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts formats and units next to real copy", () => {
|
it("accepts formats and units next to real copy", () => {
|
||||||
|
|||||||
@@ -178,13 +178,7 @@ export default tseslint.config(
|
|||||||
i18n: i18nPlugin,
|
i18n: i18nPlugin,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"i18n/no-hardcoded-user-text": [
|
"i18n/no-hardcoded-user-text": "warn",
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
// The product name is a brand, not translatable text.
|
|
||||||
allowWords: ["easy-png-tools"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
+9
-11
@@ -1,6 +1,4 @@
|
|||||||
// Generates static/og/default.png: the default 1200x630 OG card.
|
// Generates static/og/default.png: the default 1200x630 OG card.
|
||||||
// Run: pnpm --dir web gen:og. The card is rendered in a browser (text,
|
|
||||||
// icons); per-tool cards will reuse this harness (docs/plan-seo.md S1e).
|
|
||||||
import { mkdirSync } from "node:fs";
|
import { mkdirSync } from "node:fs";
|
||||||
import { chromium } from "playwright";
|
import { chromium } from "playwright";
|
||||||
|
|
||||||
@@ -13,26 +11,26 @@ function cardHtml() {
|
|||||||
return `<!doctype html><html><body style="margin:0">
|
return `<!doctype html><html><body style="margin:0">
|
||||||
<div style="
|
<div style="
|
||||||
width:${W}px;height:${H}px;box-sizing:border-box;
|
width:${W}px;height:${H}px;box-sizing:border-box;
|
||||||
background:#0a120c;
|
background:#272827;
|
||||||
background-image:linear-gradient(#12281a 1px, transparent 1px),linear-gradient(90deg,#12281a 1px, transparent 1px);
|
background-image:linear-gradient(#373a37 1px, transparent 1px),linear-gradient(90deg,#373a37 1px, transparent 1px);
|
||||||
background-size:40px 40px;
|
background-size:40px 40px;
|
||||||
display:flex;align-items:center;justify-content:space-between;
|
display:flex;align-items:center;justify-content:space-between;
|
||||||
padding:80px;font-family:ui-monospace,'Cascadia Mono',Consolas,monospace;
|
padding:80px;font-family:ui-monospace,'Cascadia Mono',Consolas,monospace;
|
||||||
border:3px solid #16a34a;
|
border:3px solid #009a5a;
|
||||||
">
|
">
|
||||||
<div>
|
<div>
|
||||||
<div style="
|
<div style="
|
||||||
width:72px;height:72px;background:#86efac;color:#052e16;
|
width:72px;height:72px;background:#86efac;color:#052e16;
|
||||||
display:flex;align-items:center;justify-content:center;
|
display:flex;align-items:center;justify-content:center;
|
||||||
font-size:34px;font-weight:700;margin-bottom:32px;
|
font-size:34px;font-weight:700;margin-bottom:32px;
|
||||||
">EP</div>
|
">EIT</div>
|
||||||
<div style="color:#86efac;font-size:56px;font-weight:700;letter-spacing:-1px;">easy-png-tools</div>
|
<div style="color:#c3eccd;font-size:56px;font-weight:700;letter-spacing:-1px;">easy-image-tools</div>
|
||||||
<div style="color:#a7c4b1;font-size:28px;margin-top:24px;">PNG utilities right in your browser</div>
|
<div style="color:#92b39a;font-size:28px;margin-top:24px;">Image utilities right in your browser</div>
|
||||||
<div style="color:#4d7c5f;font-size:22px;margin-top:16px;">Convert · Crop · Resize — locally, no uploads</div>
|
<div style="color:#009a5a;font-size:22px;margin-top:16px;">Convert · Crop · Resize — locally, no uploads</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="
|
<div style="
|
||||||
color:#86efac;width:300px;height:300px;border:3px solid #16a34a;
|
color:#86efac;width:300px;height:300px;border:3px solid #009a5a;
|
||||||
background:#0f1f14;display:flex;align-items:center;justify-content:center;
|
background:#3c3e3c;display:flex;align-items:center;justify-content:center;
|
||||||
"><div style="width:240px;height:240px;">${IMAGE_ICON}</div></div>
|
"><div style="width:240px;height:240px;">${IMAGE_ICON}</div></div>
|
||||||
</div>
|
</div>
|
||||||
</body></html>`;
|
</body></html>`;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { t } from "$lib/i18n/t";
|
||||||
import { SITE_URL } from "$lib/seo";
|
import { SITE_URL } from "$lib/seo";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<link rel="canonical" href={url} />
|
<link rel="canonical" href={url} />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:site_name" content="easy-png-tools" />
|
<meta property="og:site_name" content={t("brand")} />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:url" content={url} />
|
<meta property="og:url" content={url} />
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { version } from "../../../../package.json";
|
import { version } from "../../../../package.json";
|
||||||
|
import { t } from "$lib/i18n/t";
|
||||||
|
|
||||||
const versionLabel = `v${version}`;
|
const versionLabel = `v${version}`;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<footer class="preview-footer">
|
<footer class="preview-footer">
|
||||||
<span>easy-png-tools <b class="version">{versionLabel}</b></span>
|
<span>{t("brand")} <b class="version">{versionLabel}</b></span>
|
||||||
<span>© 2026</span>
|
<span>© 2026</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<a class="brand" href={resolve("/")}>
|
<a class="brand" href={resolve("/")}>
|
||||||
<span class="brand-mark">EP</span>
|
<span class="brand-mark">EIT</span>
|
||||||
<span>easy-png-tools</span>
|
<span>{t("brand")}</span>
|
||||||
{#if crumb}
|
{#if crumb}
|
||||||
<span class="crumb">{crumb}</span>
|
<span class="crumb">{crumb}</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<div class="top-actions">
|
<div class="top-actions">
|
||||||
<a
|
<a
|
||||||
class="social"
|
class="social"
|
||||||
href="https://github.com/Ku6epXBOCTuK/easy-png-tools"
|
href="https://github.com/Ku6epXBOCTuK/easy-image-tools"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label={t("ui.github")}
|
aria-label={t("ui.github")}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
<a href={resolve("/")}>{t("header.home")}</a>
|
<a href={resolve("/")}>{t("header.home")}</a>
|
||||||
<a href={resolve("/list-tools")}>{t("header.catalog")}</a>
|
<a href={resolve("/list-tools")}>{t("header.catalog")}</a>
|
||||||
<a href={resolve("/chains")}>{t("header.chains")}</a>
|
<a href={resolve("/chains")}>{t("header.chains")}</a>
|
||||||
<a href={resolve("/kit")}>{t("header.uiKit")}</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export type HeaderStrings = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Dict = {
|
export type Dict = {
|
||||||
|
/** Product brand name; identical in every locale, kept here as the single source. */
|
||||||
|
brand: string;
|
||||||
header: HeaderStrings;
|
header: HeaderStrings;
|
||||||
categories: Record<CategoryId | "all", string>;
|
categories: Record<CategoryId | "all", string>;
|
||||||
home: Record<string, string>;
|
home: Record<string, string>;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Dict } from "./dict";
|
import type { Dict } from "./dict";
|
||||||
|
|
||||||
export const en: Dict = {
|
export const en: Dict = {
|
||||||
|
brand: "easy-image-tools",
|
||||||
header: {
|
header: {
|
||||||
workspace: "Workspace",
|
workspace: "Workspace",
|
||||||
catalog: "Catalog",
|
catalog: "Catalog",
|
||||||
@@ -24,14 +25,14 @@ export const en: Dict = {
|
|||||||
generate: "Generate",
|
generate: "Generate",
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
defaultTitle: "easy-png-tools — PNG utilities right in your browser",
|
defaultTitle: "easy-image-tools — PNG utilities right in your browser",
|
||||||
heroTitle: "What do you want to do with the image?",
|
heroTitle: "What do you want to do with the image?",
|
||||||
heroLead: "Find a tool — everything runs locally in your browser.",
|
heroLead: "Find a tool — everything runs locally in your browser.",
|
||||||
restoreLast: "↩ Restore last: {title}",
|
restoreLast: "↩ Restore last: {title}",
|
||||||
changeTool: "← Change tool",
|
changeTool: "← Change tool",
|
||||||
},
|
},
|
||||||
catalog: {
|
catalog: {
|
||||||
pageTitle: "All tools — easy-png-tools",
|
pageTitle: "All tools — easy-image-tools",
|
||||||
metaDescription:
|
metaDescription:
|
||||||
"Full catalog of PNG utilities: convert, transparency, color, geometry, analysis and image generation.",
|
"Full catalog of PNG utilities: convert, transparency, color, geometry, analysis and image generation.",
|
||||||
heading: "Tool catalog",
|
heading: "Tool catalog",
|
||||||
@@ -76,7 +77,7 @@ export const en: Dict = {
|
|||||||
warnSizeLimitMissShort: "Over size limit",
|
warnSizeLimitMissShort: "Over size limit",
|
||||||
},
|
},
|
||||||
savedChains: {
|
savedChains: {
|
||||||
pageTitle: "Saved pipelines — easy-png-tools",
|
pageTitle: "Saved pipelines — easy-image-tools",
|
||||||
metaDescription:
|
metaDescription:
|
||||||
"Your saved image pipelines: named tool chains stored locally in your browser.",
|
"Your saved image pipelines: named tool chains stored locally in your browser.",
|
||||||
heading: "Saved pipelines",
|
heading: "Saved pipelines",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Dict } from "./dict";
|
import type { Dict } from "./dict";
|
||||||
|
|
||||||
export const ru: Dict = {
|
export const ru: Dict = {
|
||||||
|
brand: "easy-image-tools",
|
||||||
header: {
|
header: {
|
||||||
workspace: "Рабочая область",
|
workspace: "Рабочая область",
|
||||||
catalog: "Каталог",
|
catalog: "Каталог",
|
||||||
@@ -24,7 +25,7 @@ export const ru: Dict = {
|
|||||||
generate: "Генерация",
|
generate: "Генерация",
|
||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
defaultTitle: "easy-png-tools — PNG-утилиты прямо в браузере",
|
defaultTitle: "easy-image-tools — PNG-утилиты прямо в браузере",
|
||||||
heroTitle: "Что делаем с изображением?",
|
heroTitle: "Что делаем с изображением?",
|
||||||
heroLead:
|
heroLead:
|
||||||
"Найдите инструмент — все операции выполняются локально в браузере.",
|
"Найдите инструмент — все операции выполняются локально в браузере.",
|
||||||
@@ -32,7 +33,7 @@ export const ru: Dict = {
|
|||||||
changeTool: "← Сменить инструмент",
|
changeTool: "← Сменить инструмент",
|
||||||
},
|
},
|
||||||
catalog: {
|
catalog: {
|
||||||
pageTitle: "Все инструменты — easy-png-tools",
|
pageTitle: "Все инструменты — easy-image-tools",
|
||||||
metaDescription:
|
metaDescription:
|
||||||
"Полный каталог PNG-утилит: конвертация, прозрачность, цвет, геометрия, анализ и генерация изображений.",
|
"Полный каталог PNG-утилит: конвертация, прозрачность, цвет, геометрия, анализ и генерация изображений.",
|
||||||
heading: "Каталог инструментов",
|
heading: "Каталог инструментов",
|
||||||
@@ -78,7 +79,7 @@ export const ru: Dict = {
|
|||||||
warnSizeLimitMissShort: "Больше лимита",
|
warnSizeLimitMissShort: "Больше лимита",
|
||||||
},
|
},
|
||||||
savedChains: {
|
savedChains: {
|
||||||
pageTitle: "Сохранённые пайплайны — easy-png-tools",
|
pageTitle: "Сохранённые пайплайны — easy-image-tools",
|
||||||
metaDescription:
|
metaDescription:
|
||||||
"Ваши сохранённые пайплайны изображений: именованные цепочки инструментов, хранятся локально в браузере.",
|
"Ваши сохранённые пайплайны изображений: именованные цепочки инструментов, хранятся локально в браузере.",
|
||||||
heading: "Сохранённые пайплайны",
|
heading: "Сохранённые пайплайны",
|
||||||
|
|||||||
+8
-4
@@ -1,6 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Public site URL for canonical/OG (absolute URLs including the GitHub Pages
|
* Public site URL for canonical/OG. From build-time `PUBLIC_SITE_ORIGIN` (set in
|
||||||
* base path). After the domain purchase, change it here; `docs/plan-seo.md` section 6.
|
* CI), falling back to the production domain; `$app/paths.base` is relative and
|
||||||
* Note: `$app/paths.base` does not work for this; it is relative.
|
* won't work here. `docs/plan-seo.md` section 6.
|
||||||
*/
|
*/
|
||||||
export const SITE_URL = "https://ku6epxboctuk.github.io/easy-png-tools";
|
const rawOrigin = import.meta.env.PUBLIC_SITE_ORIGIN;
|
||||||
|
export const SITE_URL =
|
||||||
|
typeof rawOrigin === "string" && rawOrigin.trim() !== ""
|
||||||
|
? rawOrigin.trim().replace(/\/+$/, "")
|
||||||
|
: "https://easy-image-tools.com";
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>easy-png-tools / {chain?.name ?? t("header.pipeline")}</title>
|
<title>{t("brand")} / {chain?.name ?? t("header.pipeline")}</title>
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,14 @@
|
|||||||
|
|
||||||
{#if page}
|
{#if page}
|
||||||
<Seo
|
<Seo
|
||||||
title={`easy-png-tools / ${pageTitle(page)}`}
|
title={`${t("brand")} / ${pageTitle(page)}`}
|
||||||
description={pageDescription(page)}
|
description={pageDescription(page)}
|
||||||
path={`/tools/${page.slug}`}
|
path={`/tools/${page.slug}`}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
{#if !page}
|
{#if !page}
|
||||||
<title>easy-png-tools / {t("toolPage.fallbackTitle")}</title>
|
<title>{t("brand")} / {t("toolPage.fallbackTitle")}</title>
|
||||||
{/if}
|
{/if}
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
easy-image-tools.com
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
<title>easy-png-tools</title>
|
<title>easy-image-tools</title>
|
||||||
<rect width="32" height="32" rx="7" fill="#2563eb"/>
|
<rect width="32" height="32" rx="7" fill="#2563eb"/>
|
||||||
<rect x="5" y="5" width="22" height="22" rx="4" fill="#ffffff"/>
|
<rect x="5" y="5" width="22" height="22" rx="4" fill="#ffffff"/>
|
||||||
<clipPath id="pic">
|
<clipPath id="pic">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 920 B After Width: | Height: | Size: 922 B |
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@@ -1,4 +1,4 @@
|
|||||||
// Stylelint config: design-token enforcement for easy-png-tools.
|
// Stylelint config: design-token enforcement for easy-image-tools.
|
||||||
// FWHM: keeps the design system a single source of truth. Colors and sizes must
|
// FWHM: keeps the design system a single source of truth. Colors and sizes must
|
||||||
// come from prefixed tokens; direct color values are allowed only for the two
|
// come from prefixed tokens; direct color values are allowed only for the two
|
||||||
// brand tokens.
|
// brand tokens.
|
||||||
|
|||||||
Reference in New Issue
Block a user