Summary
PageMeta.layout is part of the public type and appears in definePageMeta's own documentation example, but no code path reads it. A page that declares definePageMeta({ layout: 'app' }) type-checks, looks like the idiomatic declaration, and renders with no layout at all. Layout selection in practice has exactly three sources — @extends/@layout, an auto-discovered _layout.stx, and defaultLayout — and none of them consult page meta. Separately, naming a layout that does not exist is only a runtime warning, so the same no-layout render is what an author gets from a typo.
Impact (real app)
analyticshq has 33 views on @extends and two pages that reached for a page-meta-style declaration instead. resources/views/dashboard.stx:1067 and resources/views/account.stx:86 both carry:
<meta name="stx-layout" content="app">That is a hand-forged copy of the marker stx normally emits (process.ts:504-517), written because there was no working typed way to say "this page belongs to the app layout group" — definePageMeta({ layout: 'app' }) compiles and does nothing, so it was never a candidate. resources/layouts/ contains only auth.stx and marketing.stx; the string "app" names a group with no layout file behind it, and neither page uses @extends, so both render unlayouted while asserting a layout to the router.
The consequence we documented in resources/layouts/auth.stx:9-12 while migrating three other pages off the same pattern:
Replaces the hand-written
<meta name="stx-layout" content="auth">that login, register and sites/new each carried. That meta was a hint the pages asserted about themselves; the FILENAME is what actually names a layout group, so the two could disagree and nothing would say so.
The router's full-document-vs-fragment swap decision hinges on that group string (packages/router/src/client.ts:436-440), so a page asserting a group it does not belong to is a real routing hazard, not a cosmetic one.
Evidence in stx
Declared and documented — packages/stx/src/head.ts:
107 export interface PageMeta {
108 title?: string
109 description?: string
111 status?: number
112 layout?: string | false
113 middleware?: string | string[]
118 validate?: (route: { params: Record<string, string> }) => boolean | Promise<boolean>
119 [key: string]: unknown
120 }340 * definePageMeta({
341 * title: 'Dashboard',
342 * layout: 'admin',
343 * middleware: ['auth', 'admin'],
344 * keepAlive: true
345 * })
348 export function definePageMeta(config: PageMeta): void {
349 currentPageMeta = { ...currentPageMeta, ...config }Never read. grep -rn "pageMeta\b" packages/stx/src packages/bun-plugin/src (excluding head.ts) returns only:
packages/stx/src/ssg.ts:1006-1015—pageMeta.middlewareonlypackages/stx/src/dev-server/serve-app.ts:681-714—pageMeta.validateandpageMeta.middlewareonlypackages/bun-plugin/src/serve.ts:2342—siteConfig.pages, a different thing entirely
Actual layout selection, packages/stx/src/process.ts:
802 const layoutMatch = output.match(/@(?:layout|extends)\(\s*['"]([^'"]+)['"]\s*\)/)
804 layoutPath = layoutMatch[1]
848 layoutPath = autoLayoutPath // _layout.stx walked up the tree
857 layoutPath = defaultLayoutFile // defaultLayout in layoutsDirlayoutPath is never assigned from page meta. And an unresolvable name degrades quietly — process.ts:1034-1043:
if (!layoutFullPath) {
const warning = `Layout not found: ${layoutPath} (referenced from ${filePath})`
errorLogger.log(new Error(warning), { layoutPath, filePath }, 'warning')
if (resolvedOptions.debug) { throw new StxRuntimeError(warning, filePath) }
return output // page renders with no layout
}Prior art
- Nuxt —
definePageMeta({ layout: 'app' }), wherelayoutis typed as a union of the layout files on disk, so a wrong name is a type error before it is a runtime one. - Next — nested
layout.tsxby directory; there is no second mechanism that can disagree. - Blade —
@extendsonly.
Ask
- Read
PageMeta.layoutin the render pipeline: extract it before layout resolution (the same cheap source scanserve.ts:1136-1160already does formiddleware) and let it setlayoutPathinprocess.ts:802-807. Define the precedence explicitly — suggested:@extends>definePageMeta({ layout })> auto_layout.stx>defaultLayout, withlayout: falseequivalent to@nolayout. - If it is not going to be implemented, remove
layoutfromPageMeta(head.ts:112) and from the doc example at:340-345. A typed field that silently does nothing is worse than an absent one. - Generate a union type of the layouts on disk so a wrong name is a type error, matching Nuxt.
- Make an unresolvable layout name fail the build rather than warn-and-render-unlayouted (
process.ts:1034-1043), or at minimum log aterrorseverity with the list of layouts that do exist. - Document that
<meta name="stx-layout">is stx output (emitted atprocess.ts:504-517) and not an authoring surface — hand-writing it desynchronises the router's layout-group check atpackages/router/src/client.ts:436-440.
Related: #1849 (same shape: PageMeta.middleware/validate declared and never read on the client path), #1798 (same shape: htmlAttrs typed and never read — fixed)