type SeoOverrides = { /** локаль, на которую должен вести canonical вместо текущей (у контента нет перевода) */ canonicalLocale?: string; /** страница единственная в пагинации — из canonical выкидывается sort */ alonePage?: boolean; /** страница закрыта от индексации — hreflang-кластер ей не нужен */ noindex?: boolean; }; type SeoOverridesState = SeoOverrides & { path: string; }; /** * Уточнения к SEO-ссылкам, которые знает только страница или конкретный компонент, * а выпускает теги один app.vue. Привязаны к route.path, чтобы значение * не «протекало» на следующую страницу при клиентской навигации. */ export const useSeoOverridesState = () => useState('seo-overrides', () => null); /** * Текущие уточнения для активного маршрута. */ export const useSeoOverrides = () => { const route = useRoute(); const state = useSeoOverridesState(); return computed(() => state.value?.path === route.path ? state.value : {}); }; const isSameOverride = (a: SeoOverridesState | null, b: SeoOverridesState) => !!a && a.path === b.path && a.canonicalLocale === b.canonicalLocale && a.alonePage === b.alonePage && a.noindex === b.noindex; /** * Записывает уточнение для текущего маршрута. Значения от разных источников * (страница, пагинация) складываются; при смене маршрута состояние сбрасывается. */ export const useSetSeoOverride = (overrides: MaybeRefOrGetter) => { const route = useRoute(); const state = useSeoOverridesState(); // именно watch с явными источниками, а не watchEffect: чтение state внутри колбэка // не отслеживается, иначе запись сюда же перезапускала бы эффект по кругу watch( [() => route.path, () => toValue(overrides)], ([path, patch]) => { const next: SeoOverridesState = state.value?.path === path ? { ...state.value, ...patch } : { path, ...patch }; if (isSameOverride(state.value, next)) return; state.value = next; }, { immediate: true } ); };