nuxtseo-layer-devtools 0.4.5 → 0.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/components/DevtoolsChecklistBadge.vue +65 -0
- package/components/DevtoolsChecklistItem.vue +171 -0
- package/components/DevtoolsLayout.vue +91 -7
- package/components/DevtoolsModuleSplash.vue +270 -51
- package/components/DevtoolsPlaygrounds.vue +99 -0
- package/components/DevtoolsSetupChecklist.vue +60 -0
- package/components/DevtoolsTroubleshooting.vue +332 -0
- package/composables/checklist.ts +486 -0
- package/composables/modules.ts +48 -13
- package/composables/package-manager.ts +41 -0
- package/composables/update-check.ts +39 -0
- package/error.vue +1 -0
- package/package.json +3 -2
- package/skills/devtools-layer-skilld/SKILL.md +17 -8
- package/skills/devtools-layer-skilld/reference.md +70 -7
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import type { ChecklistItemDefinition, NuxtSEOModule } from 'nuxtseo-shared/const'
|
|
2
|
+
import { computed, ref, toValue } from 'vue'
|
|
3
|
+
import { installedModules } from './modules'
|
|
4
|
+
import { appFetch } from './rpc'
|
|
5
|
+
|
|
6
|
+
export interface ChecklistDetectResult {
|
|
7
|
+
passed: boolean
|
|
8
|
+
detail?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ChecklistItemWithDetect extends ChecklistItemDefinition {
|
|
12
|
+
detect: (data: Record<string, any>, ctx: DetectContext) => ChecklistDetectResult
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DetectContext {
|
|
16
|
+
installedModuleSlugs: Set<string>
|
|
17
|
+
debugData: Map<string, Record<string, any>>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ChecklistItemResult extends ChecklistItemDefinition {
|
|
21
|
+
passed: boolean
|
|
22
|
+
detail?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ModuleChecklistResult {
|
|
26
|
+
moduleSlug: NuxtSEOModule['slug']
|
|
27
|
+
moduleLabel: string
|
|
28
|
+
moduleIcon: string
|
|
29
|
+
items: ChecklistItemResult[]
|
|
30
|
+
requiredPending: number
|
|
31
|
+
recommendedPending: number
|
|
32
|
+
totalPending: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ChecklistSummary {
|
|
36
|
+
total: number
|
|
37
|
+
passed: number
|
|
38
|
+
requiredPending: number
|
|
39
|
+
recommendedPending: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Debug endpoint paths for each module
|
|
43
|
+
const DEBUG_ENDPOINTS: Partial<Record<NuxtSEOModule['slug'], string>> = {
|
|
44
|
+
'site-config': '/__site-config__/debug',
|
|
45
|
+
'robots': '/__robots__/debug.json',
|
|
46
|
+
'sitemap': '/__sitemap__/debug.json',
|
|
47
|
+
'og-image': '/__nuxt-og-image/debug.json',
|
|
48
|
+
'schema-org': '/__schema-org__/debug.json',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Module slug used internally by devtools → catalog slug mapping
|
|
52
|
+
const DEVTOOLS_NAME_TO_SLUG: Record<string, NuxtSEOModule['slug']> = {
|
|
53
|
+
'nuxt-robots': 'robots',
|
|
54
|
+
'sitemap': 'sitemap',
|
|
55
|
+
'nuxt-og-image': 'og-image',
|
|
56
|
+
'nuxt-schema-org': 'schema-org',
|
|
57
|
+
'nuxt-seo-utils': 'seo-utils',
|
|
58
|
+
'nuxt-link-checker': 'link-checker',
|
|
59
|
+
'nuxt-site-config': 'site-config',
|
|
60
|
+
'nuxt-ai-ready': 'ai-ready',
|
|
61
|
+
'nuxt-skew-protection': 'skew-protection',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const MODULE_META: Record<string, { label: string, icon: string }> = {
|
|
65
|
+
'site-config': { label: 'Site Config', icon: 'carbon:settings-check' },
|
|
66
|
+
'robots': { label: 'Robots', icon: 'carbon:bot' },
|
|
67
|
+
'sitemap': { label: 'Sitemap', icon: 'carbon:load-balancer-application' },
|
|
68
|
+
'og-image': { label: 'OG Image', icon: 'carbon:image-search' },
|
|
69
|
+
'seo-utils': { label: 'SEO Utils', icon: 'carbon:tools' },
|
|
70
|
+
'schema-org': { label: 'Schema.org', icon: 'carbon:chart-relationship' },
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isValidUrl(url?: string): boolean {
|
|
74
|
+
if (!url)
|
|
75
|
+
return false
|
|
76
|
+
return !url.includes('localhost') && !url.includes('127.0.0.1') && url.length > 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Checklist definitions with detection logic per module
|
|
80
|
+
const CHECKLIST_DEFINITIONS: Partial<Record<NuxtSEOModule['slug'], ChecklistItemWithDetect[]>> = {
|
|
81
|
+
'site-config': [
|
|
82
|
+
{
|
|
83
|
+
id: 'site-url',
|
|
84
|
+
label: 'Site URL configured',
|
|
85
|
+
description: 'A production site URL is needed for canonical URLs, sitemaps, and OG images to work correctly.',
|
|
86
|
+
level: 'required',
|
|
87
|
+
docsUrl: 'https://nuxtseo.com/docs/site-config/getting-started/how-it-works',
|
|
88
|
+
detect: (data) => {
|
|
89
|
+
const url = data?.config?.url || ''
|
|
90
|
+
const passed = isValidUrl(url)
|
|
91
|
+
return { passed, detail: passed ? url : 'Not set or using localhost' }
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'site-name',
|
|
96
|
+
label: 'Site name set',
|
|
97
|
+
description: 'Used for default meta tags, Schema.org, and OG tags across all modules.',
|
|
98
|
+
level: 'required',
|
|
99
|
+
docsUrl: 'https://nuxtseo.com/docs/site-config/getting-started/how-it-works',
|
|
100
|
+
detect: (data) => {
|
|
101
|
+
const name = data?.config?.name || ''
|
|
102
|
+
const passed = !!name && name !== 'My Site'
|
|
103
|
+
return { passed, detail: passed ? name : 'Not configured' }
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'default-locale',
|
|
108
|
+
label: 'Default locale configured',
|
|
109
|
+
description: 'Ensures correct hreflang tags and locale-aware sitemaps when using i18n.',
|
|
110
|
+
level: 'recommended',
|
|
111
|
+
docsUrl: 'https://nuxtseo.com/docs/site-config/guides/setting-site-config',
|
|
112
|
+
detect: (data) => {
|
|
113
|
+
const locale = data?.config?.defaultLocale
|
|
114
|
+
const passed = !!locale
|
|
115
|
+
return { passed, detail: passed ? locale : 'Not set' }
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'trailing-slash',
|
|
120
|
+
label: 'Trailing slash preference set',
|
|
121
|
+
description: 'Prevents duplicate content from inconsistent URL formats. Set explicitly to true or false.',
|
|
122
|
+
level: 'recommended',
|
|
123
|
+
docsUrl: 'https://nuxtseo.com/docs/site-config/guides/setting-site-config',
|
|
124
|
+
detect: (data) => {
|
|
125
|
+
const trailingSlash = data?.config?.trailingSlash
|
|
126
|
+
const passed = typeof trailingSlash === 'boolean'
|
|
127
|
+
return { passed, detail: passed ? (trailingSlash ? 'Enabled' : 'Disabled') : 'Not explicitly set' }
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'robots-installed',
|
|
132
|
+
label: 'Robots module installed',
|
|
133
|
+
description: 'Controls crawling and indexing across all SEO modules. Strongly recommended.',
|
|
134
|
+
level: 'recommended',
|
|
135
|
+
docsUrl: 'https://nuxtseo.com/docs/robots/getting-started/installation',
|
|
136
|
+
detect: (_data, ctx) => {
|
|
137
|
+
const passed = ctx.installedModuleSlugs.has('robots')
|
|
138
|
+
return { passed, detail: passed ? 'Installed' : 'Not installed' }
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
'robots': [
|
|
143
|
+
{
|
|
144
|
+
id: 'no-validation-errors',
|
|
145
|
+
label: 'No robots.txt validation errors',
|
|
146
|
+
description: 'Your robots.txt should be free of syntax errors that could confuse crawlers.',
|
|
147
|
+
level: 'required',
|
|
148
|
+
docsUrl: 'https://nuxtseo.com/docs/robots/getting-started/installation',
|
|
149
|
+
detect: (data) => {
|
|
150
|
+
const errors = data?.validation?.errors || []
|
|
151
|
+
const passed = errors.length === 0
|
|
152
|
+
return { passed, detail: passed ? 'No errors' : `${errors.length} error(s) found` }
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'ai-directives',
|
|
157
|
+
label: 'AI bot directives configured',
|
|
158
|
+
description: 'Configure how AI crawlers interact with your content using blockAiBots or content signal directives.',
|
|
159
|
+
level: 'recommended',
|
|
160
|
+
docsUrl: 'https://nuxtseo.com/docs/robots/guides/ai-bots',
|
|
161
|
+
detect: (data) => {
|
|
162
|
+
const groups = data?.runtimeConfig?.groups || []
|
|
163
|
+
const hasContentSignal = groups.some((g: any) => g.contentSignal?.length || g.contentUsage?.length)
|
|
164
|
+
const aiAgents = ['gptbot', 'chatgpt-user', 'anthropic-ai', 'claudebot', 'claude-web', 'google-extended', 'ccbot']
|
|
165
|
+
const hasAiAgent = groups.some((g: any) =>
|
|
166
|
+
(g.userAgent || []).some((ua: string) => aiAgents.includes(ua.toLowerCase())),
|
|
167
|
+
)
|
|
168
|
+
const passed = hasContentSignal || hasAiAgent
|
|
169
|
+
return { passed, detail: passed ? (hasContentSignal ? 'Content signals configured' : 'AI agent rules configured') : 'No AI bot directives found' }
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'bot-detection',
|
|
174
|
+
label: 'Bot detection enabled',
|
|
175
|
+
description: 'Classify bots via headers and fingerprinting to reduce server load from non-SEO crawlers.',
|
|
176
|
+
level: 'recommended',
|
|
177
|
+
docsUrl: 'https://nuxtseo.com/docs/robots/guides/bot-detection',
|
|
178
|
+
detect: (data) => {
|
|
179
|
+
const enabled = data?.runtimeConfig?.botDetection
|
|
180
|
+
return { passed: !!enabled, detail: enabled ? 'Enabled' : 'Disabled' }
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'sitemap-reference',
|
|
185
|
+
label: 'Sitemap referenced in robots.txt',
|
|
186
|
+
description: 'Crawlers use the Sitemap directive in robots.txt to discover your sitemap URL.',
|
|
187
|
+
level: 'recommended',
|
|
188
|
+
docsUrl: 'https://nuxtseo.com/docs/robots/guides/robots-txt',
|
|
189
|
+
detect: (data) => {
|
|
190
|
+
const sitemaps = data?.validation?.sitemaps || []
|
|
191
|
+
const passed = sitemaps.length > 0
|
|
192
|
+
return { passed, detail: passed ? `${sitemaps.length} sitemap(s) referenced` : 'No sitemap directive found' }
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
'sitemap': [
|
|
197
|
+
{
|
|
198
|
+
id: 'site-url-set',
|
|
199
|
+
label: 'Site URL set for sitemaps',
|
|
200
|
+
description: 'Sitemaps require an absolute site URL. Without it, URLs will use localhost in production.',
|
|
201
|
+
level: 'required',
|
|
202
|
+
docsUrl: 'https://nuxtseo.com/docs/sitemap/getting-started/installation',
|
|
203
|
+
detect: (data) => {
|
|
204
|
+
const url = data?.siteConfig?.url || ''
|
|
205
|
+
const passed = isValidUrl(url)
|
|
206
|
+
return { passed, detail: passed ? url : 'Site URL not configured' }
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'has-sources',
|
|
211
|
+
label: 'URL sources configured',
|
|
212
|
+
description: 'Add dynamic URL sources for CMS or database content so all pages appear in your sitemap.',
|
|
213
|
+
level: 'recommended',
|
|
214
|
+
docsUrl: 'https://nuxtseo.com/docs/sitemap/guides/dynamic-urls',
|
|
215
|
+
detect: (data) => {
|
|
216
|
+
const sources = data?.globalSources || []
|
|
217
|
+
const sitemaps = data?.sitemaps || {}
|
|
218
|
+
const userSources = sources.filter((s: any) => s.sourceType === 'user' || (s.context?.name && !s.context.name.startsWith('nuxt:')))
|
|
219
|
+
const sitemapUserSources = Object.values(sitemaps).flatMap((s: any) =>
|
|
220
|
+
(s.sources || []).filter((src: any) => src.sourceType === 'user'),
|
|
221
|
+
)
|
|
222
|
+
const totalUser = userSources.length + sitemapUserSources.length
|
|
223
|
+
const passed = totalUser > 0
|
|
224
|
+
return { passed, detail: passed ? `${totalUser} custom source(s)` : 'Only default app sources detected' }
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: 'no-source-errors',
|
|
229
|
+
label: 'No sitemap source errors',
|
|
230
|
+
description: 'All configured sitemap sources should resolve successfully.',
|
|
231
|
+
level: 'required',
|
|
232
|
+
docsUrl: 'https://nuxtseo.com/docs/sitemap/guides/dynamic-urls',
|
|
233
|
+
detect: (data) => {
|
|
234
|
+
const sources = data?.globalSources || []
|
|
235
|
+
const sitemaps = data?.sitemaps || {}
|
|
236
|
+
const allSources = [
|
|
237
|
+
...sources,
|
|
238
|
+
...Object.values(sitemaps).flatMap((s: any) => s.sources || []),
|
|
239
|
+
]
|
|
240
|
+
const failures = allSources.filter((s: any) => s._isFailure || s.error)
|
|
241
|
+
const passed = failures.length === 0
|
|
242
|
+
return { passed, detail: passed ? 'All sources OK' : `${failures.length} source(s) failing` }
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: 'url-warnings',
|
|
247
|
+
label: 'No URL validation warnings',
|
|
248
|
+
description: 'URLs in your sitemap should follow best practices (no whitespace, lowercase, etc).',
|
|
249
|
+
level: 'recommended',
|
|
250
|
+
docsUrl: 'https://nuxtseo.com/docs/sitemap/guides/best-practices',
|
|
251
|
+
detect: (data) => {
|
|
252
|
+
const sources = data?.globalSources || []
|
|
253
|
+
const sitemaps = data?.sitemaps || {}
|
|
254
|
+
const allSources = [
|
|
255
|
+
...sources,
|
|
256
|
+
...Object.values(sitemaps).flatMap((s: any) => s.sources || []),
|
|
257
|
+
]
|
|
258
|
+
const warningCount = allSources.reduce((sum: number, s: any) => sum + (s._urlWarnings?.length || 0), 0)
|
|
259
|
+
const passed = warningCount === 0
|
|
260
|
+
return { passed, detail: passed ? 'No warnings' : `${warningCount} URL warning(s)` }
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
'og-image': [
|
|
265
|
+
{
|
|
266
|
+
id: 'renderer',
|
|
267
|
+
label: 'Renderer installed',
|
|
268
|
+
description: 'A renderer (Takumi, Satori, or Browser) is required to generate OG images.',
|
|
269
|
+
level: 'required',
|
|
270
|
+
docsUrl: 'https://nuxtseo.com/docs/og-image/getting-started/installation',
|
|
271
|
+
detect: (data) => {
|
|
272
|
+
const compat = data?.compatibility || {}
|
|
273
|
+
const hasTakumi = compat.takumi && compat.takumi !== false
|
|
274
|
+
const hasSatori = compat.satori && compat.satori !== false
|
|
275
|
+
const hasBrowser = compat.browser && compat.browser !== false
|
|
276
|
+
const passed = hasTakumi || hasSatori || hasBrowser
|
|
277
|
+
const renderers = [hasTakumi && 'takumi', hasSatori && 'satori', hasBrowser && 'browser'].filter(Boolean)
|
|
278
|
+
return { passed, detail: passed ? `Available: ${renderers.join(', ')}` : 'No renderer installed' }
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: 'custom-template',
|
|
283
|
+
label: 'Custom OG template created',
|
|
284
|
+
description: 'Community templates are for development only. Create a custom template for production.',
|
|
285
|
+
level: 'recommended',
|
|
286
|
+
docsUrl: 'https://nuxtseo.com/docs/og-image/guides/templates',
|
|
287
|
+
detect: (data) => {
|
|
288
|
+
const components = data?.componentNames || []
|
|
289
|
+
const appTemplates = components.filter((c: any) => c.category === 'app')
|
|
290
|
+
const communityTemplates = components.filter((c: any) => c.category === 'community')
|
|
291
|
+
const passed = appTemplates.length > 0
|
|
292
|
+
if (passed)
|
|
293
|
+
return { passed, detail: `${appTemplates.length} custom template(s)` }
|
|
294
|
+
if (communityTemplates.length > 0)
|
|
295
|
+
return { passed: false, detail: `${communityTemplates.length} community template(s), eject before production` }
|
|
296
|
+
return { passed: false, detail: 'No templates found' }
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
'seo-utils': [
|
|
301
|
+
{
|
|
302
|
+
id: 'schema-org-installed',
|
|
303
|
+
label: 'Schema.org module installed',
|
|
304
|
+
description: 'Adds structured data to your pages, improving rich search results. Pairs well with SEO Utils.',
|
|
305
|
+
level: 'recommended',
|
|
306
|
+
docsUrl: 'https://nuxtseo.com/docs/schema-org/getting-started/installation',
|
|
307
|
+
detect: (_data, ctx) => {
|
|
308
|
+
const passed = ctx.installedModuleSlugs.has('schema-org')
|
|
309
|
+
return { passed, detail: passed ? 'Installed' : 'Not installed' }
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: 'sitemap-installed',
|
|
314
|
+
label: 'Sitemap module installed',
|
|
315
|
+
description: 'Generates XML sitemaps so search engines can discover all your pages efficiently.',
|
|
316
|
+
level: 'recommended',
|
|
317
|
+
docsUrl: 'https://nuxtseo.com/docs/sitemap/getting-started/installation',
|
|
318
|
+
detect: (_data, ctx) => {
|
|
319
|
+
const passed = ctx.installedModuleSlugs.has('sitemap')
|
|
320
|
+
return { passed, detail: passed ? 'Installed' : 'Not installed' }
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
'schema-org': [
|
|
325
|
+
{
|
|
326
|
+
id: 'identity',
|
|
327
|
+
label: 'Identity configured',
|
|
328
|
+
description: 'Set up your Organization or Person identity for rich Schema.org knowledge graph results.',
|
|
329
|
+
level: 'recommended',
|
|
330
|
+
docsUrl: 'https://nuxtseo.com/docs/schema-org/guides/setup-identity',
|
|
331
|
+
detect: (data) => {
|
|
332
|
+
const config = data?.runtimeConfig || {}
|
|
333
|
+
const identity = config.identity
|
|
334
|
+
if (!identity)
|
|
335
|
+
return { passed: false, detail: 'No identity set' }
|
|
336
|
+
const type = typeof identity === 'string' ? identity : identity['@type'] || 'Unknown'
|
|
337
|
+
const name = typeof identity === 'object' ? (identity.name || '') : ''
|
|
338
|
+
return { passed: true, detail: name ? `${type}: ${name}` : type }
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: 'robots-companion',
|
|
343
|
+
label: 'Robots module installed',
|
|
344
|
+
description: 'Robots module auto-excludes non-indexable paths from Schema.org output.',
|
|
345
|
+
level: 'recommended',
|
|
346
|
+
docsUrl: 'https://nuxtseo.com/docs/schema-org/getting-started/installation',
|
|
347
|
+
detect: (_data, ctx) => {
|
|
348
|
+
const passed = ctx.installedModuleSlugs.has('robots')
|
|
349
|
+
return { passed, detail: passed ? 'Installed' : 'Not installed' }
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const debugCache = ref<Map<string, Record<string, any>>>(new Map())
|
|
356
|
+
const loading = ref(false)
|
|
357
|
+
const evaluated = ref(false)
|
|
358
|
+
|
|
359
|
+
function getInstalledSlugs(): Set<string> {
|
|
360
|
+
const slugs = new Set<string>()
|
|
361
|
+
for (const mod of toValue(installedModules)) {
|
|
362
|
+
const slug = DEVTOOLS_NAME_TO_SLUG[mod.name]
|
|
363
|
+
if (slug)
|
|
364
|
+
slugs.add(slug)
|
|
365
|
+
}
|
|
366
|
+
return slugs
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function fetchDebugData(): Promise<void> {
|
|
370
|
+
const fetch = toValue(appFetch)
|
|
371
|
+
if (!fetch)
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
const slugs = getInstalledSlugs()
|
|
375
|
+
const cache = new Map<string, Record<string, any>>()
|
|
376
|
+
|
|
377
|
+
const fetches = Object.entries(DEBUG_ENDPOINTS)
|
|
378
|
+
.filter(([slug]) => slugs.has(slug))
|
|
379
|
+
.map(async ([slug, endpoint]) => {
|
|
380
|
+
const data = await fetch(endpoint!).catch(() => null)
|
|
381
|
+
if (data)
|
|
382
|
+
cache.set(slug, data)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
await Promise.all(fetches)
|
|
386
|
+
debugCache.value = cache
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function evaluateModule(slug: NuxtSEOModule['slug'], ctx: DetectContext): ModuleChecklistResult | undefined {
|
|
390
|
+
const definitions = CHECKLIST_DEFINITIONS[slug]
|
|
391
|
+
if (!definitions?.length)
|
|
392
|
+
return undefined
|
|
393
|
+
|
|
394
|
+
const meta = MODULE_META[slug]
|
|
395
|
+
if (!meta)
|
|
396
|
+
return undefined
|
|
397
|
+
|
|
398
|
+
const data = debugCache.value.get(slug) || {}
|
|
399
|
+
const items: ChecklistItemResult[] = definitions.map((def) => {
|
|
400
|
+
const result = def.detect(data, ctx)
|
|
401
|
+
return {
|
|
402
|
+
id: def.id,
|
|
403
|
+
label: def.label,
|
|
404
|
+
description: def.description,
|
|
405
|
+
level: def.level,
|
|
406
|
+
docsUrl: def.docsUrl,
|
|
407
|
+
passed: result.passed,
|
|
408
|
+
detail: result.detail,
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
const requiredPending = items.filter(i => i.level === 'required' && !i.passed).length
|
|
413
|
+
const recommendedPending = items.filter(i => i.level === 'recommended' && !i.passed).length
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
moduleSlug: slug,
|
|
417
|
+
moduleLabel: meta.label,
|
|
418
|
+
moduleIcon: meta.icon,
|
|
419
|
+
items,
|
|
420
|
+
requiredPending,
|
|
421
|
+
recommendedPending,
|
|
422
|
+
totalPending: requiredPending + recommendedPending,
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const results = computed<ModuleChecklistResult[]>(() => {
|
|
427
|
+
const slugs = getInstalledSlugs()
|
|
428
|
+
const ctx: DetectContext = { installedModuleSlugs: slugs, debugData: debugCache.value }
|
|
429
|
+
const moduleResults: ModuleChecklistResult[] = []
|
|
430
|
+
|
|
431
|
+
// Evaluate in a consistent order
|
|
432
|
+
const orderedSlugs: NuxtSEOModule['slug'][] = ['site-config', 'robots', 'sitemap', 'og-image', 'schema-org', 'seo-utils']
|
|
433
|
+
for (const slug of orderedSlugs) {
|
|
434
|
+
if (!slugs.has(slug) && slug !== 'site-config')
|
|
435
|
+
continue
|
|
436
|
+
// Site config is always evaluated (it's the foundation)
|
|
437
|
+
const result = evaluateModule(slug, ctx)
|
|
438
|
+
if (result)
|
|
439
|
+
moduleResults.push(result)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return moduleResults
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
const summary = computed<ChecklistSummary>(() => {
|
|
446
|
+
let total = 0; let passed = 0; let requiredPending = 0; let recommendedPending = 0
|
|
447
|
+
for (const r of results.value) {
|
|
448
|
+
total += r.items.length
|
|
449
|
+
passed += r.items.filter(i => i.passed).length
|
|
450
|
+
requiredPending += r.requiredPending
|
|
451
|
+
recommendedPending += r.recommendedPending
|
|
452
|
+
}
|
|
453
|
+
return { total, passed, requiredPending, recommendedPending }
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
export function getModuleResult(slug: string): ModuleChecklistResult | undefined {
|
|
457
|
+
return results.value.find(r => r.moduleSlug === slug)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function getModuleResultByName(devtoolsName: string): ModuleChecklistResult | undefined {
|
|
461
|
+
const slug = DEVTOOLS_NAME_TO_SLUG[devtoolsName]
|
|
462
|
+
if (!slug)
|
|
463
|
+
return undefined
|
|
464
|
+
return getModuleResult(slug)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export async function evaluate(): Promise<void> {
|
|
468
|
+
if (loading.value)
|
|
469
|
+
return
|
|
470
|
+
loading.value = true
|
|
471
|
+
await fetchDebugData().catch(() => {})
|
|
472
|
+
evaluated.value = true
|
|
473
|
+
loading.value = false
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function useSetupChecklist() {
|
|
477
|
+
return {
|
|
478
|
+
results,
|
|
479
|
+
summary,
|
|
480
|
+
loading,
|
|
481
|
+
evaluated,
|
|
482
|
+
evaluate,
|
|
483
|
+
getModuleResult,
|
|
484
|
+
getModuleResultByName,
|
|
485
|
+
}
|
|
486
|
+
}
|
package/composables/modules.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { NuxtDevtoolsIframeClient } from '@nuxt/devtools-kit/types'
|
|
2
|
+
import type { NuxtSEOModule } from 'nuxtseo-shared/const'
|
|
2
3
|
import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
|
|
4
|
+
import { modules as seoModules } from 'nuxtseo-shared/const'
|
|
3
5
|
import { computed, ref } from 'vue'
|
|
4
6
|
import { isConnected } from './state'
|
|
5
7
|
|
|
@@ -17,22 +19,51 @@ export interface SeoModuleCatalogEntry {
|
|
|
17
19
|
icon: string
|
|
18
20
|
installed: boolean
|
|
19
21
|
route?: string
|
|
20
|
-
|
|
22
|
+
npm: string
|
|
23
|
+
repo: string
|
|
21
24
|
pro?: boolean
|
|
25
|
+
playgrounds?: Record<string, string>
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
// Map slug to the internal module name used by devtools routing
|
|
29
|
+
const SLUG_TO_MODULE_NAME: Record<string, string> = {
|
|
30
|
+
'robots': 'nuxt-robots',
|
|
31
|
+
'sitemap': 'sitemap',
|
|
32
|
+
'og-image': 'nuxt-og-image',
|
|
33
|
+
'schema-org': 'nuxt-schema-org',
|
|
34
|
+
'seo-utils': 'nuxt-seo-utils',
|
|
35
|
+
'link-checker': 'nuxt-link-checker',
|
|
36
|
+
'site-config': 'nuxt-site-config',
|
|
37
|
+
'ai-ready': 'nuxt-ai-ready',
|
|
38
|
+
'skew-protection': 'nuxt-skew-protection',
|
|
39
|
+
'ai-kit': 'nuxt-ai-kit',
|
|
40
|
+
'nuxt-seo': 'nuxt-seo',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ICONIFY_RE = /^i-([^-]+)-/
|
|
44
|
+
|
|
45
|
+
function toIconify(icon: string): string {
|
|
46
|
+
// Convert i-carbon-bot -> carbon:bot
|
|
47
|
+
return icon.replace(ICONIFY_RE, '$1:')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function moduleToCatalogEntry(mod: NuxtSEOModule): Omit<SeoModuleCatalogEntry, 'installed' | 'route'> {
|
|
51
|
+
return {
|
|
52
|
+
name: SLUG_TO_MODULE_NAME[mod.slug] || mod.slug,
|
|
53
|
+
title: mod.label,
|
|
54
|
+
description: mod.description,
|
|
55
|
+
icon: toIconify(mod.icon),
|
|
56
|
+
npm: mod.npm,
|
|
57
|
+
repo: mod.repo,
|
|
58
|
+
pro: mod.pro,
|
|
59
|
+
playgrounds: mod.playgrounds,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Exclude the meta 'nuxt-seo' entry from the catalog, it's not a standalone devtools module
|
|
64
|
+
const MODULE_CATALOG = seoModules
|
|
65
|
+
.filter(m => m.slug !== 'nuxt-seo')
|
|
66
|
+
.map(moduleToCatalogEntry)
|
|
36
67
|
|
|
37
68
|
export const installedModules = ref<SeoModuleInfo[]>([])
|
|
38
69
|
export const showModuleSplash = ref(false)
|
|
@@ -48,6 +79,10 @@ export const moduleCatalog = computed<SeoModuleCatalogEntry[]>(() => {
|
|
|
48
79
|
})
|
|
49
80
|
})
|
|
50
81
|
|
|
82
|
+
export function findModuleByName(moduleName: string): SeoModuleCatalogEntry | undefined {
|
|
83
|
+
return moduleCatalog.value.find(m => m.name === moduleName)
|
|
84
|
+
}
|
|
85
|
+
|
|
51
86
|
export function fetchInstalledModules(): void {
|
|
52
87
|
const inIframe = window.parent !== window
|
|
53
88
|
if (!inIframe)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm'
|
|
4
|
+
|
|
5
|
+
export const packageManager = ref<PackageManager>('npm')
|
|
6
|
+
|
|
7
|
+
const PM_COMMANDS: Record<PackageManager, { run: string, exec: string, update: string, dedupe: string }> = {
|
|
8
|
+
pnpm: { run: 'pnpm', exec: 'pnpm dlx', update: 'pnpm update', dedupe: 'pnpm dedupe' },
|
|
9
|
+
yarn: { run: 'yarn', exec: 'yarn dlx', update: 'yarn upgrade', dedupe: 'yarn dedupe' },
|
|
10
|
+
bun: { run: 'bun', exec: 'bunx', update: 'bun update', dedupe: '' },
|
|
11
|
+
npm: { run: 'npm', exec: 'npx', update: 'npm update', dedupe: 'npm dedupe' },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function pmCommands() {
|
|
15
|
+
return PM_COMMANDS[packageManager.value]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Detect by checking lock files via Vite's @fs route (dev only)
|
|
19
|
+
const LOCK_FILES: [string, PackageManager][] = [
|
|
20
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
21
|
+
['yarn.lock', 'yarn'],
|
|
22
|
+
['bun.lockb', 'bun'],
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
let detected = false
|
|
26
|
+
export function detectPackageManager() {
|
|
27
|
+
if (detected)
|
|
28
|
+
return
|
|
29
|
+
detected = true
|
|
30
|
+
|
|
31
|
+
// Check each lock file with a HEAD request
|
|
32
|
+
for (const [file, pm] of LOCK_FILES) {
|
|
33
|
+
fetch(`/${file}`, { method: 'HEAD' })
|
|
34
|
+
.then((r) => {
|
|
35
|
+
if (r.ok) {
|
|
36
|
+
packageManager.value = pm
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.catch(() => {})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ComputedRef } from 'vue'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface ModuleUpdateInfo {
|
|
5
|
+
currentVersion: string
|
|
6
|
+
latestVersion: string
|
|
7
|
+
hasUpdate: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const updateCache = ref<Record<string, ModuleUpdateInfo>>({})
|
|
11
|
+
|
|
12
|
+
export function useModuleUpdate(npmPackage: string | undefined, currentVersion: string | undefined): { hasUpdate: ComputedRef<boolean>, latestVersion: ComputedRef<string | undefined>, info: ComputedRef<ModuleUpdateInfo | undefined> } {
|
|
13
|
+
const info = computed(() => {
|
|
14
|
+
if (!npmPackage || !currentVersion)
|
|
15
|
+
return undefined
|
|
16
|
+
return updateCache.value[npmPackage]
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const hasUpdate = computed(() => info.value?.hasUpdate ?? false)
|
|
20
|
+
const latestVersion = computed(() => info.value?.latestVersion)
|
|
21
|
+
|
|
22
|
+
if (npmPackage && currentVersion && !updateCache.value[npmPackage]) {
|
|
23
|
+
fetch(`https://registry.npmjs.org/${npmPackage}/latest`)
|
|
24
|
+
.then(r => r.json())
|
|
25
|
+
.then((data) => {
|
|
26
|
+
const latest = data.version as string
|
|
27
|
+
if (!latest)
|
|
28
|
+
return
|
|
29
|
+
updateCache.value[npmPackage] = {
|
|
30
|
+
currentVersion,
|
|
31
|
+
latestVersion: latest,
|
|
32
|
+
hasUpdate: latest !== currentVersion,
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
.catch(() => {})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { hasUpdate, latestVersion, info }
|
|
39
|
+
}
|
package/error.vue
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxtseo-layer-devtools",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"description": "Shared Nuxt layer for Nuxt SEO devtools clients.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"@vueuse/nuxt": "^14.2.1",
|
|
37
37
|
"ofetch": "^1.5.1",
|
|
38
38
|
"shiki": "^4.0.2",
|
|
39
|
-
"ufo": "^1.6.3"
|
|
39
|
+
"ufo": "^1.6.3",
|
|
40
|
+
"nuxtseo-shared": "0.9.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"nuxt": "^4.4.2",
|