Plume views in PlumeKit
Plume is PlumeKit's view layer. A .plume template compiles to an Embedded-Swift render function that writes into a PlumeRuntime HTML buffer. A handler calls that function and returns HTML.bytes as the response. The same rendering runs natively (plumekit serve) and on Cloudflare Workers (Wasm), and the output is byte-identical.
The embedded compiler
You do not install Plume separately. The plumekit CLI embeds the Plume compiler as a library dependency and compiles templates in-process, so there is no separate Plume install and no PLUME_PATH to configure.
plumekit new, serve, console and build all run the embedded compiler:
Views/*.plume ──(embedded Plume compiler, in `plumekit`)──▶ Sources/App/Generated/*.swiftYour app depends only on the PlumeRuntime product, which SwiftPM fetches like any other package dependency. That is what the generated code imports and links against, native and Wasm alike.
Writing views
Views are split across .plume files rather than kept in one monolithic template: a shared Layout component plus one file per page. A page fills the layout's slot.
Views/Layout.plume defines the shared shell:
@component Layout(title: String) {<!doctype html>
<html><head><title>{title}</title></head>
<body>@slot</body></html>}Views/ItemsPage.plume fills it with the page content:
@component ItemsPage(title: String, items: [Item]) {@Layout(title: title) {
<h1>{title}</h1>@if items.size > 0 {<ul>@for item in items {<li>{forloop.index}. {item.name}</li>}</ul>} else {<p>No items.</p>}}}plumekit compile Views -o Sources/App/Generated compiles every .plume file to its own generated Swift file, one render function per component.
The app provides the data types (here Item) and calls the page's render function in a handler:
import PlumeKit
import PlumeRuntime
app.get("/items") { _ in
let items = [Item(name: "alpha"), Item(name: "Hello & <World>")]
return .view(itemsPage(title: "PlumeKit + Plume", items: items)) // itemsPage(...) is generated
}Each component is generated in two forms. itemsPage(title:items:) -> HTML is the convenience used above. itemsPage(title:items:into: &out) writes into an existing buffer; it is the fast path the compiler uses to compose components together.
Escaping
{item.name} is HTML-escaped by default, so Hello & <World> renders as Hello & <World>. Escaping behaves identically on the edge.
Organising views
plumekit compile recurses into subfolders, so you can group views however keeps the directory tidy as the app grows. A common shape is a shared Layout and partials at the root, with a folder per resource or section:
Views/
Layout.plume # @component Layout, the shared shell
HomePage.plume # @component HomePage
Post/
Index.plume # @component PostIndex
Show.plume # @component PostShow
Admin/
Dashboard.plume # @component AdminDashboard
Emails/
VerifyEmail.plume # email bodies are views too; keep them in their own folderFolders are PascalCase like the rest of the tree. plumekit generate resource follows this layout automatically: each resource's views land in Views/<Name>/.
The folder is purely for organisation. Every @component compiles to a top-level render function, so its name must be unique across the whole tree (PostIndex, not just Index). Generated files are named by path (Post/Index.plume becomes Post.Index.plume.swift), so same-named files in different folders never collide.
How it fits together
Three pieces cooperate, and each stays in its lane:
- The
plumekitCLI depends on Plume'sPlumecompiler library;compileTemplatesruns it in-process before everyserve,consoleandbuild, and once atnew. - The app depends on
PlumeRuntimeand includesSources/App/Generated/*.swift. - PlumeKit's core stays view-engine-agnostic: it knows only
[UInt8]. The Plume coupling lives in the CLI (compiler) and the app (runtime).
Response.html(bytes:) in PlumeKit core turns the rendered HTML.bytes into a response. Response.view(_ HTML) is a one-line app-level convenience over it.
See Syntax for the Plume language reference and Components for reusable markup.