nuxtseo-layer-devtools 0.2.5 → 0.2.7

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.
@@ -2,7 +2,7 @@
2
2
  import { onClickOutside } from '@vueuse/core'
3
3
  import { computed, ref } from 'vue'
4
4
  import { colorMode } from '../composables/rpc'
5
- import { hasProductionUrl, isProductionMode, previewSource, productionUrl } from '../composables/state'
5
+ import { hasProductionUrl, isConnected, isProductionMode, isStandalone, path, previewSource, productionUrl, standaloneUrl } from '../composables/state'
6
6
 
7
7
  export interface DevtoolsNavItem {
8
8
  value: string
@@ -70,6 +70,21 @@ function selectMode(mode: 'local' | 'production') {
70
70
  previewSource.value = mode
71
71
  modeDropdownOpen.value = false
72
72
  }
73
+
74
+ const standaloneHostname = computed(() => {
75
+ try {
76
+ return new URL(standaloneUrl.value).host
77
+ }
78
+ catch {
79
+ return standaloneUrl.value
80
+ }
81
+ })
82
+
83
+ const showStandaloneSetup = computed(() => !isConnected.value && !isStandalone.value)
84
+
85
+ function disconnectStandalone() {
86
+ standaloneUrl.value = ''
87
+ }
73
88
  </script>
74
89
 
75
90
  <template>
@@ -106,7 +121,8 @@ function selectMode(mode: 'local' | 'production') {
106
121
  >
107
122
  v{{ version }}
108
123
  </UBadge>
109
- <div v-if="hasProductionUrl" ref="modeDropdownRef" class="mode-dropdown-wrapper">
124
+ <!-- Mode dropdown: embedded with production URL -->
125
+ <div v-if="hasProductionUrl && !isStandalone" ref="modeDropdownRef" class="mode-dropdown-wrapper">
110
126
  <button type="button" class="devtools-mode-btn" @click="modeDropdownOpen = !modeDropdownOpen">
111
127
  <UIcon :name="isProductionMode ? 'carbon:cloud' : 'carbon:laptop'" class="w-3.5 h-3.5" />
112
128
  <span class="hidden sm:inline">{{ isProductionMode ? 'Production' : 'Local' }}</span>
@@ -135,6 +151,16 @@ function selectMode(mode: 'local' | 'production') {
135
151
  </div>
136
152
  </Transition>
137
153
  </div>
154
+ <!-- Standalone mode indicator -->
155
+ <div v-if="isStandalone" class="standalone-indicator">
156
+ <UIcon name="carbon:plug" class="w-3.5 h-3.5 text-[var(--seo-green)]" />
157
+ <span class="text-xs font-mono">{{ standaloneHostname }}</span>
158
+ <UTooltip text="Disconnect">
159
+ <button type="button" class="standalone-disconnect" @click="disconnectStandalone">
160
+ <UIcon name="carbon:close" class="w-3 h-3" />
161
+ </button>
162
+ </UTooltip>
163
+ </div>
138
164
  </div>
139
165
  </div>
140
166
 
@@ -218,11 +244,28 @@ function selectMode(mode: 'local' | 'production') {
218
244
  </div>
219
245
  </header>
220
246
 
247
+ <!-- Standalone path input -->
248
+ <div v-if="isStandalone" class="standalone-path-bar">
249
+ <div class="standalone-path-inner">
250
+ <UIcon name="carbon:document" class="w-3.5 h-3.5 text-[var(--color-text-muted)]" />
251
+ <span class="text-xs text-[var(--color-text-muted)]">Path:</span>
252
+ <input
253
+ :value="path"
254
+ type="text"
255
+ class="standalone-path-input"
256
+ placeholder="/"
257
+ @change="path = ($event.target as HTMLInputElement).value"
258
+ @keydown.enter="($event.target as HTMLInputElement).blur()"
259
+ >
260
+ </div>
261
+ </div>
262
+
221
263
  <!-- Main Content -->
222
264
  <div class="devtools-main">
223
265
  <main class="devtools-main-content">
224
- <DevtoolsLoading v-if="loading" />
225
- <div v-show="!loading">
266
+ <DevtoolsStandaloneConnect v-if="showStandaloneSetup" />
267
+ <DevtoolsLoading v-show="!showStandaloneSetup && loading" />
268
+ <div v-show="!showStandaloneSetup && !loading">
226
269
  <slot />
227
270
  </div>
228
271
  </main>
@@ -285,4 +328,64 @@ function selectMode(mode: 'local' | 'production') {
285
328
  opacity: 0;
286
329
  transform: translateY(-4px);
287
330
  }
331
+
332
+ .standalone-indicator {
333
+ display: flex;
334
+ align-items: center;
335
+ gap: 0.375rem;
336
+ padding: 0.25rem 0.5rem;
337
+ border-radius: var(--radius-sm);
338
+ background: oklch(from var(--seo-green) l c h / 0.1);
339
+ border: 1px solid oklch(from var(--seo-green) l c h / 0.2);
340
+ font-size: 0.75rem;
341
+ color: var(--color-text-muted);
342
+ }
343
+
344
+ .standalone-disconnect {
345
+ display: flex;
346
+ align-items: center;
347
+ justify-content: center;
348
+ width: 1.25rem;
349
+ height: 1.25rem;
350
+ border-radius: var(--radius-sm);
351
+ color: var(--color-text-muted);
352
+ cursor: pointer;
353
+ transition: background 100ms, color 100ms;
354
+ }
355
+
356
+ .standalone-disconnect:hover {
357
+ background: var(--color-surface-sunken);
358
+ color: var(--color-text);
359
+ }
360
+
361
+ .standalone-path-bar {
362
+ border-bottom: 1px solid var(--color-border);
363
+ background: var(--color-surface);
364
+ padding: 0.375rem 1rem;
365
+ }
366
+
367
+ .standalone-path-inner {
368
+ display: flex;
369
+ align-items: center;
370
+ gap: 0.5rem;
371
+ max-width: 80rem;
372
+ margin: 0 auto;
373
+ }
374
+
375
+ .standalone-path-input {
376
+ flex: 1;
377
+ background: var(--color-surface-sunken);
378
+ border: 1px solid var(--color-border);
379
+ border-radius: var(--radius-sm);
380
+ padding: 0.25rem 0.5rem;
381
+ font-size: 0.75rem;
382
+ font-family: var(--font-mono);
383
+ color: var(--color-text);
384
+ outline: none;
385
+ transition: border-color 150ms;
386
+ }
387
+
388
+ .standalone-path-input:focus {
389
+ border-color: var(--seo-green);
390
+ }
288
391
  </style>
@@ -1,8 +1,11 @@
1
1
  <template>
2
- <div class="flex items-center justify-center py-20">
2
+ <div class="flex flex-col items-center justify-center gap-3 py-20">
3
3
  <UIcon
4
4
  name="carbon:circle-dash"
5
5
  class="animate-spin text-3xl text-[var(--color-text-muted)]"
6
6
  />
7
+ <p class="text-sm text-[var(--color-text-muted)]">
8
+ Connecting to devtools…
9
+ </p>
7
10
  </div>
8
11
  </template>
@@ -0,0 +1,88 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import { standaloneUrl } from '../composables/state'
4
+
5
+ const urlInput = ref(standaloneUrl.value || 'http://localhost:3000')
6
+ const error = ref('')
7
+ const connecting = ref(false)
8
+
9
+ async function connect() {
10
+ error.value = ''
11
+ connecting.value = true
12
+ const url = urlInput.value.replace(/\/+$/, '')
13
+ try {
14
+ // Verify the dev server is reachable
15
+ await fetch(url, { mode: 'no-cors', signal: AbortSignal.timeout(5000) })
16
+ standaloneUrl.value = url
17
+ }
18
+ catch {
19
+ error.value = `Could not reach ${url}. Is the dev server running?`
20
+ }
21
+ finally {
22
+ connecting.value = false
23
+ }
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <div class="standalone-connect">
29
+ <div class="standalone-connect-card">
30
+ <UIcon name="carbon:plug" class="text-4xl text-[var(--seo-green)]" />
31
+ <h2 class="text-lg font-semibold">
32
+ Connect to Dev Server
33
+ </h2>
34
+ <p class="text-sm text-[var(--color-text-muted)] text-center max-w-sm leading-relaxed">
35
+ Running in standalone mode. Enter the URL of your Nuxt dev server to start inspecting.
36
+ </p>
37
+ <form class="standalone-connect-form" @submit.prevent="connect">
38
+ <UInput
39
+ v-model="urlInput"
40
+ placeholder="http://localhost:3000"
41
+ size="lg"
42
+ icon="carbon:link"
43
+ class="flex-1"
44
+ :disabled="connecting"
45
+ />
46
+ <UButton
47
+ type="submit"
48
+ icon="carbon:connect"
49
+ size="lg"
50
+ :loading="connecting"
51
+ >
52
+ Connect
53
+ </UButton>
54
+ </form>
55
+ <p v-if="error" class="text-xs text-[var(--color-warning)] text-center max-w-sm">
56
+ {{ error }}
57
+ </p>
58
+ </div>
59
+ </div>
60
+ </template>
61
+
62
+ <style scoped>
63
+ .standalone-connect {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ min-height: 60vh;
68
+ }
69
+
70
+ .standalone-connect-card {
71
+ display: flex;
72
+ flex-direction: column;
73
+ align-items: center;
74
+ gap: 1rem;
75
+ padding: 2.5rem;
76
+ border-radius: var(--radius-lg);
77
+ border: 1px solid var(--color-border);
78
+ background: var(--color-surface-elevated);
79
+ max-width: 28rem;
80
+ width: 100%;
81
+ }
82
+
83
+ .standalone-connect-form {
84
+ display: flex;
85
+ gap: 0.5rem;
86
+ width: 100%;
87
+ }
88
+ </style>
@@ -2,7 +2,9 @@ import type { NuxtDevtoolsClient } from '@nuxt/devtools-kit/types'
2
2
  import type { $Fetch } from 'nitropack/types'
3
3
  import type { Ref } from 'vue'
4
4
  import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
5
- import { ref, watchEffect } from 'vue'
5
+ import { ofetch } from 'ofetch'
6
+ import { ref, watch, watchEffect } from 'vue'
7
+ import { isConnected, refreshSources, standaloneUrl } from './state'
6
8
 
7
9
  export const appFetch: Ref<$Fetch | undefined> = ref()
8
10
  export const devtools: Ref<NuxtDevtoolsClient | undefined> = ref()
@@ -16,25 +18,45 @@ export interface DevtoolsConnectionOptions {
16
18
  /**
17
19
  * Initialize the base devtools connection.
18
20
  * Call this in your module's devtools client setup.
21
+ *
22
+ * Supports two modes:
23
+ * - **Embedded**: running inside Nuxt DevTools iframe (automatic)
24
+ * - **Standalone**: running directly in a browser tab with a manual dev server URL
19
25
  */
20
26
  export function useDevtoolsConnection(options: DevtoolsConnectionOptions = {}): void {
21
- onDevtoolsClientConnected(async (client) => {
22
- // @ts-expect-error untyped
23
- appFetch.value = client.host.app.$fetch
24
- watchEffect(() => {
25
- colorMode.value = client.host.app.colorMode.value
26
- })
27
- devtools.value = client.devtools
28
- options.onConnected?.(client)
27
+ const inIframe = window.parent !== window
29
28
 
30
- if (options.onRouteChange) {
31
- const $route = client.host.nuxt.vueApp.config.globalProperties?.$route
32
- options.onRouteChange($route)
33
- const removeAfterEach = client.host.nuxt.$router.afterEach((route: any) => {
34
- options.onRouteChange!(route)
29
+ // Embedded mode: connect via devtools-kit iframe client
30
+ if (inIframe) {
31
+ onDevtoolsClientConnected(async (client) => {
32
+ isConnected.value = true
33
+ // @ts-expect-error untyped
34
+ appFetch.value = client.host.app.$fetch
35
+ watchEffect(() => {
36
+ colorMode.value = client.host.app.colorMode.value
35
37
  })
36
- // Clean up when devtools client disconnects
37
- client.host.nuxt.hook('app:unmount', removeAfterEach)
38
+ devtools.value = client.devtools
39
+ options.onConnected?.(client)
40
+
41
+ if (options.onRouteChange) {
42
+ const $route = client.host.nuxt.vueApp.config.globalProperties?.$route
43
+ options.onRouteChange($route)
44
+ const removeAfterEach = client.host.nuxt.$router.afterEach((route: any) => {
45
+ options.onRouteChange!(route)
46
+ })
47
+ // Clean up when devtools client disconnects
48
+ client.host.nuxt.hook('app:unmount', removeAfterEach)
49
+ }
50
+ })
51
+ }
52
+
53
+ // Standalone mode: create appFetch from manually entered URL
54
+ watch(() => standaloneUrl.value, (url) => {
55
+ if (url && !isConnected.value) {
56
+ appFetch.value = ofetch.create({ baseURL: url }) as unknown as $Fetch
57
+ // Use system color scheme preference
58
+ colorMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
59
+ refreshSources()
38
60
  }
39
- })
61
+ }, { immediate: true })
40
62
  }
@@ -9,7 +9,11 @@ export const path = ref('/')
9
9
  export const query = ref()
10
10
  export const base = ref('/')
11
11
 
12
- export const host = computed(() => withBase(base.value, `${window.location.protocol}//${hostname}`))
12
+ export const host = computed(() => {
13
+ if (isStandalone.value)
14
+ return standaloneUrl.value
15
+ return withBase(base.value, `${window.location.protocol}//${hostname}`)
16
+ })
13
17
 
14
18
  export const refreshSources = useDebounceFn(() => {
15
19
  refreshTime.value = Date.now()
@@ -31,3 +35,8 @@ export const hasProductionUrl = computed(() => {
31
35
  })
32
36
 
33
37
  export const isProductionMode = computed(() => previewSource.value === 'production' && hasProductionUrl.value)
38
+
39
+ // Standalone mode state
40
+ export const standaloneUrl = useLocalStorage<string>('nuxt-seo:standalone-url', '')
41
+ export const isConnected = ref(false)
42
+ export const isStandalone = computed(() => !isConnected.value && !!standaloneUrl.value)
package/nuxt.config.ts CHANGED
@@ -8,10 +8,15 @@ export default defineNuxtConfig({
8
8
  modules: [
9
9
  '@nuxt/fonts',
10
10
  '@nuxt/ui',
11
+ '@vueuse/nuxt',
11
12
  ],
12
13
 
13
14
  css: [resolve('./assets/css/global.css')],
14
15
 
16
+ robots: false,
17
+ content: false,
18
+ sitemap: false,
19
+
15
20
  fonts: {
16
21
  families: [
17
22
  { name: 'Hubot Sans' },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxtseo-layer-devtools",
3
3
  "type": "module",
4
- "version": "0.2.5",
4
+ "version": "0.2.7",
5
5
  "description": "Shared Nuxt layer for Nuxt SEO devtools clients.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -31,7 +31,8 @@
31
31
  "@nuxt/ui": "^4.6.0",
32
32
  "@shikijs/langs": "^4.0.2",
33
33
  "@shikijs/themes": "^4.0.2",
34
- "@vueuse/core": "^14.2.1",
34
+ "@vueuse/nuxt": "^14.2.1",
35
+ "ofetch": "^1.5.1",
35
36
  "shiki": "^4.0.2",
36
37
  "ufo": "^1.6.3"
37
38
  },