Skip to content
Open
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
5 changes: 4 additions & 1 deletion playground/pages/hydration.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ const isServer = import.meta.server ? 1 : 0

<template>
<div>
<p :data-test="isServer ? 'server' : 'client'" class="fzejkebkzjefbjkzebfkjzebfkjzebfzebfkjzebkfjbzekjjjjjjjjjjjjjjjjjjjjkfzebjkfbzkjebfjkjk">
<p
:data-test="isServer ? 'server' : 'client'"
class="fzejkebkzjefbjkzebfkjzebfkjzebfzebfkjzebkfjbzekjjjjjjjjjjjjjjjjjjjjkfzebjkfbzkjebfjkjk"
>
isServer: {{ isServer }} <span v-if="!isServer">test</span>
</p>

Expand Down
25 changes: 25 additions & 0 deletions playground/pages/prerender/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<template>
<div class="test">
Hello. Could this page be prerendered ? {{ shouldPrerender }}
</div>
</template>

<script setup lang="ts">
import { useNuxtApp, useState } from '#app'
import { onMounted } from 'vue'

const shouldPrerender = useState('shouldPrerender', () => true)

if (import.meta.server) {
// ssr context access will be detected by the prerender plugin
console.log(useNuxtApp().ssrContext?.event.context.shouldPrerender)
}

onMounted(() => {
// @ts-expect-error for demo purposes
if (typeof window.__NUXT_HINTS_SHOULD_PRERENDER__ !== 'undefined') {
// @ts-expect-error for demo purposes
shouldPrerender.value = window.__NUXT_HINTS_SHOULD_PRERENDER__
}
})
</script>
4 changes: 4 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export default defineNuxtModule<ModuleOptions>({
addPlugin(resolver.resolve('./runtime/third-party-scripts/plugin.client'))
addServerPlugin(resolver.resolve('./runtime/third-party-scripts/nitro.plugin'))

// prerender
addServerPlugin(resolver.resolve('./runtime/prerender/nitro.plugin'))
addPlugin(resolver.resolve('./runtime/prerender/plugin.server'))

nuxt.hook('prepare:types', ({ references }) => {
references.push({
types: resolver.resolve('./runtime/types.d.ts'),
Expand Down
20 changes: 20 additions & 0 deletions src/runtime/prerender/nitro.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { NitroAppPlugin } from 'nitropack'

export default <NitroAppPlugin> function nitroHintsPlugin(nitroApp) {
nitroApp.hooks.hook('render:before', ({ event }) => {
event.context.shouldPrerender = true
})

nitroApp.hooks.hook('render:html', (htmlContext, { event }) => {
if (event.context.shouldPrerender === false) {
htmlContext.bodyAppend.push(
`<script>window.__NUXT_HINTS_SHOULD_PRERENDER__ = false</script>`,
)
}
else {
htmlContext.bodyAppend.push(
`<script>window.__NUXT_HINTS_SHOULD_PRERENDER__ = true</script>`,
)
}
})
}
44 changes: 44 additions & 0 deletions src/runtime/prerender/plugin.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { defineNuxtPlugin } from '#imports'
import { getStackTraceLines } from './utils'

export default defineNuxtPlugin({
name: 'hints:prerender-detection',
setup(nuxtApp) {
const event = nuxtApp.ssrContext!.event

let watching = true

const ssrContext = nuxtApp.ssrContext
// Access to any property on ssrContext will mark the page as non-prerenderable
Object.defineProperty(nuxtApp, 'ssrContext', {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it depends what you're accessing. ssrContext.event.url would be fine, for example.

Copy link
Member Author

@huang-julien huang-julien Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any other property you think it is fine ? or should we check request headers and event.context acess only ?

get() {
if (watching && isUserLandCode()) {
// Mark as non-prerenderable
// we only want to do this when user-land code is being executed
// to avoid false positives from internal framework code
// it's better to be slightly overzealous here than miss actual user code
event.context.shouldPrerender = false
}
return ssrContext
},
})

nuxtApp.hook('app:rendered', () => {
watching = false
})
},
order: -100000,
})

/**
* Determine if the current execution context is user-land code
* by analyzing the stack trace.
* Should ignore the first line as the fn call is this function itself.
*/
function isUserLandCode(offset: number = 1): boolean {
const stack = getStackTraceLines()
const lines = stack.slice(2)
const line = lines[offset]
const isUserLand = !line?.includes('node_modules') && !line?.includes('node:internal')
return isUserLand
}
Comment on lines +38 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: undefined line is incorrectly classified as user-land code.

If line is undefined (e.g., when the stack is shorter than expected), the expression !line?.includes('node_modules') && !line?.includes('node:internal') evaluates to !undefined && !undefined = true && true = true, incorrectly marking the code as user-land.

πŸ› Proposed fix
 function isUserLandCode(offset: number = 1): boolean {
   const stack = getStackTraceLines()
   const lines = stack.slice(2)
   const line = lines[offset]
+  if (!line) {
+    return false
+  }
-  const isUserLand = !line?.includes('node_modules') && !line?.includes('node:internal')
+  const isUserLand = !line.includes('node_modules') && !line.includes('node:internal')
   return isUserLand
 }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function isUserLandCode(offset: number = 1): boolean {
const stack = getStackTraceLines()
const lines = stack.slice(2)
const line = lines[offset]
const isUserLand = !line?.includes('node_modules') && !line?.includes('node:internal')
return isUserLand
}
function isUserLandCode(offset: number = 1): boolean {
const stack = getStackTraceLines()
const lines = stack.slice(2)
const line = lines[offset]
if (!line) {
return false
}
const isUserLand = !line.includes('node_modules') && !line.includes('node:internal')
return isUserLand
}
πŸ€– Prompt for AI Agents
In `@src/runtime/prerender/plugin.server.ts` around lines 38 - 44, The function
isUserLandCode misclassifies an undefined stack line as user-land; modify
isUserLandCode so it first checks that the selected line exists (from
getStackTraceLines and the local variable line) and returns false if line is
undefined or empty, then only evaluate !line.includes('node_modules') &&
!line.includes('node:internal') to determine user-land; locate function
isUserLandCode and add an explicit existence check for line before applying
includes.

13 changes: 13 additions & 0 deletions src/runtime/prerender/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Get all stacktrace lines without the current file
* Don't use in build files if we ever goes into build mode nuxt hiints. Thank you.
*/
Comment on lines +1 to +4
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Typo in comment.

"hiints" should be "hints".

 /**
  * Get all stacktrace lines without the current file
- * Don't use in build files if we ever goes into build mode nuxt hiints. Thank you.
+ * Don't use in build files if we ever go into build mode nuxt hints. Thank you.
  */
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Get all stacktrace lines without the current file
* Don't use in build files if we ever goes into build mode nuxt hiints. Thank you.
*/
/**
* Get all stacktrace lines without the current file
* Don't use in build files if we ever go into build mode nuxt hints. Thank you.
*/
πŸ€– Prompt for AI Agents
In `@src/runtime/prerender/utils.ts` around lines 1 - 4, Fix the typo in the top
comment block that begins with "Get all stacktrace lines without the current
file" by replacing "hiints" with "hints" so the comment reads correctly; update
that docstring/comment text in the utils file (the block containing "Don't use
in build files if we ever goes into build mode nuxt hiints. Thank you.") to use
"hints".

export function getStackTraceLines(): string[] {
const stackObject: { stack: string } = {} as { stack: string }
Error.captureStackTrace(stackObject)

return stackObject.stack
.split('\n')
.slice(1)
.map(line => line.trim())
}
Loading