erudit 4.3.1 → 4.3.3-dev.1

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 (40) hide show
  1. package/app/app.vue +1 -0
  2. package/app/components/aside/major/PaneHolder.vue +3 -3
  3. package/app/components/aside/major/contentNav/items/Flags.vue +4 -8
  4. package/app/components/main/MainBreadcrumbs.vue +27 -19
  5. package/app/components/main/MainKeyLinks.vue +13 -7
  6. package/app/components/main/MainSubTitle.vue +4 -3
  7. package/app/components/main/MainTopicPartPage.vue +3 -1
  8. package/app/components/main/connections/MainConnections.vue +47 -40
  9. package/app/components/main/contentStats/MainContentStats.vue +16 -17
  10. package/app/composables/analytics.ts +15 -8
  11. package/app/composables/favicon.ts +63 -16
  12. package/app/composables/formatText.ts +9 -9
  13. package/app/composables/jsonLd.ts +123 -0
  14. package/app/composables/lastmod.ts +6 -0
  15. package/app/composables/og.ts +23 -0
  16. package/app/pages/book/[...bookId].vue +3 -1
  17. package/app/pages/group/[...groupId].vue +3 -1
  18. package/app/pages/page/[...pageId].vue +3 -1
  19. package/app/plugins/prerender.server.ts +1 -0
  20. package/modules/erudit/setup/runtimeConfig.ts +39 -3
  21. package/nuxt.config.ts +1 -1
  22. package/package.json +12 -11
  23. package/server/api/main/content/[...contentTypePath].ts +2 -0
  24. package/server/api/prerender/favicons.ts +31 -0
  25. package/server/erudit/build.ts +2 -0
  26. package/server/erudit/content/lastmod.ts +206 -0
  27. package/server/erudit/content/repository/lastmod.ts +12 -0
  28. package/server/erudit/db/schema/content.ts +1 -0
  29. package/server/erudit/favicon/convertToPng.ts +48 -0
  30. package/server/erudit/favicon/loadSource.ts +139 -0
  31. package/server/erudit/favicon/shared.ts +48 -0
  32. package/server/erudit/language/list/en.ts +1 -9
  33. package/server/erudit/language/list/ru.ts +3 -11
  34. package/server/erudit/repository.ts +2 -0
  35. package/server/routes/favicon/[...path].ts +89 -0
  36. package/server/routes/sitemap.xml.ts +19 -10
  37. package/shared/types/language.ts +1 -6
  38. package/shared/types/mainContent.ts +1 -0
  39. package/shared/types/runtimeConfig.ts +4 -1
  40. package/app/composables/lastChanged.ts +0 -61
package/app/app.vue CHANGED
@@ -2,6 +2,7 @@
2
2
  initFavicon();
3
3
  initOgImage();
4
4
  initOgSiteName();
5
+ initWebSiteJsonLd();
5
6
  initAnalytics();
6
7
  initScrollUpWatcher();
7
8
 
@@ -71,9 +71,9 @@ await usePhrases(
71
71
  'no_content',
72
72
  'to_index',
73
73
  'about_textbook',
74
- 'flag_title_dev',
75
- 'flag_title_advanced',
76
- 'flag_title_secondary',
74
+ 'flag_dev',
75
+ 'flag_advanced',
76
+ 'flag_secondary',
77
77
  );
78
78
  </script>
79
79
 
@@ -5,28 +5,24 @@ import type { MyIconName } from '#my-icons';
5
5
 
6
6
  defineProps<{ flags: ContentFlags }>();
7
7
 
8
- const phrase = await usePhrases(
9
- 'flag_title_dev',
10
- 'flag_title_advanced',
11
- 'flag_title_secondary',
12
- );
8
+ const phrase = await usePhrases('flag_dev', 'flag_advanced', 'flag_secondary');
13
9
 
14
10
  const flagData = ((flagType: ContentFlag) => {
15
11
  switch (flagType) {
16
12
  case 'dev':
17
13
  return {
18
14
  icon: 'construction',
19
- title: phrase.flag_title_dev,
15
+ title: phrase.flag_dev,
20
16
  };
21
17
  case 'advanced':
22
18
  return {
23
19
  icon: 'asterisk',
24
- title: phrase.flag_title_advanced,
20
+ title: phrase.flag_advanced,
25
21
  };
26
22
  case 'secondary':
27
23
  return {
28
24
  icon: 'puzzle',
29
- title: phrase.flag_title_secondary,
25
+ title: phrase.flag_secondary,
30
26
  };
31
27
  }
32
28
  }) as (flagType: ContentFlag) => { icon: MyIconName; title: string };
@@ -1,28 +1,36 @@
1
1
  <script lang="ts" setup>
2
2
  defineProps<{ breadcrumbs: Breadcrumbs }>();
3
+
4
+ const phrase = await usePhrases('breadcrumb');
3
5
  </script>
4
6
 
5
7
  <template>
6
- <section
8
+ <nav
7
9
  v-if="breadcrumbs.length > 0"
8
- class="gap-small max-micro:justify-center px-main py-main-half flex
9
- flex-wrap"
10
+ :aria-label="phrase.breadcrumb"
11
+ class="px-main py-main-half"
10
12
  >
11
- <EruditLink
12
- v-for="(breadcrumb, i) of breadcrumbs"
13
- :to="breadcrumb.link"
14
- class="gap-small text-text-dimmed hocus:text-text-muted flex items-center
15
- transition-[color]"
13
+ <ol
14
+ class="gap-small max-micro:justify-center m-0 flex list-none flex-wrap
15
+ p-0"
16
16
  >
17
- <MaybeMyIcon :name="breadcrumb.icon" class="text-[1.2em]" />
18
- <span>{{ formatText(breadcrumb.title) }}</span>
19
- <MyIcon
20
- name="chevron-right"
21
- :class="{
22
- 'relative -left-[3px]': true,
23
- 'rotate-90': i === breadcrumbs.length - 1,
24
- }"
25
- />
26
- </EruditLink>
27
- </section>
17
+ <li v-for="(breadcrumb, i) of breadcrumbs">
18
+ <EruditLink
19
+ :to="breadcrumb.link"
20
+ class="gap-small text-text-dimmed hocus:text-text-muted flex
21
+ items-center transition-[color]"
22
+ >
23
+ <MaybeMyIcon :name="breadcrumb.icon" class="text-[1.2em]" />
24
+ <span>{{ formatText(breadcrumb.title) }}</span>
25
+ <MyIcon
26
+ name="chevron-right"
27
+ :class="{
28
+ 'relative -left-[3px]': true,
29
+ 'rotate-90': i === breadcrumbs.length - 1,
30
+ }"
31
+ />
32
+ </EruditLink>
33
+ </li>
34
+ </ol>
35
+ </nav>
28
36
  </template>
@@ -19,16 +19,22 @@ const phrase = await usePhrases('key_elements');
19
19
 
20
20
  <template>
21
21
  <template v-if="keyLinks">
22
- <section v-if="mode === 'single'" class="px-main py-main-half">
22
+ <nav
23
+ v-if="mode === 'single'"
24
+ :aria-label="phrase.key_elements"
25
+ class="px-main py-main-half"
26
+ >
23
27
  <MainSubTitle :title="phrase.key_elements + ':'" />
24
- <div
28
+ <ul
25
29
  :style="{ '--keyBg': 'var(--color-bg-aside)' }"
26
- class="gap-small micro:gap-normal micro:justify-start flex flex-wrap
27
- justify-center"
30
+ class="gap-small micro:gap-normal micro:justify-start m-0 flex list-none
31
+ flex-wrap justify-center p-0"
28
32
  >
29
- <MainKeyLink v-for="keyLink of keyLinks" :keyLink />
30
- </div>
31
- </section>
33
+ <li v-for="keyLink of keyLinks">
34
+ <MainKeyLink :keyLink />
35
+ </li>
36
+ </ul>
37
+ </nav>
32
38
  <div
33
39
  v-else
34
40
  :style="{ '--keyBg': 'var(--color-bg-main)' }"
@@ -3,9 +3,10 @@ defineProps<{ title: string }>();
3
3
  </script>
4
4
 
5
5
  <template>
6
- <div
7
- class="text-main-sm micro:text-left pb-main-half text-center font-semibold"
6
+ <h2
7
+ class="text-main-sm micro:text-left pb-main-half m-0 text-center
8
+ font-semibold"
8
9
  >
9
10
  {{ formatText(title) }}
10
- </div>
11
+ </h2>
11
12
  </template>
@@ -24,7 +24,7 @@ const phrase = await usePhrases(
24
24
  'summary_seo_description',
25
25
  'practice_seo_description',
26
26
  );
27
- const lastChangedDate = useLastChanged(() => mainContent.contentRelativePath);
27
+ const lastChangedDate = useLastChanged(() => mainContent.lastmod);
28
28
 
29
29
  await useContentSeo({
30
30
  title: mainContent.title,
@@ -47,6 +47,8 @@ await useContentSeo({
47
47
  : undefined,
48
48
  seo: mainContent.seo,
49
49
  snippets: mainContent.snippets,
50
+ breadcrumbs: mainContent.breadcrumbs,
51
+ lastmod: mainContent.lastmod,
50
52
  });
51
53
  </script>
52
54
 
@@ -27,11 +27,15 @@ const parentExternalsCount = computed(() => {
27
27
  </script>
28
28
 
29
29
  <template>
30
- <section v-if="connections" class="px-main py-main-half">
30
+ <section
31
+ v-if="connections"
32
+ :aria-label="phrase.connections"
33
+ class="px-main py-main-half"
34
+ >
31
35
  <MainSubTitle :title="phrase.connections + ':'" />
32
- <div
33
- class="gap-small micro:gap-normal micro:justify-start flex flex-wrap
34
- justify-center"
36
+ <ul
37
+ class="gap-small micro:gap-normal micro:justify-start m-0 flex list-none
38
+ flex-wrap justify-center p-0"
35
39
  >
36
40
  <template
37
41
  v-for="(items, type) of {
@@ -40,49 +44,52 @@ const parentExternalsCount = computed(() => {
40
44
  dependents: connections.dependents,
41
45
  }"
42
46
  >
47
+ <li v-if="items && items.length > 0">
48
+ <MainConnectionsButton
49
+ :type="type"
50
+ :count="items.length"
51
+ :active="currentType === type"
52
+ @click="
53
+ currentType === type
54
+ ? (currentType = undefined)
55
+ : (currentType = type)
56
+ "
57
+ />
58
+ </li>
59
+ </template>
60
+ <li v-if="connections.externals">
43
61
  <MainConnectionsButton
44
- v-if="items && items.length > 0"
45
- :type="type"
46
- :count="items.length"
47
- :active="currentType === type"
62
+ type="externals"
63
+ :active="currentType === 'externals'"
48
64
  @click="
49
- currentType === type
65
+ currentType === 'externals'
50
66
  ? (currentType = undefined)
51
- : (currentType = type)
67
+ : (currentType = 'externals')
52
68
  "
53
- />
54
- </template>
55
- <MainConnectionsButton
56
- v-if="connections.externals"
57
- type="externals"
58
- :active="currentType === 'externals'"
59
- @click="
60
- currentType === 'externals'
61
- ? (currentType = undefined)
62
- : (currentType = 'externals')
63
- "
64
- >
65
- <template #after>
66
- <div
67
- v-if="connections.externals"
68
- class="gap-small *:border-border *:pl-small flex items-center
69
- font-bold *:border-l"
70
- >
69
+ >
70
+ <template #after>
71
71
  <div
72
- v-if="ownExternalsCount"
73
- class="flex items-center gap-1 text-amber-600 dark:text-amber-400"
72
+ v-if="connections.externals"
73
+ class="gap-small *:border-border *:pl-small flex items-center
74
+ font-bold *:border-l"
74
75
  >
75
- <MyIcon name="arrow/left" class="-scale-x-100" />
76
- <span>{{ ownExternalsCount }}</span>
77
- </div>
78
- <div v-if="parentExternalsCount" class="flex items-center gap-1">
79
- <MyIcon name="arrow/up-to-right" />
80
- <span>{{ parentExternalsCount }}</span>
76
+ <div
77
+ v-if="ownExternalsCount"
78
+ class="flex items-center gap-1 text-amber-600
79
+ dark:text-amber-400"
80
+ >
81
+ <MyIcon name="arrow/left" class="-scale-x-100" />
82
+ <span>{{ ownExternalsCount }}</span>
83
+ </div>
84
+ <div v-if="parentExternalsCount" class="flex items-center gap-1">
85
+ <MyIcon name="arrow/up-to-right" />
86
+ <span>{{ parentExternalsCount }}</span>
87
+ </div>
81
88
  </div>
82
- </div>
83
- </template>
84
- </MainConnectionsButton>
85
- </div>
89
+ </template>
90
+ </MainConnectionsButton>
91
+ </li>
92
+ </ul>
86
93
  <template v-if="currentType && connections[currentType]">
87
94
  <Deps
88
95
  v-if="currentType !== 'externals'"
@@ -15,27 +15,26 @@ const phrase = await usePhrases('stats');
15
15
  <template>
16
16
  <section
17
17
  v-if="mode === 'single' && (stats || lastChangedDate)"
18
+ :aria-label="phrase.stats"
18
19
  class="px-main py-main-half"
19
20
  >
20
21
  <MainSubTitle :title="phrase.stats + ':'" />
21
- <div
22
- class="micro:justify-start gap-small micro:gap-normal flex flex-wrap
23
- justify-center"
22
+ <ul
23
+ class="micro:justify-start gap-small micro:gap-normal m-0 flex list-none
24
+ flex-wrap justify-center p-0"
24
25
  >
25
- <ItemMaterials
26
- v-if="stats?.materials"
27
- :count="stats.materials"
28
- mode="detailed"
29
- />
30
- <ItemElement
31
- v-if="stats?.elements"
32
- v-for="(count, schemaName) of stats.elements"
33
- :schemaName
34
- :count
35
- mode="detailed"
36
- />
37
- <ItemLastChanged v-if="lastChangedDate" :date="lastChangedDate" />
38
- </div>
26
+ <li v-if="stats?.materials">
27
+ <ItemMaterials :count="stats.materials" mode="detailed" />
28
+ </li>
29
+ <template v-if="stats?.elements">
30
+ <li v-for="(count, schemaName) of stats.elements">
31
+ <ItemElement :schemaName :count mode="detailed" />
32
+ </li>
33
+ </template>
34
+ <li v-if="lastChangedDate">
35
+ <ItemLastChanged :date="lastChangedDate" />
36
+ </li>
37
+ </ul>
39
38
  </section>
40
39
  <div
41
40
  v-else-if="mode === 'children' && stats"
@@ -73,17 +73,24 @@ function yandexAnalytics(analytics: YandexAnalytics) {
73
73
  (function(m,e,t,r,i,k,a){
74
74
  m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
75
75
  m[i].l=1*new Date();
76
- k=e.createElement(t),a=e.getElementsByTagName(t)[0],
77
- k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
78
- })(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
76
+ for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
77
+ k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
78
+ })(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=${analytics.metricsId}', 'ym');
79
79
 
80
- ym(${analytics.metricsId}, "init", {
80
+ ym(
81
+ ${analytics.metricsId},
82
+ 'init',
83
+ {
84
+ ssr:true,
85
+ webvisor:true,
81
86
  clickmap:true,
82
- trackLinks:true,
87
+ referrer: document.referrer,
88
+ url: location.href,
83
89
  accurateTrackBounce:true,
84
- webvisor:true
85
- });
86
- `.trim(),
90
+ trackLinks:true
91
+ }
92
+ );
93
+ `.trim(),
87
94
  },
88
95
  ],
89
96
  });
@@ -1,18 +1,41 @@
1
1
  import { isTopicPart, type TopicPart } from '@erudit-js/core/content/topic';
2
2
  import { isContentType, type ContentType } from '@erudit-js/core/content/type';
3
3
 
4
- const fallbackFaviconHref = eruditPublic('favicons/default.svg');
4
+ const FALLBACK_FAVICON_EXT = '.svg';
5
5
 
6
- export const useFaviconHref = () => {
7
- return useState<string>('favicon-href', () => fallbackFaviconHref);
6
+ const mimeByExt: Record<string, string> = {
7
+ '.svg': 'image/svg+xml',
8
+ '.png': 'image/png',
9
+ '.ico': 'image/x-icon',
10
+ '.jpg': 'image/jpeg',
11
+ '.jpeg': 'image/jpeg',
12
+ '.gif': 'image/gif',
13
+ '.webp': 'image/webp',
14
+ '.bmp': 'image/bmp',
8
15
  };
9
16
 
17
+ function extFromConfigHref(key: string): string {
18
+ const href =
19
+ (ERUDIT.config.favicon as Record<string, string> | undefined)?.[key] ||
20
+ ERUDIT.config.favicon?.default;
21
+ if (!href) return FALLBACK_FAVICON_EXT;
22
+ const path = href.split(/[?#]/)[0] ?? '';
23
+ const dot = path.lastIndexOf('.');
24
+ return dot === -1 ? '' : path.slice(dot).toLowerCase();
25
+ }
26
+
27
+ function mimeFromConfigHref(key: string): string | undefined {
28
+ return mimeByExt[extFromConfigHref(key)];
29
+ }
30
+
31
+ export const useFaviconKey = () =>
32
+ useState<string>('favicon-key', () => 'default');
33
+
10
34
  export function useFavicon() {
11
- const faviconHref = useFaviconHref();
35
+ const faviconKey = useFaviconKey();
12
36
 
13
37
  function showDefaultFavicon() {
14
- const defaultFaviconHref = ERUDIT.config.favicon?.default;
15
- faviconHref.value = defaultFaviconHref || fallbackFaviconHref;
38
+ faviconKey.value = 'default';
16
39
  }
17
40
 
18
41
  function showContentFavicon(
@@ -21,15 +44,13 @@ export function useFavicon() {
21
44
  | { type: 'topic'; part: TopicPart },
22
45
  ) {
23
46
  if (args.type === 'topic') {
24
- const topicPartHref = ERUDIT.config.favicon?.[args.part];
25
- if (topicPartHref) {
26
- faviconHref.value = topicPartHref;
47
+ if (ERUDIT.config.favicon?.[args.part]) {
48
+ faviconKey.value = args.part;
27
49
  return;
28
50
  }
29
51
  } else {
30
- const typeHref = ERUDIT.config.favicon?.[args.type];
31
- if (typeHref) {
32
- faviconHref.value = typeHref;
52
+ if (ERUDIT.config.favicon?.[args.type]) {
53
+ faviconKey.value = args.type;
33
54
  return;
34
55
  }
35
56
  }
@@ -45,16 +66,42 @@ export function useFavicon() {
45
66
 
46
67
  export function initFavicon() {
47
68
  const withBaseUrl = useBaseUrl();
48
- const faviconHref = useFaviconHref();
69
+ const faviconKey = useFaviconKey();
49
70
  const { showDefaultFavicon, showContentFavicon } = useFavicon();
50
71
  showDefaultFavicon();
51
72
 
52
73
  useHead({
53
74
  link: [
75
+ computed(() => {
76
+ const key = faviconKey.value;
77
+ const ext = extFromConfigHref(key);
78
+ const mime = mimeFromConfigHref(key);
79
+ return {
80
+ key: 'favicon',
81
+ rel: 'icon',
82
+ href: withBaseUrl(`/favicon/${key}/source${ext}`),
83
+ ...(mime && { type: mime }),
84
+ };
85
+ }),
54
86
  computed(() => ({
55
- key: 'favicon',
87
+ key: 'favicon-png-48',
56
88
  rel: 'icon',
57
- href: withBaseUrl(faviconHref.value),
89
+ type: 'image/png',
90
+ sizes: '48x48',
91
+ href: withBaseUrl(`/favicon/${faviconKey.value}/48.png`),
92
+ })),
93
+ computed(() => ({
94
+ key: 'favicon-png-32',
95
+ rel: 'icon',
96
+ type: 'image/png',
97
+ sizes: '32x32',
98
+ href: withBaseUrl(`/favicon/${faviconKey.value}/32.png`),
99
+ })),
100
+ computed(() => ({
101
+ key: 'apple-touch-icon',
102
+ rel: 'apple-touch-icon',
103
+ sizes: '180x180',
104
+ href: withBaseUrl(`/favicon/${faviconKey.value}/180.png`),
58
105
  })),
59
106
  ],
60
107
  });
@@ -65,7 +112,7 @@ export function initFavicon() {
65
112
  let stopWatchingRoute: ReturnType<typeof watch> | undefined;
66
113
  onMounted(() => {
67
114
  stopWatchingRoute = watch(
68
- route,
115
+ () => route.path,
69
116
  () => {
70
117
  clearTimeout(contentFaviconChangeTimeout);
71
118
 
@@ -1,9 +1,9 @@
1
- import type { FormatText } from '@erudit-js/core/formatText';
2
- import { createFormatTextFn } from '../../shared/utils/formatText';
3
-
4
- export let formatText: FormatText;
5
-
6
- export async function initFormatText() {
7
- const languageCode = ERUDIT.config.language.current;
8
- formatText = createFormatTextFn(languageCode);
9
- }
1
+ import type { FormatText } from '@erudit-js/core/formatText';
2
+ import { createFormatTextFn } from '../../shared/utils/formatText';
3
+
4
+ export let formatText: FormatText;
5
+
6
+ export async function initFormatText() {
7
+ const languageCode = ERUDIT.config.language.current;
8
+ formatText = createFormatTextFn(languageCode);
9
+ }
@@ -0,0 +1,123 @@
1
+ import type { Breadcrumbs } from '../../shared/types/breadcrumbs';
2
+ import type { ElementSnippet } from '../../shared/types/elementSnippet';
3
+
4
+ export function useJsonLd(key: string, data: Record<string, unknown>) {
5
+ useHead({
6
+ script: [
7
+ {
8
+ key,
9
+ type: 'application/ld+json',
10
+ innerHTML: JSON.stringify(data),
11
+ },
12
+ ],
13
+ });
14
+ }
15
+
16
+ export function initWebSiteJsonLd() {
17
+ const siteTitle =
18
+ ERUDIT.config.seo?.siteTitle || ERUDIT.config.asideMajor?.siteInfo?.title;
19
+
20
+ if (!siteTitle) {
21
+ return;
22
+ }
23
+
24
+ const runtimeConfig = useRuntimeConfig();
25
+ const siteUrl = runtimeConfig.public.siteUrl as string;
26
+
27
+ if (!siteUrl) {
28
+ return;
29
+ }
30
+
31
+ useJsonLd('jsonld-website', {
32
+ '@context': 'https://schema.org',
33
+ '@type': 'WebSite',
34
+ name: siteTitle,
35
+ url: siteUrl,
36
+ });
37
+ }
38
+
39
+ export function useContentBreadcrumbsJsonLd(breadcrumbs?: Breadcrumbs) {
40
+ if (!breadcrumbs || breadcrumbs.length === 0) {
41
+ return;
42
+ }
43
+
44
+ const withSiteUrl = useSiteUrl();
45
+
46
+ useJsonLd('jsonld-breadcrumbs', {
47
+ '@context': 'https://schema.org',
48
+ '@type': 'BreadcrumbList',
49
+ itemListElement: breadcrumbs.map((item, index) => ({
50
+ '@type': 'ListItem',
51
+ position: index + 1,
52
+ name: formatText(item.title),
53
+ item: withSiteUrl(item.link),
54
+ })),
55
+ });
56
+ }
57
+
58
+ export function useContentArticleJsonLd(args: {
59
+ title: string;
60
+ description?: string;
61
+ urlPath: string;
62
+ contentType: string;
63
+ lastmod?: string;
64
+ keyElements?: ElementSnippet[];
65
+ breadcrumbs?: Breadcrumbs;
66
+ }) {
67
+ const withSiteUrl = useSiteUrl();
68
+
69
+ const siteTitle =
70
+ ERUDIT.config.seo?.siteTitle || ERUDIT.config.asideMajor?.siteInfo?.title;
71
+
72
+ const schemaType =
73
+ args.contentType === 'book'
74
+ ? 'Book'
75
+ : args.contentType === 'group'
76
+ ? 'Course'
77
+ : 'Article';
78
+
79
+ const data: Record<string, unknown> = {
80
+ '@context': 'https://schema.org',
81
+ '@type': schemaType,
82
+ ...(schemaType === 'Article'
83
+ ? { headline: formatText(args.title) }
84
+ : { name: formatText(args.title) }),
85
+ url: withSiteUrl(args.urlPath),
86
+ };
87
+
88
+ if (args.description) {
89
+ data.description = formatText(args.description);
90
+ }
91
+
92
+ if (args.lastmod) {
93
+ data.dateModified = args.lastmod;
94
+ }
95
+
96
+ const parentBreadcrumb =
97
+ args.breadcrumbs && args.breadcrumbs.length >= 1
98
+ ? args.breadcrumbs[args.breadcrumbs.length - 1]
99
+ : undefined;
100
+
101
+ if (parentBreadcrumb) {
102
+ data.isPartOf = {
103
+ '@type': 'WebPage',
104
+ name: formatText(parentBreadcrumb.title),
105
+ url: withSiteUrl(parentBreadcrumb.link),
106
+ };
107
+ } else if (siteTitle) {
108
+ data.isPartOf = {
109
+ '@type': 'WebSite',
110
+ name: siteTitle,
111
+ };
112
+ }
113
+
114
+ if (args.keyElements && args.keyElements.length > 0) {
115
+ data.hasPart = args.keyElements.map((el) => ({
116
+ '@type': 'DefinedTerm',
117
+ name: formatText(el.seo?.title || el.key?.title || el.title),
118
+ url: withSiteUrl(el.link),
119
+ }));
120
+ }
121
+
122
+ useJsonLd('jsonld-content', data);
123
+ }
@@ -0,0 +1,6 @@
1
+ export function useLastChanged(lastmod: MaybeRefOrGetter<string | undefined>) {
2
+ return computed(() => {
3
+ const val = toValue(lastmod);
4
+ return val ? new Date(val) : undefined;
5
+ });
6
+ }