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:
- Fast initial load - HTML is pre-rendered
- Interactive - Preact takes over after load
- 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
Head
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
- Styling - Add CSS and UnoCSS
- Build & Deploy - Ship your site