@nuxtjs/sitemap 8.1.0 → 8.2.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.
@@ -0,0 +1,148 @@
1
+ <script setup lang="ts">
2
+ import type { SitemapSourceResolved } from '../../lib/sitemap/types'
3
+ import { joinURL } from 'ufo'
4
+ import { computed } from 'vue'
5
+ import { data } from '../../lib/sitemap/state'
6
+
7
+ const props = defineProps<{ source: SitemapSourceResolved, showContext?: boolean }>()
8
+
9
+ const fetchUrl = computed(() => {
10
+ const url = typeof props.source.fetch === 'string' ? props.source.fetch : props.source.fetch![0]
11
+ if (url.includes('http'))
12
+ return url
13
+ return joinURL(data.value?.nitroOrigin || 'localhost', url)
14
+ })
15
+
16
+ function normaliseTip(tip: string) {
17
+ return tip.replace(/`([^`]+)`/g, '<code>$1</code>')
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <DevtoolsSection :class="source.error ? 'source-error' : ''">
23
+ <template #text>
24
+ <div class="flex items-center gap-3">
25
+ <div
26
+ v-if="source.fetch"
27
+ class="flex items-center gap-1.5"
28
+ >
29
+ <UIcon
30
+ name="carbon:api-1"
31
+ class="text-[var(--color-text-muted)]"
32
+ />
33
+ <DevtoolsMetric
34
+ v-if="source.timeTakenMs"
35
+ :value="source.timeTakenMs"
36
+ label="ms"
37
+ variant="info"
38
+ />
39
+ </div>
40
+ <span class="font-semibold">{{ source.context.name }}</span>
41
+ <DevtoolsMetric
42
+ :value="source.urls?.length || 0"
43
+ label="URLs"
44
+ :variant="source.error ? 'danger' : !source.urls?.length ? 'warning' : 'success'"
45
+ />
46
+ </div>
47
+ </template>
48
+ <template #description>
49
+ <div class="flex items-center gap-3">
50
+ <a
51
+ v-if="source.fetch"
52
+ :href="fetchUrl"
53
+ target="_blank"
54
+ class="link-external text-sm"
55
+ >
56
+ {{ source.fetch }}
57
+ </a>
58
+ <span
59
+ v-if="source.context.description"
60
+ class="text-xs text-[var(--color-text-muted)]"
61
+ >
62
+ {{ source.context.description }}
63
+ </span>
64
+ </div>
65
+ </template>
66
+ <DevtoolsAlert
67
+ v-if="source.error"
68
+ variant="error"
69
+ >
70
+ {{ source.error }}
71
+ </DevtoolsAlert>
72
+ <template v-else>
73
+ <DevtoolsAlert
74
+ v-if="source._urlWarnings?.length"
75
+ variant="warning"
76
+ >
77
+ <div>
78
+ <div class="text-xs font-semibold mb-1">
79
+ {{ source._urlWarnings.length }} URL warning{{ source._urlWarnings.length > 1 ? 's' : '' }}
80
+ </div>
81
+ <ul class="url-warnings-list">
82
+ <li
83
+ v-for="(w, i) in source._urlWarnings"
84
+ :key="i"
85
+ >
86
+ <code>{{ w.loc }}</code> — {{ w.message }}
87
+ </li>
88
+ </ul>
89
+ </div>
90
+ </DevtoolsAlert>
91
+ <DevtoolsSnippet
92
+ :code="JSON.stringify(source.urls, null, 2)"
93
+ lang="json"
94
+ label="URLs"
95
+ />
96
+ </template>
97
+ <DevtoolsAlert
98
+ v-if="source.context.tips?.length"
99
+ :variant="!source.urls?.length && !source.error ? 'warning' : 'info'"
100
+ >
101
+ <div>
102
+ <h3 class="text-xs font-semibold mb-1.5 text-[var(--color-text)] uppercase tracking-wide opacity-70">
103
+ Hints
104
+ </h3>
105
+ <ul class="space-y-1">
106
+ <li
107
+ v-for="(tip, key) in source.context.tips"
108
+ :key="key"
109
+ class="text-sm text-[var(--color-text-muted)] leading-relaxed"
110
+ v-html="normaliseTip(tip)"
111
+ />
112
+ </ul>
113
+ </div>
114
+ </DevtoolsAlert>
115
+ </DevtoolsSection>
116
+ </template>
117
+
118
+ <style scoped>
119
+ .source-error {
120
+ border-color: oklch(55% 0.15 25 / 0.35);
121
+ }
122
+
123
+ .source-error:hover {
124
+ border-color: oklch(55% 0.15 25 / 0.5);
125
+ }
126
+
127
+ .url-warnings-list {
128
+ list-style: none;
129
+ padding: 0;
130
+ margin: 0;
131
+ font-size: 0.6875rem;
132
+ line-height: 1.5;
133
+ color: var(--color-text-muted);
134
+ }
135
+
136
+ .url-warnings-list li {
137
+ padding: 0.125rem 0;
138
+ }
139
+
140
+ .url-warnings-list code {
141
+ font-family: var(--font-mono);
142
+ font-size: 0.625rem;
143
+ padding: 0.0625rem 0.3125rem;
144
+ border-radius: 3px;
145
+ background: var(--color-surface-sunken);
146
+ color: var(--color-text);
147
+ }
148
+ </style>
@@ -0,0 +1,5 @@
1
+ import { useDevtoolsConnection } from 'nuxtseo-layer-devtools/composables/rpc'
2
+
3
+ // The layer refreshes data on connect and on every host route change, so sitemap
4
+ // needs no module-level host access.
5
+ useDevtoolsConnection()
@@ -0,0 +1,71 @@
1
+ import type { ModuleRuntimeConfig, ProductionDebugResponse, SitemapDefinition, SitemapSourceResolved } from './types'
2
+ import { appFetch } from 'nuxtseo-layer-devtools/composables/rpc'
3
+ import { isProductionMode, productionUrl, refreshTime } from 'nuxtseo-layer-devtools/composables/state'
4
+ import { ref, watch } from 'vue'
5
+
6
+ export const data = ref<{
7
+ nitroOrigin: string
8
+ globalSources: SitemapSourceResolved[]
9
+ sitemaps: SitemapDefinition[]
10
+ runtimeConfig: ModuleRuntimeConfig
11
+ siteConfig?: { url?: string }
12
+ } | null>(null)
13
+
14
+ // Production debug data from the remote /__sitemap__/debug.json (requires debug: true in production)
15
+ export const productionRemoteDebugData = ref<typeof data.value | null>(null)
16
+
17
+ export const productionData = ref<ProductionDebugResponse | null>(null)
18
+ export const productionLoading = ref(false)
19
+
20
+ export async function refreshSources() {
21
+ if (!appFetch.value)
22
+ return
23
+ data.value = await appFetch.value('/__sitemap__/debug.json').catch((err) => {
24
+ console.error('Failed to fetch sitemap debug data:', err)
25
+ return null
26
+ }) as typeof data.value
27
+ }
28
+
29
+ export async function refreshProductionData() {
30
+ if (!appFetch.value || !productionUrl.value)
31
+ return
32
+ productionLoading.value = true
33
+ productionRemoteDebugData.value = null
34
+
35
+ // Try fetching the full debug endpoint from production first (proxied through local server)
36
+ const remoteDebug = await appFetch.value('/__sitemap__/debug-production.json', {
37
+ query: { url: productionUrl.value, mode: 'debug' },
38
+ }).catch(() => null) as (typeof data.value & { error?: string }) | null
39
+ if (remoteDebug && !remoteDebug.error && remoteDebug.sitemaps && !Array.isArray(remoteDebug.sitemaps)) {
40
+ // Response has object sitemaps (debug.json format) rather than array (XML fallback format)
41
+ productionRemoteDebugData.value = remoteDebug
42
+ productionLoading.value = false
43
+ return
44
+ }
45
+
46
+ // Fall back to XML-based validation
47
+ productionData.value = await appFetch.value('/__sitemap__/debug-production.json', {
48
+ query: { url: productionUrl.value },
49
+ }).catch((err: Error) => {
50
+ console.error('Failed to fetch production sitemap data:', err)
51
+ return null
52
+ }) as ProductionDebugResponse | null
53
+ productionLoading.value = false
54
+ }
55
+
56
+ // Re-fetch the (global) debug data when the host connects or a manual refresh fires
57
+ watch([appFetch, refreshTime], () => {
58
+ refreshSources()
59
+ })
60
+
61
+ // Sync production URL from siteConfig when debug data loads
62
+ watch(data, (val) => {
63
+ if (val?.siteConfig?.url)
64
+ productionUrl.value = val.siteConfig.url
65
+ }, { immediate: true })
66
+
67
+ // Fetch production data when switching to production mode
68
+ watch(isProductionMode, (isProd) => {
69
+ if (isProd && !productionData.value && !productionRemoteDebugData.value)
70
+ refreshProductionData()
71
+ })
@@ -0,0 +1,2 @@
1
+ export type { ProductionDebugResponse } from '../../../src/runtime/server/routes/__sitemap__/debug-production'
2
+ export type { ModuleRuntimeConfig, SitemapDefinition, SitemapSourceResolved } from '../../../src/runtime/types'
@@ -0,0 +1,7 @@
1
+ import { resolve } from 'pathe'
2
+
3
+ // Nuxt SEO devtools panel, shipped as a layer (Model C). Components flat-registered
4
+ // so intra-panel references resolve by name.
5
+ export default defineNuxtConfig({
6
+ components: [{ path: resolve(__dirname, './components'), pathPrefix: false }],
7
+ })
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import Source from '../../components/sitemap/Source.vue'
4
+ import { data } from '../../lib/sitemap/state'
5
+
6
+ const appSources = computed(() => (data.value?.globalSources || []).filter(s => s.sourceType === 'app'))
7
+ </script>
8
+
9
+ <template>
10
+ <div class="space-y-5 animate-fade-up">
11
+ <div>
12
+ <h2 class="text-lg font-semibold mb-1">
13
+ App Sources
14
+ </h2>
15
+ <p class="text-xs text-[var(--color-text-muted)]">
16
+ Automatic global sources generated from your application.
17
+ </p>
18
+ </div>
19
+ <template v-if="appSources.length">
20
+ <Source
21
+ v-for="(source, key) in appSources"
22
+ :key="key"
23
+ :source="source"
24
+ />
25
+ </template>
26
+ <DevtoolsEmptyState
27
+ v-else
28
+ title="No app sources detected"
29
+ description="App sources are automatically discovered from your Nuxt application routes and pages."
30
+ icon="carbon:bot"
31
+ />
32
+ </div>
33
+ </template>
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import { data } from '../../lib/sitemap/state'
3
+ </script>
4
+
5
+ <template>
6
+ <div class="space-y-5 animate-fade-up">
7
+ <DevtoolsSection>
8
+ <template #text>
9
+ <h3 class="opacity-80 text-base mb-1">
10
+ <UIcon name="carbon:settings" class="mr-1" />
11
+ Runtime Config
12
+ </h3>
13
+ </template>
14
+ <DevtoolsSnippet
15
+ :code="JSON.stringify(data?.runtimeConfig, null, 2)"
16
+ lang="json"
17
+ label="Runtime Config"
18
+ />
19
+ </DevtoolsSection>
20
+ </div>
21
+ </template>
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <DevtoolsDocs url="https://nuxtseo.com/sitemap" />
3
+ </template>
@@ -0,0 +1,456 @@
1
+ <script setup lang="ts">
2
+ import type { SitemapDefinition, SitemapSourceResolved } from '../../lib/sitemap/types'
3
+ import { isProductionMode, productionUrl } from 'nuxtseo-layer-devtools/composables/state'
4
+ import { joinURL } from 'ufo'
5
+ import { computed } from 'vue'
6
+ import Source from '../../components/sitemap/Source.vue'
7
+ import { data, productionData, productionLoading, productionRemoteDebugData, refreshProductionData } from '../../lib/sitemap/state'
8
+
9
+ const appSourcesExcluded = computed(() => data.value?.runtimeConfig?.excludeAppSources || [])
10
+
11
+ function resolveSitemapOrigin() {
12
+ if (isProductionMode.value && productionUrl.value)
13
+ return `${productionUrl.value.replace(/\/$/, '')}/`
14
+ return data.value?.nitroOrigin || ''
15
+ }
16
+
17
+ function resolveSitemapPath(sitemapName: string) {
18
+ const source = productionRemoteDebugData.value || data.value
19
+ if (!source)
20
+ return ''
21
+ const prefix = source.runtimeConfig?.sitemapsPathPrefix || ''
22
+ if (sitemapName === 'sitemap' || sitemapName === 'sitemap.xml')
23
+ return '/sitemap.xml'
24
+ if (sitemapName === 'index')
25
+ return '/sitemap_index.xml'
26
+ return joinURL('/', prefix, `${sitemapName}-sitemap.xml`)
27
+ }
28
+
29
+ function resolveSitemapUrl(sitemapName: string) {
30
+ return `${resolveSitemapOrigin()}${resolveSitemapPath(sitemapName).replace(/^\//, '')}`
31
+ }
32
+
33
+ function resolveSitemapOptions(definition: SitemapDefinition) {
34
+ const options: Record<string, any> = {}
35
+ Object.entries(definition).forEach(([key, value]) => {
36
+ if (value !== undefined && (!Array.isArray(value) || value.length > 0) && key !== 'includeAppSources')
37
+ options[key] = value
38
+ })
39
+ return options
40
+ }
41
+
42
+ function sitemapOptionsAsKeyValues(definition: SitemapDefinition) {
43
+ const options = resolveSitemapOptions(definition)
44
+ return Object.entries(options).map(([key, value]) => {
45
+ const isObject = typeof value === 'object'
46
+ return {
47
+ key,
48
+ value: isObject ? JSON.stringify(value, null, 2) : value,
49
+ mono: true,
50
+ code: isObject ? 'json' as const : undefined,
51
+ }
52
+ })
53
+ }
54
+
55
+ function sitemapPathFromUrl(url: string) {
56
+ try {
57
+ return new URL(url).pathname
58
+ }
59
+ catch {
60
+ return url
61
+ }
62
+ }
63
+
64
+ const hasRemoteDebug = computed(() => !!productionRemoteDebugData.value)
65
+
66
+ const totalProductionUrls = computed(() =>
67
+ productionData.value?.sitemaps.reduce((sum, s) => sum + s.urlCount, 0) ?? 0,
68
+ )
69
+
70
+ const totalProductionWarnings = computed(() =>
71
+ productionData.value?.sitemaps.reduce((sum, s) => sum + s.warnings.length, 0) ?? 0,
72
+ )
73
+ </script>
74
+
75
+ <template>
76
+ <div class="space-y-5 animate-fade-up">
77
+ <!-- Production mode -->
78
+ <template v-if="isProductionMode">
79
+ <div class="flex items-center justify-between">
80
+ <div>
81
+ <h2 class="text-lg font-semibold mb-1">
82
+ Production Sitemaps
83
+ </h2>
84
+ <p class="text-xs text-[var(--color-text-muted)]">
85
+ Fetched from {{ productionUrl }}<template v-if="hasRemoteDebug">
86
+ with debug mode enabled
87
+ </template>.
88
+ </p>
89
+ </div>
90
+ <UButton
91
+ icon="carbon:reset"
92
+ size="xs"
93
+ variant="ghost"
94
+ :loading="productionLoading"
95
+ @click="refreshProductionData()"
96
+ >
97
+ Re-validate
98
+ </UButton>
99
+ </div>
100
+
101
+ <DevtoolsLoading v-if="productionLoading && !productionData && !productionRemoteDebugData" />
102
+
103
+ <!-- Full debug view (production has debug: true) -->
104
+ <template v-if="hasRemoteDebug">
105
+ <DevtoolsSection
106
+ v-for="(sitemap, key) in productionRemoteDebugData!.sitemaps"
107
+ :key="key"
108
+ >
109
+ <template #text>
110
+ <div class="flex items-center gap-2">
111
+ <span class="font-semibold">{{ sitemap.sitemapName }}</span>
112
+ <a
113
+ target="_blank"
114
+ :href="resolveSitemapUrl(sitemap.sitemapName)"
115
+ class="link-external text-xs font-mono text-[var(--color-text-muted)]"
116
+ >
117
+ {{ resolveSitemapPath(sitemap.sitemapName) }}
118
+ </a>
119
+ <UIcon
120
+ v-if="(sitemap.sources || []).some(s => typeof s !== 'string' && 'error' in s && !!s.error)"
121
+ name="carbon:warning"
122
+ class="text-red-500"
123
+ />
124
+ <UIcon
125
+ v-else-if="(sitemap.sources || []).some(s => typeof s !== 'string' && '_urlWarnings' in s && s._urlWarnings?.length)"
126
+ name="carbon:warning-alt"
127
+ class="text-amber-500"
128
+ />
129
+ </div>
130
+ </template>
131
+ <div class="space-y-5">
132
+ <template v-if="sitemap.sitemapName === 'index'">
133
+ <DevtoolsAlert variant="info">
134
+ Links to your other sitemaps.
135
+ <a
136
+ href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps"
137
+ target="_blank"
138
+ class="link-external"
139
+ >
140
+ Learn more
141
+ </a>
142
+ </DevtoolsAlert>
143
+ </template>
144
+ <template v-else>
145
+ <div
146
+ v-if="sitemap.sources && sitemap.sources.length"
147
+ class="flex gap-4"
148
+ >
149
+ <div class="w-32 flex-shrink-0">
150
+ <div class="font-semibold text-sm">
151
+ Sources
152
+ </div>
153
+ </div>
154
+ <div class="flex-grow space-y-2">
155
+ <Source
156
+ v-for="(source, k) in (sitemap.sources as SitemapSourceResolved[])"
157
+ :key="k"
158
+ :source="source"
159
+ />
160
+ </div>
161
+ </div>
162
+ <div class="flex gap-4">
163
+ <div class="w-32 flex-shrink-0">
164
+ <div class="font-semibold text-sm">
165
+ Options
166
+ </div>
167
+ </div>
168
+ <div class="flex-grow">
169
+ <DevtoolsKeyValue
170
+ :items="sitemapOptionsAsKeyValues(sitemap)"
171
+ striped
172
+ />
173
+ </div>
174
+ </div>
175
+ </template>
176
+ </div>
177
+ </DevtoolsSection>
178
+ </template>
179
+
180
+ <!-- XML-based fallback view -->
181
+ <template v-else-if="productionData?.error">
182
+ <DevtoolsProductionError :error="productionData.error" />
183
+ </template>
184
+
185
+ <template v-else-if="productionData">
186
+ <!-- Summary -->
187
+ <div class="flex items-center gap-4">
188
+ <DevtoolsMetric
189
+ :value="productionData.sitemaps.length"
190
+ :label="productionData.isIndex ? 'child sitemaps' : 'sitemap'"
191
+ variant="info"
192
+ />
193
+ <DevtoolsMetric
194
+ :value="totalProductionUrls"
195
+ label="total URLs"
196
+ variant="success"
197
+ />
198
+ <DevtoolsMetric
199
+ v-if="totalProductionWarnings > 0"
200
+ :value="totalProductionWarnings"
201
+ label="warnings"
202
+ variant="warning"
203
+ />
204
+ </div>
205
+
206
+ <!-- Each production sitemap -->
207
+ <DevtoolsSection
208
+ v-for="(sitemap, i) in productionData.sitemaps"
209
+ :key="i"
210
+ >
211
+ <template #text>
212
+ <div class="flex items-center gap-2">
213
+ <span class="font-semibold">{{ sitemapPathFromUrl(sitemap.loc) }}</span>
214
+ <DevtoolsMetric
215
+ :value="sitemap.urlCount"
216
+ label="URLs"
217
+ variant="success"
218
+ />
219
+ <UIcon
220
+ v-if="sitemap.error"
221
+ name="carbon:warning"
222
+ class="text-red-500"
223
+ />
224
+ <UIcon
225
+ v-else-if="sitemap.warnings.length"
226
+ name="carbon:warning-alt"
227
+ class="text-amber-500"
228
+ />
229
+ </div>
230
+ </template>
231
+ <template #description>
232
+ <a
233
+ :href="sitemap.loc"
234
+ target="_blank"
235
+ class="link-external text-xs font-mono text-[var(--color-text-muted)]"
236
+ >
237
+ {{ sitemap.loc }}
238
+ </a>
239
+ </template>
240
+ <div v-if="sitemap.error || sitemap.warnings.length" class="space-y-3">
241
+ <DevtoolsAlert
242
+ v-if="sitemap.error"
243
+ variant="warning"
244
+ >
245
+ {{ sitemap.error }}
246
+ </DevtoolsAlert>
247
+ <DevtoolsAlert
248
+ v-if="sitemap.warnings.length"
249
+ variant="warning"
250
+ >
251
+ <div>
252
+ <div class="text-xs font-semibold mb-1">
253
+ {{ sitemap.warnings.length }} validation warning{{ sitemap.warnings.length > 1 ? 's' : '' }}
254
+ </div>
255
+ <ul class="prod-warnings-list">
256
+ <li
257
+ v-for="(w, wi) in sitemap.warnings"
258
+ :key="wi"
259
+ >
260
+ <template v-if="w.context?.url">
261
+ <code>{{ w.context.url }}</code>:
262
+ </template>
263
+ {{ w.message }}
264
+ </li>
265
+ </ul>
266
+ </div>
267
+ </DevtoolsAlert>
268
+ </div>
269
+ </DevtoolsSection>
270
+
271
+ <!-- Hint about debug mode -->
272
+ <DevtoolsAlert variant="info">
273
+ Want to see full source details and URL validation? Deploy with <code>sitemap: { debug: true }</code> to get the same detailed view as development mode.
274
+ </DevtoolsAlert>
275
+ </template>
276
+ </template>
277
+
278
+ <!-- Local mode -->
279
+ <template v-else>
280
+ <div>
281
+ <h2 class="text-lg font-semibold mb-1">
282
+ Sitemaps
283
+ </h2>
284
+ <p class="text-xs text-[var(--color-text-muted)]">
285
+ The sitemaps generated from your site.
286
+ </p>
287
+ </div>
288
+ <DevtoolsSection
289
+ v-for="(sitemap, key) in data?.sitemaps"
290
+ :key="key"
291
+ >
292
+ <template #text>
293
+ <div class="flex items-center gap-2">
294
+ <span class="font-semibold">{{ sitemap.sitemapName }}</span>
295
+ <a
296
+ target="_blank"
297
+ :href="resolveSitemapUrl(sitemap.sitemapName)"
298
+ class="link-external text-xs font-mono text-[var(--color-text-muted)]"
299
+ >
300
+ {{ resolveSitemapPath(sitemap.sitemapName) }}
301
+ </a>
302
+ <UIcon
303
+ v-if="(sitemap.sources || []).some(s => typeof s !== 'string' && 'error' in s && !!s.error)"
304
+ name="carbon:warning"
305
+ class="text-red-500"
306
+ />
307
+ <UIcon
308
+ v-else-if="(sitemap.sources || []).some(s => typeof s !== 'string' && '_urlWarnings' in s && s._urlWarnings?.length)"
309
+ name="carbon:warning-alt"
310
+ class="text-amber-500"
311
+ />
312
+ </div>
313
+ </template>
314
+ <div class="space-y-5">
315
+ <template v-if="sitemap.sitemapName === 'index'">
316
+ <DevtoolsAlert variant="info">
317
+ Links to your other sitemaps.
318
+ <a
319
+ href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps"
320
+ target="_blank"
321
+ class="link-external"
322
+ >
323
+ Learn more
324
+ </a>
325
+ </DevtoolsAlert>
326
+ </template>
327
+ <template v-else>
328
+ <div
329
+ v-if="sitemap.sources && sitemap.sources.length"
330
+ class="flex gap-4"
331
+ >
332
+ <div class="w-32 flex-shrink-0">
333
+ <div class="font-semibold text-sm">
334
+ Sources
335
+ </div>
336
+ </div>
337
+ <div class="flex-grow space-y-2">
338
+ <Source
339
+ v-for="(source, k) in (sitemap.sources as SitemapSourceResolved[])"
340
+ :key="k"
341
+ :source="source"
342
+ />
343
+ </div>
344
+ </div>
345
+ <div class="flex gap-4">
346
+ <div class="w-32 flex-shrink-0">
347
+ <div class="font-semibold text-sm">
348
+ App Sources
349
+ </div>
350
+ </div>
351
+ <div class="flex-grow flex items-center gap-3">
352
+ <div
353
+ v-if="sitemap.includeAppSources && appSourcesExcluded !== true"
354
+ class="status-enabled"
355
+ >
356
+ <UIcon
357
+ name="carbon:checkmark"
358
+ class="text-sm"
359
+ />
360
+ <span>Enabled</span>
361
+ </div>
362
+ <div
363
+ v-else
364
+ class="status-disabled"
365
+ >
366
+ <UIcon
367
+ name="carbon:close"
368
+ class="text-sm"
369
+ />
370
+ <span>Disabled</span>
371
+ </div>
372
+ <NuxtLink
373
+ to="/app-sources"
374
+ class="text-xs text-[var(--seo-green)] hover:underline"
375
+ >
376
+ View details
377
+ </NuxtLink>
378
+ </div>
379
+ </div>
380
+ <div class="flex gap-4">
381
+ <div class="w-32 flex-shrink-0">
382
+ <div class="font-semibold text-sm">
383
+ Options
384
+ </div>
385
+ </div>
386
+ <div class="flex-grow">
387
+ <DevtoolsKeyValue
388
+ :items="sitemapOptionsAsKeyValues(sitemap)"
389
+ striped
390
+ />
391
+ </div>
392
+ </div>
393
+ </template>
394
+ </div>
395
+ </DevtoolsSection>
396
+ </template>
397
+ </div>
398
+ </template>
399
+
400
+ <style scoped>
401
+ .status-enabled {
402
+ display: inline-flex;
403
+ align-items: center;
404
+ gap: 0.375rem;
405
+ padding: 0.25rem 0.625rem;
406
+ font-size: 0.75rem;
407
+ font-weight: 500;
408
+ border-radius: var(--radius-sm);
409
+ background: oklch(75% 0.15 145 / 0.12);
410
+ color: oklch(50% 0.15 145);
411
+ }
412
+
413
+ .dark .status-enabled {
414
+ background: oklch(50% 0.15 145 / 0.15);
415
+ color: oklch(75% 0.18 145);
416
+ }
417
+
418
+ .status-disabled {
419
+ display: inline-flex;
420
+ align-items: center;
421
+ gap: 0.375rem;
422
+ padding: 0.25rem 0.625rem;
423
+ font-size: 0.75rem;
424
+ font-weight: 500;
425
+ border-radius: var(--radius-sm);
426
+ background: oklch(65% 0.12 25 / 0.1);
427
+ color: oklch(55% 0.15 25);
428
+ }
429
+
430
+ .dark .status-disabled {
431
+ background: oklch(45% 0.1 25 / 0.15);
432
+ color: oklch(70% 0.12 25);
433
+ }
434
+
435
+ .prod-warnings-list {
436
+ list-style: none;
437
+ padding: 0;
438
+ margin: 0;
439
+ font-size: 0.6875rem;
440
+ line-height: 1.5;
441
+ color: var(--color-text-muted);
442
+ }
443
+
444
+ .prod-warnings-list li {
445
+ padding: 0.125rem 0;
446
+ }
447
+
448
+ .prod-warnings-list code {
449
+ font-family: var(--font-mono);
450
+ font-size: 0.625rem;
451
+ padding: 0.0625rem 0.3125rem;
452
+ border-radius: 3px;
453
+ background: var(--color-surface-sunken);
454
+ color: var(--color-text);
455
+ }
456
+ </style>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import Source from '../../components/sitemap/Source.vue'
4
+ import { data } from '../../lib/sitemap/state'
5
+
6
+ const userSources = computed(() => (data.value?.globalSources || []).filter(s => s.sourceType === 'user'))
7
+ </script>
8
+
9
+ <template>
10
+ <div class="space-y-5 animate-fade-up">
11
+ <div>
12
+ <h2 class="text-lg font-semibold mb-1">
13
+ User Sources
14
+ </h2>
15
+ <p class="text-xs text-[var(--color-text-muted)]">
16
+ Manually provided global sources provided by you.
17
+ </p>
18
+ </div>
19
+ <template v-if="userSources.length">
20
+ <Source
21
+ v-for="(source, key) in userSources"
22
+ :key="key"
23
+ :source="source"
24
+ />
25
+ </template>
26
+ <DevtoolsEmptyState
27
+ v-else
28
+ title="No user sources configured"
29
+ description="Add custom sources via the sources option in your sitemap config."
30
+ icon="carbon:add-alt"
31
+ />
32
+ </div>
33
+ </template>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ import { loadShiki } from 'nuxtseo-layer-devtools/composables/shiki'
3
+ import { isProductionMode } from 'nuxtseo-layer-devtools/composables/state'
4
+ import { computed, ref, watch } from 'vue'
5
+ import { navigateTo, useRoute } from '#imports'
6
+ import { data, productionData, productionRemoteDebugData, refreshProductionData, refreshSources } from '../lib/sitemap/state'
7
+ import '../lib/sitemap/rpc'
8
+
9
+ await loadShiki()
10
+
11
+ const refreshing = ref(false)
12
+
13
+ async function refresh() {
14
+ if (refreshing.value)
15
+ return
16
+ refreshing.value = true
17
+ data.value = null
18
+ productionData.value = null
19
+ productionRemoteDebugData.value = null
20
+ await refreshSources()
21
+ if (isProductionMode.value)
22
+ await refreshProductionData()
23
+ setTimeout(() => {
24
+ refreshing.value = false
25
+ }, 300)
26
+ }
27
+
28
+ const route = useRoute()
29
+ const currentTab = computed(() => {
30
+ const path = route.path
31
+ if (path.startsWith('/sitemap/user-sources'))
32
+ return 'user-sources'
33
+ if (path.startsWith('/sitemap/app-sources'))
34
+ return 'app-sources'
35
+ if (path.startsWith('/sitemap/debug'))
36
+ return 'debug'
37
+ if (path.startsWith('/sitemap/docs'))
38
+ return 'docs'
39
+ return 'sitemaps'
40
+ })
41
+
42
+ const navItems = [
43
+ { value: 'sitemaps', to: '/sitemap', icon: 'carbon:load-balancer-application', label: 'Sitemaps', devOnly: false },
44
+ { value: 'user-sources', to: '/sitemap/user-sources', icon: 'carbon:group-account', label: 'User Sources', devOnly: true },
45
+ { value: 'app-sources', to: '/sitemap/app-sources', icon: 'carbon:bot', label: 'App Sources', devOnly: true },
46
+ { value: 'debug', to: '/sitemap/debug', icon: 'carbon:debug', label: 'Debug', devOnly: true },
47
+ { value: 'docs', to: '/sitemap/docs', icon: 'carbon:book', label: 'Docs', devOnly: false },
48
+ ]
49
+
50
+ const runtimeVersion = computed(() => data.value?.runtimeConfig?.version)
51
+
52
+ watch(isProductionMode, (isProd) => {
53
+ if (isProd && ['user-sources', 'app-sources', 'debug'].includes(currentTab.value))
54
+ return navigateTo('/sitemap')
55
+ })
56
+ </script>
57
+
58
+ <template>
59
+ <DevtoolsLayout
60
+ v-model:active-tab="currentTab"
61
+ module-name="sitemap"
62
+ title="Sitemap"
63
+ icon="carbon:load-balancer-application"
64
+ :version="runtimeVersion"
65
+ :nav-items="navItems"
66
+ github-url="https://github.com/nuxt-modules/sitemap"
67
+ :loading="!data?.globalSources || refreshing"
68
+ @refresh="refresh"
69
+ >
70
+ <NuxtPage />
71
+ </DevtoolsLayout>
72
+ </template>
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=3.9.0"
5
5
  },
6
6
  "configKey": "sitemap",
7
- "version": "8.1.0",
7
+ "version": "8.2.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -75,6 +75,7 @@ export async function readSourcesFromFilesystem(filename) {
75
75
  if (!route.fileName?.endsWith(".html") || !html || ["/200.html", "/404.html"].includes(route.route))
76
76
  return;
77
77
  if (NuxtRedirectHtmlRegex.test(html)) {
78
+ route._sitemap = { loc: route.route, _sitemap: false };
78
79
  return;
79
80
  }
80
81
  const extractedMeta = parseHtmlExtractSitemapMeta(html, {
@@ -1128,7 +1129,13 @@ ${onUrlEntries.join("\n")}`;
1128
1129
  const nitro = await nitroPromise;
1129
1130
  const prerenderedRoutes2 = nitro._prerenderedRoutes || [];
1130
1131
  const prerenderUrlsFinal = [
1131
- ...prerenderedRoutes2.filter(isValidPrerenderRoute).map((r) => r._sitemap).filter((entry) => entry && (typeof entry === "string" || entry._sitemap !== false))
1132
+ ...prerenderedRoutes2.filter(isValidPrerenderRoute).map((r) => {
1133
+ if (r._sitemap)
1134
+ return r._sitemap;
1135
+ if (r.route.startsWith("/api/") || r.route.startsWith("/_"))
1136
+ return void 0;
1137
+ return { loc: r.route };
1138
+ }).filter((entry) => entry && (typeof entry === "string" || entry._sitemap !== false))
1132
1139
  ];
1133
1140
  if (config.debug) {
1134
1141
  logger.info("Prerendered routes:", prerenderUrlsFinal);
@@ -3,7 +3,7 @@ import { defineCachedFunction, useRuntimeConfig } from "nitropack/runtime";
3
3
  import { resolveSitePath } from "nuxt-site-config/urls";
4
4
  import { joinURL, withHttps } from "ufo";
5
5
  import staticConfig from "#sitemap-virtual/static-config.mjs";
6
- import { applyDynamicParams, createPathFilter, findPageMapping, logger, splitForLocales } from "../../../utils-pure.js";
6
+ import { applyDynamicParams, createPathFilter, findPageMapping, logger, resolveI18nSitemapLocaleKey, splitForLocales } from "../../../utils-pure.js";
7
7
  import { preNormalizeEntry } from "../urlset/normalise.js";
8
8
  import { sortInPlace } from "../urlset/sort.js";
9
9
  import { childSitemapSources, globalSitemapSources, resolveSitemapSources } from "../urlset/sources.js";
@@ -189,11 +189,12 @@ export async function buildResolvedSitemapUrls(effectiveSitemap, matchName, isCh
189
189
  };
190
190
  await nitro?.hooks.callHook("sitemap:input", resolvedCtx);
191
191
  const enhancedUrls = resolveSitemapEntries(effectiveSitemap, resolvedCtx.urls, { autoI18n, isI18nMapped }, resolvers, useRuntimeConfig().app.baseURL);
192
+ const localeSitemapKeys = isI18nMapped && autoI18n ? autoI18n.locales.map((l) => l._sitemap) : [];
192
193
  if (isMultiSitemap) {
193
194
  const sitemapNames = Object.keys(sitemaps).filter((k) => k !== "index");
194
195
  const warnedSitemaps = nitro?._sitemapWarnedSitemaps || /* @__PURE__ */ new Set();
195
196
  for (const e of enhancedUrls) {
196
- const hasMatchingSitemap = typeof e._sitemap === "string" && (sitemapNames.includes(e._sitemap) || isI18nMapped && sitemapNames.some((name) => name.startsWith(`${e._sitemap}-`)));
197
+ const hasMatchingSitemap = typeof e._sitemap === "string" && (sitemapNames.includes(e._sitemap) || isI18nMapped && sitemapNames.some((name) => resolveI18nSitemapLocaleKey(name, localeSitemapKeys) === e._sitemap));
197
198
  if (typeof e._sitemap === "string" && !hasMatchingSitemap) {
198
199
  if (!warnedSitemaps.has(e._sitemap)) {
199
200
  warnedSitemaps.add(e._sitemap);
@@ -211,7 +212,11 @@ export async function buildResolvedSitemapUrls(effectiveSitemap, matchName, isCh
211
212
  if (isMultiSitemap && e._sitemap && matchName) {
212
213
  if (isChunked)
213
214
  return e._sitemap === matchName;
214
- return e._sitemap === matchName || isI18nMapped && matchName.startsWith(`${e._sitemap}-`);
215
+ if (e._sitemap === matchName)
216
+ return true;
217
+ if (isI18nMapped)
218
+ return e._sitemap === resolveI18nSitemapLocaleKey(matchName, localeSitemapKeys);
219
+ return false;
215
220
  }
216
221
  return true;
217
222
  });
@@ -3,6 +3,16 @@ export { createFilter, type CreateFilterOptions } from 'nuxtseo-shared/utils';
3
3
  export declare const logger: import("consola").ConsolaInstance;
4
4
  export declare function mergeOnKey<T, K extends keyof T>(arr: T[], key: K): T[];
5
5
  export declare function splitForLocales(path: string, locales: string[]): [string | null, string];
6
+ /**
7
+ * Resolve which locale a multi-sitemap name belongs to.
8
+ *
9
+ * i18n-mapped sitemaps are named either `<localeSitemap>` (default) or
10
+ * `<localeSitemap>-<name>` (custom sitemaps). Locale `_sitemap` keys can share a
11
+ * prefix (e.g. `zh` and `zh-Hant`), so a naive `name.startsWith(`${key}-`)` check
12
+ * collides: `zh-Hant` would match the `zh` locale. Resolve by the longest matching
13
+ * key to disambiguate.
14
+ */
15
+ export declare function resolveI18nSitemapLocaleKey(sitemapName: string, localeSitemapKeys: string[]): string | null;
6
16
  /**
7
17
  * Transform a literal notation string regex to RegExp
8
18
  */
@@ -36,6 +36,16 @@ export function splitForLocales(path, locales) {
36
36
  return [prefix, path.replace(`/${prefix}`, "")];
37
37
  return [null, path];
38
38
  }
39
+ export function resolveI18nSitemapLocaleKey(sitemapName, localeSitemapKeys) {
40
+ let best = null;
41
+ for (const key of localeSitemapKeys) {
42
+ if (sitemapName === key || sitemapName.startsWith(`${key}-`)) {
43
+ if (best === null || key.length > best.length)
44
+ best = key;
45
+ }
46
+ }
47
+ return best;
48
+ }
39
49
  const StringifiedRegExpPattern = /\/(.*?)\/([gimsuy]*)$/;
40
50
  export function normalizeRuntimeFilters(input) {
41
51
  return (input || []).map((rule) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtjs/sitemap",
3
3
  "type": "module",
4
- "version": "8.1.0",
4
+ "version": "8.2.1",
5
5
  "description": "Powerfully flexible XML Sitemaps that integrate seamlessly, for Nuxt.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -55,12 +55,12 @@
55
55
  }
56
56
  },
57
57
  "dependencies": {
58
- "@nuxt/kit": "^4.4.7",
58
+ "@nuxt/kit": "^4.4.8",
59
59
  "consola": "^3.4.2",
60
60
  "defu": "^6.1.7",
61
61
  "fast-xml-parser": "^5.8.0",
62
- "nuxt-site-config": "^4.0.8",
63
- "nuxtseo-shared": "^5.1.4",
62
+ "nuxt-site-config": "^4.1.0",
63
+ "nuxtseo-shared": "^5.3.0",
64
64
  "ofetch": "^1.5.1",
65
65
  "pathe": "^2.0.3",
66
66
  "pkg-types": "^2.3.1",
@@ -72,7 +72,7 @@
72
72
  "@antfu/eslint-config": "^9.0.0",
73
73
  "@arethetypeswrong/cli": "^0.18.3",
74
74
  "@nuxt/content": "^3.14.0",
75
- "@nuxt/devtools-kit": "4.0.0-alpha.3",
75
+ "@nuxt/devtools-kit": "4.0.0-alpha.7",
76
76
  "@nuxt/module-builder": "^1.0.2",
77
77
  "@nuxt/test-utils": "^4.0.3",
78
78
  "@nuxt/ui": "^4.8.2",
@@ -85,10 +85,10 @@
85
85
  "eslint-plugin-harlanzw": "^0.17.0",
86
86
  "execa": "^9.6.1",
87
87
  "happy-dom": "^20.10.2",
88
- "nuxt": "^4.4.7",
88
+ "nuxt": "^4.4.8",
89
89
  "nuxt-i18n-micro": "^3.18.2",
90
- "nuxtseo-layer-devtools": "^5.1.4",
91
- "semver": "^7.8.3",
90
+ "nuxtseo-layer-devtools": "^5.3.0",
91
+ "semver": "^7.8.4",
92
92
  "sirv": "^3.0.2",
93
93
  "std-env": "^4.1.0",
94
94
  "typescript": "^6.0.3",
@@ -96,18 +96,17 @@
96
96
  "vitest": "^4.1.8",
97
97
  "vue-tsc": "^3.3.4",
98
98
  "zod": "^4.4.3",
99
- "@nuxtjs/sitemap": "8.1.0"
99
+ "@nuxtjs/sitemap": "8.2.1"
100
100
  },
101
101
  "scripts": {
102
102
  "lint": "eslint .",
103
103
  "lint:fix": "eslint . --fix",
104
- "client:build": "nuxt generate devtools",
105
- "devtools": "nuxt dev devtools --port 3030",
104
+ "client:build": "node -e \"const { cpSync } = require('fs'); const { sep } = require('path'); cpSync('devtools','dist/devtools',{recursive:true,filter:src=>!src.split(sep).some(p=>['.data','.nuxt','node_modules'].includes(p))})\"",
106
105
  "build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build && npm run client:build",
107
106
  "dev": "nuxt dev playground",
108
107
  "prepare:fixtures": "nuxt prepare test/fixtures/basic && nuxt prepare test/fixtures/i18n && nuxt prepare test/fixtures/i18n-micro",
109
108
  "dev:build": "nuxt build playground",
110
- "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
109
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground && pnpm run client:build",
111
110
  "release": "pnpm build && bumpp -x \"npx changelogen --output=CHANGELOG.md\"",
112
111
  "test": "vitest run && pnpm run test:attw",
113
112
  "test:unit": "vitest --project=unit",