TriFrost
Examples
+

TriFrost Mini Site (Node + Podman)

View LiveDownload

This example showcases a multi-page server-rendered TriFrost application running on Node.js, containerized with Podman. It demonstrates runtime-agnostic rendering, HTMX-driven interactivity (for comments), and optional observability with SigNoz.

How It Works

The app uses TriFrost’s flexible routing and JSX rendering to deliver a home page, about page, and a blog page with dynamic comments. HTMX handles comment posting and deletion via fragment swaps, making it lightweight and responsive without a heavy frontend framework.

You can run it standalone with Node.js or containerize it with Podman and Podman Compose — a perfect showcase of TriFrost’s runtime flexibility (just compare it with our Cloudflare Example and you'll not there's very little that needs to change).

This example is set up to optionally send OpenTelemetry traces to SigNoz, giving you visibility into route performance, request flows, and system health.

To enable it, provide your SIGNOZ_API_TOKEN in a .env file and uncomment the OtelHttpExporter block in index.ts and you're good to go.

Project Structure

trifrost-mini-site/
├─ src/
│  ├─ components/
│  ├─ pages/
│  ├─ css.ts
│  ├─ index.ts
│  └─ types.ts
├─ Containerfile
├─ compose.yml
├─ package.json
├─ tsconfig.json
└─ .env

App Logic

The app is initialized in index.ts using TriFrost’s App class. Middleware like security and CORS are added, then routing groups (homeRouter, aboutRouter, blogRouter) are wired in. Each page uses server-rendered JSX; the blog section includes dynamic comment handling with HTMX.

// src/index.ts
import {App, Security, Cors, OtelHttpExporter} from '@trifrost/core';
import {type Env} from './types';
import {homeRouter} from './pages/home';
import {aboutRouter} from './pages/about';
import {blogRouter} from './pages/blog';
import {notFoundHandler} from './pages/notfound';

new App<Env>({
    env: process.env as Env,
    name: 'TriFrost_Website',
    version: '1.0.0',
})
    .use(Security())
    .use(Cors())
    .group('/', homeRouter)
    .group('/about', aboutRouter)
    .group('/blog', blogRouter)
    .notfound(notFoundHandler)
    .boot({port: Number(process.env.PORT || 3000)});

Page Breakdown: Home, About, Blog

Each of these pages is set up as its own router group (homeRouter, aboutRouter, blogRouter) and connected in the main index.ts. They use a shared layout wrapper for consistency, pulling in navigation, headers, and theme-aware styles.

  • Home Page (/) → Provides a welcoming introduction to TriFrost, explaining its mission as a runtime-agnostic, fast server framework. Uses a simple JSX layout with hero text and buttons.
  • About Page (/about) → Gives background on what makes TriFrost special — its composable middleware, multi-runtime compatibility (Node, Bun, Workers), and built-in observability. This page uses bullet lists and styled sections.
  • Blog Page (/blog) → The most interactive section, listing blog posts and allowing users to submit and delete comments live via HTMX. This showcases TriFrost’s ability to handle fragment rendering and dynamic state updates without a heavy frontend framework.

CSS/Theming

Styling is centralized in css.ts using TriFrost’s createCss system. It defines dark/light themes, font families, spacing scales, and responsive helpers, enabling consistent component styling and easy overrides.

Once a var/theme variable is defined they are respectively available at css.$v.[variable name] and css.$t.[variable name] anywhere in your app.

// src/css.ts
export const css = createCss({
	reset: true,
	var: {
		font_header: "'Fira Code', monospace",
		font_body: "'Roboto', Sans-serif",
		radius: '0.5rem',
		space_s: '0.5rem',
		space_m: '1rem',
		space_l: '2rem',
		space_xl: '4rem',
    },
    theme: {
        bg: {
            light: '#f9fafb',
            dark: '#1f2937',
        },
        fg: {
            light: '#1f2937',
            dark: '#f9fafb',
        },
        nav_bg: {
            dark: '#020810',
            light: '#c7c7c7',
        },
        nav_fg: {
            dark: '#ffffff',
            light: '#000000',
        },
        ...
    },
    definitions: (mod) => ({
        f: {display: 'flex'},
        fh: {flexDirection: 'row'},
        fv: {flexDirection: 'column'},
        fa_c: {alignItems: 'center'},
        fj_c: {justifyContent: 'center'},
        sm_v_l: {marginBottom: mod.$v.space_l, marginTop: mod.$v.space_l},
        text_header: {
            fontFamily: mod.$v.font_header,
            fontWeight: 'bold',
            [mod.media.desktop]: {
                fontSize: '2.2rem',
            },
            [mod.media.tablet]: {
                fontSize: '2rem',
            },
        },
        text_title: {
            fontFamily: mod.$v.font_header,
            fontWeight: 'bold',
            fontSize: '2rem',
        },
        ...
    }),
});

PS: If you want to check out the different light/dark modes, open up dev tools in Chrome (assumption, sorry ^^):

// In the Chrome DevTools Console:
document.documentElement.setAttribute('data-theme', 'dark'); // Switch to dark mode
document.documentElement.setAttribute('data-theme', 'light'); // Switch to light mode

Containerization

The Containerfile uses a multi-stage build: first compiling the TypeScript project, then packaging only production files.

This is also the file that builds your source into a container ready for deployment.

# Containerfile

# =============================================================================
# Development Stage
# =============================================================================

    FROM node:22-alpine AS development

    WORKDIR /app

    COPY package*.json ./
    RUN npm install

    # Copy source
    COPY . .

    # Start dev server
    CMD ["npm", "run", "dev"]

# =============================================================================
# Build Stage
# =============================================================================

    FROM development as builder
    RUN npm run build

# =============================================================================
# Production Stage
# =============================================================================

    FROM node:22-alpine AS production

    # Set NODE_ENV to production
    ENV NODE_ENV=production

    WORKDIR /app
    COPY package*.json ./
    COPY --from=builder /app/dist ./dist

    # Install dependencies and prune
    RUN npm install --omit=dev && npm prune

    # Change to 1000 user
    RUN chown 1000:1000 /app

    # Switch user
    USER 1000:1000

    # Start prod server
    CMD ["node", "./dist/index.js"]

compose.yml defines the service, port mappings, and volume mounts for local orchestration.

# compose.yml
version: '3'

services:
  website:
    build:
      context: .
      target: development
    tty: true
    environment:
      - PORT=3000
    ports:
      - "3000:3000"
    volumes:
      - './:/app:z'
      - 'node_modules:/app/node_modules:z'
volumes:
  node_modules:

Environment

We define an Env type in types.ts to describe environment bindings like PORT and optional observability tokens like SIGNOZ_API_TOKEN. This lets the app pull runtime config from process.env without hardcoding values, making it deployment-flexible.

We also define our own app-specific Context and Router types here. These will automatically be aware of our Environment simply by passing it as a generic.

Important to note, when working with Podman, you will need to also provide the Environment bindings in compose.yml for local development.

// src/types.ts
import {type TriFrostRouter, type TriFrostContext} from '@trifrost/core';

export type Env = {
    PORT: string;
    SIGNOZ_API_TOKEN: string;
};

export type Context<State extends Record<string, unknown> = {}> = TriFrostContext<Env, State>;

export type Router<State extends Record<string, unknown> = {}> = TriFrostRouter<Env, State>;

Screenshots

Dark mode version of the homepage
Dark mode version of the homepage
Dark mode version of the homepage
Light mode version of the homepage
Light mode version of the homepage
Light mode version of the homepage

Resources

  • TriFrost: The runtime-agnostic server framework behind this example.
  • HTMX: Add AJAX, WebSockets, and more to HTML using attributes.
  • Podman: Open source container engine for rootless containers.
  • SigNoz: OpenTelemetry-compatible observability backend.

Loved the read? Share it with others