nuxt-ai-ready 1.3.9 → 1.5.0

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.
Files changed (39) hide show
  1. package/README.md +2 -0
  2. package/dist/devtools/lib/ai-ready/rpc.ts +6 -0
  3. package/dist/devtools/lib/ai-ready/state.ts +21 -0
  4. package/dist/devtools/lib/ai-ready/types.ts +50 -0
  5. package/dist/devtools/nuxt.config.ts +3 -0
  6. package/dist/devtools/pages/ai-ready/debug.vue +27 -0
  7. package/dist/devtools/pages/ai-ready/docs.vue +3 -0
  8. package/dist/devtools/pages/ai-ready/index.vue +177 -0
  9. package/dist/devtools/pages/ai-ready/llms-txt.vue +172 -0
  10. package/dist/devtools/pages/ai-ready.vue +50 -0
  11. package/dist/module.json +1 -1
  12. package/dist/module.mjs +6 -4
  13. package/dist/runtime/server/db/drizzle/providers/libsql.js +6 -1
  14. package/dist/runtime/server/middleware/markdown.js +12 -11
  15. package/dist/runtime/server/utils/link-header.d.ts +3 -1
  16. package/dist/runtime/server/utils/link-header.js +11 -2
  17. package/dist/runtime/types.d.ts +6 -0
  18. package/package.json +27 -27
  19. package/dist/devtools/200.html +0 -1
  20. package/dist/devtools/404.html +0 -1
  21. package/dist/devtools/_nuxt/B75sBxVw.js +0 -1
  22. package/dist/devtools/_nuxt/C5qFqPjv.js +0 -2
  23. package/dist/devtools/_nuxt/CUvdyac7.js +0 -1
  24. package/dist/devtools/_nuxt/D9Lze2B3.js +0 -1
  25. package/dist/devtools/_nuxt/D9pLQdnz.js +0 -38
  26. package/dist/devtools/_nuxt/DevtoolsPanel.BTjFkHvn.css +0 -1
  27. package/dist/devtools/_nuxt/DiEjVMsr.js +0 -1
  28. package/dist/devtools/_nuxt/Es9IinpI.js +0 -1
  29. package/dist/devtools/_nuxt/builds/latest.json +0 -1
  30. package/dist/devtools/_nuxt/builds/meta/f26345f8-0602-46ab-916a-0529f3a14be9.json +0 -1
  31. package/dist/devtools/_nuxt/entry.C01PFCnM.css +0 -1
  32. package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
  33. package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
  34. package/dist/devtools/_nuxt/index.Cz-FJ6Du.css +0 -1
  35. package/dist/devtools/debug/index.html +0 -1
  36. package/dist/devtools/docs/index.html +0 -1
  37. package/dist/devtools/index.html +0 -1
  38. package/dist/devtools/llms-txt/index.html +0 -1
  39. package/dist/devtools/pages/index.html +0 -1
package/README.md CHANGED
@@ -41,6 +41,8 @@ npx nuxi@latest module add nuxt-ai-ready
41
41
  > npx skilld add nuxt-ai-ready
42
42
  > ```
43
43
 
44
+ 💡 Made your site AI-ready? Preview how a page converts to LLM-readable markdown with the free [HTML→Markdown tool](https://nuxtseo.com/tools/html-to-markdown), or track how AI engines index, rank and cite your site with [Nuxt SEO Pro](https://nuxtseo.com/pro).
45
+
44
46
  ## Documentation
45
47
 
46
48
  [📖 Read the full documentation](https://nuxtseo.com/ai-ready) for more information.
@@ -0,0 +1,6 @@
1
+ import { useDevtoolsConnection } from 'nuxtseo-layer-devtools/composables/rpc'
2
+
3
+ // The layer owns host fetch + route tracking and refreshes on connect; ai-ready's
4
+ // state.ts watches refreshTime to reload the global debug data, so no module-level
5
+ // host access is needed here.
6
+ useDevtoolsConnection()
@@ -0,0 +1,21 @@
1
+ import type { DevtoolsGlobalData } from './types'
2
+ import { appFetch } from 'nuxtseo-layer-devtools/composables/rpc'
3
+ import { productionUrl, refreshTime } from 'nuxtseo-layer-devtools/composables/state'
4
+ import { ref, watch } from 'vue'
5
+
6
+ export const data = ref<DevtoolsGlobalData | null>(null)
7
+ export const loading = ref(true)
8
+
9
+ export async function refreshSources() {
10
+ if (!appFetch.value || typeof appFetch.value !== 'function')
11
+ return
12
+ data.value = await appFetch.value('/__ai-ready__/debug.json', { responseType: 'json' }).catch(() => null)
13
+ loading.value = false
14
+ if (data.value?.siteConfigUrl)
15
+ productionUrl.value = data.value.siteConfigUrl
16
+ }
17
+
18
+ // Re-fetch the global debug data when the host connection or manual refresh changes
19
+ watch([appFetch, refreshTime], () => {
20
+ refreshSources()
21
+ })
@@ -0,0 +1,50 @@
1
+ export interface DevtoolsConfig {
2
+ database: { type: string }
3
+ runtimeSync: { enabled: boolean, ttl: number, batchSize: number, pruneTtl: number }
4
+ indexNow: boolean
5
+ sitemapPrerendered: boolean
6
+ markdownCacheHeaders: { maxAge: number, swr: boolean }
7
+ llmsTxtCacheSeconds: number
8
+ contentSignal: false | { aiTrain: boolean, search: boolean, aiInput: boolean }
9
+ mcp: { enabled: boolean, tools: boolean, resources: boolean }
10
+ cron: boolean
11
+ }
12
+
13
+ export interface LlmsTxtLink {
14
+ title: string
15
+ description?: string
16
+ href: string
17
+ }
18
+
19
+ export interface LlmsTxtSection {
20
+ title: string
21
+ description?: string | string[]
22
+ links?: LlmsTxtLink[]
23
+ }
24
+
25
+ export interface LlmsTxtConfig {
26
+ sections?: LlmsTxtSection[]
27
+ notes?: string | string[]
28
+ }
29
+
30
+ export interface PageSummary {
31
+ route: string
32
+ title: string
33
+ description: string
34
+ updatedAt: string | null
35
+ }
36
+
37
+ export interface DevtoolsGlobalData {
38
+ version: string
39
+ siteConfigUrl: string
40
+ isDev: boolean
41
+ config: DevtoolsConfig
42
+ llmsTxt: LlmsTxtConfig
43
+ stats?: {
44
+ total: number
45
+ indexed: number
46
+ pending: number
47
+ errors: number
48
+ }
49
+ pages?: PageSummary[]
50
+ }
@@ -0,0 +1,3 @@
1
+ // Nuxt SEO devtools panel, shipped as a layer (Model C). The unified devtools client
2
+ // (assembled by nuxtseo-shared in the user's project) extends this to render /ai-ready.
3
+ export default defineNuxtConfig({})
@@ -0,0 +1,27 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue'
3
+ import { data } from '../../lib/ai-ready/state'
4
+
5
+ const config = computed(() => data.value?.config)
6
+ const llmsTxt = computed(() => data.value?.llmsTxt)
7
+
8
+ const configJson = computed(() => config.value ? JSON.stringify(config.value, null, 2) : '')
9
+ const llmsTxtJson = computed(() => llmsTxt.value ? JSON.stringify(llmsTxt.value, null, 2) : '')
10
+ const fullJson = computed(() => data.value ? JSON.stringify(data.value, null, 2) : '')
11
+ </script>
12
+
13
+ <template>
14
+ <div class="space-y-4">
15
+ <DevtoolsSection text="Module Config" icon="carbon:settings" :open="true">
16
+ <DevtoolsSnippet v-if="configJson" :code="configJson" lang="json" />
17
+ </DevtoolsSection>
18
+
19
+ <DevtoolsSection text="llms.txt Config" icon="carbon:document" :open="true">
20
+ <DevtoolsSnippet v-if="llmsTxtJson" :code="llmsTxtJson" lang="json" />
21
+ </DevtoolsSection>
22
+
23
+ <DevtoolsSection text="Full Response" icon="carbon:code" :open="false">
24
+ <DevtoolsSnippet v-if="fullJson" :code="fullJson" lang="json" />
25
+ </DevtoolsSection>
26
+ </div>
27
+ </template>
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <DevtoolsDocs url="https://nuxtseo.com/ai-ready" />
3
+ </template>
@@ -0,0 +1,177 @@
1
+ <script lang="ts" setup>
2
+ import { appFetch } from 'nuxtseo-layer-devtools/composables/rpc'
3
+ import { hasProductionUrl, isProductionMode, previewSource } from 'nuxtseo-layer-devtools/composables/state'
4
+ import { computed, ref } from 'vue'
5
+ import { useAsyncData } from '#imports'
6
+ import { data as globalData } from '../../lib/ai-ready/state'
7
+
8
+ const isDev = computed(() => globalData?.value?.isDev ?? true)
9
+ const stats = computed(() => globalData?.value?.stats)
10
+ const pages = computed(() => globalData?.value?.pages || [])
11
+
12
+ const search = ref('')
13
+ const selectedRoute = ref<string | null>(null)
14
+
15
+ const filteredPages = computed(() => {
16
+ if (!search.value)
17
+ return pages.value
18
+ const q = search.value.toLowerCase()
19
+ return pages.value.filter(p =>
20
+ p.route.toLowerCase().includes(q)
21
+ || p.title?.toLowerCase().includes(q)
22
+ || p.description?.toLowerCase().includes(q),
23
+ )
24
+ })
25
+
26
+ // Fetch markdown for selected page
27
+ const { data: markdownContent, status: mdStatus } = useAsyncData('page-markdown', async () => {
28
+ if (!appFetch.value || !selectedRoute.value)
29
+ return null
30
+ try {
31
+ const mdRoute = selectedRoute.value === '/' ? '/index.md' : `${selectedRoute.value}.md`
32
+ return await appFetch.value(mdRoute, { responseType: 'text' }) as string
33
+ }
34
+ catch {
35
+ return null
36
+ }
37
+ }, {
38
+ watch: [selectedRoute, appFetch],
39
+ })
40
+ </script>
41
+
42
+ <template>
43
+ <div class="space-y-4">
44
+ <!-- Dev mode empty state -->
45
+ <template v-if="isDev && !isProductionMode">
46
+ <DevtoolsEmptyState
47
+ icon="carbon:list"
48
+ title="No pages in development"
49
+ description="Pages are indexed during prerendering or via runtime sync. The database is empty in dev mode."
50
+ >
51
+ <div class="mt-4 space-y-2">
52
+ <button
53
+ v-if="hasProductionUrl"
54
+ type="button"
55
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md bg-[var(--seo-green)] text-white hover:opacity-90 transition-opacity cursor-pointer"
56
+ @click="previewSource = 'production'"
57
+ >
58
+ <UIcon name="carbon:cloud" class="w-3.5 h-3.5" />
59
+ Switch to Production
60
+ </button>
61
+ <p class="text-xs text-[var(--color-text-muted)]">
62
+ Or run <code class="px-1 py-0.5 rounded bg-[var(--color-surface-elevated)]">nuxi generate</code> to populate page data.
63
+ </p>
64
+ </div>
65
+ </DevtoolsEmptyState>
66
+ </template>
67
+
68
+ <!-- Page browser (production mode or has pages) -->
69
+ <template v-else>
70
+ <!-- Stats -->
71
+ <div v-if="stats" class="grid grid-cols-2 sm:grid-cols-4 gap-3">
72
+ <DevtoolsMetric
73
+ label="Total Pages"
74
+ :value="stats.total"
75
+ icon="carbon:document-multiple"
76
+ />
77
+ <DevtoolsMetric
78
+ label="Indexed"
79
+ :value="stats.indexed"
80
+ icon="carbon:checkmark-filled"
81
+ variant="success"
82
+ />
83
+ <DevtoolsMetric
84
+ label="Pending"
85
+ :value="stats.pending"
86
+ icon="carbon:time"
87
+ :variant="stats.pending > 0 ? 'warning' : 'default'"
88
+ />
89
+ <DevtoolsMetric
90
+ label="Errors"
91
+ :value="stats.errors"
92
+ icon="carbon:warning-alt"
93
+ :variant="stats.errors > 0 ? 'danger' : 'default'"
94
+ />
95
+ </div>
96
+
97
+ <!-- Search -->
98
+ <div class="flex items-center gap-2">
99
+ <div class="relative flex-1">
100
+ <UIcon name="carbon:search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--color-text-muted)]" />
101
+ <input
102
+ v-model="search"
103
+ type="text"
104
+ placeholder="Search pages..."
105
+ class="w-full pl-9 pr-3 py-2 text-sm rounded-lg bg-[var(--color-surface-sunken)] border border-[var(--color-border)] text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] outline-none focus:border-[var(--seo-green)] transition-colors"
106
+ >
107
+ </div>
108
+ <span class="text-xs text-[var(--color-text-muted)] shrink-0">
109
+ {{ filteredPages.length }} page{{ filteredPages.length === 1 ? '' : 's' }}
110
+ </span>
111
+ </div>
112
+
113
+ <!-- Page list -->
114
+ <div v-if="filteredPages.length" class="space-y-1">
115
+ <button
116
+ v-for="page in filteredPages"
117
+ :key="page.route"
118
+ type="button"
119
+ class="w-full text-left p-3 rounded-lg border transition-all cursor-pointer"
120
+ :class="selectedRoute === page.route
121
+ ? 'bg-[var(--color-surface-elevated)] border-[var(--seo-green)]'
122
+ : 'bg-[var(--color-surface)] border-[var(--color-border)] hover:bg-[var(--color-surface-elevated)]'"
123
+ @click="selectedRoute = selectedRoute === page.route ? null : page.route"
124
+ >
125
+ <div class="flex items-start justify-between gap-2">
126
+ <div class="min-w-0">
127
+ <div class="text-xs font-mono text-[var(--seo-green)] truncate">
128
+ {{ page.route }}
129
+ </div>
130
+ <div v-if="page.title" class="text-sm font-medium text-[var(--color-text)] truncate mt-0.5">
131
+ {{ page.title }}
132
+ </div>
133
+ <div v-if="page.description" class="text-xs text-[var(--color-text-muted)] line-clamp-2 mt-0.5">
134
+ {{ page.description }}
135
+ </div>
136
+ </div>
137
+ <div v-if="page.updatedAt" class="text-[10px] text-[var(--color-text-subtle)] shrink-0">
138
+ {{ new Date(page.updatedAt).toLocaleDateString() }}
139
+ </div>
140
+ </div>
141
+ </button>
142
+ </div>
143
+
144
+ <DevtoolsEmptyState
145
+ v-else-if="search"
146
+ icon="carbon:search"
147
+ title="No matching pages"
148
+ :description="`No pages match &quot;${search}&quot;`"
149
+ />
150
+
151
+ <DevtoolsEmptyState
152
+ v-else
153
+ icon="carbon:list"
154
+ title="No pages indexed"
155
+ description="The production site has no indexed pages yet. Ensure prerendering has run."
156
+ />
157
+
158
+ <!-- Markdown preview panel -->
159
+ <DevtoolsPanel
160
+ v-if="selectedRoute"
161
+ :title="`${selectedRoute}.md`"
162
+ icon="carbon:document"
163
+ >
164
+ <div v-if="mdStatus === 'pending'" class="py-4">
165
+ <DevtoolsLoading />
166
+ </div>
167
+ <div v-else-if="markdownContent" class="relative">
168
+ <DevtoolsCopyButton :text="markdownContent" class="absolute top-2 right-2 z-10" />
169
+ <pre class="text-xs font-mono whitespace-pre-wrap p-4 rounded-lg bg-[var(--color-surface-sunken)] text-[var(--color-text)] overflow-auto max-h-[500px]">{{ markdownContent }}</pre>
170
+ </div>
171
+ <p v-else class="text-xs text-[var(--color-text-muted)] p-4">
172
+ Could not load markdown for this route.
173
+ </p>
174
+ </DevtoolsPanel>
175
+ </template>
176
+ </div>
177
+ </template>
@@ -0,0 +1,172 @@
1
+ <script lang="ts" setup>
2
+ import { appFetch } from 'nuxtseo-layer-devtools/composables/rpc'
3
+ import { isProductionMode, refreshTime } from 'nuxtseo-layer-devtools/composables/state'
4
+ import { computed, ref } from 'vue'
5
+ import { useAsyncData } from '#imports'
6
+ import { data as globalData } from '../../lib/ai-ready/state'
7
+
8
+ const isDev = computed(() => globalData?.value?.isDev ?? true)
9
+ const llmsTxtConfig = computed(() => globalData?.value?.llmsTxt)
10
+
11
+ const activeTab = ref<'llms-txt' | 'llms-full'>('llms-txt')
12
+
13
+ // Fetch actual llms.txt content
14
+ const { data: llmsTxtContent } = useAsyncData('llms-txt-content', async () => {
15
+ if (!appFetch.value)
16
+ return null
17
+ try {
18
+ return await appFetch.value('/llms.txt', { responseType: 'text' }) as string
19
+ }
20
+ catch {
21
+ return null
22
+ }
23
+ }, {
24
+ watch: [appFetch, refreshTime],
25
+ })
26
+
27
+ const { data: llmsFullContent, status: fullStatus } = useAsyncData('llms-full-content', async () => {
28
+ if (!appFetch.value || activeTab.value !== 'llms-full')
29
+ return null
30
+ try {
31
+ return await appFetch.value('/llms-full.txt', { responseType: 'text' }) as string
32
+ }
33
+ catch {
34
+ return null
35
+ }
36
+ }, {
37
+ watch: [appFetch, refreshTime, activeTab],
38
+ })
39
+
40
+ const displayContent = computed(() => {
41
+ if (activeTab.value === 'llms-full')
42
+ return llmsFullContent.value
43
+ return llmsTxtContent.value
44
+ })
45
+
46
+ // Build a template preview from config (for dev mode)
47
+ const templatePreview = computed(() => {
48
+ const config = llmsTxtConfig.value
49
+ if (!config)
50
+ return ''
51
+
52
+ const lines: string[] = ['# {Site Name}', '', '> {Site Description}', '']
53
+
54
+ for (const section of config.sections || []) {
55
+ lines.push(`## ${section.title}`)
56
+ if (section.description) {
57
+ const descs = Array.isArray(section.description) ? section.description : [section.description]
58
+ for (const d of descs)
59
+ lines.push('', d)
60
+ }
61
+ for (const link of section.links || []) {
62
+ const desc = link.description ? `: ${link.description}` : ''
63
+ lines.push(`- [${link.title}](${link.href})${desc}`)
64
+ }
65
+ lines.push('')
66
+ }
67
+
68
+ if (config.notes) {
69
+ const notes = Array.isArray(config.notes) ? config.notes : [config.notes]
70
+ for (const note of notes)
71
+ lines.push(note)
72
+ }
73
+
74
+ return lines.join('\n')
75
+ })
76
+ </script>
77
+
78
+ <template>
79
+ <div class="space-y-4">
80
+ <!-- Tab switcher -->
81
+ <div class="flex items-center gap-2">
82
+ <button
83
+ v-for="tab in [{ key: 'llms-txt', label: 'llms.txt' }, { key: 'llms-full', label: 'llms-full.txt' }]"
84
+ :key="tab.key"
85
+ type="button"
86
+ class="px-3 py-1.5 text-xs font-medium rounded-md transition-all cursor-pointer"
87
+ :class="activeTab === tab.key
88
+ ? 'bg-[var(--seo-green)] text-white'
89
+ : 'bg-[var(--color-surface-elevated)] text-[var(--color-text-muted)] hover:text-[var(--color-text)]'"
90
+ @click="activeTab = tab.key as any"
91
+ >
92
+ {{ tab.label }}
93
+ </button>
94
+ </div>
95
+
96
+ <!-- Dev mode: template preview hint -->
97
+ <DevtoolsAlert
98
+ v-if="isDev && !isProductionMode"
99
+ variant="info"
100
+ >
101
+ <p class="font-medium mb-1">
102
+ Template Preview
103
+ </p>
104
+ <p class="text-sm opacity-80">
105
+ This shows the llms.txt structure from your config. Actual content with page data is generated during prerendering.
106
+ Switch to production mode to see live content.
107
+ </p>
108
+ </DevtoolsAlert>
109
+
110
+ <!-- Content display -->
111
+ <DevtoolsPanel :title="activeTab === 'llms-txt' ? 'llms.txt' : 'llms-full.txt'">
112
+ <!-- Loading state for llms-full.txt -->
113
+ <div v-if="activeTab === 'llms-full' && fullStatus === 'pending'" class="py-8 text-center">
114
+ <DevtoolsLoading />
115
+ </div>
116
+
117
+ <!-- Show content or template -->
118
+ <template v-else>
119
+ <div v-if="displayContent" class="relative">
120
+ <DevtoolsCopyButton :text="displayContent" class="absolute top-2 right-2 z-10" />
121
+ <pre class="text-xs font-mono whitespace-pre-wrap p-4 rounded-lg bg-[var(--color-surface-sunken)] text-[var(--color-text)] overflow-auto max-h-[600px]">{{ displayContent }}</pre>
122
+ </div>
123
+
124
+ <!-- Fallback: show template preview in dev -->
125
+ <div v-else-if="isDev && templatePreview" class="relative">
126
+ <pre class="text-xs font-mono whitespace-pre-wrap p-4 rounded-lg bg-[var(--color-surface-sunken)] text-[var(--color-text-muted)] overflow-auto max-h-[600px]">{{ templatePreview }}</pre>
127
+ </div>
128
+
129
+ <DevtoolsEmptyState
130
+ v-else
131
+ icon="carbon:document"
132
+ title="No content available"
133
+ description="llms.txt content is generated during prerendering. Run `nuxi generate` or switch to production mode."
134
+ />
135
+ </template>
136
+ </DevtoolsPanel>
137
+
138
+ <!-- llms.txt Structure (always visible) -->
139
+ <DevtoolsSection text="Configured Sections" icon="carbon:list-boxes">
140
+ <div v-if="llmsTxtConfig?.sections?.length" class="space-y-3">
141
+ <div
142
+ v-for="(section, i) in llmsTxtConfig.sections"
143
+ :key="i"
144
+ class="p-3 rounded-lg bg-[var(--color-surface-elevated)] border border-[var(--color-border)]"
145
+ >
146
+ <h4 class="text-sm font-semibold text-[var(--color-text)] mb-1">
147
+ {{ section.title }}
148
+ </h4>
149
+ <div v-if="section.links?.length" class="space-y-1 mt-2">
150
+ <div
151
+ v-for="(link, j) in section.links"
152
+ :key="j"
153
+ class="flex items-start gap-2 text-xs"
154
+ >
155
+ <UIcon name="carbon:link" class="w-3 h-3 mt-0.5 text-[var(--color-text-muted)]" />
156
+ <div>
157
+ <span class="font-medium text-[var(--color-text)]">{{ link.title }}</span>
158
+ <span v-if="link.description" class="text-[var(--color-text-muted)]">, {{ link.description }}</span>
159
+ <div class="font-mono text-[var(--color-text-subtle)]">
160
+ {{ link.href }}
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ <p v-else class="text-xs text-[var(--color-text-muted)]">
168
+ No custom sections configured.
169
+ </p>
170
+ </DevtoolsSection>
171
+ </div>
172
+ </template>
@@ -0,0 +1,50 @@
1
+ <script lang="ts" setup>
2
+ import { isProductionMode } from 'nuxtseo-layer-devtools/composables/state'
3
+ import { computed, watch } from 'vue'
4
+ import { navigateTo, useRoute } from '#imports'
5
+ import { data, loading, refreshSources } from '../lib/ai-ready/state'
6
+ import '../lib/ai-ready/rpc'
7
+
8
+ const route = useRoute()
9
+ const currentTab = computed(() => {
10
+ const path = route.path
11
+ if (path === '/ai-ready/llms-txt')
12
+ return 'llms-txt'
13
+ if (path === '/ai-ready/debug')
14
+ return 'debug'
15
+ if (path === '/ai-ready/docs')
16
+ return 'docs'
17
+ return 'pages'
18
+ })
19
+
20
+ const navItems = [
21
+ { value: 'pages', to: '/ai-ready', icon: 'carbon:list', label: 'Pages', devOnly: false },
22
+ { value: 'llms-txt', to: '/ai-ready/llms-txt', icon: 'carbon:document', label: 'llms.txt', devOnly: false },
23
+ { value: 'debug', to: '/ai-ready/debug', icon: 'carbon:debug', label: 'Debug', devOnly: true },
24
+ { value: 'docs', to: '/ai-ready/docs', icon: 'carbon:book', label: 'Docs', devOnly: false },
25
+ ]
26
+
27
+ const runtimeVersion = computed(() => data.value?.version || 'unknown')
28
+
29
+ // Debug data is dev-only; leave the debug tab when the header switches to Production
30
+ watch(isProductionMode, (isProd) => {
31
+ if (isProd && currentTab.value === 'debug')
32
+ return navigateTo('/ai-ready')
33
+ })
34
+ </script>
35
+
36
+ <template>
37
+ <DevtoolsLayout
38
+ module-name="nuxt-ai-ready"
39
+ title="AI Ready"
40
+ icon="carbon:bot"
41
+ :version="runtimeVersion"
42
+ :nav-items="navItems"
43
+ github-url="https://github.com/harlan-zw/nuxt-ai-ready"
44
+ :loading="loading"
45
+ :active-tab="currentTab"
46
+ @refresh="refreshSources"
47
+ >
48
+ <NuxtPage />
49
+ </DevtoolsLayout>
50
+ </template>
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=4.0.0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "1.3.9",
7
+ "version": "1.5.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -675,11 +675,13 @@ const module$1 = defineNuxtModule({
675
675
  nuxt.options.robots = robotsOpts;
676
676
  const groups = robotsOpts.groups || [];
677
677
  robotsOpts.groups = groups;
678
- groups.push({
678
+ const group = {
679
679
  userAgent: "*",
680
- contentUsage: [`train-ai=${config.contentSignal.aiTrain ? "y" : "n"}`],
681
680
  contentSignal: [`ai-train=${config.contentSignal.aiTrain ? "yes" : "no"}`, `search=${config.contentSignal.search ? "yes" : "no"}`, `ai-input=${config.contentSignal.aiInput ? "yes" : "no"}`]
682
- });
681
+ };
682
+ if (config.contentSignal.contentUsage !== false)
683
+ group.contentUsage = [`train-ai=${config.contentSignal.aiTrain ? "y" : "n"}`];
684
+ groups.push(group);
683
685
  }
684
686
  registerTypeTemplates();
685
687
  const defaultLlmsTxtSections = [];
@@ -957,7 +959,7 @@ export async function lookupContentPage(event, path) {
957
959
  });
958
960
  addServerHandler({ route: "/llms.txt", handler: resolve("./runtime/server/routes/llms.txt.get") });
959
961
  addServerHandler({ route: "/llms-full.txt", handler: resolve("./runtime/server/routes/llms-full.txt.get") });
960
- addServerHandler({ route: "/__ai-ready/devtools", handler: resolve("./runtime/server/routes/__ai-ready/devtools.get") });
962
+ addServerHandler({ route: "/__ai-ready__/debug.json", handler: resolve("./runtime/server/routes/__ai-ready/devtools.get") });
961
963
  if (config.debug) {
962
964
  addServerHandler({ route: "/__ai-ready-debug", handler: resolve("./runtime/server/routes/__ai-ready-debug.get") });
963
965
  }
@@ -4,9 +4,14 @@ import { useRuntimeConfig } from "nitropack/runtime";
4
4
  import * as schema from "#ai-ready-virtual/db-schema.mjs";
5
5
  import { logger } from "../../../logger.js";
6
6
  import { registerDriver } from "../raw.js";
7
+ import { resolveWritableDbPath } from "./dbPath.js";
7
8
  export async function createClient(event) {
8
9
  const config = useRuntimeConfig(event)["nuxt-ai-ready"];
9
- const dbUrl = config.database.url || `file:${config.database.filename || ".data/ai-ready/pages.db"}`;
10
+ let dbUrl = config.database.url || `file:${config.database.filename || ".data/ai-ready/pages.db"}`;
11
+ if (dbUrl.startsWith("file:") && !dbUrl.startsWith("file://")) {
12
+ const resolved = await resolveWritableDbPath(dbUrl.slice("file:".length));
13
+ dbUrl = `file:${resolved}`;
14
+ }
10
15
  logger.debug(`[drizzle] Connecting to LibSQL: ${dbUrl}`);
11
16
  const client = createLibSQLClient({ url: dbUrl, authToken: config.database.authToken });
12
17
  const db = drizzle(client, { schema });
@@ -9,14 +9,14 @@ import { buildFrontmatter } from "../utils/frontmatter.js";
9
9
  import { computeLocaleAlternates, resolveLocaleFromRoute } from "../utils/i18n.js";
10
10
  import { buildLinkHeader } from "../utils/link-header.js";
11
11
  const INTERNAL_HEADER = "x-ai-ready-internal";
12
- function setNegotiationHeaders(event, path, config) {
12
+ function setNegotiationHeaders(event, path, config, resolveUrl) {
13
13
  setHeader(event, "vary", "Accept, Sec-Fetch-Dest");
14
- setHeader(event, "link", buildLinkHeader(path, "html", config));
14
+ setHeader(event, "link", buildLinkHeader(path, "html", config, resolveUrl));
15
15
  }
16
- function setMarkdownHeaders(event, path, config) {
16
+ function setMarkdownHeaders(event, path, config, resolveUrl) {
17
17
  setHeader(event, "content-type", "text/markdown; charset=utf-8");
18
18
  setHeader(event, "vary", "Accept, Sec-Fetch-Dest");
19
- setHeader(event, "link", buildLinkHeader(path, "markdown", config));
19
+ setHeader(event, "link", buildLinkHeader(path, "markdown", config, resolveUrl));
20
20
  if (config.markdownCacheHeaders) {
21
21
  const { maxAge, swr } = config.markdownCacheHeaders;
22
22
  const cacheControl = swr ? `public, max-age=${maxAge}, stale-while-revalidate=${maxAge}` : `public, max-age=${maxAge}`;
@@ -65,9 +65,10 @@ export default defineEventHandler(async (event) => {
65
65
  }
66
66
  const { path, isExplicit, negotiation } = renderInfo;
67
67
  const config = useRuntimeConfig(event)["nuxt-ai-ready"];
68
- const canonicalUrl = withSiteUrl(event, path);
68
+ const resolveUrl = (path2) => withSiteUrl(event, path2);
69
+ const canonicalUrl = resolveUrl(path);
69
70
  if (negotiation === "html") {
70
- setNegotiationHeaders(event, path, config);
71
+ setNegotiationHeaders(event, path, config, resolveUrl);
71
72
  return;
72
73
  }
73
74
  if (!isExplicit) {
@@ -85,7 +86,7 @@ export default defineEventHandler(async (event) => {
85
86
  canonical_url: canonicalUrl,
86
87
  last_updated: contentPage.updatedAt || (/* @__PURE__ */ new Date()).toISOString()
87
88
  });
88
- setMarkdownHeaders(event, path, config);
89
+ setMarkdownHeaders(event, path, config, resolveUrl);
89
90
  return `${frontmatter}
90
91
  ${contentPage.markdown}`;
91
92
  }
@@ -98,7 +99,7 @@ ${contentPage.markdown}`;
98
99
  return null;
99
100
  });
100
101
  if (!response) {
101
- setMarkdownHeaders(event, path, config);
102
+ setMarkdownHeaders(event, path, config, resolveUrl);
102
103
  return notFoundMarkdown(canonicalUrl, path, config);
103
104
  }
104
105
  if (response.status >= 300 && response.status < 400) {
@@ -113,12 +114,12 @@ ${contentPage.markdown}`;
113
114
  }
114
115
  }
115
116
  if (!response.ok) {
116
- setMarkdownHeaders(event, path, config);
117
+ setMarkdownHeaders(event, path, config, resolveUrl);
117
118
  return notFoundMarkdown(canonicalUrl, path, config);
118
119
  }
119
120
  const contentType = response.headers.get("content-type") || "";
120
121
  if (!contentType.includes("text/html")) {
121
- setMarkdownHeaders(event, path, config);
122
+ setMarkdownHeaders(event, path, config, resolveUrl);
122
123
  return notFoundMarkdown(canonicalUrl, path, config);
123
124
  }
124
125
  const html = await response.text();
@@ -144,6 +145,6 @@ ${contentPage.markdown}`;
144
145
  additionalFrontmatter
145
146
  }
146
147
  );
147
- setMarkdownHeaders(event, path, config);
148
+ setMarkdownHeaders(event, path, config, resolveUrl);
148
149
  return result.markdown;
149
150
  });
@@ -7,7 +7,9 @@ import type { ModulePublicRuntimeConfig } from '../../../module.js';
7
7
  * `encodeURI` preserves `/` separators and other reserved URL characters.
8
8
  */
9
9
  export declare function encodePathForHeader(path: string): string;
10
+ type LinkUrlResolver = (path: string) => string;
10
11
  /**
11
12
  * Build a comma-joined Link header value with the standard alternates plus i18n hreflang variants.
12
13
  */
13
- export declare function buildLinkHeader(path: string, variant: 'html' | 'markdown', config: ModulePublicRuntimeConfig): string;
14
+ export declare function buildLinkHeader(path: string, variant: 'html' | 'markdown', config: ModulePublicRuntimeConfig, resolveUrl?: LinkUrlResolver): string;
15
+ export {};