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.
- package/app/app.vue +1 -0
- package/app/components/main/MainBreadcrumbs.vue +27 -19
- package/app/components/main/MainKeyLinks.vue +13 -7
- package/app/components/main/MainSubTitle.vue +4 -3
- package/app/components/main/MainTopicPartPage.vue +3 -1
- package/app/components/main/connections/MainConnections.vue +47 -40
- package/app/components/main/contentStats/MainContentStats.vue +16 -17
- package/app/composables/analytics.ts +15 -8
- package/app/composables/favicon.ts +49 -26
- package/app/composables/jsonLd.ts +123 -0
- package/app/composables/lastmod.ts +6 -0
- package/app/composables/og.ts +23 -0
- package/app/pages/book/[...bookId].vue +3 -1
- package/app/pages/group/[...groupId].vue +3 -1
- package/app/pages/page/[...pageId].vue +3 -1
- package/app/plugins/prerender.server.ts +1 -0
- package/modules/erudit/setup/runtimeConfig.ts +39 -3
- package/nuxt.config.ts +1 -1
- package/package.json +11 -10
- package/server/api/main/content/[...contentTypePath].ts +2 -0
- package/server/api/prerender/favicons.ts +31 -0
- package/server/erudit/build.ts +2 -0
- package/server/erudit/content/lastmod.ts +206 -0
- package/server/erudit/content/repository/lastmod.ts +12 -0
- package/server/erudit/db/schema/content.ts +1 -0
- package/server/erudit/favicon/convertToPng.ts +48 -0
- package/server/erudit/favicon/loadSource.ts +139 -0
- package/server/erudit/favicon/shared.ts +48 -0
- package/server/erudit/language/list/en.ts +1 -0
- package/server/erudit/language/list/ru.ts +1 -0
- package/server/erudit/repository.ts +2 -0
- package/server/routes/favicon/[...path].ts +89 -0
- package/server/routes/sitemap.xml.ts +19 -10
- package/shared/types/language.ts +1 -0
- package/shared/types/mainContent.ts +1 -0
- package/shared/types/runtimeConfig.ts +4 -1
- package/app/composables/lastChanged.ts +0 -61
package/app/app.vue
CHANGED
|
@@ -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
|
-
<
|
|
8
|
+
<nav
|
|
7
9
|
v-if="breadcrumbs.length > 0"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
:aria-label="phrase.breadcrumb"
|
|
11
|
+
class="px-main py-main-half"
|
|
10
12
|
>
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
28
|
+
<ul
|
|
25
29
|
:style="{ '--keyBg': 'var(--color-bg-aside)' }"
|
|
26
|
-
class="gap-small micro:gap-normal micro:justify-start flex
|
|
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
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
<
|
|
7
|
-
class="text-main-sm micro:text-left pb-main-half text-center
|
|
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
|
-
</
|
|
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.
|
|
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
|
|
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
|
-
<
|
|
33
|
-
class="gap-small micro:gap-normal micro:justify-start flex
|
|
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
|
-
|
|
45
|
-
:
|
|
46
|
-
:count="items.length"
|
|
47
|
-
:active="currentType === type"
|
|
62
|
+
type="externals"
|
|
63
|
+
:active="currentType === 'externals'"
|
|
48
64
|
@click="
|
|
49
|
-
currentType ===
|
|
65
|
+
currentType === 'externals'
|
|
50
66
|
? (currentType = undefined)
|
|
51
|
-
: (currentType =
|
|
67
|
+
: (currentType = 'externals')
|
|
52
68
|
"
|
|
53
|
-
|
|
54
|
-
|
|
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="
|
|
73
|
-
class="
|
|
72
|
+
v-if="connections.externals"
|
|
73
|
+
class="gap-small *:border-border *:pl-small flex items-center
|
|
74
|
+
font-bold *:border-l"
|
|
74
75
|
>
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
</
|
|
83
|
-
</
|
|
84
|
-
</
|
|
85
|
-
</
|
|
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
|
-
<
|
|
22
|
-
class="micro:justify-start gap-small micro:gap-normal flex
|
|
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
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
:
|
|
35
|
-
|
|
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
|
-
|
|
77
|
-
k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
|
78
|
-
})(window, document,
|
|
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(
|
|
80
|
+
ym(
|
|
81
|
+
${analytics.metricsId},
|
|
82
|
+
'init',
|
|
83
|
+
{
|
|
84
|
+
ssr:true,
|
|
85
|
+
webvisor:true,
|
|
81
86
|
clickmap:true,
|
|
82
|
-
|
|
87
|
+
referrer: document.referrer,
|
|
88
|
+
url: location.href,
|
|
83
89
|
accurateTrackBounce:true,
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
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
|
|
4
|
+
const FALLBACK_FAVICON_EXT = '.svg';
|
|
5
5
|
|
|
6
|
-
const
|
|
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
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
return
|
|
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
|
|
35
|
+
const faviconKey = useFaviconKey();
|
|
31
36
|
|
|
32
37
|
function showDefaultFavicon() {
|
|
33
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
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
|
|
75
|
-
const
|
|
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
|
-
...(
|
|
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
|
+
}
|
package/app/composables/og.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|