itube-specs 0.0.896 → 0.0.897

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.
Files changed (37) hide show
  1. package/composables/use-meta.ts +67 -28
  2. package/package.json +1 -1
  3. package/pages/2257.vue +1 -1
  4. package/pages/categories/all.vue +1 -1
  5. package/pages/categories/index.vue +1 -1
  6. package/pages/categories/letter/[slug].vue +7 -1
  7. package/pages/channels/[channel].vue +8 -1
  8. package/pages/channels/index.vue +6 -1
  9. package/pages/confirm/registration/[token].vue +1 -1
  10. package/pages/contacts.vue +1 -1
  11. package/pages/dmca.vue +1 -1
  12. package/pages/index.vue +8 -10
  13. package/pages/models/[slug]/channel/[channel].vue +9 -1
  14. package/pages/models/[slug]/index.vue +9 -1
  15. package/pages/models/[slug]/tag/[tag].vue +10 -1
  16. package/pages/models/index.vue +7 -1
  17. package/pages/models/letter/[slug].vue +8 -1
  18. package/pages/playlists/[playlistId]/[slug].vue +9 -1
  19. package/pages/playlists/index.vue +6 -1
  20. package/pages/policy.vue +1 -1
  21. package/pages/search/[slug]/index.vue +7 -1
  22. package/pages/search/[slug]/models.vue +7 -1
  23. package/pages/search/[slug]/videos.vue +7 -1
  24. package/pages/tag/[tag].vue +8 -1
  25. package/pages/terms.vue +1 -1
  26. package/pages/user/profile.vue +1 -1
  27. package/pages/user/recovery-password.vue +1 -1
  28. package/pages/user/settings.vue +1 -1
  29. package/pages/videos/[slug].vue +8 -1
  30. package/runtime/utils/cleaners/__tests__/clean-model-info.test.ts +13 -0
  31. package/runtime/utils/cleaners/clean-model-info.ts +1 -0
  32. package/server/api/models/list-by-niche.post.ts +3 -1
  33. package/types/card-info.d.ts +2 -0
  34. package/types/index.d.ts +1 -0
  35. package/types/page-seo.d.ts +6 -0
  36. package/types/paginated-response.d.ts +3 -0
  37. package/types/raw/raw-model-info.d.ts +2 -1
@@ -1,9 +1,27 @@
1
- import { computed, unref } from 'vue';
2
- import type { Ref } from 'vue';
1
+ import { computed, toValue } from 'vue';
2
+ import type { MaybeRefOrGetter } from 'vue';
3
3
  import { useRoute } from 'vue-router';
4
- import type { INavLinkItem } from '../types';
4
+ import type { INavLinkItem, IPageSeo } from '../types';
5
5
 
6
- type MetaOptions = {
6
+ type MetaText = MaybeRefOrGetter<string | undefined>;
7
+
8
+ type MetaParams = {
9
+ /** функция перевода (i18n) */
10
+ t: (key: string, params?: Record<string, unknown>) => string
11
+ /** ключ страницы в `meta.*` (например 'videos', 'models') */
12
+ pageKey: string
13
+ /** название бренда для подстановки в мета */
14
+ brandName: string
15
+ /** опции сортировки — задают уровень `meta.<page>.<sort>.*` в ключах */
16
+ sortOptions?: INavLinkItem[]
17
+ /** подстановка `{text}` — основной текст (slug, имя и т.д.) */
18
+ text?: MetaText
19
+ /** подстановка `{secondText}` */
20
+ secondText?: MetaText
21
+ /** подстановка `{thirdText}` */
22
+ thirdText?: MetaText
23
+ /** подстановка `{fourText}` */
24
+ fourthText?: MetaText
7
25
  /**
8
26
  * Подменяет i18n-ключ заголовка (по умолчанию 'h1') — вызывается с фактической
9
27
  * сортировкой, потому что варианты заголовка есть не у каждой из них.
@@ -11,21 +29,19 @@ type MetaOptions = {
11
29
  * висячий предлог (`… Videos in ` без страны).
12
30
  */
13
31
  h1Key?: (sortType: string) => string
32
+ /**
33
+ * Мета от API (поле `seo` в ответе листинга). Каждое непустое поле перебивает
34
+ * свой i18n-шаблон; чего API не отдал — остаётся на i18n. Бренд бэкенд уже
35
+ * подставляет сам, а номер страницы — нет, поэтому суффикс пагинации
36
+ * дописываем здесь.
37
+ */
38
+ seo?: MaybeRefOrGetter<IPageSeo | null | undefined>
14
39
  };
15
40
 
16
41
  /**
17
42
  * Генерирует реактивные meta (title, description) и h1 с учётом сортировки, страницы и i18n-ключей.
18
- * @param t - функция перевода (i18n)
19
- * @param page - ключ страницы для i18n (например 'videos', 'models')
20
- * @param brandName - название бренда для подстановки в мета
21
- * @param sortOptions - опции сортировки (если есть)
22
- * @param text - основной текст (slug, имя и т.д.)
23
- * @param secondText - второй подставляемый текст
24
- * @param thirdText - третий подставляемый текст
25
- * @param fourthText - четвёртый подставляемый текст
26
- * @param options - уточнения (подмена ключа h1)
27
43
  */
28
- export function useMeta(t: (key: string, params?: Record<string, unknown>) => string, page: string, brandName: string, sortOptions?: INavLinkItem[], text?: Ref<string>, secondText?: Ref<string>, thirdText?: Ref<string>, fourthText?: Ref<string>, options?: MetaOptions) {
44
+ export function useMeta({ t, pageKey, brandName, sortOptions, text, secondText, thirdText, fourthText, h1Key, seo }: MetaParams) {
29
45
  const route = useRoute();
30
46
 
31
47
  const sortType = computed(() => {
@@ -36,37 +52,60 @@ export function useMeta(t: (key: string, params?: Record<string, unknown>) => st
36
52
  return queryValue && values?.includes(queryValue) ? queryValue : defaultValue;
37
53
  });
38
54
 
55
+ const pageSuffix = computed(() => {
56
+ const pageNumber = route.query?.['page'] ? Number(route.query['page']) : 1;
57
+ return pageNumber === 1 ? '' : ` ${t('page')} ${pageNumber}`;
58
+ });
59
+
39
60
  function getPath(key: string) {
40
- const plainSlug = unref(text);
41
- const secondTextValue = unref(secondText);
42
- const thirdTextValue = unref(thirdText);
43
- const fourTextValue = unref(fourthText);
44
- const pageNumber = computed(() => route.query?.['page'] ? Number(route.query['page']) : 1);
45
- const pageText = unref(pageNumber) === 1 ? null : ` ${t('page')} ${unref(pageNumber)}`;
61
+ const pageText = pageSuffix.value;
46
62
 
47
63
  return t(
48
- `meta.${page}.${sortOptions && sortOptions.length > 0 ? `${sortType.value}.` : ''}${key}`,
64
+ `meta.${pageKey}.${sortOptions && sortOptions.length > 0 ? `${sortType.value}.` : ''}${key}`,
49
65
  {
50
- text: plainSlug,
51
- secondText: secondTextValue,
52
- thirdText: thirdTextValue,
53
- fourText: fourTextValue,
66
+ text: toValue(text),
67
+ secondText: toValue(secondText),
68
+ thirdText: toValue(thirdText),
69
+ fourText: toValue(fourthText),
54
70
  brandName: `| ${brandName}`, //черточка '|' нужна обязательно
55
71
  page: pageText,
56
72
  }
57
73
  );
58
74
  }
59
75
 
60
- const metaTitle = computed(() => getPath('title'));
61
- const h1 = computed(() => getPath(options?.h1Key?.(sortType.value) || 'h1'));
76
+ const apiSeo = computed(() => toValue(seo) || null);
77
+
78
+ /**
79
+ * Дописывает « Page N» так же, как это делают i18n-шаблоны — перед брендом,
80
+ * который бэкенд уже приклеил через `|`.
81
+ */
82
+ function withPageSuffix(value: string) {
83
+ const suffix = pageSuffix.value;
84
+ if (!suffix) return value;
85
+
86
+ const brandIndex = value.lastIndexOf(' | ');
87
+ return brandIndex === -1
88
+ ? `${value}${suffix}`
89
+ : `${value.slice(0, brandIndex)}${suffix}${value.slice(brandIndex)}`;
90
+ }
91
+
92
+ const metaTitle = computed(() => {
93
+ const apiTitle = apiSeo.value?.title;
94
+ return apiTitle ? withPageSuffix(apiTitle) : getPath('title');
95
+ });
96
+
97
+ const h1 = computed(() => apiSeo.value?.h1 || getPath(h1Key?.(sortType.value) || 'h1'));
62
98
 
63
99
  const meta = computed(() => {
64
- const description = getPath('meta_description');
100
+ const apiDescription = apiSeo.value?.description;
101
+ const description = apiDescription ? `${apiDescription}${pageSuffix.value}` : getPath('meta_description');
102
+ const keywords = apiSeo.value?.keywords;
65
103
 
66
104
  return {
67
105
  title: metaTitle.value,
68
106
  meta: [
69
107
  { name: 'description', content: description },
108
+ ...(keywords ? [{ name: 'keywords', content: keywords }] : []),
70
109
  { property: 'og:title', content: metaTitle.value },
71
110
  { property: 'og:description', content: description },
72
111
  { name: 'twitter:title', content: metaTitle.value },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.896",
4
+ "version": "0.0.897",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
package/pages/2257.vue CHANGED
@@ -64,7 +64,7 @@ const { t } = useI18n();
64
64
  const { generateLink } = useGenerateLink();
65
65
  const pagePureName = useGetPureRouteName();
66
66
  const { brandName, emails } = useAppConfig();
67
- const { meta, h1 } = useMeta(t, pagePureName, brandName);
67
+ const { meta, h1 } = useMeta({ t, pageKey: pagePureName, brandName });
68
68
 
69
69
  watchEffect(() => {
70
70
  useHead(meta.value);
@@ -68,7 +68,7 @@ const [
68
68
  ]);
69
69
 
70
70
  const pagePureName = useGetPureRouteName();
71
- const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName);
71
+ const { meta, h1 } = useMeta({ t, pageKey: pagePureName, brandName: useAppConfig().brandName });
72
72
 
73
73
  watchEffect(() => {
74
74
  useHead(meta.value);
@@ -69,7 +69,7 @@ const { t } = useI18n();
69
69
  const { generateLink } = useGenerateLink();
70
70
 
71
71
  const pagePureName = useGetPureRouteName();
72
- const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName);
72
+ const { meta, h1 } = useMeta({ t, pageKey: pagePureName, brandName: useAppConfig().brandName });
73
73
 
74
74
  watchEffect(() => {
75
75
  useHead(meta.value);
@@ -59,7 +59,13 @@ const slug = computed(() => route.params[ 'slug' ] as string || '');
59
59
 
60
60
  const pagePureName = useGetPureRouteName();
61
61
  const metaTitle = computed(() => convertString().toCapitalize(slug.value));
62
- const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName, [], metaTitle);
62
+ const { meta, h1 } = useMeta({
63
+ t,
64
+ pageKey: pagePureName,
65
+ brandName: useAppConfig().brandName,
66
+ sortOptions: [],
67
+ text: metaTitle,
68
+ });
63
69
 
64
70
  watchEffect(() => {
65
71
  useHead(meta.value);
@@ -117,7 +117,14 @@ const routeQueryCategories = computed(() => {
117
117
 
118
118
  const {
119
119
  meta
120
- } = useMeta(t, pageName, useAppConfig().brandName, sortItemsDefault, metaTitle, routeQueryCategories);
120
+ } = useMeta({
121
+ t,
122
+ pageKey: pageName,
123
+ brandName: useAppConfig().brandName,
124
+ sortOptions: sortItemsDefault,
125
+ text: metaTitle,
126
+ secondText: routeQueryCategories,
127
+ });
121
128
 
122
129
  watchEffect(() => {
123
130
  useHead(meta.value);
@@ -102,7 +102,12 @@ const items = computed(() => {
102
102
  });
103
103
 
104
104
  const pagePureName = useGetPureRouteName();
105
- const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName, sortChannels);
105
+ const { meta, h1 } = useMeta({
106
+ t,
107
+ pageKey: pagePureName,
108
+ brandName: useAppConfig().brandName,
109
+ sortOptions: sortChannels,
110
+ });
106
111
 
107
112
  watchEffect(() => {
108
113
  useHead(meta.value);
@@ -61,7 +61,7 @@ onMounted(() => {
61
61
  });
62
62
 
63
63
  const pagePureName = useGetPureRouteName();
64
- const { meta } = useMeta(t, pagePureName, useAppConfig().brandName);
64
+ const { meta } = useMeta({ t, pageKey: pagePureName, brandName: useAppConfig().brandName });
65
65
 
66
66
  watchEffect(() => {
67
67
  useHead(meta.value);
@@ -280,7 +280,7 @@ watch(() => route.query, (value) => {
280
280
  const placeholder = computed(() => t('contacts.type_here'));
281
281
 
282
282
  const pagePureName = useGetPureRouteName();
283
- const { meta } = useMeta(t, pagePureName, useAppConfig().brandName);
283
+ const { meta } = useMeta({ t, pageKey: pagePureName, brandName: useAppConfig().brandName });
284
284
 
285
285
  watchEffect(() => {
286
286
  useHead(meta.value);
package/pages/dmca.vue CHANGED
@@ -122,7 +122,7 @@ const { t } = useI18n();
122
122
  const { generateLink } = useGenerateLink();
123
123
  const pagePureName = useGetPureRouteName();
124
124
  const { brandName, emails } = useAppConfig();
125
- const { meta, h1 } = useMeta(t, pagePureName, brandName);
125
+ const { meta, h1 } = useMeta({ t, pageKey: pagePureName, brandName });
126
126
 
127
127
  watchEffect(() => {
128
128
  useHead(meta.value);
package/pages/index.vue CHANGED
@@ -69,18 +69,16 @@ const isCountryShown = computed(() => !route.query['sort'] && !!clientCountry.na
69
69
 
70
70
  const countryName = computed(() => (isCountryShown.value ? clientCountry.name.value : ''));
71
71
 
72
- const { meta, h1 } = useMeta(
72
+ const { meta, h1 } = useMeta({
73
73
  t,
74
- pageName,
75
- useAppConfig().brandName,
76
- sortItemsDefault,
77
- routeQueryCategories,
78
- countryName,
79
- undefined,
80
- undefined,
74
+ pageKey: pageName,
75
+ brandName: useAppConfig().brandName,
76
+ sortOptions: sortItemsDefault,
77
+ text: routeQueryCategories,
78
+ secondText: countryName,
81
79
  // Страну не определили — берём заголовок без неё, иначе останется висячий предлог.
82
- { h1Key: sortType => (sortType === 'trending' && !countryName.value ? 'h1_no_country' : 'h1') },
83
- );
80
+ h1Key: sortType => (sortType === 'trending' && !countryName.value ? 'h1_no_country' : 'h1'),
81
+ });
84
82
 
85
83
  const countryFlagImg = computed(() => (isCountryShown.value ? clientCountry.flagSrc.value : undefined));
86
84
 
@@ -68,7 +68,15 @@ const channelRoute = String(route.params[ 'channel' ]);
68
68
  const channelSlug = computed(() => convertString().fromSlug(channelRoute));
69
69
  const text = computed(() => `${metaTitle.value} ${convertString().toCapitalize(channelSlug.value)}`);
70
70
 
71
- const { meta, h1 } = useMeta(t, pageName, useAppConfig().brandName, sortItemsDefault, text, routeQueryCategories);
71
+ const { meta, h1 } = useMeta({
72
+ t,
73
+ pageKey: pageName,
74
+ brandName: useAppConfig().brandName,
75
+ sortOptions: sortItemsDefault,
76
+ text,
77
+ secondText: routeQueryCategories,
78
+ seo: () => cardInfo.value?.seo,
79
+ });
72
80
 
73
81
  const breadcrumbs: IBreadcrumbItem[] = [
74
82
  {
@@ -74,7 +74,15 @@ const routeQueryCategories = computed(() => {
74
74
  return '';
75
75
  });
76
76
 
77
- const { meta, h1 } = useMeta(t, pageName, useAppConfig().brandName, sortItemsDefault, metaTitle, routeQueryCategories);
77
+ const { meta, h1 } = useMeta({
78
+ t,
79
+ pageKey: pageName,
80
+ brandName: useAppConfig().brandName,
81
+ sortOptions: sortItemsDefault,
82
+ text: metaTitle,
83
+ secondText: routeQueryCategories,
84
+ seo: () => cardInfo.value?.seo,
85
+ });
78
86
 
79
87
  const breadcrumbs: IBreadcrumbItem[] = [
80
88
  {
@@ -65,7 +65,16 @@ const routeQueryCategories = computed(() => {
65
65
  return '';
66
66
  });
67
67
 
68
- const { meta, h1 } = useMeta(t, pageName, useAppConfig().brandName, sortItemsDefault, metaTitle, routeQueryCategories, tagSlug);
68
+ const { meta, h1 } = useMeta({
69
+ t,
70
+ pageKey: pageName,
71
+ brandName: useAppConfig().brandName,
72
+ sortOptions: sortItemsDefault,
73
+ text: metaTitle,
74
+ secondText: routeQueryCategories,
75
+ thirdText: tagSlug,
76
+ seo: () => cardInfo.value?.seo,
77
+ });
69
78
 
70
79
  const breadcrumbs: IBreadcrumbItem[] = [
71
80
  {
@@ -69,7 +69,13 @@ if (models.value?.items) {
69
69
  models.value.items = applyTestImages(models.value.items)!;
70
70
  }
71
71
 
72
- const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName, sortModels);
72
+ const { meta, h1 } = useMeta({
73
+ t,
74
+ pageKey: pagePureName,
75
+ brandName: useAppConfig().brandName,
76
+ sortOptions: sortModels,
77
+ seo: () => models.value?.seo,
78
+ });
73
79
 
74
80
  watchEffect(() => {
75
81
  useHead(meta.value);
@@ -39,7 +39,14 @@ const [
39
39
  useFetchModelsByLetter(ModelsApiService.getListByNicheAndLetter),
40
40
  ]);
41
41
 
42
- const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName, sortModels, metaTitle);
42
+ const { meta, h1 } = useMeta({
43
+ t,
44
+ pageKey: pagePureName,
45
+ brandName: useAppConfig().brandName,
46
+ sortOptions: sortModels,
47
+ text: metaTitle,
48
+ seo: () => models.value?.seo,
49
+ });
43
50
 
44
51
  watchEffect(() => {
45
52
  useHead(meta.value);
@@ -107,7 +107,15 @@ const pagePureName = useGetPureRouteName();
107
107
  const title = computed(() => videoData.value?.title || '');
108
108
  const description = computed(() => videoData.value?.description || '');
109
109
  const playlistName = computed(() => playlistsData.value?.name);
110
- const { meta } = useMeta(t, pagePureName, useAppConfig().brandName, [], title, playlistName, description);
110
+ const { meta } = useMeta({
111
+ t,
112
+ pageKey: pagePureName,
113
+ brandName: useAppConfig().brandName,
114
+ sortOptions: [],
115
+ text: title,
116
+ secondText: playlistName,
117
+ thirdText: description,
118
+ });
111
119
 
112
120
  watchEffect(() => {
113
121
  if (videoData.value?.title) {
@@ -120,7 +120,12 @@ watch(
120
120
  const { t } = useI18n();
121
121
 
122
122
  const pagePureName = useGetPureRouteName();
123
- const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName, sortPlaylists);
123
+ const { meta, h1 } = useMeta({
124
+ t,
125
+ pageKey: pagePureName,
126
+ brandName: useAppConfig().brandName,
127
+ sortOptions: sortPlaylists,
128
+ });
124
129
 
125
130
  watchEffect(() => {
126
131
  useHead(meta.value);
package/pages/policy.vue CHANGED
@@ -182,7 +182,7 @@
182
182
  const { t } = useI18n();
183
183
  const pagePureName = useGetPureRouteName();
184
184
  const { brandName, emails } = useAppConfig();
185
- const { meta, h1 } = useMeta(t, pagePureName, brandName);
185
+ const { meta, h1 } = useMeta({ t, pageKey: pagePureName, brandName });
186
186
 
187
187
  watchEffect(() => {
188
188
  useHead(meta.value);
@@ -194,7 +194,13 @@ const relatedSearchesItems = computed(() => relatedSearch.value && typeof relate
194
194
  const pagePureName = useGetPureRouteName();
195
195
  const metaTitle = computed(() => convertString().toCapitalize(slug.value));
196
196
 
197
- const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName, [], metaTitle);
197
+ const { meta, h1 } = useMeta({
198
+ t,
199
+ pageKey: pagePureName,
200
+ brandName: useAppConfig().brandName,
201
+ sortOptions: [],
202
+ text: metaTitle,
203
+ });
198
204
 
199
205
  watchEffect(() => {
200
206
  if (slug) {
@@ -39,7 +39,13 @@ useNoindexThinListing(() => modelsData.value?.items?.length);
39
39
 
40
40
  const metaTitle = computed(() => convertString().toCapitalize(slug.value));
41
41
 
42
- const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName, sortModels, metaTitle);
42
+ const { meta, h1 } = useMeta({
43
+ t,
44
+ pageKey: pagePureName,
45
+ brandName: useAppConfig().brandName,
46
+ sortOptions: sortModels,
47
+ text: metaTitle,
48
+ });
43
49
 
44
50
  watchEffect(() => {
45
51
  if (slug) {
@@ -42,7 +42,13 @@ useNoindexThinListing(() => videosData.value?.items?.length);
42
42
  const pagePureName = useGetPureRouteName();
43
43
  const metaTitle = computed(() => convertString().toCapitalize(slug.value));
44
44
 
45
- const { meta, h1 } = useMeta(t, pagePureName, useAppConfig().brandName, sortItemsSearch, metaTitle);
45
+ const { meta, h1 } = useMeta({
46
+ t,
47
+ pageKey: pagePureName,
48
+ brandName: useAppConfig().brandName,
49
+ sortOptions: sortItemsSearch,
50
+ text: metaTitle,
51
+ });
46
52
 
47
53
  watchEffect(() => {
48
54
  if (slug) {
@@ -58,7 +58,14 @@ const routeQueryCategories = computed(() => {
58
58
  return '';
59
59
  });
60
60
 
61
- const { meta, h1 } = useMeta(t, pageName, useAppConfig().brandName, sortItemsDefault, metaTitle, routeQueryCategories);
61
+ const { meta, h1 } = useMeta({
62
+ t,
63
+ pageKey: pageName,
64
+ brandName: useAppConfig().brandName,
65
+ sortOptions: sortItemsDefault,
66
+ text: metaTitle,
67
+ secondText: routeQueryCategories,
68
+ });
62
69
 
63
70
  watchEffect(() => {
64
71
  if (slug.value) {
package/pages/terms.vue CHANGED
@@ -465,7 +465,7 @@
465
465
  const { t } = useI18n();
466
466
  const pagePureName = useGetPureRouteName();
467
467
  const { brandName } = useAppConfig();
468
- const { meta, h1 } = useMeta(t, pagePureName, brandName);
468
+ const { meta, h1 } = useMeta({ t, pageKey: pagePureName, brandName });
469
469
 
470
470
  watchEffect(() => {
471
471
  useHead(meta.value);
@@ -112,7 +112,7 @@ const items = computed<Array<IDataItem>>(() => {
112
112
  });
113
113
 
114
114
  const pagePureName = useGetPureRouteName();
115
- const { meta } = useMeta(t, pagePureName, useAppConfig().brandName);
115
+ const { meta } = useMeta({ t, pageKey: pagePureName, brandName: useAppConfig().brandName });
116
116
 
117
117
  watchEffect(() => {
118
118
  useHead(meta.value);
@@ -54,7 +54,7 @@ function update() {
54
54
  }
55
55
 
56
56
  const pagePureName = useGetPureRouteName();
57
- const { meta } = useMeta(t, pagePureName, useAppConfig().brandName);
57
+ const { meta } = useMeta({ t, pageKey: pagePureName, brandName: useAppConfig().brandName });
58
58
 
59
59
  watchEffect(() => {
60
60
  useHead(meta.value);
@@ -8,7 +8,7 @@
8
8
  const { t } = useI18n();
9
9
 
10
10
  const pagePureName = useGetPureRouteName();
11
- const { meta } = useMeta(t, pagePureName, useAppConfig().brandName);
11
+ const { meta } = useMeta({ t, pageKey: pagePureName, brandName: useAppConfig().brandName });
12
12
 
13
13
  watchEffect(() => {
14
14
  useHead(meta.value);
@@ -60,7 +60,14 @@ useSetSeoOverride(() => ({
60
60
  const title = computed(() => videoData.value?.title || '');
61
61
  const description = computed(() => videoData.value?.description || '');
62
62
  const pagePureName = useGetPureRouteName();
63
- const { meta } = useMeta(t, pagePureName, useAppConfig().brandName, [], title, description);
63
+ const { meta } = useMeta({
64
+ t,
65
+ pageKey: pagePureName,
66
+ brandName: useAppConfig().brandName,
67
+ sortOptions: [],
68
+ text: title,
69
+ secondText: description,
70
+ });
64
71
 
65
72
  const webpThumbs = videoData.value?.thumbUrls?.webp;
66
73
  const webpEntries = webpThumbs ? Object.entries(webpThumbs) : [];
@@ -16,6 +16,12 @@ describe('cleanModelInfo', () => {
16
16
  aka_names: ['Alias A', 'Alias B'],
17
17
  birth_summary: 'Born 1990, USA',
18
18
  nationality_iso_codes: ['US', 'CA'],
19
+ seo: {
20
+ title: 'Model Name Videos | Brand',
21
+ description: 'meta desc',
22
+ h1: 'Model Name Porn Videos',
23
+ keywords: 'a, b',
24
+ },
19
25
  };
20
26
  const result = cleanModelInfo(raw as any);
21
27
  expect(result.title).toBe('Model Name');
@@ -29,6 +35,12 @@ describe('cleanModelInfo', () => {
29
35
  expect(result.akaNames).toBe('Alias A, Alias B');
30
36
  expect(result.birthSummary).toBe('Born 1990, USA');
31
37
  expect(result.nationalityIsoCodes).toEqual(['US', 'CA']);
38
+ expect(result.seo).toEqual({
39
+ title: 'Model Name Videos | Brand',
40
+ description: 'meta desc',
41
+ h1: 'Model Name Porn Videos',
42
+ keywords: 'a, b',
43
+ });
32
44
  });
33
45
 
34
46
  it('пустые данные → дефолты', () => {
@@ -44,5 +56,6 @@ describe('cleanModelInfo', () => {
44
56
  expect(result.akaNames).toBe('');
45
57
  expect(result.birthSummary).toBe('');
46
58
  expect(result.nationalityIsoCodes).toEqual([]);
59
+ expect(result.seo).toBeNull();
47
60
  });
48
61
  });
@@ -14,4 +14,5 @@ export const cleanModelInfo = (card: IRawModelInfo): ICardInfo => ({
14
14
  akaNames: card.aka_names?.join(', ') || '',
15
15
  birthSummary: card.birth_summary || '',
16
16
  nationalityIsoCodes: card.nationality_iso_codes || [],
17
+ seo: card.seo || null,
17
18
  });
@@ -1,5 +1,5 @@
1
1
  import { cleanModelCard, getUrlWithProxiedParams } from '../../../runtime';
2
- import type { IModelFilter, IRawModelCard, IModelCard } from '../../../types';
2
+ import type { IModelFilter, IRawModelCard, IModelCard, IPageSeo } from '../../../types';
3
3
 
4
4
  export default defineEventHandler(async (event) => {
5
5
  const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
@@ -17,6 +17,7 @@ export default defineEventHandler(async (event) => {
17
17
 
18
18
  const items: IRawModelCard[] = rawData?.data?.items || [];
19
19
  const filters: IModelFilter[] = rawData?.data?.filters || [];
20
+ const seo: IPageSeo | null = rawData?.data?.seo || null;
20
21
 
21
22
  const cleanedItems: IModelCard[] = items && items.length > 0 ? items.map(cleanModelCard) : [];
22
23
 
@@ -26,5 +27,6 @@ export default defineEventHandler(async (event) => {
26
27
  total: rawData?.data?.total,
27
28
  items: cleanedItems,
28
29
  filters: filters,
30
+ seo: seo,
29
31
  };
30
32
  });
@@ -1,5 +1,6 @@
1
1
  import type { IParameterModel } from './parameter-model';
2
2
  import type { IChipsItem } from './chips-item';
3
+ import type { IPageSeo } from './page-seo';
3
4
 
4
5
  export interface ICardInfo {
5
6
  title: string;
@@ -9,6 +10,7 @@ export interface ICardInfo {
9
10
  guid?: string;
10
11
  fullDescription?: string
11
12
  miniCardTitle?: string
13
+ seo?: IPageSeo | null;
12
14
 
13
15
  // Fields specific to Channel
14
16
  url?: string;
package/types/index.d.ts CHANGED
@@ -52,6 +52,7 @@ export * from './chips-item.d.ts';
52
52
  export * from './library-feed.d.ts';
53
53
  export * from './nav-link-item.d.ts';
54
54
  export * from './paginated-response.d.ts';
55
+ export * from './page-seo.d.ts';
55
56
  export * from './request-pagination.d.ts';
56
57
  export * from './tab-item.d.ts';
57
58
  export * from './filter-scheme.d.ts';
@@ -0,0 +1,6 @@
1
+ export interface IPageSeo {
2
+ title: string;
3
+ description: string;
4
+ h1: string;
5
+ keywords: string;
6
+ }
@@ -1,7 +1,10 @@
1
+ import type { IPageSeo } from './page-seo';
2
+
1
3
  type BasePagination = {
2
4
  currentPage: number;
3
5
  allPages: number;
4
6
  total: number;
7
+ seo?: IPageSeo | null;
5
8
  };
6
9
 
7
10
  export type PaginatedResponse<T> =
@@ -1,5 +1,5 @@
1
1
  import type { IRawThumbUrls } from './raw-thumbs-urls';
2
- import type { IParameterModel } from '~/types';
2
+ import type { IParameterModel, IPageSeo } from '~/types';
3
3
 
4
4
  export interface IRawModelInfo {
5
5
  guid: string;
@@ -41,4 +41,5 @@ export interface IRawModelInfo {
41
41
  }[];
42
42
  birth_summary: string;
43
43
  nationality_iso_codes: string[];
44
+ seo?: IPageSeo;
44
45
  }