itube-specs 0.0.840 → 0.0.841

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,7 +66,7 @@ function handler() {
66
66
  isMenuShow.value = !isMenuShow.value;
67
67
  }
68
68
 
69
- const enabledLocales = useState('enabledLocales');
69
+ const enabledLocales = useEnabledLocales();
70
70
 
71
71
  const localeName = computed(() => locales.value.find(item => item.code === locale.value)?.name || 'English');
72
72
 
@@ -120,8 +120,6 @@ defineProps<{
120
120
  }>();
121
121
 
122
122
  const countriesItems = useState<ISelectItem[]>('countriesItems', () => []);
123
- const enabledLanguageCodesState = useState<string[]>('enabledLanguageCodes', () => []);
124
-
125
123
  const clientCountryCode = useState<string>('clientCountryCode', () => '');
126
124
 
127
125
  const clientCountryIcon = computed(() => {
@@ -146,13 +144,6 @@ const filteredCountries = computed(() => {
146
144
  return countriesItems.value.filter(item => item.title.toLowerCase().includes(query));
147
145
  });
148
146
 
149
- if (!enabledLanguageCodesState.value.length) {
150
- const { data: enabledLanguageCodes } = await useFetch<string[]>('/bff/site/languages', { default: () => [] });
151
- if (enabledLanguageCodes.value?.length) {
152
- enabledLanguageCodesState.value = enabledLanguageCodes.value;
153
- }
154
- }
155
-
156
147
  const isCountryLoading = ref(false);
157
148
 
158
149
  // Список стран не приходит в payload (см. plugins/countries.server.ts) — догружаем
@@ -178,13 +169,7 @@ function onCountryToggle(e: Event) {
178
169
  if (isCountryOpen.value) loadCountries();
179
170
  }
180
171
 
181
- const enabledLocales = useState('enabledLocales', () => locales.value);
182
-
183
- watch(enabledLanguageCodesState, (codes) => {
184
- enabledLocales.value = codes?.length
185
- ? locales.value.filter(item => codes.includes(item.code))
186
- : locales.value;
187
- }, { immediate: true });
172
+ const enabledLocales = useEnabledLocales();
188
173
 
189
174
  const selectedLocale = ref(locale.value);
190
175
 
@@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest';
3
3
  import { mockNuxtImport } from '@nuxt/test-utils/runtime';
4
4
  import { useSeoLinks } from '../use-seo-links';
5
5
  import { useSeoOverridesState } from '../use-seo-overrides';
6
+ import { useEnabledLanguageCodes } from '../use-enabled-locales';
6
7
 
7
8
  let mockRoute = { path: '/videos', query: {} as Record<string, any> };
8
9
 
@@ -23,6 +24,7 @@ describe('useSeoLinks', () => {
23
24
  mockRoute = { path: '/videos', query: {} };
24
25
  mockI18n.locale.value = 'en';
25
26
  useSeoOverridesState().value = null;
27
+ useEnabledLanguageCodes().value = [];
26
28
  });
27
29
 
28
30
  it('canonical URL', () => {
@@ -63,6 +65,26 @@ describe('useSeoLinks', () => {
63
65
  expect(hreflangs).toContain('x-default');
64
66
  });
65
67
 
68
+ it('alternate links только для включённых на сайте локалей', () => {
69
+ useEnabledLanguageCodes().value = ['en', 'fr'];
70
+ const { alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath);
71
+ const hreflangs = alternateLinks.value.map(l => l.hreflang);
72
+ expect(hreflangs).toContain('en');
73
+ expect(hreflangs).toContain('fr');
74
+ expect(hreflangs).toContain('x-default');
75
+ expect(hreflangs).not.toContain('de');
76
+ expect(hreflangs).not.toContain('ja-JP');
77
+ expect(hreflangs).not.toContain('zh-CN');
78
+ });
79
+
80
+ it('текущая локаль остаётся в alternate, даже если её нет в списке включённых', () => {
81
+ mockRoute = { path: '/ja/videos', query: {} };
82
+ mockI18n.locale.value = 'ja';
83
+ useEnabledLanguageCodes().value = ['en', 'fr'];
84
+ const { alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath);
85
+ expect(alternateLinks.value.map(l => l.hreflang)).toContain('ja-JP');
86
+ });
87
+
66
88
  it('ja маппится в ja-JP', () => {
67
89
  const { alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath);
68
90
  const jaLink = alternateLinks.value.find(l => l.hreflang === 'ja-JP');
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Коды локалей, реально включённых на сайте. Заполняет plugins/enabled-locales.server.ts,
3
+ * значение уезжает в payload. Пустой список = данных нет, потребители используют все локали.
4
+ */
5
+ export const useEnabledLanguageCodes = () => useState<string[]>('enabledLanguageCodes', () => []);
6
+
7
+ /**
8
+ * Локали i18n, отфильтрованные по включённым на сайте — для переключателей языка и hreflang.
9
+ */
10
+ export const useEnabledLocales = () => {
11
+ const { locales } = useI18n();
12
+ const codes = useEnabledLanguageCodes();
13
+
14
+ return computed(() => codes.value.length
15
+ ? locales.value.filter(item => codes.value.includes(item.code))
16
+ : locales.value);
17
+ };
@@ -15,6 +15,7 @@ type LocalePathMock = (path: string, localeCode: string) => string;
15
15
  export const useSeoLinks = (baseDomain: string, i18n: I18nMock, localePath: LocalePathMock) => {
16
16
  const route = useRoute();
17
17
  const overrides = useSeoOverrides();
18
+ const enabledCodes = useEnabledLanguageCodes();
18
19
 
19
20
  const allowedParams: string[] = ['page', 'sort', 'categories'];
20
21
  const createUrlWithParams = (path: string) => {
@@ -77,10 +78,14 @@ export const useSeoLinks = (baseDomain: string, i18n: I18nMock, localePath: Loca
77
78
  });
78
79
  }
79
80
 
81
+ // локаль, выключенная на сайте, отдаёт 404 (server/middleware/locale-guard.ts) —
82
+ // такие hreflang вели бы на несуществующие страницы
83
+ const isEnabled = (code: string) => !enabledCodes.value.length || enabledCodes.value.includes(code);
84
+
80
85
  const otherLocales = i18n.locales.value.filter(
81
86
  (locale) => {
82
87
  const localeCode = typeof locale === 'string' ? locale : locale.code;
83
- return localeCode !== currentLocaleCode && localeCode !== 'en';
88
+ return localeCode !== currentLocaleCode && localeCode !== 'en' && isEnabled(localeCode);
84
89
  }
85
90
  );
86
91
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.840",
4
+ "version": "0.0.841",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -0,0 +1,20 @@
1
+ // SSR-плагин: кладёт коды реально включённых на сайте локалей в useState('enabledLanguageCodes').
2
+ // Нужен, чтобы hreflang в app.vue и переключатели языков не предлагали локали, на которые
3
+ // server/middleware/locale-guard.ts отдаёт 404. Раньше список тянул только layout/language.vue,
4
+ // а он рендерится лениво по клику — на SSR не выполнялся никогда.
5
+ // Эндпоинт читает статический SiteDataService и сам падает обратно на runtimeConfig.public.locales.
6
+ export default defineNuxtPlugin({
7
+ parallel: true,
8
+ async setup() {
9
+ const codes = useEnabledLanguageCodes();
10
+ if (codes.value.length) return;
11
+
12
+ try {
13
+ const languages = await $fetch<string[]>('/api/site/languages');
14
+ if (languages?.length) codes.value = languages;
15
+ }
16
+ catch {
17
+ // оставляем пустым — потребители падают обратно на полный список локалей
18
+ }
19
+ },
20
+ });