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
237 changes: 119 additions & 118 deletions README.md

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions docs/v12-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,42 @@ This is additive for most sites. If a layout module already exported a named `va

---

## Declare nested layouts with parentLayout

Layouts can now export a static `parentLayout` name instead of importing and invoking their parent render function.
Pages still select the innermost layout through `vars.layout`.
`parentLayout` is an optional named string export, not a field in `vars`, an import path, or a callback.
Omitting it leaves the selected layout without a parent; a non-root layout is not automatically wrapped by `root`.
See the [layout module reference](../README.md#layout-module-exports) and [nested-layout declaration contract](../README.md#declaring-nested-layouts) for name resolution, validation, rendering order, and rebuild behavior.

```ts
// article.layout.ts
export const parentLayout = 'root'
export const vars = { showSidebar: true }

export default function articleLayout ({ children }) {
return `<article>${children}</article>`
}
```

DOMStack renders from the page outward and passes intermediate values unchanged between layouts.
All renderers receive the final resolved vars, with precedence:

```text
builder/frontmatter > page.vars.* > inner layout vars > outer layout vars > global vars > defaults
```

Styles and client entry points are included automatically in default, global, outer-layout, inner-layout, page order.
Watch mode follows the resolved chain and each layout's ordinary imported helpers for both source-backed and generated pages.
Missing parents, invalid parent names, and cycles fail the build.

Existing single layouts and manual function composition from earlier versions continue to work.
Migrate manually nested layouts to `parentLayout` so DOMStack can follow their full dependency chain for reliable rebuilds and manage their assets automatically.
To migrate a manually nested layout, replace its parent call with `parentLayout`, return only its own wrapper, and remove explicit imports of the parent's layout CSS and client.
Move manually merged defaults to the appropriate layout's `vars` export.
Do not retain both the parent function call and `parentLayout`, because that renders the parent twice.
Manual composition remains responsible for its own vars, asset imports, and argument forwarding.

## Keep layout dependencies explicit

DOMStack only installs dependencies for its bundled defaults.
Expand Down
12 changes: 5 additions & 7 deletions examples/basic/src/layouts/child.layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import type { LayoutFunction } from '@domstack/static/types.js'
import { html, raw, render } from 'fragtml'
import type { HtmlResult } from 'fragtml/types.js'

import defaultRootLayout from './root.layout.ts'
import type { PageVars } from './root.layout.ts'

const articleLayout: LayoutFunction<PageVars, string | HtmlResult, string> = (args) => {
const { children, ...rest } = args
const wrappedChildren = render(html`
export const parentLayout = 'root'

const articleLayout: LayoutFunction<PageVars, string | HtmlResult, string> = ({ children, vars }) => {
return render(html`
<article class="bc-article h-entry" itemscope itemtype="http://schema.org/NewsArticle">

<h1>${rest.vars.title}</h1>
<h1>${vars.title}</h1>

<section class="e-content" itemprop="articleBody">
${typeof children === 'string'
Expand All @@ -20,8 +20,6 @@ const articleLayout: LayoutFunction<PageVars, string | HtmlResult, string> = (ar
</section>
</article>
`)

return defaultRootLayout({ children: wrappedChildren, ...rest })
}

export default articleLayout
50 changes: 33 additions & 17 deletions examples/blog/src/blog/2024/second-post/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,52 @@ tags:

# Layouts All the Way Down

Domstack layouts are just functions. The `post` layout is a TypeScript function that
receives `children` (the rendered page content), wraps it in article markup, and
delegates to the `root` layout for the full HTML shell:
Domstack layouts are functions with an explicit parent declaration.
The `post` layout receives `children` (the rendered page content) and returns its article markup.
It exports `parentLayout = 'root'` so Domstack adds the full HTML shell around that result.
Here is a simplified `post.layout.ts`:

```ts
const postLayout: LayoutFunction<PostVars> = (args) => {
const { children, ...rest } = args
const wrappedChildren = render(html`
import { html, raw, render } from 'fragtml'
import type { HtmlResult } from 'fragtml/types.js'
import type { LayoutFunction } from '@domstack/static/types.js'
import type { RootVars } from './root.layout.ts'

export const parentLayout = 'root'

const postLayout: LayoutFunction<RootVars, string | HtmlResult, string> = ({ children, vars }) => {
return render(html`
<article class="h-entry">
<header>...</header>
<div class="e-content">${children}</div>
<h1>${vars.title}</h1>
<div class="e-content">
${typeof children === 'string' ? raw(children) : children}
</div>
</article>
`)
return rootLayout({ ...rest, children: wrappedChildren })
}

export default postLayout
```

No magic inheritance, no template partials, no special syntax. Just function composition.
The page still selects its innermost layout with `layout: post` in frontmatter.
Domstack resolves the chain and renders `root(post(page()))`, with each layout running once.
Each parent can declare another parent; a layout without `parentLayout` ends the chain.
Do not also import and call the parent render function when declaring `parentLayout`, or the parent will render twice.

Manual function composition remains supported, but `parentLayout` lets Domstack manage ancestor defaults, assets, and rebuild dependencies automatically.

## Styles follow the same pattern
## Styles follow the declared chain

`post.layout.css` imports `root.layout.css` with a plain CSS `@import`. esbuild
bundles them together. Each layout advertises its own stylesheet and client script,
and domstack injects the right ones automatically based on which layout a page uses.
`post.layout.css` contains only the post layout's styles; it does not need to import `root.layout.css`.
Domstack includes the styles and client entry points for every layout in the resolved chain, ordered from the outermost parent to the innermost child, between global and page assets.
Changing an ancestor layout or one of its statically imported helpers rebuilds the pages that use that chain.

## The `vars` merge order

```
{ ...globalVars, ...globalDataVars, ...pageVars, ...builderVars }
{ ...globalVars, ...globalDataVars, ...rootLayoutVars, ...postLayoutVars, ...pageVars, ...builderVars }
Comment thread
bcomnes marked this conversation as resolved.
```

`globalDataVars` sits between global and page vars, so `global.data.ts` output is
available everywhere but can be overridden per-page if needed.
Layout defaults merge from the outermost parent to the innermost child.
Page vars override those defaults, and builder vars, including Markdown frontmatter, take precedence last.
`global.data.ts` output is merged into vars before layout defaults.
2 changes: 0 additions & 2 deletions examples/blog/src/layouts/post.layout.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@import './root.layout.css';

@layer domstack.layout {
/* ── Post layout ── */

Expand Down
9 changes: 4 additions & 5 deletions examples/blog/src/layouts/post.layout.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { html, raw, render } from 'fragtml'
import type { HtmlResult } from 'fragtml/types.js'
import type { LayoutFunction } from '@domstack/static/types.js'
import rootLayout from './root.layout.ts'
import type { RootVars } from './root.layout.ts'

export const parentLayout = 'root'

export type PostVars = RootVars & {
publishDate?: string
updatedDate?: string
Expand All @@ -16,13 +17,13 @@ export type PostVars = RootVars & {
* schema.org/h-entry microformats, publish date, author card, tag list.
*/
const postLayout: LayoutFunction<PostVars, string | HtmlResult, string> = (args) => {
const { children, page, pages, ...rest } = args
const { children, page } = args
const { vars } = args

const publishDate = vars.publishDate ? new Date(vars.publishDate) : null
const updatedDate = vars.updatedDate ? new Date(vars.updatedDate) : null

const wrappedChildren = render(html`
return render(html`
<article class="h-entry" itemscope itemtype="https://schema.org/BlogPosting">

<header class="post-header">
Expand Down Expand Up @@ -91,8 +92,6 @@ const postLayout: LayoutFunction<PostVars, string | HtmlResult, string> = (args)

</article>
`)

return rootLayout({ ...rest, page, pages, children: wrappedChildren })
}

export default postLayout
9 changes: 4 additions & 5 deletions examples/blog/src/layouts/year-index.layout.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { html, raw, render } from 'fragtml'
import type { HtmlResult } from 'fragtml/types.js'
import type { LayoutFunction } from '@domstack/static/types.js'
import rootLayout from './root.layout.ts'
import type { RootVars } from './root.layout.ts'
import type { BlogPost } from '../global.data.ts'

export const parentLayout = 'root'

export type YearIndexVars = RootVars & {
posts?: BlogPost[]
}
Expand All @@ -14,9 +15,9 @@ export type YearIndexVars = RootVars & {
* collection and `blog-indexes.pages.ts` assigns it to a generated page.
*/
const yearIndexLayout: LayoutFunction<YearIndexVars, string | HtmlResult, string> = (args) => {
const { children, ...rest } = args
const { children } = args

const wrappedChildren = render(html`
return render(html`
<div>
<h1>${args.vars.title}</h1>
<ul class="post-list">
Expand All @@ -43,8 +44,6 @@ const yearIndexLayout: LayoutFunction<YearIndexVars, string | HtmlResult, string
}
</div>
`)

return rootLayout({ ...rest, children: wrappedChildren })
}

export default yearIndexLayout
Loading