DocsStart here

Routing

terminaltui uses Next.js-style file-based routing. A project is a config.ts plus a pages/ directory.

my-site/
├── config.ts          # theme, banner, global settings, optional menu override
├── pages/
│   ├── home.ts        # /home (landing page)
│   ├── about.ts       # /about
│   ├── projects/
│   │   ├── index.ts   # /projects
│   │   └── [slug].ts  # /projects/:slug — dynamic route
│   ├── admin/
│   │   ├── layout.ts  # wraps every page under /admin
│   │   └── index.ts   # /admin
│   └── contact.ts     # /contact
└── api/
    └── stats.ts       # GET /api/stats — see api-routes.md

Page files

Each file under pages/ is a page. Default-export a function that returns content blocks; optionally export metadata.

// pages/about.ts
import { card, timeline } from "terminaltui";

export const metadata = {
  label: "About",       // menu label (defaults to title-cased filename)
  icon: "◈",
  order: 2,             // menu sort order (lower first)
  hidden: false,        // hide from auto-generated menu
};

export default function About() {
  return [
    card({ title: "About Me", body: "Full-stack developer." }),
    timeline([{ date: "2024", title: "Started terminaltui" }]),
  ];
}

Pages can be async:

export default async function Dashboard() {
  const data = await fetchData();
  return [card({ title: data.name, body: data.body })];
}

Dynamic routes — [param].ts

A bracketed filename creates a dynamic route. The params object is passed to your default export:

// pages/projects/[slug].ts
export default function Project({ params }: { params: { slug: string } }) {
  return [card({ title: params.slug, body: "Project details." })];
}

Navigate to it with navigate("projects/[slug]", { slug: "my-app" }) or via a card action.

Static pages can receive params the same way — any navigation that carries params (a card action, navigate(), or a redirect) passes them to the target page’s default export as { params }. Navigations without params call the page with no context.

Parameterized titles and loading messages

metadata.label and metadata.loading accept functions for param-driven pages. They receive the params object directly and resolve at render time:

// pages/post.ts — opened via navigate("post", { id: "42" })
export const metadata = {
  hidden: true,
  label: (p: { id: string }) => `Post #${p.id}`,           // header title
  loading: (p: { id: string }) => `Loading post #${p.id}...`,
};

export default async function Post({ params }: { params: { id: string } }) {
  const post = await fetchPost(params.id);
  return [card({ title: post.title, body: post.body })];
}

Function labels don’t appear in the auto-generated menu (there are no params to resolve them with there) — parameterized pages are usually hidden: true and reached by navigation.

Layout files — layout.ts

A layout.ts wraps every sibling and descendant page. Layouts compose from outside in (root layout → section layout → page).

// pages/admin/layout.ts
import { card } from "terminaltui";

export default function AdminLayout({ children }: { children: any[] }) {
  return [
    card({ title: "Admin", body: "You're in the admin section." }),
    ...children,
  ];
}

API routes

api/*.ts files become HTTP endpoints. Each file can export GET, POST, PUT, DELETE, etc.

// api/stats.ts
export async function GET() {
  return { users: 45231, uptime: 99.97 };
}

api/stats.tsGET /api/stats. See API Routes for parameters, request bodies, and error handling.

Auto-menu vs manual menu

By default, the home menu is generated from the filesystem (alphabetical, respecting metadata.order and skipping metadata.hidden). Override it in config.ts:

import { defineConfig } from "terminaltui";

export default defineConfig({
  name: "My Site",
  theme: "cyberpunk",
  menu: {
    items: [
      { id: "home", label: "Home", icon: "◆" },
      { id: "projects", label: "Projects", icon: "▣" },
      { id: "contact", label: "Contact", icon: "◉" },
    ],
  },
});

You can also drop a menu({ source: "auto" }) block into any page to render the auto-menu inline.

Hiding pages

Use metadata.hidden = true, or omit the page from a manual menu.items list. The page is still routable (e.g., via navigate() or [param]), it just doesn’t appear in the menu.

Programmatic navigation — navigate(pageId, params?)

Navigate to any page from event handlers, async content, or middleware:

import { navigate } from "terminaltui";

navigate("home");
navigate("projects/[slug]", { slug: "my-app" });

Card action navigation

Cards can navigate when activated:

card({
  title: "My Project",
  body: "Click to view details",
  action: { navigate: "projects/[slug]", params: { slug: "my-project" } },
})

Middleware

Middleware functions run before a page renders. They can redirect, block access, or modify behavior.

Global middleware

Runs on every page navigation. Defined on defineConfig({ middleware: [...] }):

import { defineConfig, requireEnv, rateLimit } from "terminaltui";

export default defineConfig({
  name: "My Site",
  middleware: [
    requireEnv(["API_KEY"]),
    rateLimit({ maxRequests: 100, windowMs: 60000 }),
  ],
});

Page-level middleware

Export metadata.middleware from a page file:

// pages/admin/index.ts
import { middleware, redirect, markdown } from "terminaltui";

export const metadata = {
  label: "Admin",
  middleware: [
    middleware(async (ctx) => {
      if (!isAdmin(ctx.state)) return redirect("home");
    }),
  ],
};

export default function Admin() {
  return [markdown("Admin panel.")];
}

Custom middleware

The middleware() function wraps a handler that receives a MiddlewareContext and optionally returns a redirect:

middleware(async (ctx) => {
  // ctx.page   — the target page id
  // ctx.params — route parameters (for dynamic routes)
  // ctx.state  — app state, if configured

  if (someCondition) {
    return redirect("login");  // redirect to another page
  }
  // return nothing to allow navigation
})

Built-in middleware

FunctionDescription
requireEnv(vars)Check that environment variables exist
rateLimit({ maxRequests, windowMs })Rate limit page access

redirect()

import { redirect } from "terminaltui";

redirect("home")                                          // redirect to a page
redirect("projects/[slug]", { slug: "my-app" })           // redirect to a dynamic route with params

Edit this page on GitHub