Inkstead Writer Return to Developer Hub

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:

Terminal
$./inkstead-writer theme eject

Existing 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:

TXT
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.plume

If 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.plume for the not-found page written to /404.html.
  • home.plume for the homepage.
  • category.plume for category indexes.
  • post.plume for posts.
  • page.plume for standalone pages.
  • feed.xml.plume for RSS feeds.
  • feed.json.plume for 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.

TXT
content/pages/photos.md
theme/pages/photos.plume
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:

Terminal
$./inkstead-writer theme check

Format templates:

Terminal
$./inkstead-writer theme format$./inkstead-writer theme format --check

Run the language server for editor integrations:

Terminal
$./inkstead-writer theme language-server

Inkstead 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:

PLUME
<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":

PLUME
@image("images/avatar.png", alt: "Ivo", class: "avatar", sizes: "64px")

Add widths: to generate responsive variants and a srcset automatically:

PLUME
@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.

PLUME
@style(file: "styles/site.css")
@script(file: "scripts/site.plume")

You can also write inline resource blocks:

PLUME
@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.

PLUME
@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:

PLUME
@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:

  • site
  • config
  • posts
  • pages
  • categories
  • photoPosts
  • data, containing configured build-time JSON data sources
  • collections.posts
  • collections.pages
  • collections.categories
  • collections.photoPosts
  • collections.<name>, for custom Markdown collections
  • pagination on index and category pages
  • post on post pages
  • page on page pages
  • category on category pages
  • notFound on the 404 page
  • meta
  • now, including now.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:

PLUME
@for book in collections.books {
  <article>
    <h2>{book.title}</h2>
    <p>{book.author}</p>
    {book.content}
  </article>
}

Configured data sources are exposed under data:

PLUME
@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:

TXT
theme/pages/feed.xml.plume
theme/pages/feed.json.plume

Both templates receive a feed object with:

  • title
  • description
  • url
  • path
  • items
  • category

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:

JSON
{
  "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:).

JSON
{
  "hooks": {
    "beforeBuild": ["./build-theme-assets.sh"]
  }
}

This keeps bundling outside the engine while still making ./inkstead-writer build produce a complete site.