Template helpers in Stati 2

Stati 1's filters are helpers now, with a config key of their own. And their parameters can have real types.

SNP 150 #A31 Filed stati eta

typescript
// 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`;
    },
  },
});
eta
<% 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.