Skip to content

Commit 0a3ce43

Browse files
authored
fix(devtools): show better production hints (#252)
1 parent b047bc6 commit 0a3ce43

File tree

10 files changed

+127
-9
lines changed

10 files changed

+127
-9
lines changed

.playground/pages/admin.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<template>
2+
<div>
3+
<h1>Admin Page</h1>
4+
<p>This is a protected admin page. You should see robots meta tag with content.</p>
5+
<div>
6+
<NuxtLink to="/">
7+
Back to home
8+
</NuxtLink>
9+
</div>
10+
</div>
11+
</template>

.playground/pages/index.vue

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
1+
<script setup lang="ts">
2+
async function login() {
3+
await $fetch('/api/login')
4+
location.reload()
5+
}
6+
7+
async function logout() {
8+
await $fetch('/api/logout')
9+
location.reload()
10+
}
11+
</script>
12+
113
<template>
214
<div>
15+
<div style="margin-bottom: 20px;">
16+
<h2>Auth Test</h2>
17+
<button style="margin-right: 10px;" @click="login">
18+
Login (set cookie)
19+
</button>
20+
<button @click="logout">
21+
Logout (clear cookie)
22+
</button>
23+
</div>
24+
<div>
25+
<NuxtLink to="/admin">
26+
Admin page - requires auth
27+
</NuxtLink>
28+
</div>
329
<div>
430
<NuxtLink to="/secret">
531
Secret page - not crawlable

.playground/server/api/login.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineEventHandler, setCookie } from 'h3'
2+
3+
export default defineEventHandler((e) => {
4+
setCookie(e, 'auth', 'logged-in', {
5+
maxAge: 60 * 60 * 24, // 1 day
6+
path: '/',
7+
})
8+
return { success: true }
9+
})

.playground/server/api/logout.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { defineEventHandler, deleteCookie } from 'h3'
2+
3+
export default defineEventHandler((e) => {
4+
deleteCookie(e, 'auth')
5+
return { success: true }
6+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createError, defineEventHandler, getCookie } from 'h3'
2+
3+
export default defineEventHandler((e) => {
4+
if (e.path.startsWith('/admin')) {
5+
const authCookie = getCookie(e, 'auth')
6+
if (!authCookie || authCookie !== 'logged-in') {
7+
throw createError({
8+
statusCode: 403,
9+
statusMessage: 'Forbidden - Please login first',
10+
})
11+
}
12+
}
13+
})

client/app.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ const version = computed(() => {
6767
})
6868
6969
const metaTag = computed(() => {
70-
return `<meta name="robots" content="${pathDebugData.value?.rule}">`
70+
const content = pathDebugData.value?.rule || ''
71+
return `<meta name="robots" content="${content}">`
7172
})
7273
7374
const tab = useLocalStorage('nuxt-robots:tab', 'overview')

src/module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ ${types}
558558
declare module 'h3' {
559559
interface H3EventContext {
560560
robots: RobotsContext
561+
robotsProduction?: RobotsContext
561562
}
562563
}
563564

src/runtime/app/plugins/robot-meta.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export default defineNuxtPlugin({
55
setup() {
66
const event = useRequestEvent()
77
const ctx = event?.context?.robots
8+
const productionCtx = event?.context?.robotsProduction
89
// set from nitro, not available for internal routes
910
if (!ctx)
1011
return
@@ -14,6 +15,7 @@ export default defineNuxtPlugin({
1415
'name': 'robots',
1516
'content': () => ctx.rule || '',
1617
'data-hint': () => import.meta.dev && ctx.debug?.source ? [ctx.debug?.source, ctx.debug?.line].filter(Boolean).join(',') : undefined,
18+
'data-production-content': () => import.meta.dev && productionCtx?.rule ? productionCtx.rule : undefined,
1719
},
1820
],
1921
})

src/runtime/server/middleware/injectContext.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,12 @@ export default defineEventHandler(async (e) => {
1313
setHeader(e, 'X-Robots-Tag', robotConfig.rule)
1414
}
1515
e.context.robots = robotConfig
16+
17+
// also compute production config for devtools
18+
if (import.meta.dev) {
19+
const productionRobotConfig = getPathRobotConfig(e, { skipSiteIndexable: true })
20+
setHeader(e, 'X-Robots-Production', productionRobotConfig.rule)
21+
e.context.robotsProduction = productionRobotConfig
22+
}
1623
}
1724
})

src/runtime/server/routes/__robots__/debug-path.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,60 @@
11
import { defineEventHandler, getQuery } from 'h3'
22
import { withQuery } from 'ufo'
3+
import { getPathRobotConfig } from '../../composables/getPathRobotConfig'
34

45
export default defineEventHandler(async (e) => {
56
const query = getQuery(e)
67
const path = query.path as string
8+
const isMockProduction = Boolean(query.mockProductionEnv)
79
delete query.path
8-
// we have to fetch the path to know for sure
9-
const res = await $fetch.raw(withQuery(path, query))
10-
const html = res._data
11-
const robotsHeader = String(res.headers.get('x-robots-tag'))
12-
// get robots meta tag <meta name="robots" content="noindex, nofollow" data-hint="useRobotsRule">
13-
// <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
14-
const robotsMeta = String(html).match(/<meta[^>]+name=["']robots["'][^>]+content=["']([^"']+)["'](?:[^>]+data-hint=["']([^"']+)["'])?[^>]*>/i)
15-
const [, robotsContent = null, robotsHint = null] = robotsMeta || []
10+
11+
let robotsHeader: string | null = null
12+
let robotsContent: string | null = null
13+
let robotsHint: string | null = null
14+
15+
// try to fetch the page to get actual rendered meta tag
16+
const res = await $fetch.raw(withQuery(path, query)).catch(() => null)
17+
if (res) {
18+
const html = res._data
19+
robotsHeader = String(res.headers.get('x-robots-tag'))
20+
21+
// if mocking production, use production values from headers/meta
22+
if (isMockProduction) {
23+
const productionHeader = res.headers.get('x-robots-production')
24+
if (productionHeader) {
25+
robotsHeader = String(productionHeader)
26+
}
27+
// extract production content from data-production-content attribute
28+
const productionMeta = String(html).match(/<meta[^>]+name=["']robots["'][^>]+data-production-content=["']([^"']+)["'](?:[^>]+data-hint=["']([^"']+)["'])?[^>]*>/i)
29+
if (productionMeta) {
30+
[, robotsContent = null, robotsHint = null] = productionMeta
31+
}
32+
}
33+
34+
// if not mocking production or no production values found, use regular values
35+
if (!robotsContent) {
36+
// get robots meta tag <meta name="robots" content="noindex, nofollow" data-hint="useRobotsRule">
37+
// <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
38+
const robotsMeta = String(html).match(/<meta[^>]+name=["']robots["'][^>]+content=["']([^"']+)["'](?:[^>]+data-hint=["']([^"']+)["'])?[^>]*>/i)
39+
if (robotsMeta) {
40+
[, robotsContent = null, robotsHint = null] = robotsMeta
41+
}
42+
}
43+
}
44+
45+
// fallback to computed config if fetch failed or no meta tag found
46+
if (!robotsContent) {
47+
const robotConfig = getPathRobotConfig(e, {
48+
path,
49+
skipSiteIndexable: isMockProduction,
50+
})
51+
robotsContent = robotConfig.rule
52+
robotsHint = robotConfig.debug?.source || null
53+
if (!robotsHeader) {
54+
robotsHeader = robotConfig.rule
55+
}
56+
}
57+
1658
const [source, line] = robotsHint ? robotsHint.split(',') : [null, null]
1759
return {
1860
rule: robotsContent,

0 commit comments

Comments
 (0)