Custom template filters in Stati

A date format and a reading time, registered once in the config and called like functions.

SNP 060 #495 Filed stati eta

typescript
// stati.config.ts
import { defineConfig } from '@stati/core';

export default defineConfig({
  site: { title: 'My site', baseUrl: 'https://example.com' },
  eta: {
    filters: {
      // 'October 16, 2026', the same on every machine
      formatDate: (value: unknown, locale = 'en-US') =>
        new Intl.DateTimeFormat(locale, { dateStyle: 'long', timeZone: 'UTC' }).format(new Date(String(value))),
      // '4 min read', from the rendered HTML
      readingTime: (html: unknown) => {
        const words = String(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.formatDate(stati.page.publishedAt) %></time>
<% } %>
<span><%= stati.readingTime(stati.content) %></span>
  • A filter is a plain function on stati, called like any other. There are no pipes.
  • The type of a filter is (x: unknown) => unknown, so in a TypeScript config the first parameter is unknown and the rest are optional. (value: string) => … doesn't compile.
  • Give dates a timeZone. Without one, a post published late in the evening lands on a different day depending on where the site is built.
  • Return HTML only from filters you print with <%~, because <%= escapes it.
  • Don't name a filter after something stati already has, like page or site.

In the Stati docs: Custom filters and Template API.