nuxtseo-layer-devtools
Shared Nuxt layer providing components, composables, and a design system for all Nuxt SEO module devtools clients.
Source: packages/devtools-layer/ (published as nuxtseo-layer-devtools)
Available Libraries
The layer registers these Nuxt modules, so all consumers have them available without extra config:
@nuxt/ui (v4): Full component library. Use UButton, UBadge, UIcon, UInput, UTooltip, UApp, etc. freely. Default variants via app.config.ts (primary green, buttons ghost/neutral/sm, badges subtle/neutral/xs, tooltips zero delay).
@vueuse/nuxt: All VueUse composables auto imported.
- Shiki: Syntax highlighting via the layer's
loadShiki / useRenderCodeHighlight composables.
Architecture (Model C — source layer, assembled)
Each module ships its devtools panel as a source layer under devtools/. It is NOT a standalone app the module builds itself.
- nuxtseo-shared/devtools (
packages/shared/src/devtools.ts): setupDevToolsUI() registers the Nuxt DevTools iframe tab. In dev it assembles every installed SEO module's devtools/ layer + the base layer into one unified client, builds it once, and serves it at /__nuxt-seo-devtools/<slug> (one route per module). The module never extends the layer itself — the assembler writes the extending config.
- nuxtseo-layer-devtools (
packages/devtools-layer/): the base layer — shared components, composables, CSS, fonts.
- Module client (
<module>/devtools/): pages + lib for that module's panel. Extended by the assembler; renders at /__nuxt-seo-devtools/<slug>.
Rules
- Module
devtools/nuxt.config.ts is empty — export default defineNuxtConfig({}). The assembler wires the layer extension. Only add components: [{ path: resolve(__dirname, './components'), pathPrefix: false }] if the module ships its own components/<mod>/ UI.
- Use EXPLICIT imports for layer composables —
import { useDevtoolsConnection } from 'nuxtseo-layer-devtools/composables/rpc', import { appFetch } from '.../composables/rpc', import { isProductionMode, path, refreshTime } from '.../composables/state', import { loadShiki } from '.../composables/shiki'. Do NOT rely on auto-imports / #imports for layer composables (#imports is fine for Nuxt built-ins like navigateTo, useRoute, useAsyncData).
- The consuming module's root
tsconfig.json MUST exclude both dist and devtools. The devtools client is a separate layer-extended app, typechecked only when assembled — never at the module root. Omitting dist lets the client:build copy get typechecked in the wrong context (no layer auto-imports, drags the layer's raw .ts in) and breaks nuxt typecheck.
- ALWAYS use layer components over custom HTML:
DevtoolsSection not custom details, DevtoolsKeyValue not custom tables, DevtoolsSnippet/OCodeBlock not custom code blocks, DevtoolsPanel not a custom card, DevtoolsEmptyState/DevtoolsLoading/DevtoolsAlert not custom equivalents. Use KeyValueItem.code for inline code instead of separate snippets.
- ALWAYS use
@nuxt/ui components (UButton, UInput, UBadge, UIcon, UTooltip, etc.) for interactive elements. Never hand-roll a button/input/badge/tooltip.
- NEVER add custom CSS that duplicates what the layer or Nuxt UI provides.
- NEVER enable SSR in the client (it runs in an iframe) — the layer already sets
ssr: false.
- ALWAYS disable the module itself in the assembled client (the base layer sets
robots: false, sitemap: false, content: false).
- ALWAYS guard devtools setup with
if (nuxt.options.dev) in module.ts; debug server routes are dev-only.
- Debug endpoint convention:
/__<module>__/debug.json (og-image is the historical exception: /_og/debug.json).
- Use Carbon icons consistently (
carbon: prefix). Give the debug tab devOnly: true; redirect dev-only tabs to the index in production via an isProductionMode watch.
Required File Structure
devtools/
├── nuxt.config.ts # empty defineNuxtConfig({}) (+ components reg only if components/ exists)
├── pages/
│ ├── <mod>.vue # DevtoolsLayout shell + <NuxtPage/> (REQUIRED)
│ └── <mod>/
│ ├── index.vue # overview tab
│ ├── debug.vue # devOnly tab
│ ├── docs.vue # <DevtoolsDocs url=.../>
│ └── <other-tabs>.vue
├── lib/<mod>/
│ ├── state.ts # data ref + refreshSources() + watch (REQUIRED)
│ └── rpc.ts # useDevtoolsConnection() (REQUIRED)
└── components/<mod>/ # OPTIONAL: module-specific UI only
src/
├── devtools.ts # wraps setupDevToolsUI from nuxtseo-shared/devtools
├── module.ts # setupDevToolsUI(dev only) + registers debug route
└── runtime/server/routes/__<module>__/
└── debug.json.ts # JSON debug endpoint
Implementation Templates
For full component/composable API reference, read reference.md.
devtools/nuxt.config.ts
// Assembled by nuxtseo-shared in the user's project; this extends the base layer there.
export default defineNuxtConfig({})
devtools/lib//rpc.ts
import { useDevtoolsConnection } from 'nuxtseo-layer-devtools/composables/rpc'
// The layer's connection plugin already wires appFetch + route tracking and refreshes
// on connect; state.ts watches refreshTime to reload data, so no module host access here.
useDevtoolsConnection()
devtools/lib//state.ts
import type { DebugData } from './types'
import { appFetch } from 'nuxtseo-layer-devtools/composables/rpc'
import { path, productionUrl, refreshTime } from 'nuxtseo-layer-devtools/composables/state'
import { ref, watch } from 'vue'
export const data = ref<DebugData | null>(null)
export async function refreshSources() {
if (!appFetch.value)
return
data.value = await appFetch.value('/__<mod>__/debug.json', { query: { path: path.value } }).catch(() => null)
if (data.value?.siteConfig?.url)
productionUrl.value = data.value.siteConfig.url
}
watch([path, appFetch, refreshTime], () => {
refreshSources()
})
devtools/pages/.vue (shell)
<script setup lang="ts">
import { isProductionMode } from 'nuxtseo-layer-devtools/composables/state'
import { computed, watch } from 'vue'
import { navigateTo, useRoute } from '#imports'
import { data, refreshSources } from '../lib/<mod>/state'
import '../lib/<mod>/rpc'
const route = useRoute()
const currentTab = computed(() => {
const p = route.path
if (p.startsWith('/<mod>/debug'))
return 'debug'
if (p.startsWith('/<mod>/docs'))
return 'docs'
return 'overview'
})
const navItems = [
{ value: 'overview', to: '/<mod>', icon: 'carbon:dashboard', label: 'Overview', devOnly: false },
{ value: 'debug', to: '/<mod>/debug', icon: 'carbon:debug', label: 'Debug', devOnly: true },
{ value: 'docs', to: '/<mod>/docs', icon: 'carbon:book', label: 'Docs', devOnly: false },
]
const version = computed(() => data.value?.runtimeConfig?.version || '')
watch(isProductionMode, (isProd) => {
if (isProd && currentTab.value === 'debug')
return navigateTo('/<mod>')
})
</script>
<template>
<DevtoolsLayout
v-model:active-tab="currentTab"
module-name="nuxt-<module>"
title="Title"
icon="carbon:icon"
:version="version"
:nav-items="navItems"
github-url="https://github.com/..."
:loading="!data"
@refresh="refreshSources"
>
<NuxtPage />
</DevtoolsLayout>
</template>
DevtoolsLayout derives the npm package + update-check and renders DevtoolsTroubleshooting in the debug tab automatically from module-name — do not pass an npmPackage prop or hand-roll troubleshooting.
src/module.ts (dev only)
if (nuxt.options.dev) {
addServerHandler({ route: '/__<module>__/debug.json', handler: resolve('./runtime/server/routes/__<module>__/debug.json') })
setupDevToolsUI(config, resolve)
}
Source: harlan-zw/nuxt-seo — distributed by TomeVault.
1---2name: devtools-layer-skilld3description: nuxtseo-layer-devtools shared devtools layer for Nuxt SEO modules. ALWAYS use when building, modifying, or reviewing devtools client code in any Nuxt SEO module. Consult for component API, composables, implementation patterns, or debugging devtools clients. Use when this capability is needed.4---56# nuxtseo-layer-devtools78Shared Nuxt layer providing components, composables, and a design system for all Nuxt SEO module devtools clients.910**Source:** `packages/devtools-layer/` (published as `nuxtseo-layer-devtools`)1112## Available Libraries1314The layer registers these Nuxt modules, so all consumers have them available without extra config:1516- **`@nuxt/ui`** (v4): Full component library. Use `UButton`, `UBadge`, `UIcon`, `UInput`, `UTooltip`, `UApp`, etc. freely. Default variants via `app.config.ts` (primary green, buttons ghost/neutral/sm, badges subtle/neutral/xs, tooltips zero delay).17- **`@vueuse/nuxt`**: All VueUse composables auto imported.18- **Shiki**: Syntax highlighting via the layer's `loadShiki` / `useRenderCodeHighlight` composables.1920## Architecture (Model C — source layer, assembled)2122Each module ships its devtools panel as a **source layer** under `devtools/`. It is NOT a standalone app the module builds itself.23241. **nuxtseo-shared/devtools** (`packages/shared/src/devtools.ts`): `setupDevToolsUI()` registers the Nuxt DevTools iframe tab. In dev it **assembles every installed SEO module's `devtools/` layer + the base layer into one unified client**, builds it once, and serves it at `/__nuxt-seo-devtools/<slug>` (one route per module). The module never extends the layer itself — the assembler writes the extending config.252. **nuxtseo-layer-devtools** (`packages/devtools-layer/`): the base layer — shared components, composables, CSS, fonts.263. **Module client** (`<module>/devtools/`): pages + lib for that module's panel. Extended by the assembler; renders at `/__nuxt-seo-devtools/<slug>`.2728## Rules29301. **Module `devtools/nuxt.config.ts` is empty** — `export default defineNuxtConfig({})`. The assembler wires the layer extension. Only add `components: [{ path: resolve(__dirname, './components'), pathPrefix: false }]` if the module ships its own `components/<mod>/` UI.312. **Use EXPLICIT imports for layer composables** — `import { useDevtoolsConnection } from 'nuxtseo-layer-devtools/composables/rpc'`, `import { appFetch } from '.../composables/rpc'`, `import { isProductionMode, path, refreshTime } from '.../composables/state'`, `import { loadShiki } from '.../composables/shiki'`. Do NOT rely on auto-imports / `#imports` for layer composables (`#imports` is fine for Nuxt built-ins like `navigateTo`, `useRoute`, `useAsyncData`).323. **The consuming module's root `tsconfig.json` MUST exclude both `dist` and `devtools`.** The devtools client is a separate layer-extended app, typechecked only when assembled — never at the module root. Omitting `dist` lets the `client:build` copy get typechecked in the wrong context (no layer auto-imports, drags the layer's raw `.ts` in) and breaks `nuxt typecheck`.334. ALWAYS use layer components over custom HTML: `DevtoolsSection` not custom details, `DevtoolsKeyValue` not custom tables, `DevtoolsSnippet`/`OCodeBlock` not custom code blocks, `DevtoolsPanel` not a custom card, `DevtoolsEmptyState`/`DevtoolsLoading`/`DevtoolsAlert` not custom equivalents. Use `KeyValueItem.code` for inline code instead of separate snippets.345. ALWAYS use `@nuxt/ui` components (`UButton`, `UInput`, `UBadge`, `UIcon`, `UTooltip`, etc.) for interactive elements. Never hand-roll a button/input/badge/tooltip.356. NEVER add custom CSS that duplicates what the layer or Nuxt UI provides.367. NEVER enable SSR in the client (it runs in an iframe) — the layer already sets `ssr: false`.378. ALWAYS disable the module itself in the assembled client (the base layer sets `robots: false`, `sitemap: false`, `content: false`).389. ALWAYS guard devtools setup with `if (nuxt.options.dev)` in `module.ts`; debug server routes are dev-only.3910. Debug endpoint convention: `/__<module>__/debug.json` (og-image is the historical exception: `/_og/debug.json`).4011. Use Carbon icons consistently (`carbon:` prefix). Give the debug tab `devOnly: true`; redirect dev-only tabs to the index in production via an `isProductionMode` watch.4142## Required File Structure4344```45devtools/46├── nuxt.config.ts # empty defineNuxtConfig({}) (+ components reg only if components/ exists)47├── pages/48│ ├── <mod>.vue # DevtoolsLayout shell + <NuxtPage/> (REQUIRED)49│ └── <mod>/50│ ├── index.vue # overview tab51│ ├── debug.vue # devOnly tab52│ ├── docs.vue # <DevtoolsDocs url=.../>53│ └── <other-tabs>.vue54├── lib/<mod>/55│ ├── state.ts # data ref + refreshSources() + watch (REQUIRED)56│ └── rpc.ts # useDevtoolsConnection() (REQUIRED)57└── components/<mod>/ # OPTIONAL: module-specific UI only58src/59├── devtools.ts # wraps setupDevToolsUI from nuxtseo-shared/devtools60├── module.ts # setupDevToolsUI(dev only) + registers debug route61└── runtime/server/routes/__<module>__/62 └── debug.json.ts # JSON debug endpoint63```6465## Implementation Templates6667For full component/composable API reference, read [reference.md](./reference.md).6869### devtools/nuxt.config.ts7071```ts72// Assembled by nuxtseo-shared in the user's project; this extends the base layer there.73export default defineNuxtConfig({})74```7576### devtools/lib/<mod>/rpc.ts7778```ts79import { useDevtoolsConnection } from 'nuxtseo-layer-devtools/composables/rpc'8081// The layer's connection plugin already wires appFetch + route tracking and refreshes82// on connect; state.ts watches refreshTime to reload data, so no module host access here.83useDevtoolsConnection()84```8586### devtools/lib/<mod>/state.ts8788```ts89import type { DebugData } from './types'90import { appFetch } from 'nuxtseo-layer-devtools/composables/rpc'91import { path, productionUrl, refreshTime } from 'nuxtseo-layer-devtools/composables/state'92import { ref, watch } from 'vue'9394export const data = ref<DebugData | null>(null)9596export async function refreshSources() {97 if (!appFetch.value)98 return99 data.value = await appFetch.value('/__<mod>__/debug.json', { query: { path: path.value } }).catch(() => null)100 if (data.value?.siteConfig?.url)101 productionUrl.value = data.value.siteConfig.url102}103104watch([path, appFetch, refreshTime], () => {105 refreshSources()106})107```108109### devtools/pages/<mod>.vue (shell)110111```vue112<script setup lang="ts">113import { isProductionMode } from 'nuxtseo-layer-devtools/composables/state'114import { computed, watch } from 'vue'115import { navigateTo, useRoute } from '#imports'116import { data, refreshSources } from '../lib/<mod>/state'117import '../lib/<mod>/rpc'118119const route = useRoute()120const currentTab = computed(() => {121 const p = route.path122 if (p.startsWith('/<mod>/debug'))123 return 'debug'124 if (p.startsWith('/<mod>/docs'))125 return 'docs'126 return 'overview'127})128const navItems = [129 { value: 'overview', to: '/<mod>', icon: 'carbon:dashboard', label: 'Overview', devOnly: false },130 { value: 'debug', to: '/<mod>/debug', icon: 'carbon:debug', label: 'Debug', devOnly: true },131 { value: 'docs', to: '/<mod>/docs', icon: 'carbon:book', label: 'Docs', devOnly: false },132]133const version = computed(() => data.value?.runtimeConfig?.version || '')134135watch(isProductionMode, (isProd) => {136 if (isProd && currentTab.value === 'debug')137 return navigateTo('/<mod>')138})139</script>140141<template>142 <DevtoolsLayout143 v-model:active-tab="currentTab"144 module-name="nuxt-<module>"145 title="Title"146 icon="carbon:icon"147 :version="version"148 :nav-items="navItems"149 github-url="https://github.com/..."150 :loading="!data"151 @refresh="refreshSources"152 >153 <NuxtPage />154 </DevtoolsLayout>155</template>156```157158`DevtoolsLayout` derives the npm package + update-check and renders `DevtoolsTroubleshooting` in the debug tab automatically from `module-name` — do not pass an `npmPackage` prop or hand-roll troubleshooting.159160### src/module.ts (dev only)161162```ts163if (nuxt.options.dev) {164 addServerHandler({ route: '/__<module>__/debug.json', handler: resolve('./runtime/server/routes/__<module>__/debug.json') })165 setupDevToolsUI(config, resolve)166}167```168169---170> Source: [harlan-zw/nuxt-seo](https://github.com/harlan-zw/nuxt-seo) — distributed by [TomeVault](https://tomevault.io).171<!-- tomevault:4.0:skill_md:2026-07-03 -->