# Petit — Full Documentation

## Getting Started

### Overview

Petit is a small, fast, local-first documentation platform. Write
markdown, create a config, and run a single command.

## Features

```cards
# Local-first
Docs live in your repo as plain markdown
/getting-started/installation

# Full-text search
Client-side search powered by Orama
/reference/architecture

# Rich components
Cards, tabs, steps, accordions, diagrams
/reference/markdown

# SEO out of the box
Meta tags, OG images, sitemap, JSON-LD
/reference/seo

# AI-ready
llms.txt, llms-full.md, per-page .md endpoints
/reference/seo

# Deploy anywhere
Static output for Vercel, Cloudflare, Netlify
/getting-started/deployment
```

## Quick start

```steps
# Scaffold
Run `npx @ephem-sh/petit init` in your project root. This
creates a config file and a starter docs folder.

# Start
Run `npx @ephem-sh/petit dev` to preview your docs at
`http://localhost:4321`. Changes are picked up automatically.

# Write
Add markdown files to the directories in your sidebar config.
Each `.md` file becomes a page.
```

---

### Installation

Petit requires Node.js 24 or later. It works with any project
regardless of language or framework.

## New project

Run the init command from your project root:

```command live
@ephem-sh/petit init
```

Petit asks for confirmation, then creates a config file and a
starter docs folder:

```
my-project/
  petit.config.json
  docs/
    getting-started/
      overview.md
```

The config file points directly to your content folders:

```json
{
  "title": "My Docs",
  "sidebar": [
    { "label": "Getting Started", "path": "./docs/getting-started" }
  ]
}
```

Start the dev server:

```command live
@ephem-sh/petit dev
```

Your docs are live at `http://localhost:4321`.

## Existing project

If you already have markdown files, generate just the config:

```command live
@ephem-sh/petit config
```

Then add your content folders to the sidebar. Each `path`
points directly to a folder containing `.md` files, relative
to the config:

```json
{
  "title": "My Docs",
  "sidebar": [
    { "label": "Guides", "path": "./docs/guides" },
    { "label": "API", "path": "./packages/core/docs" }
  ]
}
```

This works across monorepos too. You can pull documentation
from multiple locations in your project.

## Project structure

The config lives at your project root. Sidebar paths point
directly to folders with markdown:

```
my-project/
  petit.config.json
  src/
  docs/
    media/
      logo.png
    getting-started/
      overview.md
    guides/
      deployment.md
```

Images go in a `media/` folder inside your docs directory. See
the [media reference](/reference/media) for details.

## Package scripts

Add shortcuts to your `package.json`:

```json package.json
{
  "scripts": {
    "docs:dev": "npx @ephem-sh/petit dev",
    "docs:build": "npx @ephem-sh/petit build"
  }
}
```

## CLI reference

All commands run from your project root:

| Command | Description |
|---------|-------------|
| `npx @ephem-sh/petit init` | Create config + docs/ starter |
| `npx @ephem-sh/petit config` | Generate config file only |
| `npx @ephem-sh/petit dev` | Dev server with hot reload |
| `npx @ephem-sh/petit build` | Build static production site |
| `npx @ephem-sh/petit export` | Export as printable HTML |

`init` and `config` ask for confirmation before creating
files. Pass `--yes` to skip the prompt.

---

### Configuration

The `petit.config.json` file controls your site's appearance,
navigation, and features. It lives at your project root.

## Full example

Here is a config file using every available option:

```json
{
  "title": "My Project",
  "logo": "logo.png",
  "repository": "https://github.com/user/repo",
  "branch": "main",
  "siteUrl": "https://docs.example.com",
  "deploy": "node",
  "defaultScheme": "dark",
  "schemeSwitcher": true,
  "theme": "default",
  "fonts": {
    "sans": "Inter",
    "mono": "JetBrains Mono"
  },
  "maxWidth": "lg",
  "sidebarPosition": "left",
  "toc": true,
  "credits": true,
  "sidebar": [
    { "label": "Getting Started", "path": "./docs/getting-started" },
    { "label": "API Reference", "path": "./docs/api" },
    { "label": "Examples", "path": "./docs/examples" }
  ]
}
```

Only `title` is required. Everything else has sensible defaults.

## Options

```type-table
# PetitConfig
title | string | required | Site title in the sidebar header
logo | string | - | Logo filename, resolved from media directory
mediaDir | string | auto | Path to media/images directory, relative to config
repository | string | - | GitHub URL, adds "Edit on GitHub" links
branch | string | "main" | Git branch for "Edit on GitHub" links
siteUrl | string | - | Production URL, enables SEO (sitemap, OG images, llms.txt)
deploy | "node" \| "cloudflare" \| "netlify" \| "vercel" \| "bun" | "node" | Deploy target platform (see deployment guide)
defaultScheme | "dark" \| "light" \| "system" | "system" | Color scheme on first visit
schemeSwitcher | boolean | true | Show the light/dark toggle
theme | string | "default" | Theme preset for colors and fonts
themeOverrides | ThemeConfig | - | Override CSS variables per color scheme
fonts | { sans?: string, mono?: string } | - | Custom Google Fonts for body and code text
maxWidth | "sm" \| "md" \| "lg" \| "xl" | "lg" | Content area max width
sidebarPosition | "left" \| "right" \| "center" | "left" | Sidebar placement
toc | boolean | true | Show "On this page" table of contents
credits | boolean | false | Show "Created with petit" link at the bottom of the sidebar
sidebar | SidebarItem[] | [] | Navigation structure (see below)
```

## Sidebar

Each sidebar entry has a `label` and an optional `path`. Paths
point directly to folders containing `.md` files, relative to
the config file:

```json
{
  "sidebar": [
    { "label": "Getting Started", "path": "./docs/getting-started" },
    { "label": "Reference" },
    { "label": "Core", "path": "./docs/reference/core" },
    { "label": "Plugins", "path": "./docs/reference/plugins" }
  ]
}
```

"Reference" here is a visual divider. "Core" and "Plugins" each
list the pages found in their directories.

In a monorepo you can pull docs from multiple locations:

```json
{
  "sidebar": [
    { "label": "Getting Started", "path": "./docs/getting-started" },
    { "label": "Core API", "path": "./packages/core/docs" },
    { "label": "CLI", "path": "./packages/cli/docs" }
  ]
}
```

Subdirectories inside each path are automatically discovered as
nested categories, up to three levels deep. See the
[routing reference](/reference/routing) for details on nested
categories, page ordering, frontmatter fields, and draft pages.

## Themes

Petit ships with 18 built-in themes. Set the `theme` option
in your config:

```json
{
  "theme": "claude"
}
```

See the [themes reference](/reference/themes) for the full list,
live previews, and customization options.

---

### Deployment

Petit builds your docs for any hosting platform. Set the `deploy`
field in your config to target a specific platform, then run the
build command.

```command live
@ephem-sh/petit build
```

Petit scaffolds a `.petit/` workspace during the build with all
required dependencies. You don't need a `package.json` or
`node_modules` in your project.

## Deploy targets

Set the `deploy` field in `petit.config.json` to match your
hosting platform:

```json
{
  "title": "My Docs",
  "deploy": "cloudflare"
}
```

```type-table
# Deploy options
node | string | "node" | Default. Builds with Nitro for any Node.js server or Docker container
cloudflare | string | - | Cloudflare Workers via @cloudflare/vite-plugin
netlify | string | - | Netlify via @netlify/vite-plugin-tanstack-start
vercel | string | - | Vercel with auto-detection via Nitro
bun | string | - | Bun runtime via Nitro with the bun preset
```

## Cloudflare Workers

Cloudflare Workers provides edge deployment with global
distribution. Set the deploy target in your config:

```json petit.config.json
{
  "title": "My Docs",
  "deploy": "cloudflare",
  "siteUrl": "https://docs.example.com"
}
```

Petit generates a `wrangler.jsonc` and installs
`@cloudflare/vite-plugin` and `wrangler` automatically
inside the `.petit/` workspace during the build.

### CI/CD with Cloudflare Workers

Create a new Worker in the Cloudflare dashboard and connect
your Git repository. Configure the build settings:

```type-table
# Build configuration
Build command | string | npx @ephem-sh/petit@latest build | Scaffolds .petit/ workspace, installs deps, runs vite build
Deploy command | string | cd .petit && npx wrangler deploy | Runs wrangler from inside .petit/ where node_modules lives
Root directory | string | / | Your repository root
```

Add these build environment variables under **Settings >
Build > Variables and secrets** (the build section, not the
runtime one):

```type-table
# Build environment variables
SKIP_DEPENDENCY_INSTALL | string | true | Skips Cloudflare's auto-install step (Petit handles its own deps)
NODE_OPTIONS | string | --max-old-space-size=4096 | Increases memory for the vite build process
```

> **Note:** The `SKIP_DEPENDENCY_INSTALL` variable must be set
> in the **build** variables section. Cloudflare's auto-install
> runs before the build command and will fail on monorepo
> lockfiles.

### Local deployment

To deploy from your local machine, build first, then run
wrangler from the `.petit/` directory:

```command live
@ephem-sh/petit build
```

```bash
cd .petit && npx wrangler login && npx wrangler deploy
```

## Netlify

Netlify deploys via the `@netlify/vite-plugin-tanstack-start`
plugin, which Petit installs automatically during the build.
Set the deploy target in your config:

```json petit.config.json
{
  "title": "My Docs",
  "deploy": "netlify",
  "siteUrl": "https://docs.example.com"
}
```

Netlify requires two files in your repository to bypass its
automatic dependency installation, which fails on monorepos
and projects without a lockfile.

### Repository setup

Add these two files to your repository root:

1. Create a `netlify.toml` file:

```toml netlify.toml
[build]
  base = ".netlify-skip/"
  command = "cd .. && npx @ephem-sh/petit@latest build --netlify"
  publish = "dist/client/"

[build.environment]
  NODE_OPTIONS = "--max-old-space-size=4096"
```

2. Create a a empty `.netlify-skip` folder in your root and place a package.json inside it `.netlify-skip/package.json` file:

```json .netlify-skip/package.json
{"private":true}
```

The `base` directory points Netlify's install step at the
empty `.netlify-skip/` folder, which finishes instantly. The
build command changes back to the repository root and runs
Petit with the `--netlify` flag, which moves the SSR function,
static assets, and dependencies into the correct locations
after the build completes.

### Dashboard configuration

Import your repository in the Netlify dashboard. The
`netlify.toml` file configures everything automatically.
No dashboard overrides are needed.

## Vercel

Vercel deploys via Nitro with auto-detection. Set the deploy
target:

```json petit.config.json
{
  "title": "My Docs",
  "deploy": "vercel",
  "siteUrl": "https://docs.example.com"
}
```

Import your repository in the Vercel dashboard and configure
the build settings:

```type-table
# Vercel build settings
Build Command | string | npx @ephem-sh/petit@latest build | Scaffolds .petit/ workspace, installs deps, runs vite build
Output Directory | string | (leave empty) | Vercel auto-detects .vercel/output/ generated by Nitro
Install Command | string | echo skip | Petit handles its own install during the build step
Root Directory | string | ./ | Your repository root
```

Petit scaffolds a `.petit/` workspace during the build, installs
all dependencies there, and runs the vite build. Nitro outputs
to `.vercel/output/` which Vercel picks up automatically.

No `package.json` or `node_modules` needed in your project root.

## Railway

Railway provides instant deployments with zero configuration
files. The default `node` deploy target works with Railway's
Railpack builder. Set the deploy target in your config:

```json petit.config.json
{
  "title": "My Docs",
  "deploy": "node",
  "siteUrl": "https://docs.example.com"
}
```

Connect your repository in the Railway dashboard and configure
the service settings:

```type-table
# Service settings
Build command | string | npx @ephem-sh/petit@latest build | Scaffolds .petit/ workspace, installs deps, runs vite build
Start command | string | node .petit/.output/server/index.mjs | Starts the Nitro server (reads PORT from Railway automatically)
```

Add this variable under **Variables** in your service settings:

```type-table
# Environment variables
RAILPACK_INSTALL_CMD | string | mkdir -p node_modules | Skips Railpack's auto-install step (Petit handles its own deps)
```

Nitro reads Railway's `PORT` environment variable automatically.
No additional configuration is needed.

> **Note:** If your repository has a `package.json` with an
> `engines` field, set it to Node 24 or higher. If you don't
> have a `package.json`, set the `RAILPACK_NODE_VERSION`
> environment variable to `24` in your Railway service settings.

## Node.js and Docker

The default `node` target works for any Node.js server or Docker
container. Build and start the server:

```command live
@ephem-sh/petit build
```

```bash
node .petit/.output/server/index.mjs
```

The server starts on port 3000 by default. Static assets are
served from `.petit/.output/public`.

For Docker, use a multi-stage build:

```dockerfile Dockerfile
FROM node:24-slim AS build
WORKDIR /app
COPY . .
RUN npx @ephem-sh/petit build

FROM node:24-slim
WORKDIR /app
COPY --from=build /app/.petit/.output .output
CMD ["node", ".output/server/index.mjs"]
```

## Bun

Bun requires React 19. Set the deploy target:

```json petit.config.json
{
  "title": "My Docs",
  "deploy": "bun",
  "siteUrl": "https://docs.example.com"
}
```

```command live
@ephem-sh/petit build
```

```bash
bun .petit/.output/server/index.mjs
```

## Static export

For static hosting without a server, the build output at
`.petit/.output/public` contains all static assets. You can
serve this directory with any static file server:

```bash
npx serve .petit/.output/public
```

## SEO in production

To get full SEO support (sitemap, OG images, robots.txt, and
LLM endpoints), set `siteUrl` in your config before building:

```json
{
  "siteUrl": "https://docs.example.com"
}
```

See the [SEO reference](/docs/reference/seo) for details on
what gets generated.

## Image optimization

Petit converts PNG and JPG images to WebP at build time and
generates responsive sizes (640px, 1024px, 1920px). This runs
automatically during build if sharp is installed.

## Offline support

The built site includes a service worker that caches pages
after the first visit. Returning visitors can browse cached
pages without a network connection. The service worker updates
the cache in the background when a connection is available.

---

### SKILL.md

Petit ships with a agentic skill called `petit-writer` that
teaches agents how to write documentation using Petit's components,
configuration, and conventions. When installed, the agent knows how to
set up Petit from scratch, write pages with the correct frontmatter,
use components like tabs, steps, and cards, and configure
`petit.config.json` properly.

## What it does

The skill gives agents three reference files:

- **Writing standards** - voice, tone, formatting rules, and a
  verification checklist
- **Petit reference** - the full config schema, every CLI command,
  all components with syntax, themes, media handling, and SEO
- **Setup guide** - a playbook for creating documentation from zero,
  including information architecture and page classification

When you invoke the skill, the agent reads the relevant references and
applies them to your request. It can write new pages, edit existing
docs, configure your project, or set up Petit in a new repo.

## Install

The skill files live in the `skills/petit-writer/` directory of the
Petit repository. Copy them into your project's `.agents/skills/` or `.claude/skills`
directory:

````steps
# Copy the skill to your project

Download or copy the `skills/petit-writer/` directory from the
Petit repo into `.agents/skills/` or `.claude/skills` in your project:

# Verify the skill is available

Open Claude Code in your project. Type `/petit-writer` and you will see
the skill in the autocomplete list.
````

To install the skill globally (available in all your projects),
copy it to `~/.agents/skills/petit-writer/` or `~/.claude/skills/petit-writer/` instead.

## Usage

Invoke the skill directly with a slash command:

```
/petit-writer
```

You can also pass arguments to describe what you want:

```
/petit-writer "add a deployment guide for Railway"
/petit-writer "update the API reference for the new auth module"
/petit-writer "set up Petit docs from scratch for this project"
```

Agents could also invokes the skill automatically when you ask it to
write or edit documentation in a project that has Petit configured, based on your hook setups.

## Examples

Here are common tasks the skill handles:

- **New docs from scratch** - "Set up documentation for this
  project using Petit. It's a CLI tool written in Rust."
- **Write a page** - "Write a getting started guide that covers
  installation and the first three commands."
- **Edit existing docs** - "The auth module changed. Update the
  reference page to match the new API."
- **Configure Petit** - "Add a dark theme, enable SEO, and set up
  the sidebar for three categories."
- **Use components** - "Add install tabs for the package and a
  steps component for the setup flow."

## Customization

The skill files are plain markdown. You can modify them to fit your
project's conventions:

- Edit `documentation.md` to adjust voice, tone, or formatting
  rules for your team
- Edit `petit.md` if you add custom components or use a specific
  subset of Petit features
- Edit `setup.md` to match your project's category structure or
  content priorities

Restart your agent and try your changes.

---

## Components

### Cards

# Cards

A grid of clickable cards for navigation or feature highlights.

## Usage

Use the `cards` code fence. Each card is defined with `# Title`, a description line, and an optional link (line starting with `/`).

````markdown
```cards
# Getting Started
Learn the basics of Petit
/getting-started/overview

# Configuration
Set up your project config
/getting-started/configuration

# Markdown
Write beautiful documentation
/reference/markdown
```
````

## Example

```cards
# Getting Started
Learn the basics of Petit
/getting-started/overview

# Configuration
Set up your project config
/getting-started/configuration

# Markdown
Write beautiful documentation
/reference/markdown
```

## API Reference

```type-table
# Card Properties
title | string | required | Card heading (from `# Title` line)
description | string | - | Description text (first line after title)
href | string | - | Link URL (line starting with `/`)
```

---

### Codeblock

# Codeblock

All code blocks automatically get syntax highlighting via Shiki and a copy button on hover. Add a filename after the language to show a header bar.

## Usage

Basic code block:

````markdown
```ts
console.log("hello")
```
````

With filename header:

````markdown
```ts config.ts
export default { title: "My Docs" }
```
````

## Examples

### Basic

```ts
function greet(name: string): string {
  return `Hello, ${name}!`
}
```

### With filename

```ts config.ts
import { defineConfig } from "@ephem-sh/petit"

export default defineConfig({
  title: "My Docs",
  sidebar: [
    { label: "Getting Started", path: "./getting-started" },
  ],
})
```

```python app.py
def greet(name: str) -> str:
    return f"Hello, {name}!"
```

```json package.json
{
  "name": "my-project",
  "version": "1.0.0"
}
```

## Supported Languages

All languages supported by Shiki are available, including TypeScript, JavaScript, Python, Rust, Go, CSS, HTML, JSON, YAML, Bash, and many more.

## API Reference

```type-table
# Code Block Options
language | string | - | Language for syntax highlighting (e.g. `ts`, `python`, `bash`)
filename | string | - | Filename shown in header bar (placed after language, e.g. `ts config.ts`)
```

---

### Command

Display package manager commands with automatic tabs. Supports any
command and any combination of package managers.

## Usage

Use the `command` code fence. List the package managers after `command`.
The content is the command to run, prefixed with each manager name.

````markdown
```command npm pnpm bun yarn
install @ephem-sh/petit
```
````

If no managers are specified, all four are shown (npm, pnpm, bun, yarn).

### Live mode

Add `live` after the managers to use package runners instead of managers.
This swaps npm for npx, bun for bun x, pnpm for pnpm dlx, and yarn for
yarn dlx.

````markdown
```command npm pnpm bun yarn live
@ephem-sh/petit dev
```
````

This renders tabs showing `npx @ephem-sh/petit dev`, `pnpm dlx @ephem-sh/petit dev`,
`bun x @ephem-sh/petit dev`, and `yarn dlx @ephem-sh/petit dev`.

## Examples

### Install a package

```command
install @ephem-sh/petit
```

### Run a package

```command live
@ephem-sh/petit dev
```

### Specific managers only

```command npm pnpm live
@ephem-sh/petit init
```

## API reference

```type-table
# Command properties
managers | string[] | npm, pnpm, bun, yarn | Package managers to show (space-separated after `command`)
live | flag | false | Use package runners (npx, bun x, pnpm dlx, yarn dlx) instead of managers
command | string | required | The command to run (content of the code fence)
```

---

### Diagram

# Diagram

Render diagrams using Mermaid.js syntax. Diagrams are rendered client-side.

## Usage

Use the `mermaid` code fence with Mermaid syntax.

````markdown
```mermaid
graph TD
    A[Start] --> B{Decision}
    B -->|Yes| C[Action]
    B -->|No| D[End]
```
````

## Example

```mermaid
graph TD
    A[Start] --> B{Decision}
    B -->|Yes| C[Action]
    B -->|No| D[End]
```

## API Reference

```type-table
# Diagram Properties
syntax | string | required | Valid Mermaid.js diagram syntax
```

---

### Type Table

# Type Table

Display structured API reference tables for types, interfaces, or configuration options.

## Usage

Use the `type-table` code fence. Start with `# TypeName`, then list properties as `name | type | default | description`.

````markdown
```type-table
# PetitConfig
title | string | required | Site title
defaultScheme | "dark" \| "light" \| "system" | "system" | Default color scheme
toc | boolean | true | Show table of contents
maxWidth | "sm" \| "md" \| "lg" \| "xl" | "lg" | Content max width
```
````

## Example

```type-table
# PetitConfig
title | string | required | Site title
defaultScheme | "dark" \| "light" \| "system" | "system" | Default color scheme
schemeSwitcher | boolean | true | Show scheme toggle
toc | boolean | true | Show table of contents
maxWidth | "sm" \| "md" \| "lg" \| "xl" | "lg" | Content max width
repository | string | - | Repository URL for GitHub links
```

## API Reference

```type-table
# Row Format
name | string | required | Property name
type | string | required | Type annotation
default | string | required | Default value or `required` / `-`
description | string | required | Short description
```

---

### Accordion

# Accordion

Collapsible content sections using native HTML details/summary. No JavaScript required.

## Usage

Use the `accordion` code fence. Each `# Title` creates a collapsible section.

````markdown
```accordion
# What is Petit?
A local-first documentation platform that requires zero generated files.

# How do I install it?
Run `npx @ephem-sh/petit` in your project directory.

# Does it support dark mode?
Yes, with system, light, and dark themes.
```
````

## Example

```accordion
# What is Petit?
A local-first documentation platform that requires zero generated files in your repository. Just write markdown and run.

# How do I install it?
Run `npx @ephem-sh/petit` in your project directory. No dependencies to add to your project.

# Does it support dark mode?
Yes, with system, light, and dark themes plus full CSS variable customization via the theme system.
```

## API Reference

```type-table
# Section Properties
title | string | required | Section heading (from `# Title` line)
content | string | required | Collapsible content (supports inline markdown)
```

---

### Tabs

# Tabs

Switch between different content panels. Useful for showing framework-specific examples.

## Usage

Use the `tabs` code fence. Each `# Label` creates a tab.

````markdown
```tabs
# React
Use the React adapter for your project.

# Vue
Use the Vue adapter for your project.

# Svelte
Use the Svelte adapter for your project.
```
````

## Example

```tabs
# React
Use the React adapter. Import from `@ephem-sh/petit/react` and wrap your app.

# Vue
Use the Vue adapter. Add the plugin to your `app.use()` chain.

# Svelte
Use the Svelte adapter. Add the preprocessor to your `svelte.config.js`.
```

## API Reference

```type-table
# Tab Properties
label | string | required | Tab button label (from `# Label` line)
content | string | required | Tab panel content (supports inline markdown)
```

---

### Steps

# Steps

Display a numbered step-by-step guide with a visual timeline.

## Usage

Use the `steps` code fence. Each `# Title` creates a numbered step.

````markdown
```steps
# Install the package
Run the install command for your package manager.

# Create a config file
Add a `petit.config.json` to your project root.

# Start the dev server
Run `npx @ephem-sh/petit dev` and open the browser.
```
````

## Example

```steps
# Install the package
Run the install command for your preferred package manager.

# Create a config file
Add a `petit.config.json` to your docs directory with your title and sidebar structure.

# Write your docs
Create markdown files in the directories referenced by your sidebar config.

# Start the dev server
Run `npx @ephem-sh/petit dev` and open your browser to see the docs.
```

## API Reference

```type-table
# Step Properties
title | string | required | Step heading (from `# Title` line)
content | string | required | Step description (supports inline markdown)
```

---

### Callout

Highlight important information with colored callout blocks.
Use blockquote syntax with a type prefix.

## Usage

Start a blockquote with `[!TYPE]` where TYPE is one of: NOTE,
TIP, INFO, WARNING, or DANGER.

```markdown
> [!NOTE]
> This is a note with additional context.

> [!WARNING]
> Be careful with this operation.
```

## Examples

> [!NOTE]
> Notes are useful for supplementary information that helps
> the reader understand a concept better.

> [!TIP]
> Tips suggest a better way to accomplish something.

> [!INFO]
> Info blocks provide neutral background context.

> [!WARNING]
> Warnings alert the reader about potential issues.

> [!DANGER]
> Danger blocks flag actions that could cause problems.

## API reference

```type-table
# Callout types
NOTE | Blue | Supplementary information
TIP | Green | Helpful suggestions
INFO | Blue | Neutral context
WARNING | Amber | Potential issues
DANGER | Red | Destructive or risky actions
```

---

### Video

Embed videos from YouTube or Vimeo with a simple code fence.
Videos render as responsive iframes with lazy loading and
privacy-enhanced mode.

## Usage

Use the `youtube` or `vimeo` code fence with the video ID as
content:

````markdown
```youtube
dQw4w9WgXcQ
```
````

````markdown
```vimeo
123456789
```
````

The video ID is the part after `watch?v=` for YouTube or
after `vimeo.com/` for Vimeo.

## Examples

### YouTube

```youtube
dQw4w9WgXcQ
```

### Vimeo

```vimeo
76979871
```

## API reference

```type-table
# Video properties
platform | "youtube" \| "vimeo" | required | Code fence language determines the platform
id | string | required | Video ID (content of the code fence)
```

---

## Reference

### Markdown

Petit supports standard markdown with GitHub Flavored Markdown
extensions. All markdown files are processed through a
remark/rehype pipeline with Shiki syntax highlighting.

## GitHub Flavored Markdown

Tables, strikethrough, task lists, and autolinks are supported
via remark-gfm.

| Feature | Syntax | Result |
|---------|--------|--------|
| **Bold** | `**bold**` | **bold** |
| *Italic* | `*italic*` | *italic* |
| ~~Strikethrough~~ | `~~text~~` | ~~text~~ |
| `Code` | `` `code` `` | `code` |
| [Link](#) | `[text](url)` | clickable link |

## Headings

Headings automatically get `id` attributes and anchor links. They
also appear in the table of contents sidebar when `toc` is enabled
in your config.

```markdown
## Second level
### Third level
#### Fourth level
```

Click any heading to copy its link to the clipboard. A brief
"Link copied!" message confirms the action.

## Code blocks

All code blocks get syntax highlighting via Shiki and a copy
button on hover. Specify the language after the opening fence:

```ts
function greet(name: string): string {
  return `Hello, ${name}!`
}
```

Add a filename after the language to show a header bar:

```ts petit.config.ts
import { defineConfig } from "@ephem-sh/petit"

export default defineConfig({
  title: "My Docs",
})
```

## Math

Inline math uses single dollar signs: `$E = mc^2$` renders as
$E = mc^2$. Block math uses double dollar signs:

```markdown
$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$
```

$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$

Powered by KaTeX. Supports all standard LaTeX math syntax.

## Lists

Both ordered and unordered lists work as expected:

- First item
- Second item
  - Nested item
  - Another nested item
- Third item

1. First step
2. Second step
3. Third step

Task lists are also supported:

- [x] Completed task
- [ ] Pending task

## Blockquotes

Use blockquotes for callouts or notes:

> **Note:** This is an important piece of information that
> readers should pay attention to.

## Tables

Standard markdown tables with alignment:

| Left | Center | Right |
|:-----|:------:|------:|
| Text | Text | Text |
| More | More | More |

## Images

Reference images with `./media/` paths in your markdown:

```markdown
![Alt text](./media/screenshot.png)
```

Petit rewrites these paths and serves files from your media
directory. See the [media reference](/reference/media) for
setup, theme-aware images, favicons, and optimization.

## Components

Petit extends markdown with custom components through special
code fences. These render as interactive HTML elements:

```cards
# Code blocks
Syntax highlighting with copy button and filename header
/components/codeblock

# Commands
Package manager install tabs
/components/command

# Diagrams
Mermaid.js diagrams rendered client-side
/components/diagram

# Cards
Clickable card grids for navigation
/components/cards

# Steps
Numbered step-by-step guides
/components/steps

# Tabs
Tabbed content panels
/components/tabs

# Accordions
Collapsible content sections
/components/accordion

# Type tables
Structured API reference tables
/components/type-table

# Callouts
Styled alert blocks for notes, warnings, and tips
/components/callout

# Videos
Embedded YouTube and Vimeo players
/components/video
```

---

### Programmatic SEO

Petit generates comprehensive SEO metadata and Open Graph images
automatically. All SEO features activate when you set `siteUrl`
in your configuration file.

## Setup

Add a `siteUrl` property to your `petit.config.json` pointing to
your site's production URL:

```json
{
	"title": "My Project",
	"siteUrl": "https://docs.example.com",
	"sidebar": [...]
}
```

Once `siteUrl` is set, Petit generates canonical URLs, OG images,
a sitemap, and robots.txt for every page in your documentation.

## Meta tags

Every page gets meta tags derived from its frontmatter `title`
and `description`. The `max-snippet:-1` directive tells search
engines and AI crawlers to use unlimited snippet text.

```accordion
# Page title and description
Petit generates a `title` tag formatted as "Page Title | Site
Name", a `description` meta tag, a `robots` directive with
`max-snippet:-1` for unlimited AI snippets, and a `canonical`
link pointing to the page's absolute URL.

# Open Graph tags
Each page gets `og:type`, `og:title`, `og:description`,
`og:site_name`, `og:url`, and `og:image` tags. Petit also
generates matching Twitter Card tags (`twitter:card`,
`twitter:title`, `twitter:description`, `twitter:image`) so
previews work on all social platforms.

# JSON-LD structured data
When a page has a `description` in its frontmatter, Petit injects
a JSON-LD script block with `TechArticle` schema containing the
headline, description, URL, and OG image. Pages without a
description don't get JSON-LD output.
```

## Last updated date

Every page displays a "last updated" date below the content. Petit
resolves this date using two sources in priority order:

1. **Frontmatter** -- set `updated` in your frontmatter to use an
   explicit date
2. **File modification time** -- if no frontmatter date is set, Petit
   uses the filesystem modification time (reflects the build time in
   CI environments)

```yaml
---
title: My page
updated: 2026-03-15
---
```

The date appears in the page footer and is included in the
`article:modified_time` Open Graph tag and `dateModified` JSON-LD
field for search engines.

## Open Graph images

Petit auto-generates OG images at build time using satori and
resvg. Each image is a 1200x630 PNG with a dark theme background
that displays the site name, page title, and description. Images
use the Inter font in Regular and Bold weights.

Images are written to `public/og/{slug}.png`. Slashes in the slug
become dashes, for example `getting-started/overview` produces
`public/og/getting-started-overview.png`.

OG image generation is skipped in dev mode because it's too slow
for the development feedback loop. The meta tags still render with
the correct image URLs so you can verify your markup.

## Sitemap and robots.txt

Petit generates both files as static assets in `public/` at build
time.

### Sitemap

The sitemap at `public/sitemap.xml` includes the root URL and
every non-draft documentation page in standard XML sitemap format:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
	<url>
		<loc>https://docs.example.com</loc>
	</url>
	<url>
		<loc>https://docs.example.com/getting-started</loc>
	</url>
</urlset>
```

### robots.txt

The generated `public/robots.txt` allows all crawlers and points
them to the sitemap:

```
User-agent: *
Allow: /

Sitemap: https://docs.example.com/sitemap.xml
```

---

### LLMs

Petit makes your documentation directly consumable by large
language models and AI agents through several machine-readable
endpoints. All LLM endpoints activate when you set `siteUrl`
in your configuration file.

## llms.txt

A machine-readable index of all documentation pages, served at
`/llms.txt`. It lists every non-draft page with a link to its
individual markdown file:

```
# My Project

> My Project documentation

This file lists all documentation pages for My Project.
For the full documentation in a single file,
see: https://docs.example.com/llms-full.md

## Docs

- [Overview](https://docs.example.com/getting-started/overview.md)
- [Configuration](https://docs.example.com/getting-started/configuration.md)
```

## llms-full.md

All documentation concatenated in sidebar order into a single
markdown file, served at `/llms-full.md`. This is useful for
feeding an entire documentation site into an LLM context window.

Petit also adds an alternate link tag in the HTML head of every
page, pointing browsers and crawlers to this file:

```html
<link rel="alternate" type="text/markdown"
	  href="/llms-full.md" />
```

## Individual markdown endpoints

Every documentation page is available as raw markdown at
`/{slug}.md`. For example, `https://docs.example.com/getting-started/overview.md`
serves the raw markdown for the overview page, prefixed with
the page title and description as a heading and blockquote.

All LLM endpoints work in both dev and production. In dev mode,
they are served from memory via Vite middleware. In production,
they are generated as static files.

---

### Architecture

Petit is a static documentation generator built on Vite and
TanStack Start. It reads your markdown, generates a React app
with full-text search, and outputs static files you can host
anywhere. This page explains what happens at each stage.

## The big picture

When you run `npx @ephem-sh/petit dev` or `npx @ephem-sh/petit build`, the pipeline flows
through four stages:

```mermaid
graph TD
    A[Markdown files + config] --> B[Parse and scan]
    B --> C[Virtual modules]
    C --> D[Vite build]
    D --> E[SEO assets]

    B -.- B1[frontmatter, HTML, headings]
    C -.- C1[virtual:petit/config, sidebar, docs]
    D -.- D1[static HTML + JS + CSS]
    E -.- E1[sitemap, robots.txt, OG images, llms.txt]
```

## Markdown processing

Petit uses a unified/remark/rehype pipeline to transform your
markdown into HTML.

The processing chain runs in this order:

1. **gray-matter** extracts YAML frontmatter (title, description,
   order, draft)
2. **remark-parse** turns markdown into an AST
3. **remark-gfm** adds GitHub Flavored Markdown (tables,
   strikethrough, task lists)
4. **remark-mdx** enables MDX syntax support
5. **remark-rehype** converts the markdown AST to an HTML AST
6. **rehype-slug** adds `id` attributes to headings
7. **rehype-autolink-headings** makes headings clickable
8. **rehype-shiki** applies syntax highlighting to code blocks
   using the theme's Shiki configuration
9. **rehype-stringify** serializes the HTML AST to a string

Additional plugins handle math expressions (remark-math +
rehype-katex), callout blocks, video embeds, and theme-aware
images.

Custom rehype plugins handle Petit-specific components like
install tabs, type tables, cards, steps, accordions, tabs, and
mermaid diagrams. These plugins detect special code fence
languages and transform them into interactive HTML.

## The Vite plugin

The core of Petit is a Vite plugin that bridges your markdown
files and the React frontend.

During startup, the plugin:

1. Walks up from the Vite root to find `petit.config.json`
2. Loads and validates the config with Zod
3. Scans sidebar directories for `.md` and `.mdx` files
4. Parses every document through the markdown pipeline
5. Serves generated data as virtual modules (`virtual:petit/*`)

The generated data is served as Vite virtual modules. The React
app imports these directly:

| Module | Contents |
|--------|----------|
| `virtual:petit/config` | Site title, theme, layout settings, siteUrl |
| `virtual:petit/sidebar` | Categories and entries with labels, slugs, draft status |
| `virtual:petit/docs` | All parsed docs: HTML, raw markdown, frontmatter, headings |
| `virtual:petit/theme.css` | CSS variables for colors, fonts, and prose styling |
| `virtual:petit/error` | Config validation error or null |
| `virtual:petit/search` | Serialized Orama search index with all page and heading entries |

In dev mode, the plugin watches your docs directory and config
file. When you save a change, it rebuilds state, invalidates the
virtual modules, and triggers a full reload. The debounce interval
is 300ms to avoid excessive rebuilds during rapid edits.

## Search

Full-text search is powered by Orama and runs entirely in the
browser. No server-side search infrastructure is needed.

At build time, Petit strips HTML tags from every document's
rendered output and indexes the plain text along with the title,
description, slug, and category. The search index is serialized
to JSON and shipped as part of the client bundle.

On the client, the index is hydrated into a live Orama database.
Search queries run locally with no network requests, which keeps
results fast regardless of hosting infrastructure.

## Theme system

Themes define colors using oklch values, font stacks, Shiki
code highlighting themes, and prose typography settings. The
default theme ships with both light and dark schemes.

At build time, the theme definition is converted into CSS custom
properties and served as the `virtual:petit/theme.css` virtual
module. The frontend
reads these variables for all styling, which means changing a
theme takes effect across every component without modifying React
code.

You can override individual CSS variables per scheme using the
`themeOverrides` config option.

## Build pipeline

The `npx @ephem-sh/petit build` command runs through these steps:

```steps
# Load and scan
Reads your config and scans every sidebar directory for `.md`
and `.mdx` files. Draft pages are skipped.

# Parse markdown
Runs documents through the remark/rehype pipeline to produce
HTML, extract headings, and process components. Unchanged files
are served from cache for faster rebuilds.

# Build search index
Strips HTML from rendered output and indexes the plain text with
Orama for client-side full-text search.

# Generate SEO assets
If `siteUrl` is configured, generates sitemap.xml, robots.txt,
OG images, llms.txt, llms-full.md, and individual .md files.

# Vite build
Spawns Vite with the bundled app. The Vite plugin loads config
and docs as virtual modules. TanStack Start pre-renders all
pages to static HTML, JS, and CSS.
```

The output is a standard static site. HTML pages are pre-rendered
via TanStack Start's SSR, and the client-side React app hydrates
for interactive features like search, theme switching, and code
block copy buttons.

## Dev vs production

| Aspect | Dev | Production |
|--------|-----|------------|
| Markdown parsing | On startup + watch | Once at build time |
| Search index | Rebuilt on changes | Built once, serialized |
| OG images | Skipped (too slow) | Generated as PNGs |
| .md endpoints | Served from memory | Static files in public/ |
| Theme CSS | Virtual module | Bundled by Vite |
| HMR | Yes (300ms debounce) | N/A |

---

### Routing and navigation

Petit generates your site's URL structure and sidebar navigation
from two things: the `sidebar` array in your config and the
markdown files in those directories.

## How URLs are generated

Petit scans each sidebar directory for `.md` and `.mdx` files.
The URL for each page is built from the sidebar path and filename:

```
sidebar path:  ./getting-started
filename:      overview.md
URL:           /getting-started/overview
```

Another example with a nested structure:

```
sidebar path:  ./api
filename:      authentication.md
URL:           /api/authentication
```

The root URL (`/`) automatically redirects to the first non-draft
page in your sidebar.

## Sidebar structure

The `sidebar` array in your config controls which directories
Petit scans for content. Each `path` is relative to the config
file:

```
my-project/
  petit.config.json
  docs/
    getting-started/          <-- path: "./docs/getting-started"
      overview.md
      installation.md
    api/                      <-- path: "./docs/api"
      authentication.md
```

```json
{
  "sidebar": [
    { "label": "Getting Started", "path": "./getting-started" },
    { "label": "API Reference", "path": "./api" },
    { "label": "Examples", "path": "./examples" }
  ]
}
```

Each item becomes a category in the sidebar. The `label` is the
category heading, and `path` points to a directory relative to
the config file. Petit reads every `.md` and `.mdx` file in that
directory and lists them under the category.

Items without a `path` appear as non-clickable section headers.
This is useful for grouping related categories:

```json
{
  "sidebar": [
    { "label": "Getting Started", "path": "./getting-started" },
    { "label": "Reference" },
    { "label": "Core API", "path": "./reference/core" },
    { "label": "Plugins", "path": "./reference/plugins" }
  ]
}
```

## Nested categories

Petit automatically discovers subdirectories inside each sidebar
path and renders them as nested categories. You don't need to list
every subdirectory in your config. Point a sidebar entry at a
parent directory and Petit walks into its subdirectories, up to
three levels deep.

Given this directory structure:

```
docs/
  examples/
    overview.md
    go/
      hello-world.md
      middleware.md
    python/
      fastapi.md
      hello-world.md
    rust/
      hello-world.md
```

And this config:

```json
{
  "sidebar": [
    { "label": "Examples", "path": "./docs/examples" }
  ]
}
```

Petit creates an "Examples" category with `overview.md` as a
direct entry, plus three subcategories ("Go," "Python," "Rust")
each containing their own entries.

Subcategory labels are derived from the directory name using the
same kebab-to-title conversion as filenames. A directory named
`getting-started` becomes "Getting Started."

### Sorting in nested categories

Top-level entries still respect the `order` frontmatter field.
Entries inside subdirectories are sorted alphabetically by label.
Subcategories themselves are also sorted alphabetically.

### Depth limit

Petit recurses up to three levels deep. Directories beyond the
third level are ignored. For most documentation sites, one or two
levels of nesting is sufficient.

```
examples/           <-- depth 0 (top-level category)
  go/               <-- depth 1 (subcategory)
    advanced/       <-- depth 2 (sub-subcategory)
      internals/    <-- depth 3 (maximum)
        deeper/     <-- ignored
```

## Sort order

Pages within each category are sorted in two steps:

1. By the `order` field in frontmatter (lower numbers first)
2. Alphabetically by label when order values are equal

```yaml
---
title: Quick start
order: 1
---
```

Pages without an `order` field appear after all ordered pages.

## Frontmatter

Every markdown file can include YAML frontmatter to control how
it appears in the sidebar, on the page, and in search results:

```yaml
---
title: API Reference
description: Complete API documentation
order: 2
draft: false
updated: 2026-03-15
---
```

```type-table
# Frontmatter
title | string | - | Page title used in sidebar, browser tab, and SEO
description | string | - | Short summary for SEO meta tags and previews
order | number | - | Sort position within its section (lower first)
draft | boolean | false | Hide the page from sidebar, search, and navigation
updated | string | - | Override the "last updated" date shown on the page
```

## Page labels

The sidebar label for each page comes from the `title` field in
its frontmatter. If no title is set, Petit converts the filename
from kebab-case to title case. For example, `api-reference.md`
becomes "Api Reference."

Set explicit titles for full control:

```yaml
---
title: API Reference
---
```

## Draft pages

Mark a page as a draft to hide it from the sidebar and all
navigation, including pagination and search:

```yaml
---
title: Work in progress
draft: true
---
```

Draft pages are excluded from the build output entirely. They
don't appear in the sidebar, pagination, search index, sitemap,
or LLM endpoints.

## Pagination

Every page shows previous and next links at the bottom. The order
follows the sidebar: top to bottom, category by category.
Pagination skips draft pages.

## 404 handling

If someone visits a URL that doesn't match any document, Petit
shows a simple 404 page with the requested path. This applies to
both dev mode and the production build.

---

### Themes

Petit ships with 18 built-in themes. Set the `theme` option in
your `petit.config.json`:

```json
{
  "theme": "claude"
}
```

## Available themes

Click a theme name to preview its colors on this page. Code
block highlighting uses the configured theme and doesn't
change in the preview.

```theme-preview
```

| Theme | Description |
|-------|-------------|
| `default` | Neutral grayscale |
| `2077` | Cyberpunk neon aesthetic |
| `amber` | Warm amber monospace |
| `burgundy` | Rich burgundy and cream |
| `claude` | Warm earthy tones |
| `deep` | Deep blue and purple |
| `ghibli` | Soft pastel palette |
| `itadori` | Vibrant anime-inspired |
| `offworld` | Cold futuristic grays |
| `pine` | Soft muted greens |
| `starbucks` | Green and cream |
| `stella` | Purple and lavender |
| `supabase` | Green developer brand |
| `supra` | Bold and high contrast |
| `t3chat` | Clean modern neutral |
| `vercel` | Pure black and white |
| `void` | Ultra dark minimal |
| `zen` | Calm and balanced |

## Custom fonts

Override the default fonts with any Google Font:

```json
{
  "fonts": {
    "sans": "Inter",
    "mono": "Fira Code"
  }
}
```

Petit loads the fonts from Google Fonts automatically and adds
system font fallbacks.

## CSS variable overrides

Fine-tune colors per scheme using `themeOverrides`:

```json
{
  "theme": "claude",
  "themeOverrides": {
    "dark": {
      "custom": {
        "primary": "oklch(0.7 0.2 200)"
      }
    }
  }
}
```

Each theme defines colors as CSS custom properties using oklch
values. You can override any variable individually without
replacing the entire theme.

---

### Media

Petit serves media files from a directory relative to your
config. This page covers how images are resolved, served, and
optimized.

## How it works

When Petit starts, it looks for a media directory. It checks
`docs/media/` first, then `media/`, both relative to your
config file. The first one that exists wins. You can override
this with `mediaDir` in your config:

```json
{
  "mediaDir": "./assets/images"
}
```

In dev mode, Petit serves this directory at `/media/*` via
Vite middleware. At build time, files are copied to the output.

## Images in markdown

Use `./media/` paths in your markdown:

```markdown
![Dashboard screenshot](./media/screenshot.png)
```

Petit rewrites `./media/` and `media/` to `/media/` so files
resolve correctly. Images with alt text render inside a
`<figure>` with a `<figcaption>`.

## Theme-aware images

Name files with `.light.` or `.dark.` suffixes to show
different versions per color scheme:

```markdown
![Diagram](./media/diagram.light.png)
![Diagram](./media/diagram.dark.png)
```

Petit hides the light variant in dark mode and vice versa
using CSS classes. No JavaScript involved.

## Logo

The `logo` config option resolves from your media directory:

```json
{
  "logo": "logo.png"
}
```

This looks for `logo.png` inside the detected media folder.
The logo appears in the sidebar header. Without it, the site
title displays as text.

## Favicon

Petit checks your media directory for these files in order:

1. `favicon.ico`
2. `favicon.png`
3. `logo.png`

The first match is copied to the site's public directory and
added as a `<link rel="icon">` tag.

## Build-time optimization

When you run `npx @ephem-sh/petit build`, images are converted to WebP and
resized to 640px, 1024px, and 1920px widths. This requires
`sharp` as a dependency. Without it, images are served as-is.

Optimization runs on PNG, JPG, and JPEG files in the media
directory. Original files are not modified.

---
