docus 5.5.0 → 5.5.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 -1
- package/app/composables/useSeo.ts +207 -0
- package/app/pages/[[lang]]/[...slug].vue +8 -7
- package/app/templates/landing.vue +4 -10
- package/app/utils/navigation.ts +31 -0
- package/modules/assistant/index.ts +1 -1
- package/modules/assistant/runtime/components/AssistantFloatingInput.vue +38 -33
- package/modules/assistant/runtime/components/AssistantPanel.vue +1 -1
- package/modules/config.ts +1 -1
- package/modules/css.ts +3 -1
- package/nuxt.config.ts +9 -0
- package/package.json +16 -16
- package/server/routes/sitemap.xml.ts +24 -5
package/app/app.vue
CHANGED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import type { BreadcrumbItem } from '../utils/navigation'
|
|
3
|
+
import { joinURL, withoutTrailingSlash } from 'ufo'
|
|
4
|
+
|
|
5
|
+
export interface UseSeoOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Page title
|
|
8
|
+
*/
|
|
9
|
+
title: MaybeRefOrGetter<string | undefined>
|
|
10
|
+
/**
|
|
11
|
+
* Page description
|
|
12
|
+
*/
|
|
13
|
+
description: MaybeRefOrGetter<string | undefined>
|
|
14
|
+
/**
|
|
15
|
+
* Page type for og:type (default: 'article' for docs, 'website' for landing)
|
|
16
|
+
*/
|
|
17
|
+
type?: MaybeRefOrGetter<'website' | 'article'>
|
|
18
|
+
/**
|
|
19
|
+
* Custom OG image URL (absolute)
|
|
20
|
+
*/
|
|
21
|
+
ogImage?: MaybeRefOrGetter<string | undefined>
|
|
22
|
+
/**
|
|
23
|
+
* Published date for article schema
|
|
24
|
+
*/
|
|
25
|
+
publishedAt?: MaybeRefOrGetter<string | undefined>
|
|
26
|
+
/**
|
|
27
|
+
* Modified date for article schema
|
|
28
|
+
*/
|
|
29
|
+
modifiedAt?: MaybeRefOrGetter<string | undefined>
|
|
30
|
+
/**
|
|
31
|
+
* Breadcrumb items for BreadcrumbList schema
|
|
32
|
+
*/
|
|
33
|
+
breadcrumbs?: MaybeRefOrGetter<BreadcrumbItem[] | undefined>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Composable for comprehensive SEO setup including:
|
|
38
|
+
* - Meta tags (title, description, og:*, twitter:*)
|
|
39
|
+
* - Canonical URLs
|
|
40
|
+
* - Hreflang tags for i18n
|
|
41
|
+
* - JSON-LD structured data
|
|
42
|
+
*/
|
|
43
|
+
export function useSeo(options: UseSeoOptions) {
|
|
44
|
+
const route = useRoute()
|
|
45
|
+
const site = useSiteConfig()
|
|
46
|
+
const { locale, locales, isEnabled: isI18nEnabled, switchLocalePath } = useDocusI18n()
|
|
47
|
+
|
|
48
|
+
const title = computed(() => toValue(options.title))
|
|
49
|
+
const description = computed(() => toValue(options.description))
|
|
50
|
+
const type = computed(() => toValue(options.type) || 'article')
|
|
51
|
+
const ogImage = computed(() => toValue(options.ogImage))
|
|
52
|
+
const publishedAt = computed(() => toValue(options.publishedAt))
|
|
53
|
+
const modifiedAt = computed(() => toValue(options.modifiedAt))
|
|
54
|
+
const breadcrumbs = computed(() => toValue(options.breadcrumbs))
|
|
55
|
+
|
|
56
|
+
// Build canonical URL
|
|
57
|
+
const canonicalUrl = computed(() => {
|
|
58
|
+
if (!site.url) return undefined
|
|
59
|
+
return joinURL(site.url, route.path)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Base URL for building other URLs
|
|
63
|
+
const baseUrl = computed(() => site.url ? withoutTrailingSlash(site.url) : '')
|
|
64
|
+
|
|
65
|
+
// Set meta tags
|
|
66
|
+
useSeoMeta({
|
|
67
|
+
title,
|
|
68
|
+
description,
|
|
69
|
+
ogTitle: title,
|
|
70
|
+
ogDescription: description,
|
|
71
|
+
ogType: type,
|
|
72
|
+
ogUrl: canonicalUrl,
|
|
73
|
+
ogLocale: computed(() => isI18nEnabled.value ? locale.value : undefined),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Set canonical link
|
|
77
|
+
useHead({
|
|
78
|
+
link: computed(() => {
|
|
79
|
+
const links: Array<{ rel: string, href?: string, hreflang?: string }> = []
|
|
80
|
+
|
|
81
|
+
// Canonical URL
|
|
82
|
+
if (canonicalUrl.value) {
|
|
83
|
+
links.push({
|
|
84
|
+
rel: 'canonical',
|
|
85
|
+
href: canonicalUrl.value,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Hreflang tags for i18n
|
|
90
|
+
if (isI18nEnabled.value && baseUrl.value) {
|
|
91
|
+
for (const loc of locales) {
|
|
92
|
+
const localePath = switchLocalePath(loc.code)
|
|
93
|
+
if (localePath) {
|
|
94
|
+
links.push({
|
|
95
|
+
rel: 'alternate',
|
|
96
|
+
hreflang: loc.code,
|
|
97
|
+
href: joinURL(baseUrl.value, localePath),
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// x-default hreflang (points to default locale)
|
|
103
|
+
const defaultLocalePath = switchLocalePath(locales[0]?.code || 'en')
|
|
104
|
+
if (defaultLocalePath) {
|
|
105
|
+
links.push({
|
|
106
|
+
rel: 'alternate',
|
|
107
|
+
hreflang: 'x-default',
|
|
108
|
+
href: joinURL(baseUrl.value, defaultLocalePath),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return links
|
|
114
|
+
}),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Custom OG image handling
|
|
118
|
+
if (ogImage.value) {
|
|
119
|
+
useSeoMeta({
|
|
120
|
+
ogImage: ogImage.value,
|
|
121
|
+
twitterImage: ogImage.value,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// JSON-LD structured data
|
|
126
|
+
useHead({
|
|
127
|
+
script: computed(() => {
|
|
128
|
+
const scripts: Array<{ type: string, innerHTML: string }> = []
|
|
129
|
+
|
|
130
|
+
if (!baseUrl.value || !title.value) return scripts
|
|
131
|
+
|
|
132
|
+
const pageUrl = joinURL(baseUrl.value, route.path)
|
|
133
|
+
|
|
134
|
+
// Article schema for documentation pages
|
|
135
|
+
if (type.value === 'article') {
|
|
136
|
+
const articleSchema: Record<string, unknown> = {
|
|
137
|
+
'@context': 'https://schema.org',
|
|
138
|
+
'@type': 'Article',
|
|
139
|
+
'headline': title.value,
|
|
140
|
+
'description': description.value,
|
|
141
|
+
'url': pageUrl,
|
|
142
|
+
'mainEntityOfPage': {
|
|
143
|
+
'@type': 'WebPage',
|
|
144
|
+
'@id': pageUrl,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (publishedAt.value) {
|
|
149
|
+
articleSchema.datePublished = publishedAt.value
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (modifiedAt.value) {
|
|
153
|
+
articleSchema.dateModified = modifiedAt.value
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (site.name) {
|
|
157
|
+
articleSchema.publisher = {
|
|
158
|
+
'@type': 'Organization',
|
|
159
|
+
'name': site.name,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
scripts.push({
|
|
164
|
+
type: 'application/ld+json',
|
|
165
|
+
innerHTML: JSON.stringify(articleSchema),
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// WebSite schema for landing pages
|
|
170
|
+
if (type.value === 'website') {
|
|
171
|
+
const websiteSchema: Record<string, unknown> = {
|
|
172
|
+
'@context': 'https://schema.org',
|
|
173
|
+
'@type': 'WebSite',
|
|
174
|
+
'name': site.name || title.value,
|
|
175
|
+
'description': description.value,
|
|
176
|
+
'url': baseUrl.value,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
scripts.push({
|
|
180
|
+
type: 'application/ld+json',
|
|
181
|
+
innerHTML: JSON.stringify(websiteSchema),
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// BreadcrumbList schema for navigation
|
|
186
|
+
if (breadcrumbs.value && breadcrumbs.value.length > 0) {
|
|
187
|
+
const breadcrumbSchema = {
|
|
188
|
+
'@context': 'https://schema.org',
|
|
189
|
+
'@type': 'BreadcrumbList',
|
|
190
|
+
'itemListElement': breadcrumbs.value.map((item, index) => ({
|
|
191
|
+
'@type': 'ListItem',
|
|
192
|
+
'position': index + 1,
|
|
193
|
+
'name': item.title,
|
|
194
|
+
'item': joinURL(baseUrl.value, item.path),
|
|
195
|
+
})),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
scripts.push({
|
|
199
|
+
type: 'application/ld+json',
|
|
200
|
+
innerHTML: JSON.stringify(breadcrumbSchema),
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return scripts
|
|
205
|
+
}),
|
|
206
|
+
})
|
|
207
|
+
}
|
|
@@ -31,14 +31,16 @@ if (!page.value) {
|
|
|
31
31
|
const title = page.value.seo?.title || page.value.title
|
|
32
32
|
const description = page.value.seo?.description || page.value.description
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
const headline = ref(findPageHeadline(navigation?.value, page.value?.path))
|
|
35
|
+
const breadcrumbs = computed(() => findPageBreadcrumbs(navigation?.value, page.value?.path || ''))
|
|
36
|
+
|
|
37
|
+
useSeo({
|
|
35
38
|
title,
|
|
36
|
-
ogTitle: title,
|
|
37
39
|
description,
|
|
38
|
-
|
|
40
|
+
type: 'article',
|
|
41
|
+
modifiedAt: (page.value as unknown as Record<string, unknown>).modifiedAt as string | undefined,
|
|
42
|
+
breadcrumbs,
|
|
39
43
|
})
|
|
40
|
-
|
|
41
|
-
const headline = ref(findPageHeadline(navigation?.value, page.value?.path))
|
|
42
44
|
watch(() => navigation?.value, () => {
|
|
43
45
|
headline.value = findPageHeadline(navigation?.value, page.value?.path) || headline.value
|
|
44
46
|
})
|
|
@@ -99,9 +101,8 @@ addPrerenderPath(`/raw${route.path}.md`)
|
|
|
99
101
|
:value="page"
|
|
100
102
|
/>
|
|
101
103
|
|
|
102
|
-
<USeparator>
|
|
104
|
+
<USeparator v-if="github">
|
|
103
105
|
<div
|
|
104
|
-
v-if="github"
|
|
105
106
|
class="flex items-center gap-2 text-sm text-muted"
|
|
106
107
|
>
|
|
107
108
|
<UButton
|
|
@@ -15,20 +15,14 @@ if (!page.value) {
|
|
|
15
15
|
const title = page.value.seo?.title || page.value.title
|
|
16
16
|
const description = page.value.seo?.description || page.value.description
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
useSeo({
|
|
19
19
|
title,
|
|
20
20
|
description,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
type: 'website',
|
|
22
|
+
ogImage: page.value?.seo?.ogImage as string | undefined,
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
if (page.value?.seo?.ogImage) {
|
|
26
|
-
useSeoMeta({
|
|
27
|
-
ogImage: page.value.seo.ogImage,
|
|
28
|
-
twitterImage: page.value.seo.ogImage,
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
25
|
+
if (!page.value?.seo?.ogImage) {
|
|
32
26
|
defineOgImageComponent('Landing', {
|
|
33
27
|
title,
|
|
34
28
|
description,
|
package/app/utils/navigation.ts
CHANGED
|
@@ -5,3 +5,34 @@ export const flattenNavigation = (items?: ContentNavigationItem[]): ContentNavig
|
|
|
5
5
|
? flattenNavigation(item.children)
|
|
6
6
|
: [item],
|
|
7
7
|
) || []
|
|
8
|
+
|
|
9
|
+
export interface BreadcrumbItem {
|
|
10
|
+
title: string
|
|
11
|
+
path: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find breadcrumb path to a page in the navigation tree
|
|
16
|
+
*/
|
|
17
|
+
export function findPageBreadcrumbs(
|
|
18
|
+
navigation: ContentNavigationItem[] | undefined,
|
|
19
|
+
path: string,
|
|
20
|
+
currentPath: BreadcrumbItem[] = [],
|
|
21
|
+
): BreadcrumbItem[] | undefined {
|
|
22
|
+
if (!navigation) return undefined
|
|
23
|
+
|
|
24
|
+
for (const item of navigation) {
|
|
25
|
+
const itemPath = [...currentPath, { title: item.title, path: item.path }]
|
|
26
|
+
|
|
27
|
+
if (item.path === path) {
|
|
28
|
+
return itemPath
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (item.children) {
|
|
32
|
+
const found = findPageBreadcrumbs(item.children, path, itemPath)
|
|
33
|
+
if (found) return found
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
@@ -62,43 +62,48 @@ defineShortcuts(shortcuts)
|
|
|
62
62
|
:animate="{ y: 0, opacity: 1 }"
|
|
63
63
|
:exit="{ y: 100, opacity: 0 }"
|
|
64
64
|
:transition="{ duration: 0.2, ease: 'easeOut' }"
|
|
65
|
-
class="fixed
|
|
65
|
+
class="fixed inset-x-0 z-10 px-4 sm:px-80 bottom-[max(1.5rem,env(safe-area-inset-bottom))]"
|
|
66
66
|
style="will-change: transform"
|
|
67
67
|
>
|
|
68
|
-
<form
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
68
|
+
<form
|
|
69
|
+
class="flex w-full justify-center"
|
|
70
|
+
@submit.prevent="handleSubmit"
|
|
71
|
+
>
|
|
72
|
+
<div class="w-full max-w-96">
|
|
73
|
+
<UInput
|
|
74
|
+
ref="inputRef"
|
|
75
|
+
v-model="input"
|
|
76
|
+
:placeholder="placeholder"
|
|
77
|
+
size="lg"
|
|
78
|
+
maxlength="1000"
|
|
79
|
+
:ui="{
|
|
80
|
+
root: 'group w-full! min-w-0 sm:max-w-96 transition-all duration-300 ease-out [@media(hover:hover)]:hover:scale-105 [@media(hover:hover)]:focus-within:scale-105',
|
|
81
|
+
base: 'bg-default shadow-lg rounded-xl text-base',
|
|
82
|
+
trailing: 'pe-2',
|
|
83
|
+
}"
|
|
84
|
+
@keydown.enter.exact.prevent="handleSubmit"
|
|
85
|
+
>
|
|
86
|
+
<template #trailing>
|
|
87
|
+
<div class="flex items-center gap-2">
|
|
88
|
+
<div class="hidden sm:flex group-focus-within:hidden items-center gap-1">
|
|
89
|
+
<UKbd
|
|
90
|
+
v-for="key in shortcutDisplayKeys"
|
|
91
|
+
:key="key"
|
|
92
|
+
:value="key"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<UButton
|
|
97
|
+
type="submit"
|
|
98
|
+
icon="i-lucide-arrow-up"
|
|
99
|
+
color="primary"
|
|
100
|
+
size="xs"
|
|
101
|
+
:disabled="!input.trim()"
|
|
89
102
|
/>
|
|
90
103
|
</div>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
icon="i-lucide-arrow-up"
|
|
95
|
-
color="primary"
|
|
96
|
-
size="xs"
|
|
97
|
-
:disabled="!input.trim()"
|
|
98
|
-
/>
|
|
99
|
-
</div>
|
|
100
|
-
</template>
|
|
101
|
-
</UInput>
|
|
104
|
+
</template>
|
|
105
|
+
</UInput>
|
|
106
|
+
</div>
|
|
102
107
|
</form>
|
|
103
108
|
</motion.div>
|
|
104
109
|
</AnimatePresence>
|
package/modules/config.ts
CHANGED
|
@@ -16,7 +16,7 @@ export default defineNuxtModule({
|
|
|
16
16
|
const url = inferSiteURL()
|
|
17
17
|
const meta = await getPackageJsonMetadata(dir)
|
|
18
18
|
const gitInfo = await getLocalGitInfo(dir) || getGitEnv()
|
|
19
|
-
const siteName = nuxt.options
|
|
19
|
+
const siteName = (typeof nuxt.options.site === 'object' && nuxt.options.site?.name) || meta.name || gitInfo?.name || ''
|
|
20
20
|
|
|
21
21
|
nuxt.options.llms = defu(nuxt.options.llms, {
|
|
22
22
|
domain: url,
|
package/modules/css.ts
CHANGED
package/nuxt.config.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docus",
|
|
3
3
|
"description": "Nuxt layer for Docus documentation theme",
|
|
4
|
-
"version": "5.5.
|
|
4
|
+
"version": "5.5.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
7
7
|
"repository": {
|
|
@@ -22,27 +22,27 @@
|
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ai-sdk/gateway": "^3.0.
|
|
26
|
-
"@ai-sdk/mcp": "^1.0.
|
|
27
|
-
"@ai-sdk/vue": "3.0.
|
|
28
|
-
"@iconify-json/lucide": "^1.2.
|
|
29
|
-
"@iconify-json/simple-icons": "^1.2.
|
|
30
|
-
"@iconify-json/vscode-icons": "^1.2.
|
|
31
|
-
"@nuxt/content": "^3.11.
|
|
25
|
+
"@ai-sdk/gateway": "^3.0.39",
|
|
26
|
+
"@ai-sdk/mcp": "^1.0.19",
|
|
27
|
+
"@ai-sdk/vue": "3.0.78",
|
|
28
|
+
"@iconify-json/lucide": "^1.2.90",
|
|
29
|
+
"@iconify-json/simple-icons": "^1.2.70",
|
|
30
|
+
"@iconify-json/vscode-icons": "^1.2.41",
|
|
31
|
+
"@nuxt/content": "^3.11.2",
|
|
32
32
|
"@nuxt/image": "^2.0.0",
|
|
33
|
-
"@nuxt/kit": "^4.3.
|
|
33
|
+
"@nuxt/kit": "^4.3.1",
|
|
34
34
|
"@nuxt/ui": "^4.4.0",
|
|
35
|
-
"@nuxtjs/i18n": "^10.2.
|
|
36
|
-
"@nuxtjs/mcp-toolkit": "^0.6.
|
|
37
|
-
"@nuxtjs/mdc": "^0.20.
|
|
35
|
+
"@nuxtjs/i18n": "^10.2.3",
|
|
36
|
+
"@nuxtjs/mcp-toolkit": "^0.6.3",
|
|
37
|
+
"@nuxtjs/mdc": "^0.20.1",
|
|
38
38
|
"@nuxtjs/robots": "^5.7.0",
|
|
39
|
-
"@vueuse/core": "^14.2.
|
|
40
|
-
"ai": "6.0.
|
|
39
|
+
"@vueuse/core": "^14.2.1",
|
|
40
|
+
"ai": "6.0.78",
|
|
41
41
|
"defu": "^6.1.4",
|
|
42
42
|
"exsolve": "^1.0.8",
|
|
43
43
|
"git-url-parse": "^16.1.0",
|
|
44
|
-
"minimark": "^0.
|
|
45
|
-
"motion-v": "^1.10.
|
|
44
|
+
"minimark": "^1.0.0",
|
|
45
|
+
"motion-v": "^1.10.3",
|
|
46
46
|
"nuxt-llms": "^0.2.0",
|
|
47
47
|
"nuxt-og-image": "^5.1.13",
|
|
48
48
|
"pkg-types": "^2.3.0",
|
|
@@ -2,6 +2,11 @@ import { queryCollection } from '@nuxt/content/server'
|
|
|
2
2
|
import { getAvailableLocales, getCollectionsToQuery } from '../utils/content'
|
|
3
3
|
import { inferSiteURL } from '../../utils/meta'
|
|
4
4
|
|
|
5
|
+
interface SitemapUrl {
|
|
6
|
+
loc: string
|
|
7
|
+
lastmod?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export default defineEventHandler(async (event) => {
|
|
6
11
|
const config = useRuntimeConfig(event)
|
|
7
12
|
const siteUrl = inferSiteURL() || ''
|
|
@@ -18,7 +23,7 @@ export default defineEventHandler(async (event) => {
|
|
|
18
23
|
collections.push('landing')
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
const urls:
|
|
26
|
+
const urls: SitemapUrl[] = []
|
|
22
27
|
|
|
23
28
|
for (const collection of collections) {
|
|
24
29
|
try {
|
|
@@ -34,9 +39,16 @@ export default defineEventHandler(async (event) => {
|
|
|
34
39
|
// Skip .navigation files (used for navigation configuration)
|
|
35
40
|
if (pagePath.endsWith('.navigation') || pagePath.includes('/.navigation')) continue
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
const urlEntry: SitemapUrl = {
|
|
38
43
|
loc: pagePath,
|
|
39
|
-
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Add lastmod if available (modifiedAt from content)
|
|
47
|
+
if (meta.modifiedAt && typeof meta.modifiedAt === 'string') {
|
|
48
|
+
urlEntry.lastmod = meta.modifiedAt.split('T')[0] // Use date part only (YYYY-MM-DD)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
urls.push(urlEntry)
|
|
40
52
|
}
|
|
41
53
|
}
|
|
42
54
|
catch {
|
|
@@ -50,11 +62,18 @@ export default defineEventHandler(async (event) => {
|
|
|
50
62
|
return sitemap
|
|
51
63
|
})
|
|
52
64
|
|
|
53
|
-
function generateSitemap(urls:
|
|
65
|
+
function generateSitemap(urls: SitemapUrl[], siteUrl: string): string {
|
|
54
66
|
const urlEntries = urls
|
|
55
67
|
.map((url) => {
|
|
56
68
|
const loc = siteUrl ? `${siteUrl}${url.loc}` : url.loc
|
|
57
|
-
|
|
69
|
+
let entry = ` <url>\n <loc>${escapeXml(loc)}</loc>`
|
|
70
|
+
|
|
71
|
+
if (url.lastmod) {
|
|
72
|
+
entry += `\n <lastmod>${escapeXml(url.lastmod)}</lastmod>`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
entry += `\n </url>`
|
|
76
|
+
return entry
|
|
58
77
|
})
|
|
59
78
|
.join('\n')
|
|
60
79
|
|