Configuration (stati.config.ts)

Stati is designed to work great out of the box, but it’s also highly configurable. The stati.config.js file (or stati.config.ts for TypeScript projects) is where you customize every aspect of your site’s behavior, from basic metadata to advanced build optimizations.

Configuration File

Basic Setup

Create stati.config.js in your project root:

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

export default defineConfig({
  site: {
    title: 'My Stati Site',
    baseUrl: 'https://my-site.com',
    defaultLocale: 'en-US',
  },
});

TypeScript Configuration

For full type safety, use stati.config.ts:

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

const config: StatiConfig = {
  site: {
    title: 'My Stati Site',
    baseUrl: 'https://my-site.com',
    defaultLocale: 'en-US',
  },

  // Type-safe plugin configuration
  markdown: {
    plugins: [['footnote', { backref: true }]],
  },

  // ISG options with autocomplete
  isg: {
    enabled: true,
    ttlSeconds: 21600,
  },
};

export default defineConfig(config);

Configuration Sections

Site Metadata

The foundation of your site configuration:

export default defineConfig({
  site: {
    title: 'Stati Documentation',        // Required
    baseUrl: 'https://stati.build',      // Required
    description: 'Build static sites',   // Optional
    defaultLocale: 'en-US',              // Optional
  },
});

Required Fields:

  • title (string) - The site’s title, used in templates and metadata
  • baseUrl (string) - Base URL for the site, used for absolute URL generation

Optional Fields:

  • defaultLocale (string) - Default locale for internationalization (e.g., ‘en-US’, ‘fr-FR’)
  • description (string) - Default site description for meta tags (used as fallback when page has no description)

Markdown Configuration

Configure markdown processing with plugins and custom renderer modifications:

export default defineConfig({
  markdown: {
    // Plugin configuration - array of plugin names or [name, options] tuples
    plugins: [
      'footnote',                          // markdown-it-footnote - Footnotes
      ['container', { validate: () => true }],  // markdown-it-container with options
    ],

    // Custom markdown-it configuration function
    configure: (md) => {
      // Add custom plugins or modify renderer
      md.use(customPlugin, options);

      // Modify rendering rules
      md.renderer.rules.code_inline = (tokens, idx) => {
        const token = tokens[idx];
        return `<code class="inline-code">${token.content}</code>`;
      };
    },
  },
});

Note: TOC extraction and heading anchor generation are built into Stati and enabled by default. You don’t need markdown-it-anchor or markdown-it-toc-done-right plugins.

Available Options:

  • plugins (array) - Array of markdown-it plugin names (strings) or [name, options] tuples
  • configure (function) - Function that receives the markdown-it instance for custom configuration
  • toc (boolean) - Enable/disable TOC extraction and heading anchor generation (default: true)

Template Configuration

Configure the Eta template engine:

export default defineConfig({
  eta: {
    // Custom filters for Eta templates
    filters: {
      // Date formatting
      date: (date) => {
        return new Intl.DateTimeFormat('en-US', {
          dateStyle: 'long',
        }).format(new Date(date));
      },

      // Slugify text
      slug: (text) => {
        return String(text)
          .toLowerCase()
          .replace(/[^\w\s-]/g, '')
          .replace(/[\s_-]+/g, '-')
          .replace(/^-+|-+$/g, '');
      },

      // Uppercase transform
      upper: (text) => String(text).toUpperCase(),
    },
  },
});

Note: Filters are added to the stati context and called as functions.

Using filters in templates:

<!-- Apply date filter -->
<time><%= stati.date(stati.page.date) %></time>

<!-- Apply slug filter -->
<a href="/<%= stati.slug(stati.page.title) %>/">Link</a>

<!-- Apply uppercase filter -->
<span><%= stati.upper(stati.page.category) %></span>

<!-- Chain filters -->
<span><%= stati.upper(stati.slug(stati.page.title)) %></span>

ISG Configuration

Configure Incremental Static Generation for smart caching:

export default defineConfig({
  isg: {
    // Enable ISG caching
    enabled: true,

    // Default cache TTL in seconds
    ttlSeconds: 21600, // 6 hours

    // Maximum age cap in days
    maxAgeCapDays: 365,

    // Aging rules for progressive cache extension
    aging: [
      { untilDays: 7, ttlSeconds: 86400 },     // 1 day for week-old content
      { untilDays: 30, ttlSeconds: 604800 },   // 1 week for month-old content
      { untilDays: 365, ttlSeconds: 2592000 }, // 30 days for year-old content
    ],
  },
});

**Available Options:**

- `enabled` (boolean) - Enable or disable ISG caching (default: true)
- `ttlSeconds` (number) - Default cache time-to-live in seconds (default: 21600)
- `maxAgeCapDays` (number) - Maximum age in days for applying aging rules (default: 365)
- `aging` (array) - Array of aging rules with `untilDays` and `ttlSeconds` properties

**Aging Rules:**

Aging rules allow you to progressively extend cache TTL based on content age. Each rule specifies a time threshold (`untilDays`) and the cache duration (`ttlSeconds`) to apply to content that reaches that age.

```javascript
// Example: older content gets cached longer
aging: [
  { untilDays: 7, ttlSeconds: 86400 },    // 1 day cache for content 7+ days old
  { untilDays: 30, ttlSeconds: 604800 },  // 1 week cache for content 30+ days old
]

Development Server

Configure the development server:

export default defineConfig({
  dev: {
    // Server configuration
    port: 3000,        // Port for development server
    host: 'localhost', // Host to bind to
    open: false,       // Whether to open browser automatically
  },
});

Available Options:

  • port (number) - Port for development server (default: 3000)
  • host (string) - Host to bind to (default: ‘localhost’)
  • open (boolean) - Whether to open browser automatically (default: false)

Preview Server

Configure the preview server:

export default defineConfig({
  preview: {
    // Server configuration
    port: 4000,        // Port for preview server
    host: 'localhost', // Host to bind to
    open: false,       // Whether to open browser automatically
  },
});

Available Options:

  • port (number) - Port for preview server (default: 4000)
  • host (string) - Host to bind to (default: ‘localhost’)
  • open (boolean) - Whether to open browser automatically (default: false)

Note: CLI options (e.g., stati dev --port 8080 or stati preview --port 8080) take precedence over config file settings.

Advanced Configuration

Build Hooks

Add custom logic at various stages of the build process:

export default defineConfig({
  hooks: {
    // Before build starts - receives BuildContext
    async beforeAll(context) {
      console.log('Starting build...');
      console.log(`Building ${context.pages.length} pages`);
      console.log(`Output directory: ${context.config.outDir}`);
    },

    // After build completes - receives BuildContext
    async afterAll(context) {
      console.log('Build completed!');
      console.log(`Generated ${context.pages.length} pages`);
    },

    // Before each page renders - receives PageContext
    async beforeRender(context) {
      // Add computed properties to the page
      context.page.frontMatter.readingTime = calculateReadingTime(context.page.content);
      context.page.frontMatter.wordCount = countWords(context.page.content);
    },

    // After each page renders - receives PageContext
    async afterRender(context) {
      console.log(`Rendered: ${context.page.slug}`);
    },
  },
});

Available Hooks:

  • beforeAll (function) - Called before starting the build process, receives BuildContext
  • afterAll (function) - Called after completing the build process, receives BuildContext
  • beforeRender (function) - Called before rendering each page, receives PageContext
  • afterRender (function) - Called after rendering each page, receives PageContext

Context Types:

// BuildContext - passed to beforeAll and afterAll
interface BuildContext {
  config: StatiConfig;  // The resolved configuration
  pages: PageModel[];   // Array of all loaded pages
}

// PageContext - passed to beforeRender and afterRender
interface PageContext {
  page: PageModel;      // The page being processed
  config: StatiConfig;  // The resolved configuration
}

Multiple Configuration Files

You can split configuration across multiple files:

// config/base.js
export const baseConfig = {
  site: {
    title: 'My Site',
    baseUrl: 'https://example.com',
  },
};

// stati.config.js
import { baseConfig } from './config/base.js';

export default defineConfig({
  ...baseConfig,

  // Environment-specific overrides
  site: {
    ...baseConfig.site,
    baseUrl: process.env.SITE_URL || baseConfig.site.baseUrl,
  },
});

Best Practices

Configuration Organization

  1. Keep it readable

    // Good: organized and commented
    export default defineConfig({
      // Site metadata
      site: {
        title: 'My Site',
        baseUrl: 'https://example.com',
      },
    
      // Markdown processing
      markdown: {
        plugins: ['footnote'],  // markdown-it-footnote
      },
    });
    
  2. Use environment variables for secrets

    export default defineConfig({
      site: {
        title: 'My Site',
        baseUrl: process.env.SITE_URL || 'http://localhost:3000',
      },
    });
    
  3. Split large configurations

    // config/site.js
    export const siteConfig = {
      title: 'My Site',
      baseUrl: 'https://example.com',
    };
    
    // config/markdown.js
    export const markdownConfig = {
      plugins: ['footnote', 'emoji'],  // Stati auto-prepends 'markdown-it-'
    };
    
    // stati.config.js
    import { siteConfig } from './config/site.js';
    import { markdownConfig } from './config/markdown.js';
    
    export default defineConfig({
      site: siteConfig,
      markdown: markdownConfig,
    });
    

Multi-Environment Configuration

Configure different settings for development, staging, and production:

// config/environments.js
const environments = {
  development: {
    site: {
      title: 'My Site (Dev)',
      baseUrl: 'http://localhost:3000',
    },
    isg: { enabled: false },
    dev: { port: 3000, open: true },
  },

  staging: {
    site: {
      title: 'My Site (Staging)',
      baseUrl: 'https://staging.mysite.com',
    },
    isg: { enabled: true, ttlSeconds: 300 },
  },

  production: {
    site: {
      title: 'My Site',
      baseUrl: 'https://mysite.com',
    },
    isg: {
      enabled: true,
      ttlSeconds: 21600,
      aging: [
        { untilDays: 7, ttlSeconds: 21600 },
        { untilDays: 30, ttlSeconds: 86400 },
      ],
    },
  },
};

export default environments;
// stati.config.js
import { defineConfig } from '@stati/core';
import environments from './config/environments.js';

const env = process.env.NODE_ENV || 'development';
const envConfig = environments[env] || environments.development;

export default defineConfig({
  srcDir: 'site',
  outDir: 'dist',
  staticDir: 'public',

  ...envConfig,
});

Configuration Reference

For detailed information about specific configuration options, see:

The configuration system is designed to grow with your needs while maintaining simplicity for basic use cases. Start with minimal configuration and add complexity as your site evolves.