@nuxtjs/sitemap 8.2.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.
- package/dist/devtools/components/sitemap/Source.vue +148 -0
- package/dist/devtools/lib/sitemap/rpc.ts +5 -0
- package/dist/devtools/lib/sitemap/state.ts +71 -0
- package/dist/devtools/lib/sitemap/types.ts +2 -0
- package/dist/devtools/nuxt.config.ts +7 -0
- package/dist/devtools/pages/sitemap/app-sources.vue +33 -0
- package/dist/devtools/pages/sitemap/debug.vue +21 -0
- package/dist/devtools/pages/sitemap/docs.vue +3 -0
- package/dist/devtools/pages/sitemap/index.vue +456 -0
- package/dist/devtools/pages/sitemap/user-sources.vue +33 -0
- package/dist/devtools/pages/sitemap.vue +72 -0
- package/dist/module.json +1 -1
- package/package.json +7 -7
|
@@ -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,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,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,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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtjs/sitemap",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "8.2.
|
|
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",
|
|
@@ -59,8 +59,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
|
|
63
|
-
"nuxtseo-shared": "^5.
|
|
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.
|
|
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",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"happy-dom": "^20.10.2",
|
|
88
88
|
"nuxt": "^4.4.8",
|
|
89
89
|
"nuxt-i18n-micro": "^3.18.2",
|
|
90
|
-
"nuxtseo-layer-devtools": "^5.
|
|
90
|
+
"nuxtseo-layer-devtools": "^5.3.0",
|
|
91
91
|
"semver": "^7.8.4",
|
|
92
92
|
"sirv": "^3.0.2",
|
|
93
93
|
"std-env": "^4.1.0",
|
|
@@ -96,12 +96,12 @@
|
|
|
96
96
|
"vitest": "^4.1.8",
|
|
97
97
|
"vue-tsc": "^3.3.4",
|
|
98
98
|
"zod": "^4.4.3",
|
|
99
|
-
"@nuxtjs/sitemap": "8.2.
|
|
99
|
+
"@nuxtjs/sitemap": "8.2.1"
|
|
100
100
|
},
|
|
101
101
|
"scripts": {
|
|
102
102
|
"lint": "eslint .",
|
|
103
103
|
"lint:fix": "eslint . --fix",
|
|
104
|
-
"client:build": "node -e \"require('fs')
|
|
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))})\"",
|
|
105
105
|
"build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build && npm run client:build",
|
|
106
106
|
"dev": "nuxt dev playground",
|
|
107
107
|
"prepare:fixtures": "nuxt prepare test/fixtures/basic && nuxt prepare test/fixtures/i18n && nuxt prepare test/fixtures/i18n-micro",
|