HTML Layout Plugin
staticbolt has a plugin that adds two special HTML tags for composing pages from reusable building blocks.
<layout>defines the structural shell of a page (e.g., HTML document wrapper, header, footer).<part>defines a self-contained chunk of page content (e.g., a button, a card, a nav).
Both tags work the same way under the hood. The difference is only semantic.
Plugin Setup
Register the plugin and optionally configure its options:
import { defineConfig } from "@staticbolt/core";import * as plugins from "@staticbolt/core/plugins";
export default defineConfig({ plugins: [ plugins.htmlLayoutPlugin({ tags: ["layout", "part"], // default layoutSourceAttribute: "src", // default dataSourceAttribute: "dataSource", // default dataJsonAttribute: "dataJson", // default inheritAttribute: "inherit", // default alwaysIncludeAttribute: "always-include", // default addAttributeAttribute: "add-attr", // default removeAttributeAttribute: "remove-attr", // default }), ],});| Option | Type | Default | Description |
|---|---|---|---|
tags | string[] | ['layout', 'part'] | Tag names registered as valid HTML elements. Each name is also the required file suffix. |
layoutSourceAttribute | string | 'src' | Attribute name used to point to the layout or part file. |
dataSourceAttribute | string | 'dataSource' | Attribute name used to point to an external JSON data file. |
dataJsonAttribute | string | 'dataJson' | Attribute name used to pass an inline JSON string as data. |
inheritAttribute | string | 'inherit' | Boolean attribute that bases a tag's data on the $data of the layout it sits in. |
alwaysIncludeAttribute | string | 'always-include' | Attribute name that opts a <script>, <style>, or <link rel="stylesheet"> tag out of deduplication. |
addAttributeAttribute | string | 'add-attr' | Prefix for conditionally adding boolean attributes. Append the target attribute name as a suffix. |
removeAttributeAttribute | string | 'remove-attr' | Prefix for conditionally removing boolean attributes. Append the target attribute name as a suffix. |
File Naming
Every layout and part file must end with its tag name:
| Tag | Required filename pattern | Example |
|---|---|---|
<layout> | *.layout.html | main.layout.html |
<part> | *.part.html | header.part.html |
Basic Usage
Reference a layout or part using its src attribute:
<layout src="main.layout.html"></layout>
<part src="header.part.html"></part><part src="footer.part.html"></part>Parts can be nested inside layouts, and inside other parts. There is no limit to the depth of nesting.
Placeholders
The {{ }} syntax evaluates any JavaScript expression. Every attribute passed to a layout or part is available as $data.<attribute>.
| Syntax | Behavior |
|---|---|
{{ $data.title }} | Attribute access: evaluates to the passed value |
{{ $data.subtitle ?? "" }} | Nullish coalescing: falls back to empty string |
{{ $data.theme ?? "default" }} | Nullish coalescing: falls back to a default |
{{ $data.count * 2 }} | Any JS expression is valid |
{{ $data.items.join(", ") }} | Method calls, array operations, etc. |
\{{ escaped }} | Escaped: rendered as literal {{ escaped }} |
With the Staticbolt language server, the expressions in *.layout.html and *.part.html files get completions, hover and type errors from the project's own TypeScript (7 or newer). $data is typed as Record<string, any> unless the file declares its props. The server also flags a layout tag without src, a src or dataSource that points nowhere, a src whose file is not named after the tag (<part> takes a .part.html file), a dataJson that is not a JSON object, and add-attr-* / remove-attr-* used outside a layout file.
Declaring Props
A <!--props --> comment at the top of a layout or part declares what its $data holds. Its body is TypeScript, and the type it declares must be named Props:
<!--props interface Props { title: string; description?: string; }-->
<h1>{{ $data.title }}</h1>$data is then typed as Props throughout that file. A typo like {{ $data.titel }} is an error rather than any. The declaration is editor-only. It is dropped when the file is loaded and never reaches the page.
Notes:
- It is a comment so that no HTML parser reads markup inside it. A
<in a type such asArray<string>stays a<, and formatters leave the body alone. Prettier will not format it either. - The word
propssits against the<!--. A comment merely starting with the word,<!-- props come from the page -->, is an ordinary comment. - Each file's declaration is its own. Two layouts can both declare
Propswithout colliding. interfaceandtypeboth work.- Only in
*.layout.htmland*.part.htmlfiles. Elsewhere it is a plain comment and stays in the output.
Formatting
Prettier's HTML parser has no idea what {{ }} is. It reads a placeholder as text. A long expression then gets wrapped at whitespace, and an expression that returns markup is reformatted as if it were part of the page.
prettier-plugin-jinja-template treats {{ }} as one opaque token, which is what these files need. Scaffolded projects have it already. To add it to an existing one:
npm i -D prettier-plugin-jinja-template.prettierrc
{ "plugins": ["prettier-plugin-jinja-template"], "overrides": [ { "files": ["*.layout.html", "*.part.html"], "options": { "parser": "jinja-template" } } ]}The plugin leaves the expression itself as written and formats the HTML around it. It does re-indent the inside of a multi-line placeholder, so a placeholder whose output is whitespace-sensitive (the contents of a <pre>, say) is worth keeping on one line.
Prettier's angular parser is the other option. It formats the expressions properly, including inside attribute values, but it rejects any placeholder that holds a tag, so {{ $data.x ? "<b>y</b>" : "" }} is a parse error. Use it only where no expression builds markup.
Example
page.html
<part src="button.part.html" label="Click me"></part>button.part.html
<button>{{ $data.label }}</button>Output
<button>Click me</button>Slots
Slots let you pass child content into a layout or part from the call site.
Default Slot
Declared in the layout/part file:
<slot />Any children not assigned to a named slot are placed here.
Named Slot
Declared in the layout/part file:
<slot name="header" />Assigned at the call site using the slot attribute:
<layout src="main.layout.html"> <h1 slot="header">Welcome</h1> <p>Page content here.</p></layout>Example
main.layout.html
<html> <body> <header> <slot name="header" /> </header> <main> <slot /> </main> </body></html>index.html
<layout src="main.layout.html"> <h1 slot="header">Welcome to My Site</h1> <p>This is the main page content.</p></layout>Output
<html> <body> <header> <h1>Welcome to My Site</h1> </header> <main> <p>This is the main page content.</p> </main> </body></html>Nested Props
A dotted attribute name is a path. car.type="sedan" sets car to { type: "sedan" }, and attributes that share a prefix build one object.
<part src="car.part.html" car.type="sedan" car.year="2020"></part>car.part.html
<p>{{ $data.car.type }} — {{ $data.car.year }}</p>Output
<p>sedan — 2020</p>The path merges into an object already there, whether it came from dataSource, dataJson or inherit, and leaves the rest of that object alone. Only the key it names is replaced.
Data Source
A layout or part can load props from an external JSON file using the dataSource attribute (configurable via dataSourceAttribute). The JSON file must be an object. Its properties are merged into $data alongside any inline attributes.
Attribute values take priority. If an inline attribute has the same name as a key in the JSON file, the attribute wins.
<part src="card.part.html" dataSource="card-data.json"></part>{ "title": "Hello", "theme": "dark"}<div class="{{ $data.theme }}"> <h2>{{ $data.title }}</h2></div>Output
<div class="dark"> <h2>Hello</h2></div>Overriding Data Source Values
Inline attributes override matching keys from the JSON file:
<part src="card.part.html" dataSource="card-data.json" title="Override"></part>Output
<div class="dark"> <h2>Override</h2></div>With Markdown Frontmatter
dataSource can also be set from frontmatter. Frontmatter props still follow the same priority rule. A frontmatter key overrides the same key in the JSON file.
---layout: "main.layout.html"dataSource: "page-data.json"title: "Override Title"---Inline JSON Data
A layout or part can receive props as an inline JSON string using the dataJson attribute (configurable via dataJsonAttribute). The value is parsed and merged into $data.
Priority order when keys overlap: inline attributes > dataJson > dataSource file.
<part src="card.part.html" dataJson='{"title": "Hello", "theme": "dark"}'></part><div class="{{ $data.theme }}"> <h2>{{ $data.title }}</h2></div>Output
<div class="dark"> <h2>Hello</h2></div>Overriding Inline JSON Values
Inline attributes override matching keys from the dataJson value:
<part src="card.part.html" dataJson='{"title": "Hello", "theme": "dark"}' title="Override"></part>Output
<div class="dark"> <h2>Override</h2></div>Inheriting Data
A tag inside a layout or part file can take that file's $data as the base of its own with the inherit boolean attribute (configurable via inheritAttribute). The page passes data to the layout once, and the layout hands it down without repeating every attribute.
Priority order when keys overlap: inline attributes > dataJson > dataSource file > inherited.
index.html
<layout src="document.layout.html" title="Home" theme="dark"></layout>document.layout.html
<part inherit src="header.part.html" theme="light"></part><slot />header.part.html
<header class="{{ $data.theme }}">{{ $data.title }}</header>Output
<header class="light">Home</header>Inheritance chains as long as every hop opts in. A part with inherit inside header.part.html sees the page's data through both hops. A tag without inherit breaks the chain, and the tags inside its file only see what it sets explicitly. On a page, there is nothing to inherit, so the attribute does nothing there.
Boolean Attribute Manipulation
Standard placeholder syntax ({{ }}) sets an attribute's value, but boolean HTML attributes like open, disabled, and checked are controlled by their presence. The value is irrelevant. add-attr-* and remove-attr-* let you control that presence conditionally. Append the target attribute name as a suffix.
The suffix is the attribute to add or remove. The value is a JS expression, evaluated after placeholder resolution. Any non-empty, non-"false" result counts as truthy.
add-attr-*
Adds the named attribute when the expression is truthy.
<dialog add-attr-open="{{ $data.open }}"> <slot /></dialog>Usage
<part src="dialog.part.html" open="true"></part>Output
<dialog open>…</dialog>When $data.open is falsy the attribute is not added.
remove-attr-*
Removes the named attribute when the expression is truthy. Useful when a template needs an attribute present by default but removed at render time under some condition.
<button disabled remove-attr-disabled="{{ $data.enabled }}">{{ $data.label }}</button>Usage
<part src="button.part.html" label="Submit" enabled="true"></part>Output
<button>Submit</button>Multiple Attributes
Both add-attr-* and remove-attr-* can appear multiple times on the same element and can be combined with each other.
<input add-attr-disabled="{{ $data.disabled }}" add-attr-required="{{ $data.required }}" add-attr-readonly="{{ $data.readonly }}"/>Note on Truthy Values
Because all resolved placeholder values are strings, "false" is explicitly treated as falsy even though it is a non-empty string. A stringified boolean from an attribute or frontmatter therefore works as expected, and disabled="false" will not add or remove the attribute.
Styles and Scripts
If a layout or part contains <style> or <script> tags and is inlined multiple times on the same page, staticbolt deduplicates them. Each block is emitted only once in the final output.
Always Include
To opt a specific <script>, <style>, or <link rel="stylesheet"> tag out of deduplication, add the always-include boolean attribute. Tagged blocks are emitted once per layout or part instance, regardless of how many times it appears on the page.
<script always-include src="./chart-init.js"></script>This is useful for scripts that read instance-specific data (such as attributes or a dataSource) and must run separately for each use.
The attribute name is configurable via the alwaysIncludeAttribute option.
Markdown Pages
Markdown pages use frontmatter to point to a layout and pass props into it. The key used to reference the file must match the configured tag name (e.g., layout for <layout>). All other frontmatter keys are forwarded as props and read as $data.<key>, the same as attribute-passed props in HTML.
example.md
---layout: "main.layout.html"title: "Welcome"subtitle: "Getting Started"---
Page content goes here.main.layout.html
<h1>{{ $data.title }}</h1><p>{{ $data.subtitle }}</p><slot />Plugin Options
tags
- Type:
string[] - Default:
["layout", "part"]
The tag names registered as valid HTML elements. Each name is also the required file suffix. "layout" means files must end in .layout.html, and "part" means .part.html.
htmlLayoutPlugin({ tags: ["template", "component"],});layoutSourceAttribute
- Type:
string - Default:
"src"
The attribute name used to point to the layout or part file.
htmlLayoutPlugin({ layoutSourceAttribute: "href",});dataSourceAttribute
- Type:
string - Default:
"dataSource"
The attribute name used to point to an external JSON file whose properties are merged into $data. Inline attributes take priority over file keys.
htmlLayoutPlugin({ dataSourceAttribute: "json-src",});dataJsonAttribute
- Type:
string - Default:
"dataJson"
The attribute name used to pass an inline JSON string as data. The value is parsed and merged into $data. Priority order when keys overlap: inline attributes > dataJson > dataSource file.
htmlLayoutPlugin({ dataJsonAttribute: "props",});inheritAttribute
- Type:
string - Default:
"inherit"
The boolean attribute that gives a tag inside a layout file the $data of that layout as the base of its own $data. Priority order when keys overlap: inline attributes > dataJson > dataSource file > inherited. Inheritance chains through every tag that sets it, and stops at the first that does not.
htmlLayoutPlugin({ inheritAttribute: "extends",});alwaysIncludeAttribute
- Type:
string - Default:
"always-include"
The attribute name that opts a <script>, <style>, or <link rel="stylesheet"> tag out of deduplication. By default, duplicate scripts and styles are removed when a layout or part is used more than once on the same page. Marking a tag with this boolean attribute forces it to be emitted once per instance.
htmlLayoutPlugin({ alwaysIncludeAttribute: "no-dedup",});addAttributeAttribute
- Type:
string - Default:
"add-attr"
The prefix for conditionally adding boolean attributes. Append the target attribute name as a suffix, for example add-attr-open or add-attr-disabled. The value is a JS expression. If it evaluates to a non-empty, non-"false" string, the named attribute is added to the element.
htmlLayoutPlugin({ addAttributeAttribute: "attr-add",});removeAttributeAttribute
- Type:
string - Default:
"remove-attr"
The prefix for conditionally removing boolean attributes. Append the target attribute name as a suffix, for example remove-attr-disabled or remove-attr-hidden. The value is a JS expression. If it evaluates to a non-empty, non-"false" string, the named attribute is removed from the element.
htmlLayoutPlugin({ removeAttributeAttribute: "attr-remove",});