[feature][share] Кастомные адреса для шаринга: /l/:alias (отдельная таблица share_aliases, перенацеливаемый адрес) #205
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Описание
Кастомные («красивые») адреса для публично расшаренных страниц, вида:
Сейчас публичная ссылка выглядит как
https://docs.vvzvlad.xyz/share/<random-key>/p/<title>-<slugId>— длинная и нечитаемая. Нужен короткий человекочитаемый алиас на отдельном префиксе/l/, который не пересекается с текущим/share/...и существует параллельно с ним.Зафиксированные решения
/share/...не трогаем — продолжает работать как есть (на него много кто завязан: внутренние API, закладки, embed)./l/для красивых ссылок (изолированный неймспейс; из коротких заняты только/s— space и/p— page).share_aliases(не колонка вshares) — алиас как самостоятельный долгоживущий указатель.[a-z0-9-]), без кириллицы.Почему отдельная таблица, а не колонка в
sharesТребование «подменять страницу за стабильным адресом» плохо ложится на колонку в
shares: строкаsharesпривязана кpage_idсON DELETE CASCADE, поэтому при удалении/пересоздании страницы её шара (а с ней и алиас) исчезли бы, а «переезд» адреса превратился бы в перенос значения между строкамиshares. Отдельная таблица делает алиас самостоятельным указателем, не зависящим от жизненного цикла конкретной шары; подмена — это одинUPDATE, таблицаsharesне затрагивается.Модель данных
page_idnullable +ON DELETE SET NULL: при удалении целевой страницы адрес сохраняется (отдаёт 404 до перенацеливания), имя остаётся занятым. Точно отражает требование №5.ON DELETE CASCADE— проще, но адрес исчезает вместе со страницей.page_idуказывает на страницу, а не на шару — публичная доступность проверяется в момент захода через существующий share-граф (getShareForPage).UNIQUE (workspace_id, alias)— имя уникально в воркспейсе (как и текущийkey).Резолв
/l/:alias(сервер)Новый контроллер
@Controller('l')(по аналогии сshare-seo.controller.ts), исключённый из глобального префикса/apiвapps/server/src/main.ts(добавить'l/:alias'вexclude).Почему так:
/share/:key/p/:slug— краулеры (Google/Telegram/Slack) идут по 302 и получают корректное превью./lсам вызываетbuildShareMetaHtmlи ставит<link rel="canonical">на/l/<alias>, чтобы индексировался именно красивый адрес.Валидация слага (ASCII-only)
Общий util (используется сервером и клиентом):
[a-z0-9-], дефис только как разделитель; никакой кириллицы (slugifyне нужен).(workspace_id, alias); нарушение →409./l/изолированный неймспейс, коллизий с/share,/s,/p,/homeнет by design.Подмена страницы (ключевой сценарий)
Операция
setAlias(pageId, alias, confirmReassign?):alias; проверяем право редактироватьpageIdи что страница публично-читаема (тот же share-boundary).share_aliasesпо(workspace_id, alias):INSERT(новый адрес);page_id === pageId→ no-op;confirmReassignвозвращаем409с{ currentPageId, currentPageTitle }; клиент показывает «Адрес занят страницей "X". Перенести сюда?» и повторяет сconfirmReassign: true→UPDATE … SET page_id = :pageId.removeAlias(aliasId)—DELETEстроки (адрес освобождается; истории не ведём).Перенацеливание —
UPDATEодной строки; благодаря 302 все существующие ссылки/l/fooсразу ведут на новую страницу.UI (Share-модалка)
В
apps/client/src/features/share/components/share-modal.tsxпод текущим блоком ссылки — блок «Custom address»:docs.vvzvlad.xyz/l/+TextInputдля слага (live-нормализация,isValidShareAlias, debounce-проверка доступности через API).409— диалог «Перенести адрес сюда?» (подмена).…/l/<alias>сCopyTextButtonрядом со старой случайной…/share/:key/p/...(она остаётся).Точки изменений
Backend
…/migrations/<ts>-share-aliases.ts(новый) — таблица +UNIQUE(workspace_id, alias)+ индекс поpage_id.…/database/types/entity.types.ts+db.ts— типыShareAliases/ShareAlias/Insertable/Updatable.…/database/repos/share-alias/share-alias.repo.ts(новый) —findByAliasAndWorkspace,findByPageId,insert,updatePageId,delete.…/core/share/dto/share-alias.dto.ts(новый) —SetShareAliasDto { pageId, alias, confirmReassign? },RemoveShareAliasDto,ResolveAliasDto.…/core/share/share-alias.util.ts(новый) —normalizeShareAlias,isValidShareAlias.ShareServiceили новыйShareAliasService— set/remove/resolve + проверки доступа и доступности + обработка409.…/core/share/share-alias.controller.ts(новый) —@Controller('share-aliases')под/api: set/remove/availability.…/core/share/share-alias-redirect.controller.ts(новый) —@Controller('l'):GET :alias→ 302.apps/server/src/main.ts—'l/:alias'вexcludeглобального префикса.…/core/share/share.module.ts— регистрация репо/сервиса/контроллеров.Frontend
…/features/share/services/share-service.ts—setShareAlias,removeShareAlias,checkShareAliasAvailability.…/features/share/queries/share-query.ts— мутации/хуки + инвалидация.…/features/share/types/share.types.ts— поля алиаса.…/features/share/components/share-modal.tsx— блок «Custom address».apps/client/src/App.tsx+ новыйShareAliasRedirect— клиентский/l/:aliasдля in-app навигации (прямые заходы и так обрабатывает сервер 302).…/pages/settings/shares/shares.tsx— колонка с алиасом в списке шар.Граничные случаи
setAliasна занятый алиас →409 {currentPage}→ подтверждённый reassign =UPDATE page_id; 302 даёт мгновенный эффект без устаревшего кэша.page_id → NULL(адрес сохраняется,/l/foo→ 404 до перенацеливания).getShareForPageвернёт пусто → 404; повторный шаринг восстанавливает.includeSubPages-предком: алиас можно навести на неё; редирект уходит на/share/:ancestorKey/p/:subSlug./l/FOOв любом регистре → нормализуется кfoo→ резолвится.409.Этапы внедрения
ShareAliasRepo, контроллер/l,excludeвmain.ts.ShareAliasService/DTO,setAlias/removeAlias, обработка409, эндпоинт проверки доступности./l/:aliasс self-canonical, если редиректа не хватит для каких-то краулеров.Открытый вопрос (не блокирующий)
ON DELETE SET NULL(рекомендую — адрес переживает удаление страницы) vsCASCADE(проще, но адрес исчезает вместе со страницей) дляshare_aliases.page_id.Связанные части кода (текущее состояние):
apps/server/src/core/share/share.service.ts,apps/server/src/database/repos/share/share.repo.ts,apps/server/src/core/share/share-seo.controller.ts,apps/server/src/main.ts,apps/client/src/App.tsx,apps/client/src/features/share/components/share-modal.tsx,apps/client/src/pages/share/share-redirect.tsx.[feature][share] Кастомные адреса для шаринга: /l/<alias> (отдельная таблица share_aliases, перенацеливаемый адрес)to [feature][share] Кастомные адреса для шаринга: /l/:alias (отдельная таблица share_aliases, перенацеливаемый адрес)vvzvlad referenced this issue2026-06-26 17:40:11 +03:00