Themes
Inkstead Writer ships with a default theme. Add .plume files when you want to replace part of it with your own design.
Plume is the template language used by Inkstead Writer. This page focuses on how Writer loads themes and what data it passes to them. For the language itself, use the Plume syntax reference, component guide, resources guide, and behaviour guide.
Start From The Default Theme
Copy the default templates into your site:
$./inkstead-writer theme ejectExisting files are left alone. Use --force if you want to overwrite them.
You do not need to eject everything to customise a site. Any file you add under theme overrides the matching built-in file, and missing files continue to use the default theme.
Theme Folder
The recommended shape is:
theme/
layouts/
default.plume
pages/
404.plume
home.plume
category.plume
post.plume
page.plume
feed.xml.plume
feed.json.plume
components/
PostCard.plume
styles/
site.css
scripts/
site.plumeIf you want to keep themes somewhere else, set theme.path in Site Configuration.
Page Templates
Writer looks for these page templates under theme/pages:
404.plumefor the not-found page written to/404.html.home.plumefor the homepage.category.plumefor category indexes.post.plumefor posts.page.plumefor standalone pages.feed.xml.plumefor RSS feeds.feed.json.plumefor JSON Feed.
Standalone pages can also use slug-specific templates. For example, content/pages/photos.md normally renders with page.plume, but theme/pages/photos.plume takes over that page when it exists.
content/pages/photos.md
theme/pages/photos.plume<h1>{page.title}</h1>
<div class="photo-grid">
@for post in photoPosts {
<a href="{post.urlPath}">
@image(post.firstImage, alt: post.alt | default(""))
</a>
}
</div>The not-found page receives a notFound object with title, description, and message fields.
Plume In Writer
Writer marks generated content HTML as safe, so {post.html}, {post.excerpt}, {page.html}, and {content} render as HTML without | raw. Ordinary strings still escape by default.
Use | raw only when you intentionally need to render your own trusted string as HTML.
Theme Commands
Check a theme without building the site:
$./inkstead-writer theme checkFormat templates:
$./inkstead-writer theme format$./inkstead-writer theme format --checkRun the language server for editor integrations:
$./inkstead-writer theme language-serverInkstead Writer includes Plume, so the generated ./inkstead-writer command uses the Plume version tied to that site. Standalone Plume CLI and editor details live in the Plume tooling docs.
Assets And Images
Use asset() for theme files that should be copied to the built site with a fingerprinted URL:
<img src="{asset('images/avatar.png')}" alt="Ivo">Relative asset paths are resolved relative to the template or component file first, then relative to the theme folder. Theme assets are emitted under /assets/plume/.
For images, prefer @image. Writer resolves the asset, fingerprints theme images, adds dimensions when it can read them, and defaults to loading="lazy" and decoding="async":
@image("images/avatar.png", alt: "Ivo", class: "avatar", sizes: "64px")Add widths: to generate responsive variants and a srcset automatically:
@image("images/hero.jpg", alt: "Coastal path", widths: [480, 960, 1440], sizes: "(min-width: 960px) 960px, 100vw")Site media references such as /media/photo.jpg keep their public media path. Theme-local images and responsive variants are copied to /assets/plume/.
For static files that should be copied without Plume processing, use passthrough assets in inkstead-writer.json. See Site Configuration.
Styles And Scripts
Plume templates can declare styles and scripts next to the markup that uses them. Writer extracts those resources into fingerprinted files under /assets/plume/ and injects them into the page.
@style(file: "styles/site.css")
@script(file: "scripts/site.plume")You can also write inline resource blocks:
@style(scoped) {
.photo-grid {
display: grid;
gap: 1rem;
}
}
@script {
let menu = page.query("#menu")
on ".menu-toggle".click {
menu.toggleClass("is-open")
}
}For the full resource model, see Plume resources.
Interactivity
Writer emits /assets/plume-runtime.js only on pages that use Plume state, state bindings, style bindings, browser actions, or enhanced navigation.
@state expanded = false
<button on:click="{expanded.toggle()}" aria-expanded="{expanded}">
{expanded ? "Hide" : "Show"} details
</button>
<section hidden?="{!expanded}" class:open="{expanded}">
{post.excerpt}
</section>Use @navigation in theme/layouts/default.plume when same-origin links should fetch and swap page content without a full reload:
@navigation(root: "main", viewTransitions: true, scroll: "top") {
on:beforeSwap {
page.addClass("is-leaving")
}
on:afterSwap {
page.removeClass("is-leaving")
}
}See the Plume behaviour guide for state, actions, measurement, viewport events, scripts, and navigation hooks.
Template Context
Templates receive:
siteconfigpostspagescategoriesphotoPostsdata, containing configured build-time JSON data sourcescollections.postscollections.pagescollections.categoriescollections.photoPostscollections.<name>, for custom Markdown collectionspaginationon index and category pagesposton post pagespageon page pagescategoryon category pagesnotFoundon the 404 pagemetanow, includingnow.year
meta includes title, canonicalUrl, and description. Post and page pages use an excerpt of their own content for meta.description; index and category pages use site.description.
Post objects include previous and next so themes can add post navigation.
photoPosts is intended for photography grids. It includes photo notes whose primary image is not a PNG, so screenshots and other PNG notes do not appear there by default.
Custom collections are exposed by folder name. For example, Markdown files in content/collections/books are available as collections.books:
@for book in collections.books {
<article>
<h2>{book.title}</h2>
<p>{book.author}</p>
{book.content}
</article>
}Configured data sources are exposed under data:
@for event in data.events {
<article>
<h2>{event.title}</h2>
<time datetime="{event.date}">{event.date}</time>
</article>
}See Site Configuration for configuring collections and data sources.
Layouts
If a template returns a full HTML document, Writer uses it as-is. Otherwise, Writer wraps the rendered content in a layout.
Override theme/layouts/default.plume to control the document shell.
Feed Templates
Writer writes RSS at /feed.xml and JSON Feed at /feed.json.
Override these files to customise them:
theme/pages/feed.xml.plume
theme/pages/feed.json.plumeBoth templates receive a feed object with:
titledescriptionurlpathitemscategory
Category feeds use the same templates with feed.category populated and feed.items scoped to that category.
RSS browser presentation is optional. In theme/pages/feed.xml.plume, declare @style and @script resources, then include feed.presentationScriptSrc where you want the browser presentation script to appear. RSS readers still receive RSS.
Footer Attribution
The default templates include a copyright notice and a small Powered by Inkstead Writer link. To remove the attribution without ejecting or editing the default templates, set:
{
"theme": {
"showPoweredBy": false
}
}Build Hooks
Use hooks when a theme needs generated assets before Writer copies passthrough files or resolves Plume asset references. A common use is bundling larger JavaScript modules or compiling CSS before referencing the output with @script(file:) or @style(file:).
{
"hooks": {
"beforeBuild": ["./build-theme-assets.sh"]
}
}This keeps bundling outside the engine while still making ./inkstead-writer build produce a complete site.