diff --git a/docs/content/2.guide/3.directory-structure/7.layouts.md b/docs/content/2.guide/3.directory-structure/7.layouts.md
index 9a1f93acc8c..86264e0bf01 100644
--- a/docs/content/2.guide/3.directory-structure/7.layouts.md
+++ b/docs/content/2.guide/3.directory-structure/7.layouts.md
@@ -121,9 +121,8 @@ You can also use a ref or computed property for your layout.
+
+
+
+
+ Back to home
+
+
+
diff --git a/packages/nuxt/src/app/composables/index.ts b/packages/nuxt/src/app/composables/index.ts
index b3a31a9f43d..2fd1c252c8a 100644
--- a/packages/nuxt/src/app/composables/index.ts
+++ b/packages/nuxt/src/app/composables/index.ts
@@ -10,6 +10,6 @@ export type { FetchResult, UseFetchOptions } from './fetch'
export { useCookie } from './cookie'
export type { CookieOptions, CookieRef } from './cookie'
export { useRequestHeaders, useRequestEvent, setResponseStatus } from './ssr'
-export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, navigateTo, useRoute, useActiveRoute, useRouter } from './router'
+export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, setPageLayout, navigateTo, useRoute, useActiveRoute, useRouter } from './router'
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'
export { preloadComponents, prefetchComponents } from './preload'
diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts
index 188d8c0bca9..eb964e4dacf 100644
--- a/packages/nuxt/src/app/composables/router.ts
+++ b/packages/nuxt/src/app/composables/router.ts
@@ -2,7 +2,7 @@ import { getCurrentInstance, inject } from 'vue'
import type { Router, RouteLocationNormalizedLoaded, NavigationGuard, RouteLocationNormalized, RouteLocationRaw, NavigationFailure, RouteLocationPathRaw } from 'vue-router'
import { sendRedirect } from 'h3'
import { hasProtocol, joinURL, parseURL } from 'ufo'
-import { useNuxtApp, useRuntimeConfig } from '#app'
+import { useNuxtApp, useRuntimeConfig, useState } from '#app'
export const useRouter = () => {
return useNuxtApp()?.$router as Router
@@ -114,3 +114,20 @@ export const abortNavigation = (err?: Error | string) => {
}
return false
}
+
+export const setPageLayout = (layout: string) => {
+ if (process.server) {
+ useState('_layout').value = layout
+ }
+ const nuxtApp = useNuxtApp()
+ const inMiddleware = isProcessingMiddleware()
+ if (inMiddleware || process.server || nuxtApp.isHydrating) {
+ const unsubscribe = useRouter().beforeResolve((to) => {
+ to.meta.layout = layout
+ unsubscribe()
+ })
+ }
+ if (!inMiddleware) {
+ useRoute().meta.layout = layout
+ }
+}
diff --git a/packages/nuxt/src/app/plugins/router.ts b/packages/nuxt/src/app/plugins/router.ts
index 18954954549..64bf5d50a6c 100644
--- a/packages/nuxt/src/app/plugins/router.ts
+++ b/packages/nuxt/src/app/plugins/router.ts
@@ -1,7 +1,7 @@
import { reactive, h } from 'vue'
import { parseURL, stringifyParsedURL, parseQuery, stringifyQuery, withoutBase, isEqual, joinURL } from 'ufo'
import { createError } from 'h3'
-import { defineNuxtPlugin, clearError, navigateTo, showError, useRuntimeConfig } from '..'
+import { defineNuxtPlugin, clearError, navigateTo, showError, useRuntimeConfig, useState } from '..'
import { callWithNuxt } from '../nuxt'
// @ts-ignore
import { globalMiddleware } from '#build/middleware'
@@ -218,9 +218,13 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
named: {}
}
+ const initialLayout = useState('_layout')
nuxtApp.hooks.hookOnce('app:created', async () => {
router.beforeEach(async (to, from) => {
to.meta = reactive(to.meta || {})
+ if (nuxtApp.isHydrating) {
+ to.meta.layout = initialLayout.value ?? to.meta.layout
+ }
nuxtApp._processingMiddleware = true
const middlewareEntries = new Set([...globalMiddleware, ...nuxtApp._middleware.global])
diff --git a/packages/nuxt/src/imports/presets.ts b/packages/nuxt/src/imports/presets.ts
index 6985af5b27e..b76ebe7a624 100644
--- a/packages/nuxt/src/imports/presets.ts
+++ b/packages/nuxt/src/imports/presets.ts
@@ -36,6 +36,7 @@ const appPreset = defineUnimportPreset({
'useRequestHeaders',
'useRequestEvent',
'setResponseStatus',
+ 'setPageLayout',
'useRouter',
'useRoute',
'useActiveRoute',
diff --git a/packages/nuxt/src/pages/runtime/router.ts b/packages/nuxt/src/pages/runtime/router.ts
index cb12db1f574..32163890039 100644
--- a/packages/nuxt/src/pages/runtime/router.ts
+++ b/packages/nuxt/src/pages/runtime/router.ts
@@ -9,7 +9,7 @@ import {
import { createError } from 'h3'
import { withoutBase, isEqual } from 'ufo'
import NuxtPage from './page'
-import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, showError, clearError, navigateTo, useError } from '#app'
+import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, showError, clearError, navigateTo, useError, useState } from '#app'
// @ts-ignore
import routes from '#build/routes'
// @ts-ignore
@@ -114,8 +114,12 @@ export default defineNuxtPlugin(async (nuxtApp) => {
callWithNuxt(nuxtApp, showError, [error])
}
+ const initialLayout = useState('_layout')
router.beforeEach(async (to, from) => {
to.meta = reactive(to.meta)
+ if (nuxtApp.isHydrating) {
+ to.meta.layout = initialLayout.value ?? to.meta.layout
+ }
nuxtApp._processingMiddleware = true
type MiddlewareDef = string | NavigationGuard
diff --git a/test/basic.test.ts b/test/basic.test.ts
index 893f40ef7ce..497aa9c6425 100644
--- a/test/basic.test.ts
+++ b/test/basic.test.ts
@@ -273,6 +273,16 @@ describe('layouts', () => {
expect(html).toContain('with-layout.vue')
expect(html).toContain('Custom Layout:')
})
+ it('should work with a dynamically set layout', async () => {
+ const html = await $fetch('/with-dynamic-layout')
+
+ // Snapshot
+ // expect(html).toMatchInlineSnapshot()
+
+ expect(html).toContain('with-dynamic-layout')
+ expect(html).toContain('Custom Layout:')
+ await expectNoClientErrors('/with-dynamic-layout')
+ })
})
describe('reactivity transform', () => {
diff --git a/test/fixtures/basic/middleware/sets-layout.ts b/test/fixtures/basic/middleware/sets-layout.ts
new file mode 100644
index 00000000000..72985a38dcb
--- /dev/null
+++ b/test/fixtures/basic/middleware/sets-layout.ts
@@ -0,0 +1,4 @@
+export default defineNuxtRouteMiddleware(async () => {
+ await new Promise(resolve => setTimeout(resolve, 10))
+ setPageLayout('custom')
+})
diff --git a/test/fixtures/basic/pages/with-dynamic-layout.vue b/test/fixtures/basic/pages/with-dynamic-layout.vue
new file mode 100644
index 00000000000..6e2d5895489
--- /dev/null
+++ b/test/fixtures/basic/pages/with-dynamic-layout.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
with-dynamic-layout.vue
+
+