erudit 4.3.2 → 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 (37) hide show
  1. package/app/app.vue +1 -0
  2. package/app/components/main/MainBreadcrumbs.vue +27 -19
  3. package/app/components/main/MainKeyLinks.vue +13 -7
  4. package/app/components/main/MainSubTitle.vue +4 -3
  5. package/app/components/main/MainTopicPartPage.vue +3 -1
  6. package/app/components/main/connections/MainConnections.vue +47 -40
  7. package/app/components/main/contentStats/MainContentStats.vue +16 -17
  8. package/app/composables/analytics.ts +15 -8
  9. package/app/composables/favicon.ts +49 -26
  10. package/app/composables/jsonLd.ts +123 -0
  11. package/app/composables/lastmod.ts +6 -0
  12. package/app/composables/og.ts +23 -0
  13. package/app/pages/book/[...bookId].vue +3 -1
  14. package/app/pages/group/[...groupId].vue +3 -1
  15. package/app/pages/page/[...pageId].vue +3 -1
  16. package/app/plugins/prerender.server.ts +1 -0
  17. package/modules/erudit/setup/runtimeConfig.ts +39 -3
  18. package/nuxt.config.ts +1 -1
  19. package/package.json +11 -10
  20. package/server/api/main/content/[...contentTypePath].ts +2 -0
  21. package/server/api/prerender/favicons.ts +31 -0
  22. package/server/erudit/build.ts +2 -0
  23. package/server/erudit/content/lastmod.ts +206 -0
  24. package/server/erudit/content/repository/lastmod.ts +12 -0
  25. package/server/erudit/db/schema/content.ts +1 -0
  26. package/server/erudit/favicon/convertToPng.ts +48 -0
  27. package/server/erudit/favicon/loadSource.ts +139 -0
  28. package/server/erudit/favicon/shared.ts +48 -0
  29. package/server/erudit/language/list/en.ts +1 -0
  30. package/server/erudit/language/list/ru.ts +1 -0
  31. package/server/erudit/repository.ts +2 -0
  32. package/server/routes/favicon/[...path].ts +89 -0
  33. package/server/routes/sitemap.xml.ts +19 -10
  34. package/shared/types/language.ts +1 -0
  35. package/shared/types/mainContent.ts +1 -0
  36. package/shared/types/runtimeConfig.ts +4 -1
  37. 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
 
@@ -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,9 +1,9 @@
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
- const faviconMimeByExt: Record<string, string> = {
6
+ const mimeByExt: Record<string, string> = {
7
7
  '.svg': 'image/svg+xml',
8
8
  '.png': 'image/png',
9
9
  '.ico': 'image/x-icon',
@@ -14,24 +14,28 @@ const faviconMimeByExt: Record<string, string> = {
14
14
  '.bmp': 'image/bmp',
15
15
  };
16
16
 
17
- function getFaviconMimeType(href: string): string | undefined {
18
- const pathPart = href.split(/[?#]/)[0] ?? '';
19
- const dot = pathPart.lastIndexOf('.');
20
- if (dot === -1) return undefined;
21
- const ext = pathPart.slice(dot).toLowerCase();
22
- return faviconMimeByExt[ext];
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();
23
25
  }
24
26
 
25
- export const useFaviconHref = () => {
26
- return useState<string>('favicon-href', () => fallbackFaviconHref);
27
- };
27
+ function mimeFromConfigHref(key: string): string | undefined {
28
+ return mimeByExt[extFromConfigHref(key)];
29
+ }
30
+
31
+ export const useFaviconKey = () =>
32
+ useState<string>('favicon-key', () => 'default');
28
33
 
29
34
  export function useFavicon() {
30
- const faviconHref = useFaviconHref();
35
+ const faviconKey = useFaviconKey();
31
36
 
32
37
  function showDefaultFavicon() {
33
- const defaultFaviconHref = ERUDIT.config.favicon?.default;
34
- faviconHref.value = defaultFaviconHref || fallbackFaviconHref;
38
+ faviconKey.value = 'default';
35
39
  }
36
40
 
37
41
  function showContentFavicon(
@@ -40,15 +44,13 @@ export function useFavicon() {
40
44
  | { type: 'topic'; part: TopicPart },
41
45
  ) {
42
46
  if (args.type === 'topic') {
43
- const topicPartHref = ERUDIT.config.favicon?.[args.part];
44
- if (topicPartHref) {
45
- faviconHref.value = topicPartHref;
47
+ if (ERUDIT.config.favicon?.[args.part]) {
48
+ faviconKey.value = args.part;
46
49
  return;
47
50
  }
48
51
  } else {
49
- const typeHref = ERUDIT.config.favicon?.[args.type];
50
- if (typeHref) {
51
- faviconHref.value = typeHref;
52
+ if (ERUDIT.config.favicon?.[args.type]) {
53
+ faviconKey.value = args.type;
52
54
  return;
53
55
  }
54
56
  }
@@ -64,22 +66,43 @@ export function useFavicon() {
64
66
 
65
67
  export function initFavicon() {
66
68
  const withBaseUrl = useBaseUrl();
67
- const faviconHref = useFaviconHref();
69
+ const faviconKey = useFaviconKey();
68
70
  const { showDefaultFavicon, showContentFavicon } = useFavicon();
69
71
  showDefaultFavicon();
70
72
 
71
73
  useHead({
72
74
  link: [
73
75
  computed(() => {
74
- const href = withBaseUrl(faviconHref.value);
75
- const type = getFaviconMimeType(faviconHref.value);
76
+ const key = faviconKey.value;
77
+ const ext = extFromConfigHref(key);
78
+ const mime = mimeFromConfigHref(key);
76
79
  return {
77
80
  key: 'favicon',
78
81
  rel: 'icon',
79
- href,
80
- ...(type && { type }),
82
+ href: withBaseUrl(`/favicon/${key}/source${ext}`),
83
+ ...(mime && { type: mime }),
81
84
  };
82
85
  }),
86
+ computed(() => ({
87
+ key: 'favicon-png-48',
88
+ rel: 'icon',
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`),
105
+ })),
83
106
  ],
84
107
  });
85
108
 
@@ -89,7 +112,7 @@ export function initFavicon() {
89
112
  let stopWatchingRoute: ReturnType<typeof watch> | undefined;
90
113
  onMounted(() => {
91
114
  stopWatchingRoute = watch(
92
- route,
115
+ () => route.path,
93
116
  () => {
94
117
  clearTimeout(contentFaviconChangeTimeout);
95
118
 
@@ -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
+ }
@@ -1,6 +1,8 @@
1
1
  import type { ContentSeo } from '@erudit-js/core/content/seo';
2
2
  import { toSeoSnippet } from '@erudit-js/prose';
3
3
 
4
+ import type { Breadcrumbs } from '../../shared/types/breadcrumbs';
5
+
4
6
  export function initOgSiteName() {
5
7
  const siteTitle =
6
8
  ERUDIT.config.seo?.siteTitle || ERUDIT.config.asideMajor?.siteInfo?.title;
@@ -91,6 +93,8 @@ export async function useContentSeo(args: {
91
93
  description?: string;
92
94
  seo?: ContentSeo;
93
95
  snippets?: ElementSnippet[];
96
+ breadcrumbs?: Breadcrumbs;
97
+ lastmod?: string;
94
98
  }) {
95
99
  const canUseBookTitle = ERUDIT.config.seo?.useBookSiteTitle;
96
100
 
@@ -142,6 +146,25 @@ export async function useContentSeo(args: {
142
146
  });
143
147
  }
144
148
 
149
+ //
150
+ // JSON-LD structured data
151
+ //
152
+
153
+ useContentBreadcrumbsJsonLd(args.breadcrumbs);
154
+
155
+ useContentArticleJsonLd({
156
+ title: args.seo?.title || args.title,
157
+ description: args.seo?.description || args.description,
158
+ urlPath: canonicalPath,
159
+ contentType:
160
+ args.contentTypePath.type === 'topic'
161
+ ? 'article'
162
+ : args.contentTypePath.type,
163
+ lastmod: args.lastmod,
164
+ keyElements: args.snippets?.filter((snippet) => !!snippet.key),
165
+ breadcrumbs: args.breadcrumbs,
166
+ });
167
+
145
168
  //
146
169
  // SEO snippets
147
170
  //
@@ -19,7 +19,7 @@ if (ERUDIT.config.contributors?.enabled) {
19
19
  }
20
20
 
21
21
  const phrase = await usePhrases('begin_learning');
22
- const lastChangedDate = useLastChanged(() => mainContent.contentRelativePath);
22
+ const lastChangedDate = useLastChanged(() => mainContent.lastmod);
23
23
 
24
24
  await useContentSeo({
25
25
  title: mainContent.title,
@@ -29,6 +29,8 @@ await useContentSeo({
29
29
  contentId: mainContent.shortId,
30
30
  },
31
31
  seo: mainContent.seo,
32
+ breadcrumbs: mainContent.breadcrumbs,
33
+ lastmod: mainContent.lastmod,
32
34
  });
33
35
  </script>
34
36
 
@@ -19,7 +19,7 @@ if (ERUDIT.config.contributors?.enabled) {
19
19
  }
20
20
 
21
21
  const phrase = await usePhrases('group', 'begin_learning');
22
- const lastChangedDate = useLastChanged(() => mainContent.contentRelativePath);
22
+ const lastChangedDate = useLastChanged(() => mainContent.lastmod);
23
23
 
24
24
  await useContentSeo({
25
25
  title: mainContent.title,
@@ -31,6 +31,8 @@ await useContentSeo({
31
31
  contentId: mainContent.shortId,
32
32
  },
33
33
  seo: mainContent.seo,
34
+ breadcrumbs: mainContent.breadcrumbs,
35
+ lastmod: mainContent.lastmod,
34
36
  });
35
37
  </script>
36
38