Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .playground/pages/admin.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<div>
<h1>Admin Page</h1>
<p>This is a protected admin page. You should see robots meta tag with content.</p>
<div>
<NuxtLink to="/">
Back to home
</NuxtLink>
</div>
</div>
</template>
26 changes: 26 additions & 0 deletions .playground/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
<script setup lang="ts">
async function login() {
await $fetch('/api/login')
location.reload()
}

async function logout() {
await $fetch('/api/logout')
location.reload()
}
</script>

<template>
<div>
<div style="margin-bottom: 20px;">
<h2>Auth Test</h2>
<button style="margin-right: 10px;" @click="login">
Login (set cookie)
</button>
<button @click="logout">
Logout (clear cookie)
</button>
</div>
<div>
<NuxtLink to="/admin">
Admin page - requires auth
</NuxtLink>
</div>
<div>
<NuxtLink to="/secret">
Secret page - not crawlable
Expand Down
9 changes: 9 additions & 0 deletions .playground/server/api/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineEventHandler, setCookie } from 'h3'

export default defineEventHandler((e) => {
setCookie(e, 'auth', 'logged-in', {
maxAge: 60 * 60 * 24, // 1 day
path: '/',
})
return { success: true }
})
6 changes: 6 additions & 0 deletions .playground/server/api/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineEventHandler, deleteCookie } from 'h3'

export default defineEventHandler((e) => {
deleteCookie(e, 'auth')
return { success: true }
})
13 changes: 13 additions & 0 deletions .playground/server/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createError, defineEventHandler, getCookie } from 'h3'

export default defineEventHandler((e) => {
if (e.path.startsWith('/admin')) {
const authCookie = getCookie(e, 'auth')
if (!authCookie || authCookie !== 'logged-in') {
throw createError({
statusCode: 403,
statusMessage: 'Forbidden - Please login first',
})
}
}
})
3 changes: 2 additions & 1 deletion client/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ const version = computed(() => {
})

const metaTag = computed(() => {
return `<meta name="robots" content="${pathDebugData.value?.rule}">`
const content = pathDebugData.value?.rule || ''
return `<meta name="robots" content="${content}">`
})

const tab = useLocalStorage('nuxt-robots:tab', 'overview')
Expand Down
1 change: 1 addition & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ ${types}
declare module 'h3' {
interface H3EventContext {
robots: RobotsContext
robotsProduction?: RobotsContext
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/runtime/app/plugins/robot-meta.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default defineNuxtPlugin({
setup() {
const event = useRequestEvent()
const ctx = event?.context?.robots
const productionCtx = event?.context?.robotsProduction
// set from nitro, not available for internal routes
if (!ctx)
return
Expand All @@ -14,6 +15,7 @@ export default defineNuxtPlugin({
'name': 'robots',
'content': () => ctx.rule || '',
'data-hint': () => import.meta.dev && ctx.debug?.source ? [ctx.debug?.source, ctx.debug?.line].filter(Boolean).join(',') : undefined,
'data-production-content': () => import.meta.dev && productionCtx?.rule ? productionCtx.rule : undefined,
},
],
})
Expand Down
7 changes: 7 additions & 0 deletions src/runtime/server/middleware/injectContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,12 @@ export default defineEventHandler(async (e) => {
setHeader(e, 'X-Robots-Tag', robotConfig.rule)
}
e.context.robots = robotConfig

// also compute production config for devtools
if (import.meta.dev) {
const productionRobotConfig = getPathRobotConfig(e, { skipSiteIndexable: true })
setHeader(e, 'X-Robots-Production', productionRobotConfig.rule)
e.context.robotsProduction = productionRobotConfig
}
}
})
58 changes: 50 additions & 8 deletions src/runtime/server/routes/__robots__/debug-path.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,60 @@
import { defineEventHandler, getQuery } from 'h3'
import { withQuery } from 'ufo'
import { getPathRobotConfig } from '../../composables/getPathRobotConfig'

export default defineEventHandler(async (e) => {
const query = getQuery(e)
const path = query.path as string
const isMockProduction = Boolean(query.mockProductionEnv)
delete query.path
// we have to fetch the path to know for sure
const res = await $fetch.raw(withQuery(path, query))
const html = res._data
const robotsHeader = String(res.headers.get('x-robots-tag'))
// get robots meta tag <meta name="robots" content="noindex, nofollow" data-hint="useRobotsRule">
// <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
const robotsMeta = String(html).match(/<meta[^>]+name=["']robots["'][^>]+content=["']([^"']+)["'](?:[^>]+data-hint=["']([^"']+)["'])?[^>]*>/i)
const [, robotsContent = null, robotsHint = null] = robotsMeta || []

let robotsHeader: string | null = null
let robotsContent: string | null = null
let robotsHint: string | null = null

// try to fetch the page to get actual rendered meta tag
const res = await $fetch.raw(withQuery(path, query)).catch(() => null)
if (res) {
const html = res._data
robotsHeader = String(res.headers.get('x-robots-tag'))

// if mocking production, use production values from headers/meta
if (isMockProduction) {
const productionHeader = res.headers.get('x-robots-production')
if (productionHeader) {
robotsHeader = String(productionHeader)
}
// extract production content from data-production-content attribute
const productionMeta = String(html).match(/<meta[^>]+name=["']robots["'][^>]+data-production-content=["']([^"']+)["'](?:[^>]+data-hint=["']([^"']+)["'])?[^>]*>/i)
if (productionMeta) {
[, robotsContent = null, robotsHint = null] = productionMeta
}
}

// if not mocking production or no production values found, use regular values
if (!robotsContent) {
// get robots meta tag <meta name="robots" content="noindex, nofollow" data-hint="useRobotsRule">
// <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
const robotsMeta = String(html).match(/<meta[^>]+name=["']robots["'][^>]+content=["']([^"']+)["'](?:[^>]+data-hint=["']([^"']+)["'])?[^>]*>/i)
if (robotsMeta) {
[, robotsContent = null, robotsHint = null] = robotsMeta
}
}
}

// fallback to computed config if fetch failed or no meta tag found
if (!robotsContent) {
const robotConfig = getPathRobotConfig(e, {
path,
skipSiteIndexable: isMockProduction,
})
robotsContent = robotConfig.rule
robotsHint = robotConfig.debug?.source || null
if (!robotsHeader) {
robotsHeader = robotConfig.rule
}
}

const [source, line] = robotsHint ? robotsHint.split(',') : [null, null]
return {
rule: robotsContent,
Expand Down