API Reference

Stati provides a comprehensive API for templates, configuration, and programmatic usage. This reference documents all available interfaces, functions, and options.

Template API

Template Context (it)

Every Eta template receives a TemplateContext object with site metadata, front matter, and rendered content:

interface TemplateContext {
  site: SiteConfig;
  page: {
    path: string;
    url: string;
    content: string;
    title?: string;
    description?: string;
    [key: string]: unknown; // Front matter fields
  };
  content: string; // Rendered HTML content
  navigation: NavNode[];
  collection?: CollectionData; // Only for index pages
  partials: Record<string, string>; // Rendered partial markup
}

interface SiteConfig {
  title: string;
  baseUrl: string;
  defaultLocale?: string;
}

Front matter values are exposed through stati.page. Additional properties you place on site in stati.config.ts are available at runtime, but the public type includes the three fields above by default.

Stati Configuration Structure

import type MarkdownIt from 'markdown-it';

interface StatiConfig {
  srcDir?: string;
  outDir?: string;
  staticDir?: string;
  site: SiteConfig;
  markdown?: {
    plugins?: (string | [string, unknown])[];
    configure?: (md: MarkdownIt) => void;
  };
  eta?: {
    filters?: Record<string, (value: unknown) => unknown>;
  };
  isg?: ISGConfig;
  dev?: {
    port?: number;
    host?: string;
    open?: boolean;
  };
  hooks?: BuildHooks;
}

interface SiteConfig {
  title: string;
  baseUrl: string;
  defaultLocale?: string;
}

At runtime Stati preserves any additional keys you place under site, so you can augment the type locally if you need editor support for custom metadata.

Markdown Options

interface MarkdownConfig {
  plugins?: (string | [string, unknown])[];
  configure?: (md: MarkdownIt) => void;
}

Text Processing

<!-- Truncate text -->
<%
function truncate(text, length = 150) {
  if (!text || text.length <= length) return text;
  return text.slice(0, length).replace(/\s+\S*$/, '') + '...';
}
%>
<p class="excerpt"><%= truncate(stati.page.description as string) %></p>

<!-- Slugify text -->
<%
function slugify(text) {
  return text
    .toLowerCase()
    .replace(/[^\w\s-]/g, '')
    .replace(/[\s_-]+/g, '-')
    .replace(/^-+|-+$/g, '');
}
%>
<a href="/tags/<%= slugify(tag) %>/" class="tag">#<%= tag %></a>

<!-- Strip HTML -->
<%
function stripHtml(html) {
  return html.replace(/<[^>]*>/g, '');
}
%>
<meta name="description" content="<%= truncate(stripHtml(stati.content), 160) %>">

Built-in Helpers

stati.propValue(...args)

Builds property values from various inputs, similar to the classnames library. Accepts strings, arrays, and objects, filtering out falsy values. Especially useful for building dynamic HTML attributes that support space-separated tokens (like class or data-analytics).

Signature:

stati.propValue(...args: (string | number | boolean | null | undefined | Record<string, any> | any[])[]): string

Examples:

<!-- Basic usage -->
<button class="<%= stati.propValue('btn', `btn-${variant}`, isActive && 'is-active') %>">
  Click me
</button>

<!-- Array syntax -->
<div class="<%= stati.propValue(['card', 'card-hover', featured && 'featured']) %>">
  Card content
</div>

<!-- Object syntax (keys with truthy values are included) -->
<article class="<%= stati.propValue({
  'post': true,
  'post-featured': stati.page.featured,
  'post-draft': stati.page.draft,
  [`post-${stati.page.category}`]: stati.page.category
}) %>">
  Article content
</article>

<!-- Data attributes -->
<div
  class="<%= stati.propValue('card', `hover:border-${color}-300`) %>"
  data-analytics="<%= stati.propValue('card-click', `category-${stati.page.category}`) %>"
>
  Card with analytics
</div>

Important Notes:

  • Eta does NOT support partial dynamic attributes like class="static-<%= dynamic %>-morestatic". All attribute values must be fully dynamic.
  • For single concatenated values (like data-id="item-42"), use template literals: data-id="<%=item-${id}%>".
  • propValue() also tracks Tailwind classes for inventory when Tailwind integration is enabled, ensuring dynamically-generated classes are included in the CSS build.

For more details on Eta template limitations and best practices, see Template Configuration.

Partial Templates

Partials are reusable templates from underscore-prefixed directories (e.g., _partials/, _components/, _layouts/), available as callable functions in stati.partials.

Basic Usage:

<!-- Without props (pre-rendered) -->
<%~ stati.partials.header %>
<%~ stati.partials.footer %>

<!-- With props (re-rendered dynamically) -->
<%~ stati.partials.card({ title: 'Hello', featured: true }) %>

<!-- Conditional -->
<% if (stati.partials.sidebar) { %>
  <%~ stati.partials.sidebar %>
<% } %>

Defining Partials:

<!-- site/_partials/card.eta or site/_components/card.eta -->
<article class="<%= stati.propValue('card', stati.props.featured && 'featured') %>">
  <h2><%= stati.props.title %></h2>
  <p><%= stati.props.description %></p>
  <a href="<%= stati.props.url %>">Read more</a>
</article>

Props vs Context:

  • Props passed to partials are available as stati.props within the partial
  • Partials also have full access to stati.page, stati.site, stati.content, etc.
  • Without props: uses pre-rendered content (fast)
  • With props: re-renders with merged context (dynamic)
<!-- Fallback pattern: props → page → site -->
<%= stati.props?.author || stati.page.author || stati.site.author %>

Configuration API

Core Configuration Types

interface StatiConfig {
  site: SiteConfig;
  markdown?: MarkdownConfig;
  templates?: TemplateConfig;
  isg?: ISGConfig;
  dev?: DevConfig;
  build?: BuildConfig;
  hooks?: HooksConfig;
  plugins?: Plugin[];
}

Site Configuration

interface SiteConfig {
  title: string;
  description?: string;
  baseUrl: string;
  defaultLocale?: string;
  alternateLocales?: string[];
  author?: string;
  social?: Record<string, string>;
  navigation?: NavigationItem[];
  meta?: Record<string, any>;
}

interface NavigationItem {
  title: string;
  url: string;
  children?: NavigationItem[];
  external?: boolean;
}

Markdown Configuration

interface MarkdownConfig {
  options?: {
    html?: boolean;
    linkify?: boolean;
    typographer?: boolean;
    breaks?: boolean;
    xhtmlOut?: boolean;
  };
  plugins?: {
    anchor?: AnchorPluginOptions;
    toc?: TOCPluginOptions;
    footnote?: FootnotePluginOptions;
  };
  setup?: (md: MarkdownIt) => void;
}

interface AnchorPluginOptions {
  permalink?: boolean;
  permalinkBefore?: boolean;
  permalinkSymbol?: string;
  permalinkClass?: string;
}

ISG Configuration

interface ISGConfig {
  enabled?: boolean;
  ttlSeconds?: number;
  maxAgeCapDays?: number;
  aging?: AgingRule[];
}

interface AgingRule {
  untilDays: number;
  ttlSeconds: number;
}

Eta Configuration

interface EtaConfig {
  filters?: Record<string, (value: unknown) => unknown>;
}

Environment & Utilities

Environment Management

import { setEnv, getEnv } from '@stati/core';

// Set environment for builds
setEnv('production'); // 'development' | 'production' | 'test'

// Get current environment
const env = getEnv(); // Returns current environment string

Build Hooks

Stati provides lifecycle hooks for custom build logic:

import { defineConfig } from '@stati/core';
import type { BuildHooks } from '@stati/core';

export default defineConfig({
  hooks: {
    // Called before starting the build process
    beforeAll: async (ctx) => {
      console.log(`Building ${ctx.pages.length} pages`);
      // Setup logic, external data fetching, etc.
    },

    // Called after completing the build process
    afterAll: async (ctx) => {
      console.log(`Built ${ctx.pages.length} pages successfully`);
      // Post-build tasks, deployment preparation, etc.
    },

    // Called before rendering each individual page
    beforeRender: async (ctx) => {
      // Modify page front matter before rendering
      ctx.page.frontMatter.buildTime = new Date().toISOString();
    },

    // Called after rendering each individual page
    afterRender: async (ctx) => {
      // Post-process rendered HTML, analytics, etc.
      console.log(`Rendered: ${ctx.page.url}`);
    },
  },
});

Available Hook Contexts:

interface BuildContext {
  config: StatiConfig;
  pages: PageModel[];
}

interface PageContext {
  page: PageModel;
  config: StatiConfig;
}

CLI API

Programmatic Usage

import { build, createDevServer, createPreviewServer, invalidate, loadConfig } from '@stati/core';
import type { BuildOptions, DevServerOptions, PreviewServerOptions } from '@stati/core';

// Configuration Loading
const config = await loadConfig();

Build Process

// Programmatic build
const buildOptions: BuildOptions = {
  force: false,
  clean: true,
  includeDrafts: false,
  configPath: './stati.config.js',
};

await build(buildOptions);

Development Server

import { createDevServer } from '@stati/core';

const devServer = await createDevServer({
  port: 3000,
  host: 'localhost',
  open: true,
  configPath: './stati.config.js',
});

await devServer.start();
console.log(`Dev server running at ${devServer.url}`);

// Graceful shutdown
process.on('SIGINT', async () => {
  await devServer.stop();
});

Preview Server

import { createPreviewServer } from '@stati/core';

const previewServer = await createPreviewServer({
  port: 4000,
  host: 'localhost',
  open: false,
  configPath: './stati.config.js',
});

await previewServer.start();
console.log(`Preview server running at ${previewServer.url}`);

Cache Management

import { invalidate } from '@stati/core';

// Clear entire cache
const cleared = await invalidate();

// Invalidate by tag
const tagResult = await invalidate('tag:blog');

// Invalidate by path prefix
const pathResult = await invalidate('path:/blog/');

// Invalidate entries rendered within the last 3 months (exact calendar arithmetic)
const recentResult = await invalidate('age:3months');

// Invalidate using a glob pattern
const globResult = await invalidate('glob:blog/**');

console.log(`Invalidated ${tagResult.invalidatedCount} cache entries`);
console.log(tagResult.invalidatedPaths);

// Result objects share the shape: { invalidatedCount, invalidatedPaths, clearedAll }

Error Handling

Stati throws regular Error instances with descriptive messages. Wrap build steps in try/catch blocks to surface problems in your own tooling, and rely on the CLI for formatted logs and, during development, the in-browser overlay.

try {
  await build();
} catch (error) {
  if (error instanceof Error) {
    console.error('Build failed:', error.message);
  }
  throw error;
}

Handling Errors in Hooks

export default defineConfig({
  hooks: {
    async beforeBuild(context) {
      try {
        await generateExternalData();
      } catch (error) {
        throw new StatiError('Failed to generate external data', 'EXTERNAL_DATA_ERROR', 'build');
      }
    },

    beforeRender(page) {
      try {
        page.customData = processPageData(page);
      } catch (error) {
        console.warn(`Failed to process ${page.path}:`, error.message);
        page.customData = null; // Graceful fallback
      }
    },
  },
});

TypeScript Support

Typed Hooks and Context

import type { BuildContext, BuildHooks, PageContext } from '@stati/core';

const hooks: BuildHooks = {
  beforeAll: (ctx: BuildContext) => {
    console.log(`Building ${ctx.pages.length} pages`);
  },
  beforeRender: ({ page }: PageContext) => {
    page.frontMatter.generatedAt = new Date().toISOString();
  },
};

Template Helpers with defineConfig

import { defineConfig } from '@stati/core';

const hooks: BuildHooks = {
  beforeAll: (ctx) => {
    console.log(`Building ${ctx.pages.length} pages`);
  },
};

export default defineConfig({
  eta: {
    filters: {
      formatDate: (value: string | Date, locale = 'en-US') =>
        new Intl.DateTimeFormat(locale).format(new Date(value)),
    },
  },
  hooks,
});

This API reference covers all the major interfaces and functions available in Stati. For implementation examples and practical usage, refer to the Examples section or explore the specific configuration guides in the Configuration section.