Recipes
Short, copy-pasteable answers to "how do I do X". Each recipe links back to the reference doc that explains the mechanism in full, this page is about the goal, not the theory.
Add an RSS feed, Atom feed, and sitemap
Add the official SEO plugin, no custom code needed:
plugins:
- package: jsr:@steno/plugin-seo
options:
siteUrl: https://example.com
title: My Site
description: A concise description
authorName: Ada Lovelace
This writes sitemap.xml, feed.xml (RSS), and atom.xml from your pages on
every build. See Plugins.
Highlight code blocks
Add the official Shiki plugin:
plugins:
- package: jsr:@steno/plugin-shiki
options:
theme: github-dark
Every fenced code block in your Markdown (```ts, ```html, and so on)
gets syntax highlighted automatically, no per-page setup. See
Plugins.
Resize and optimize images
Add the official image plugin:
plugins:
- package: jsr:@steno/plugin-image
options:
widths: [400, 800, 1200]
formats: [webp]
quality: 80
It processes images your theme references and swaps in optimized, appropriately sized versions. See Plugins.
Build a blog listing page
Put your posts under content/posts/, each one becomes an item in
collections.posts automatically. List them from any layout or Markdown page's
frontmatter-driven layout:
<ul>
{#each collections.posts.items as post}
<li>
<a href="{post.url}">{post.frontmatter.title}</a>
{#if post.frontmatter.date}<time>{post.frontmatter.date | date}</time>{/if}
</li>
{/each}
</ul>
Want them newest first? Configure the collection instead of sorting by hand:
collections:
posts:
sortBy: date
order: desc
See Collections for limit, filter, and frontmatter
schema too.
Add a custom 404 page
Create content/404.md, Steno writes it to dist/404.html automatically, the
filename most static hosts look for when a page is missing:
---
title: Page not found
layout: layout
---
# Page not found
Sorry, that page doesn't exist. [Go home](/).
Rename or move a page without breaking old links
Add an entry to redirects in config, this writes a small static HTML page at
the old URL that sends visitors to the new one:
redirects:
/old-post: /blog/new-post-name
See Redirects.
Add dark mode
Steno doesn't have a special dark mode feature, this is plain CSS and a tiny bit
of JavaScript in your theme, the same as on any site. A common approach: default
to the visitor's system preference with prefers-color-scheme, and let a button
override it by toggling a data-theme attribute:
/* theme/assets/style.css */
:root {
--bg: #fff;
--fg: #111;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111;
--fg: #eee;
}
}
:root[data-theme="dark"] {
--bg: #111;
--fg: #eee;
}
:root[data-theme="light"] {
--bg: #fff;
--fg: #111;
}
body {
background: var(--bg);
color: var(--fg);
}
// theme/scripts/theme-toggle.ts
const stored = localStorage.getItem("theme");
if (stored) document.documentElement.dataset.theme = stored;
document.getElementById("theme-toggle")?.addEventListener("click", () => {
const next = document.documentElement.dataset.theme === "dark"
? "light"
: "dark";
document.documentElement.dataset.theme = next;
localStorage.setItem("theme", next);
});
Reference the compiled script from your layout through the assets map so it
gets a cache-busted URL automatically, see
Layout context:
<script src="/assets/{assets['theme-toggle.js']}" defer></script>
<button id="theme-toggle">Toggle theme</button>
Build a search index
A plugin's afterBuild(config) hook receives config.pages, an array of every
generated page's slug, title, description, and date, without having to
re-scan contentDir yourself. Write it to a JSON file your site's own
JavaScript can fetch and search client side:
import type { StenoPlugin } from "jsr:@steno/steno";
export default function searchIndexPlugin(): StenoPlugin {
return {
name: "search-index",
afterBuild: async (config) => {
const index = (config.pages ?? []).map((page) => ({
title: page.title,
url: page.slug,
description: page.description,
}));
await Deno.writeTextFile(
`${config.output}/search-index.json`,
JSON.stringify(index),
);
},
};
}
See Transactional builds for why
config.output (not a hardcoded dist/) is the path to write to, a plugin
writes into Steno's staging directory, not the final output, so the write above
is safe even if a later step in the same build fails.
What to read next
- Content for everything collections and frontmatter support.
- Plugins for the full list of official plugins and how to write your own.
- Themes and Tau for the template language used in the layout examples above.