Components

Mage Pages supports TSX pages for interactive content and provides built-in components for common patterns.

TSX Pages

Create .tsx files for pages that need Preact components:

// pages/counter.tsx
import { useState } from "preact/hooks";

export const frontmatter = {
  title: "Counter",
  description: "An interactive counter demo",
};

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}

Frontmatter Export

TSX pages export frontmatter as a JavaScript object instead of YAML:

export const frontmatter = {
  title: "Page Title", // Required
  description: "Description", // Optional
  customField: "any value", // Custom fields
};

Default Export

The default export is the page component. It renders inside any applicable layouts.

Client Hydration

TSX pages are server-rendered and then hydrated on the client. This means:

  1. Fast initial load - HTML is pre-rendered
  2. Interactive - Preact takes over after load
  3. SEO friendly - Content is in the HTML

State, effects, and event handlers work after hydration:

import { useEffect, useState } from "preact/hooks";

export const frontmatter = { title: "Interactive Page" };

export default function Page() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // Runs after hydration on the client
    fetch("/api/data")
      .then((res) => res.json())
      .then(setData);
  }, []);

  return (
    <div>
      <h1>Data Fetching</h1>
      {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Loading...</p>}
    </div>
  );
}

Built-in Components

Add elements to <head> from anywhere in your component tree:

import { Head } from "@mage/app/pages/head";

export const frontmatter = { title: "My Page" };

export default function Page() {
  return (
    <div>
      <Head>
        <link rel="canonical" href="https://example.com/page" />
        <meta property="og:title" content="My Page" />
        <meta property="og:image" content="/public/og-image.png" />
        <script src="/public/analytics.js" defer />
      </Head>
      <h1>My Page</h1>
    </div>
  );
}

Use <Head> in layouts to add global elements:

import type { LayoutProps } from "@mage/app/pages";
import { Head } from "@mage/app/pages/head";
import { useFrontmatter } from "@mage/app/pages/hooks";

export default function Layout(props: LayoutProps) {
  const { title, description } = useFrontmatter();

  return (
    <>
      <Head>
        <meta property="og:title" content={title} />
        {description && (
          <meta
            property="og:description"
            content={description}
          />
        )}
        <link rel="icon" href="/public/favicon.ico" />
      </Head>
      {props.children}
    </>
  );
}

Markdown

Embed markdown content with syntax highlighting directly in TSX pages:

import { Markdown } from "@mage/app/pages/markdown";

export const frontmatter = { title: "My Page" };

export default function Page() {
  return (
    <div>
      <h1>Documentation</h1>
      <Markdown>
        {`
Here's some **bold** text and a code example:

\`\`\`typescript
const app = new MageApp();
app.get("/", (c) => c.text("Hello"));
\`\`\`
      `}
      </Markdown>
    </div>
  );
}

The Markdown component uses the same rendering pipeline as .md pages, including Shiki for syntax highlighting.

Props

Prop Type Default Description
children string (required) Markdown content to render
theme string "github-dark" Shiki theme for code blocks
className string - CSS class for the wrapper div

Custom Theme

<Markdown theme="github-light">
  {`
# Light Theme

\`\`\`typescript
const x = 1;
\`\`\`
`}
</Markdown>;

With Styling

<Markdown className="prose dark:prose-invert">
  {`
# Styled Content

Markdown with typography styling applied.
`}
</Markdown>;

useFrontmatter

Access page frontmatter from any component:

import { useFrontmatter } from "@mage/app/pages/hooks";

export default function PageHeader() {
  const { title, description, author } = useFrontmatter();

  return (
    <header>
      <h1>{title}</h1>
      {description && <p>{description}</p>}
      {author && <p>By {author}</p>}
    </header>
  );
}

Works in layouts, pages, and any child components.

Mixing Markdown and TSX

Use TSX pages when you need:

  • Interactive elements (forms, counters, tabs)
  • Complex data fetching
  • Conditional rendering
  • State management

Use Markdown pages (.md files) for:

  • Documentation
  • Blog posts
  • Static content

Use the Markdown component in TSX when you want:

  • Markdown content alongside interactive components
  • Dynamic markdown (e.g., from an API or database)
  • Code examples with syntax highlighting in a TSX page
import { useState } from "preact/hooks";
import { Markdown } from "@mage/app/pages/markdown";

export const frontmatter = { title: "Interactive Docs" };

export default function InteractiveDocs() {
  const [language, setLanguage] = useState("typescript");

  const examples = {
    typescript: `\`\`\`typescript
const greeting: string = "Hello";
\`\`\``,
    python: `\`\`\`python
greeting: str = "Hello"
\`\`\``,
  };

  return (
    <div>
      <select onChange={(e) => setLanguage(e.currentTarget.value)}>
        <option value="typescript">TypeScript</option>
        <option value="python">Python</option>
      </select>
      <Markdown>{examples[language]}</Markdown>
    </div>
  );
}

You can also import components into layouts that wrap markdown pages:

// pages/docs/_layout.tsx
import type { LayoutProps } from "@mage/app/pages";
import { TableOfContents } from "../_components/toc.tsx";
import { SearchBox } from "../_components/search.tsx";

export default function DocsLayout(props: LayoutProps) {
  return (
    <div className="flex">
      <aside>
        <SearchBox />
        <TableOfContents />
      </aside>
      <main>{props.children}</main>
    </div>
  );
}

Component Organization

Keep components outside the pages/ directory or in _ prefixed directories:

my-site/
├── components/         # Shared components
│   ├── button.tsx
│   └── header.tsx
├── pages/
│   ├── _components/    # Page-specific components (not routed)
│   │   └── hero.tsx
│   ├── _layout.tsx
│   └── index.tsx
└── main.ts

Import components as needed:

// pages/index.tsx
import { Button } from "../components/button.tsx";
import { Hero } from "./_components/hero.tsx";

export const frontmatter = { title: "Home" };

export default function Home() {
  return (
    <div>
      <Hero />
      <Button>Get Started</Button>
    </div>
  );
}

Props Type Reference

LayoutProps

interface LayoutProps {
  children: ComponentChildren;
}

HtmlTemplateProps

interface HtmlTemplateProps {
  title: string;
  description?: string;
  children: ComponentChildren;
}

Frontmatter

interface Frontmatter {
  title: string;
  description?: string;
  [key: string]: unknown;
}

Next Steps