// stati.config.ts
import { defineConfig } from '@stati/core';
export default defineConfig({
site: { title: 'My site', baseUrl: 'https://example.com' },
helpers: {
// 'October 19, 2026', the same on every machine
formatDate: (value: string | Date, locale = 'en-US') =>
new Intl.DateTimeFormat(locale, { dateStyle: 'long', timeZone: 'UTC' }).format(new Date(value)),
// '4 min read', from the rendered HTML
readingTime: (html: string) => {
const words = html.replace(/<[^>]+>/g, ' ').split(/\s+/).filter(Boolean).length;
return `${Math.max(1, Math.round(words / 220))} min read`;
},
},
});
<% if (stati.page.publishedAt) { %>
<time datetime="<%= stati.page.publishedAt %>"><%= stati.helpers.formatDate(stati.page.publishedAt) %></time>
<% } %>
<span><%= stati.helpers.readingTime(stati.content) %></span>
Helpers sit under stati.helpers, and the rest of stati belongs to Stati. A helper called slugify is stati.helpers.slugify, while stati.slugify() keeps making the same keys as Stati's generated pages. In Stati 1 a filter by that name replaced Stati's own without a word.
The type of a helper is (...args: any[]) => unknown, so (html: string) compiles. stati build doesn't type-check the config, though, so it's your editor or tsc --noEmit that holds you to those types.
A Stati 1 call left in a template, like stati.formatDate(), stops the page in stati dev with Unknown property "stati.formatDate". Did you mean "stati.helpers.formatDate"?. For a helper that may not be there, 'formatDate' in stati.helpers asks without tripping that check.
Helpers are config code, and cached pages don't notice when one changes. After editing a helper whose output is on them, build once with stati build --force.
In the Stati docs: Helpers, Using helpers and Move filters to helpers.