reroute-js
v0.45.11
Published
Reroute is a feature-rich file-based routing framework for building fullstack React apps on top of Bun and Elysia.
Maintainers
Readme
Reroute
Reroute is a feature-rich file-based routing framework for building fullstack React apps on top of Bun and Elysia.
📚 Table of Contents
- ✨ Features
- 🚀 Quick Start
- 🔌 External Requests SSR
- ⚡ Streaming SSR
- 🎭 Client-Only Rendering
- 🔐 Environment Variables
- 📝 Markdown Routes
- 🗺️ Sitemap & RSS Feed Generation
- 🔍 Content Search
- 📑 Table of Contents
- 🎨 OG Image Generation
- 📊 OpenTelemetry
- 📖 Learn More
- 🏗️ Build & Deployment
- 📝 License
✨ Features
🎯 Core Framework
- ⚡ Built on Bun - Lightning-fast JavaScript runtime for everything
- 🔄 File-Based Routing - Automatic route generation from your file structure
- 🎨 Server-Side Rendering (SSR) - SEO-friendly React rendering on the server
- ⚡ Streaming SSR - Progressive rendering with React 19 for 50-70% faster TTFB
- 🔥 Live Reload - Live reload in development mode with instant feedback
- 📦 Zero Config - Works out of the box with sensible defaults
🛠️ CLI Tools
- 🚀
reroute init- Scaffold new projects with templates (basic, blog, store) - 🔨
reroute gen- Generate content registry and route artifacts - ⚡
reroute dev- Start development environment with side-by-side logs and colored output - 📊
reroute analyze- Bundle analyzer with dependency treemap and size analysis
📄 Content Management
- 📚 Content Collections - Organize content by collections (blog posts, docs, etc.)
- 🔍 Content Discovery - Automatic scanning and indexing of content files
- 🏷️ Metadata Extraction - Support for content metadata
- 📑 Content Registry - Pre-generated content index for fast lookups
- 🎯 Dynamic Imports - Code-split content chunks for optimal loading
- 📦 Collection Chunking - Individual module bundles per content item
📝 Markdown & MDX Support
- 📄 Native Markdown Routes - Drop
.mdor.mdxfiles directly in your routes directory - 🎨 Syntax Highlighting - Beautiful code blocks powered by Shiki with VS Code themes
- 📋 Frontmatter Support - YAML frontmatter for page metadata and multiline values using
|pipe notation - ⚛️ True MDX Support - Import and use React components inside markdown with
@mdx-js/mdx - 🧩 Component Composition - Mix JSX components with markdown content seamlessly
- 🔧 Zero Config - Automatic detection and processing when streamdown is installed
- 🎯 GitHub Flavored Markdown - Built-in tables, task lists, strikethrough, code highlighting, and more
- ⚡ Streamdown Powered - AI-optimized streaming markdown with complete feature set built-in
- 🧩 Markdown Component - Programmatic markdown rendering with
<Markdown>component
🔍 Search & Table of Contents
- 🔍 Full-Text Search - Build-time index generation with smart ranking (title → headings → metadata)
- 🎯 Client-Side Search -
useSearch()hook with debouncing, pagination, and real-time results - 🌐 SSR Search - Server-rendered search pages with
collectionSearch()for SEO - 📊 Search Endpoint - HTTP API at
/__reroute_searchfor external integrations - 📄 Pagination - Configurable page size with full pagination support
- 📑 Auto TOC - Table of contents extracted from markdown headings during build
- 🎯 Auto-Detection -
useToc()automatically detects collection and slug from route - ⚓ Anchor Scrolling - Smooth scroll to sections with fragment navigation
- 💾 Lightweight - Optional content inclusion (default: headings + metadata only)
- 🎛️ Flexible - Filter by collections, configure heading levels
⚛️ React Integration
- ⚡ React 19 - Built with the latest React version for optimal performance
- 🪝 Router Hooks:
useNavigate()- Programmatic navigationuseParams()- Access route parametersuseSearchParams()- Query string manipulationuseRouter()- Access router state and utilities
- 📚 Content Hooks:
useContent()- Load and manage content collections with MongoDB-style filtering and sortinguseFeed()- Discover RSS feed URLs for current routeuseLlms()- Discover LLM-friendly content URLs for current routeuseSearch()- Client-side content search with debouncinguseToc()- Auto-extract table of contents from markdown headings
- 🧩 Components:
<Link>- Client-side navigation with automatic prefetching<Image>- Optimized images with automatic format negotiation and lazy loading<Outlet>- Render nested route components<ContentRoute>- Dynamic content rendering with metadata injection<ClientOnly>- Client-side only rendering with optional layout preservation (className/style props)
- 🎭 Providers:
<RerouteProvider>- USE THIS - All-in-one provider with routing + content + artifacts<RouterProvider>- Low-level router only (internal use, prefer RerouteProvider)<ContentProvider>- Content system context (internal use)
- 🗂️ SSR Data:
useData()- Read route-level SSR data without loadersexport const ssr = { data() {} }- Route data function executed on server
🖼️ Image Optimization
- 🎨 Format Control - Support for auto, AVIF, WebP, JPEG, and PNG formats
- 📐 Responsive Images - Automatic srcset generation for multiple device sizes (640-3840px)
- ⚡ On-Demand Processing - Server-side image transformation using Sharp
- 💾 Smart Caching - In-memory and disk cache for optimized images (1-year cache headers)
- 🔍 Lazy Loading - Native lazy loading with IntersectionObserver fallback for older browsers
- ⏫ Priority Loading - Opt-in eager loading for above-the-fold images
- 🌫️ Blur Placeholder - Optional blur-up effect with custom blur data URL support
- 🎯 Quality Control - Configurable quality (1-100, default: 75)
- 🔄 Loading States - Built-in spinner with customization (size, color, background, custom component)
- 🎨 Custom Loader - Override default image URL generation
- 📏 Callbacks - onLoad and onError event handlers
- 📱 Sizes Attribute - Control responsive image selection with custom sizes
🎨 OG Image Generation
- 🖼️ Auto-Generate - Dynamic Open Graph images for social media previews
- ⚡ Lightweight - Uses @vercel/og (Satori + resvg) - only ~5MB, no Chrome dependency
- 🎨 React Components - Design OG images with React and inline styles
- 🔄 Runtime Generation - Generate images on-demand with smart caching
- 📝 Frontmatter Support - Auto-extract title, description, author, date from content
- 🎯 Route Colocated - Place
[og].tsxfiles alongside routes - 🖼️ Avatar Support - Add logos/avatars from external URLs or local files
- ⚙️ Per-Route Override - Customize via
export const ssr = { ogImage: { ... } } - 🎭 Default Template - Built-in template or configure your own
- 🏷️ Auto Meta Tags - Automatic injection of og:image and twitter:card tags
- 🎯 CLI Preview -
reroute ogcommand to preview images in browser
🚀 Performance Optimizations
- 💾 Smart Caching - LRU cache for files and bundles
- 🔗 Link Prefetching - Automatic content prefetching on hover/focus
- 📦 Bundle Hashing - Content-based hashing for optimal cache invalidation
- ⚡ Module Preloading - Browser hints for critical resources
- 🎯 SSR Module Seeding - Pre-populate modules for instant hydration
- 📊 Collection Inlining - Inline collection data for zero-latency rendering
- 🌊 Progressive Streaming - Shell HTML sent immediately while data loads
- 🎭 Automatic Suspense - Zero-config streaming wrappers via
[skeleton].tsxconvention - ⚡ React 19 Streaming - Native
renderToReadableStreamfor optimal performance
🎨 Styling & Theming
- 🎨 Tailwind CSS v4 Support - Automatic detection and compilation with CSS-first configuration
- ⚡ Live CSS Reload - Instant style updates in development mode
- 🎯 Modern CSS Features -
@themeand@utilitydirectives support - 🚀 Oxide Engine - 5x faster builds with the new Tailwind compiler
- 🔧 Zero Config - Works out of the box when
@tailwindcss/cliis installed
🎨 Developer Experience
- 📁 Project Templates - Quick start with
basic,blog, orstoretemplates - 🔄 Live Reload - Server-Sent Events (SSE) for instant browser updates
- 🎯 TypeScript Support - Full type safety throughout the stack
- 🗂️ File System Watcher - Automatic rebuilds on source changes
- 🎪 404 Handling - Custom NotFound routes with pattern matching
- 🏗️ Layout System - Shared layouts across route groups
🌐 Server Features (Elysia Plugin)
- 🔌 Elysia Plugin - Drop-in plugin for Elysia apps
- 🎨 SSR Routes - Server-rendered React pages
- 📄 Static File Serving - Optimized bundle delivery
- 📚 Content API Routes - Dynamic content endpoints
- 🛠️ Dev Mode Routes - Development tooling and live reload
- 📦 Artifact Routes - Serve generated content chunks and registry
- 📊 Optional OpenTelemetry - Built-in tracing for SSR, 404s, cache hits
🎯 Build System
- 🔧 Transpilation - TypeScript/JSX to optimized JavaScript
- 🗜️ Minification - Optional code minification for production
- 🗺️ Source Maps - Debug support with source map generation
- 📦 Module Bundling - Efficient code splitting and bundling
- 🎯 Tree Shaking - Remove unused code automatically
🎨 Head Management
- 🏷️ Meta Tags - Automatic meta tag injection from content metadata
- 📝 Per-Page Head - Route-specific head elements
- 🌍 Language Attribute - Dynamic
langattribute support - 🎯 SEO Optimization - Title, description, and custom meta tags
🗺️ Sitemap Generation
- 🔍 Auto-Discovery - Automatically discovers static routes, content collections, and ssr.data routes
- 📄 XML Generation - Standards-compliant sitemap.xml following Google's specifications
- 💾 Smart Caching - Generated on first request and cached according to maxAge
- 📊 Auto-Pagination - Automatically splits into multiple files for sites with 50k+ URLs
- 🎨 Custom Extractors - Content-agnostic URL and date extraction via configuration
- 📅 Metadata Support - Includes lastmod, changefreq, and priority for each URL
🤖 Robots.txt Generation
- 🛡️ Smart Defaults - Permissive by default with automatic sitemap references
- 🔗 Deep Integration - Auto-disallows sitemap excludes and redirect sources
- 🎯 Policy Helpers - Pre-configured functions to block/allow AI crawlers and search engines
- 🧩 Composable - Mix and match policies for fine-grained crawler control
- ⚡ Runtime Generation - Generated on demand with 24-hour caching
- 🤝 Standards Compliant - Follows RFC 9309 robots.txt specification
📰 RSS Feed Generation
- 🔍 Auto-Discovery - Discovers content collections, route ssr.data, and layout ssr.data
- 📄 RSS & Atom Support - Standards-compliant RSS 2.0 and Atom 1.0 formats
- 📚 Per-Collection Feeds - Each collection gets its own feed (e.g., /blog/feed.xml)
- 💾 Smart Caching - Generated on first request and cached according to maxAge
- 🎨 Custom Extractors - Content-agnostic extraction for title, description, author, pubDate
🧠 LLM-Friendly Content
- 📄 Multiple Formats - Serve routes as
.txtor.mdextensions (e.g.,/blog/post.txt) - 🤝 Content Negotiation - RESTful Accept header support (
text/plain,text/markdown) - 📋 Site Index - Auto-generated
/llms.txtwith all content organized by section - 📦 Full Bundle -
/llms-full.txtwith complete site content in one file - 🎯 Token-Efficient - Minimal metadata, maximum useful content for AI consumption
- 🔒 Exclusions - Filter routes/collections with strings, RegExp, or custom functions
- ⚡ Smart Caching - 24h for pages, 7d for bundles (configurable)
- 🔄 Auto-Sorting - Items sorted by publication date (newest first)
- 📏 Limit Control - Configurable maximum items per feed (default: 50)
📦 Configuration Options
- 🎯 Custom Assets Directory - Configure where your client files live
- 🔧 URL Prefix - Deploy to subdirectories with custom prefixes
- 🚫 Ignore Patterns - Exclude files from static serving
- 🏷️ Custom Headers - Add headers to static file responses
- ⏰ Cache Control - Configurable max-age and cache directives for static files and
__reroute_dataJSON - 🎨 HTML Template - Custom index.html with variable substitution
- 🔐 Environment Variables - Share REROUTE_ prefixed env vars between server and browser
🚀 Quick Start
# Create a new project (basic template)
bunx reroute-js init my-app
# Or with a specific template
bunx reroute-js init my-blog --template blog
bunx reroute-js init my-store --template store
# Navigate to project
cd my-app
# Start development
bun dev🔌 External Requests SSR
Reroute can execute route-level data fetching on the server and inline the result for instant hydration. Define an optional ssr.data in a route module and read it with useData():
// routes/products/[id].tsx
import { useData, useParams } from 'reroute-js/react';
import { getProduct } from '../lib/api';
export const ssr = {
async data({ params, set }: { params: { id?: string }; set: { status: number } }) {
const id = Number.parseInt(params.id || '');
if (!Number.isFinite(id)) {
set.status = 404; // Set HTTP status code
return { product: null };
}
const product = await getProduct(id);
if (!product) {
set.status = 404;
return { product: null };
}
return { product };
},
};
export default function Page() {
const { id } = useParams<{ id: string }>();
const data = useData<{ product: any }>(); // keyed by pathname
return <div>{data?.product?.title || `Invalid product ${id}`}</div>;
}Notes:
- Works with Bun + Elysia, no client loaders needed for initial render
- Data is injected as
window.__REROUTE_DATA__and read during hydration - Use with existing content features (useContent); both can be seeded in the same page
- HTTP Status Control: Use
set.statusto set response status codes (follows Elysia's API). Non-200 responses are not cached
⚡ Streaming SSR
Reroute supports progressive server-side rendering with React 19's streaming capabilities. Routes with async data automatically stream shell content immediately (50-70% faster TTFB), then progressively send content as it becomes ready.
Quick Start
Enable streaming in your config:
// reroute.config.ts
import { defineConfig } from 'reroute-js/core';
export default defineConfig({
app: {
src: './src/client/App',
},
streaming: {
enabled: true,
},
});Run bun gen and routes with ssr.data automatically get Suspense wrappers and streaming optimization - no code changes needed!
How It Works
Without streaming (default):
- Server waits for all data → sends complete HTML → user sees page
With streaming:
- Server sends shell HTML immediately (layouts, navigation) - instant TTFB
- Shows loading skeleton while data fetches
- Streams content progressively as ready
- User sees something immediately, perceives faster load
Custom Loading Skeletons
Create custom loading states using the [skeleton].tsx convention:
// routes/blog/[slug].tsx
export const ssr = {
async data({ params }) {
return { post: await fetchPost(params.slug) };
}
};
export default function BlogPost() {
const { post } = useData();
return <article>{post.title}</article>;
}// routes/blog/[skeleton].tsx
export default function BlogSkeleton() {
return (
<div className="animate-pulse p-8">
<div className="h-8 bg-gray-200 rounded w-3/4 mb-4" />
<div className="space-y-3">
<div className="h-4 bg-gray-200 rounded" />
<div className="h-4 bg-gray-200 rounded" />
</div>
</div>
);
}The CLI automatically detects [skeleton].tsx files and uses them in generated Suspense boundaries.
Layout Streaming
Layouts can also stream with their own data and skeletons:
// routes/dashboard/[layout].tsx
export const ssr = {
async data() {
return { user: await fetchUser() };
}
};
export default function DashboardLayout() {
const { user } = useLayoutData();
return (
<div>
<nav>Welcome, {user.name}</nav>
<Outlet />
</div>
);
}// routes/dashboard/[layout+skeleton].tsx
export default function DashboardLayoutSkeleton() {
return <div className="animate-pulse">Loading dashboard...</div>;
}Streaming order:
- Shell HTML → instant
- Layout skeleton → while layout data loads
- Layout content → when ready
- Route skeleton → while route data loads
- Route content → when ready
Default Skeletons
Configure app-wide default skeletons:
// reroute.config.ts
export default defineConfig({
streaming: {
enabled: true,
defaultSkeleton: './src/client/components/DefaultSkeleton',
defaultLayoutSkeleton: './src/client/components/LayoutSkeleton',
},
});Priority order:
- Custom
[skeleton].tsxin route directory - Configured
defaultSkeleton - Built-in generic skeleton
For layouts:
- Custom
[layout+skeleton].tsx - Configured
defaultLayoutSkeleton - Falls back to
defaultSkeleton - Built-in generic skeleton
Nested Suspense
Add your own Suspense boundaries for fine-grained streaming:
// routes/feed.tsx
import { Suspense } from 'react';
export const ssr = {
async data() {
return { posts: await fetchPosts() };
}
};
export default function Feed() {
const { posts } = useData();
return (
<div>
<h1>Feed</h1>
{/* Route-level Suspense added automatically by gen */}
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
{/* Component-level Suspense you can add manually */}
<Suspense fallback={<div>Loading comments...</div>}>
<Comments postId={post.id} />
</Suspense>
</article>
))}
</div>
);
}Each Suspense boundary streams independently - shell renders first, then each boundary resolves progressively.
Performance
Example improvements with streaming enabled:
| Metric | Without Streaming | With Streaming | Improvement | |--------|-------------------|----------------|-------------| | TTFB | 400ms | 50ms | 87% faster | | FCP | 600ms | 150ms | 75% faster |
Example
Check out examples/streaming for a comprehensive demo with:
- Basic streaming routes
- Custom skeletons
- Layout streaming
- Nested Suspense boundaries
- Performance comparisons
🎭 Client-Only Rendering
The ClientOnly component prevents hydration mismatches for browser-specific code and supports layout preservation to prevent flickering:
import { ClientOnly } from 'reroute-js/react';
// Basic usage - prevent hydration mismatch
<ClientOnly fallback={<div>Loading...</div>}>
<CountdownTimer />
</ClientOnly>
// Prevent layout shift with height preservation
<ClientOnly
fallback={<PricingSkeleton />}
className="min-h-screen"
>
<PricingSection />
</ClientOnly>
// With inline styles
<ClientOnly
fallback={<Loader />}
style={{ minHeight: '600px' }}
>
<DashboardContent />
</ClientOnly>When to use:
- Components using browser APIs (
window,localStorage,navigator) - Dynamic values (
Date.now(),Math.random()) - Time-sensitive data that differs between server and client
- Large lazy-loaded components (use
className/styleto preserve height)
Props:
fallback- Content to show during SSR and before hydrationclassName- Optional className for wrapper div (useful for layout preservation)style- Optional inline styles for wrapper div (useful for layout preservation)
🔐 Environment Variables
Reroute supports sharing environment variables between server and browser code using a REROUTE_ prefix convention (similar to how Vite uses VITE_).
Usage
Create a .env file in your project root:
# .env
# Server-only variables (not exposed to browser)
DATABASE_URL=postgresql://localhost:5432/mydb
SECRET_KEY=my-secret-key
# Public variables (exposed to browser)
REROUTE_API_URL=https://api.example.com
REROUTE_ANALYTICS_ID=G-XXXXXXXXXX
REROUTE_FEATURE_FLAG=trueAccessing Variables
In Server Code:
All variables are available via process.env or Bun.env:
// src/index.ts
const dbUrl = process.env.DATABASE_URL;
const apiUrl = process.env.REROUTE_API_URL;In Client/Browser Code:
Only REROUTE_ prefixed variables are available:
// src/client/routes/index.tsx
export default function Home() {
// ✅ Available - prefixed with REROUTE_
const apiUrl = import.meta.env.REROUTE_API_URL;
const analyticsId = process.env.REROUTE_ANALYTICS_ID; // Also works
// ❌ Not available - no REROUTE_ prefix
const dbUrl = process.env.DATABASE_URL; // undefined in browser
return <div>API: {apiUrl}</div>;
}Environment Files
Reroute supports multiple environment files with priority:
.env- Base environment variables.env.production- Production-specific variables.env.local- Local overrides (gitignored).env.production.local- Local production overrides (gitignored)
Files are loaded in order, with later files overriding earlier ones.
Important: The mode (development vs production) is controlled by the --prod flag, not by NODE_ENV in .env files. Use reroute gen --prod or reroute build for production mode.
Hot Reload: Changes to any .env file automatically rebuild the bundle and reload your browser in watch mode - no server restart needed!
Security
Important: Only variables prefixed with REROUTE_ are bundled into the client code. This prevents accidentally exposing sensitive credentials like database URLs, API keys, or secrets to the browser.
- ✅
REROUTE_API_URL- Safe to expose - ✅
REROUTE_FEATURE_FLAG- Safe to expose - ❌
DATABASE_URL- Server-only - ❌
SECRET_KEY- Server-only
TypeScript Support
For type safety, create a env.d.ts file:
// env.d.ts
interface ImportMetaEnv {
readonly REROUTE_API_URL: string;
readonly REROUTE_ANALYTICS_ID: string;
readonly REROUTE_FEATURE_FLAG: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}📝 Markdown Routes
Reroute supports native markdown and MDX files as routes with frontmatter support and syntax highlighting.
Installation
# Required for markdown support (includes GFM, syntax highlighting, math, mermaid, and more)
bun add streamdown
# Optional: Syntax highlighting (if not using Streamdown's built-in)
bun add -d @shikijs/rehype
# Optional: MDX support (use React components in markdown)
bun add @mdx-js/mdxCreating Markdown Routes
Simply create .md files in your routes directory:
<!-- src/client/routes/about.md -->
---
title: About Us
description: Learn more about our company
date: 2025-01-15
---
# About Us
Welcome to our **amazing** company! We build great things.
## Our Mission
We strive to create the best developer experience possible.
```typescript
// Example code block with syntax highlighting
const greeting = "Hello, World!";
console.log(greeting);
The route will be automatically available at `/about` with:
- Automatic `<title>` and `<meta>` tag generation from frontmatter
- Built-in syntax highlighting for code blocks with Streamdown
- Built-in GitHub Flavored Markdown, math expressions, and mermaid diagrams
### Draft Content Support
Mark content as draft to exclude it from production builds:
```markdown
<!-- src/client/routes/blog/content/my-draft.md -->
---
title: Work in Progress
description: This post is not ready yet
draft: true
---
# Coming Soon
This content will be excluded from production builds.Draft Behavior:
- Development mode (
reroute gen,reroute dev): Draft content is included in content registry and search index - Production mode (
reroute gen --prod,reroute build): Draft content is completely excluded from:- Content registry and bundles
- Search index
- Sitemap generation
- RSS feeds
- All other discovery mechanisms
This allows you to work on content without deploying it to production.
Creating MDX Routes with Components
For interactive content with React components, use .mdx files. Best practice: Import components from external files rather than defining them inline.
<!-- src/client/routes/blog/content/my-post.mdx -->
---
title: Interactive Demo
description: MDX with React components
date: 2025-01-17
steps: |
Install dependencies
Configure the project
Run the development server
---
import { Link } from 'reroute-js/react';
import { Alert } from '../../../components/Alert';
import { StepsList } from '../../../components/StepsList';
# Interactive Content
The `meta` variable is automatically available from frontmatter:
<StepsList steps={meta.steps} title="Getting Started" />
<Alert type="success">
Components imported from external files work seamlessly!
</Alert>
<Link to="/">Go Home</Link>Key Features:
- Import external components from separate files (recommended)
- Frontmatter available as
metavariable automatically - Import paths automatically rewritten during compilation
- Supports multiline YAML values with
|pipe notation - Full TypeScript support for imported components
MDX Support requires:
@mdx-js/mdxpackage installed.mdxfile extension- Components imported at the top, after frontmatter.
Programmatic Markdown Rendering
Use the <Markdown> component to render markdown content dynamically:
import { Markdown } from 'reroute-js/react';
export default function MyPage() {
const content = `# Hello\nThis is **markdown** content.`;
return (
<Markdown
theme="github-dark"
gfm={true}
highlight={true}
>
{content}
</Markdown>
);
}Features
- Frontmatter: YAML metadata for SEO and content organization
- Syntax Highlighting: Powered by Shiki via @shikijs/rehype with VS Code themes
- GFM Support: Tables, task lists, strikethrough, autolinks
- MDX Support: Import and use React components in
.mdxfiles - Component Composition: Mix markdown with JSX seamlessly
- Type Safety: Full TypeScript support
- Zero Config: Automatic detection when packages are installed
- Optional: Only bundle dependencies you actually use
🗺️ Sitemap & RSS Feed Generation
Reroute provides automatic sitemap and RSS feed generation with zero configuration required.
Sitemap Generation
Generate standards-compliant XML sitemaps for search engines.
Quick Start
// reroute.config.ts
import { defineConfig } from 'reroute-js/core';
export default defineConfig({
sitemap: {
enabled: true, // opt-in (default: false)
baseUrl: 'https://your-domain.com',
},
});Your sitemap is now available at /sitemap.xml!
What Gets Included
All three sources are automatically discovered:
- Static Routes - Pages in
routes/folder (e.g.,/,/about,/contact) - Content Collections - Items in
routes/[collection]/content/with metadata - Route SSR Data - Dynamic data from route
ssr.dataexports
Custom Configuration
For non-standard data structures:
sitemap: {
enabled: true,
baseUrl: 'https://your-domain.com',
// Extract URL segment from ssr.data items
extractUrl: (item, routePattern) => {
// Products use SKU instead of slug
if (routePattern === '/products' && item.sku) {
return item.sku; // /products/laptop-pro-2024
}
return null; // fall back to defaults (slug, id, name, key)
},
// Extract lastmod date
extractLastmod: (item) => item.updatedAt || item.date,
// Default settings
changefreq: 'weekly', // or 'daily', 'monthly', etc.
priority: 0.5, // 0.0 to 1.0
}Features
- Runtime generation - Generated on first request, cached according to
maxAge - Auto-pagination - Sites with 50k+ URLs automatically split into multiple files with sitemap index
- SEO-friendly - Includes
<lastmod>,<changefreq>, and<priority>tags - Content-agnostic - Works with any data structure via custom extractors
- Smart caching - Respects your
maxAgeconfiguration
Robots.txt Generation
Control crawler access with automatic robots.txt generation and smart defaults.
Quick Start
// reroute.config.ts
import { defineConfig, blockAICrawlers } from 'reroute-js/core';
export default defineConfig({
robots: {
enabled: true,
baseUrl: 'https://your-domain.com',
policies: [
{ userAgent: '*', allow: ['/'], crawlDelay: 10 },
...blockAICrawlers(), // Block all AI crawlers
],
},
});Your robots.txt is available at /robots.txt!
Policy Helpers
import {
blockAICrawlers,
allowAICrawlers,
blockAITrainingCrawlers,
allowSearchCrawlers,
blockSearchCrawlers,
AI_CRAWLERS,
SEARCH_CRAWLERS,
} from 'reroute-js/core';
// Block all AI crawlers
...blockAICrawlers()
// Block specific AI crawlers
...blockAICrawlers([AI_CRAWLERS.GPT, AI_CRAWLERS.CLAUDE])
// Block only training crawlers (CCBot, Google-Extended, etc.)
...blockAITrainingCrawlers()
// Allow all search engines
...allowSearchCrawlers()
// Block specific search engines
...blockSearchCrawlers([SEARCH_CRAWLERS.BAIDUSPIDER])Features
- Smart defaults - Permissive by default (
Allow: /) - Sitemap integration - Auto-references
/sitemap.xmlwhen sitemap is enabled - Redirect exclusion - Auto-disallows old redirect source paths
- Sitemap exclusion - Auto-disallows routes excluded from sitemap
- Policy helpers - Pre-configured functions for common crawler control
- Composable - Mix and match policies for fine-grained control
- Standards compliant - Follows RFC 9309 robots.txt specification
RSS Feed Generation
Generate RSS 2.0 and Atom 1.0 feeds for content syndication.
Quick Start
// reroute.config.ts
import { defineConfig } from 'reroute-js/core';
export default defineConfig({
rss: {
enabled: true, // opt-in (default: false)
baseUrl: 'https://your-domain.com',
title: 'My Blog',
description: 'Latest posts and updates',
},
});Your feeds are automatically available at:
/feed.xml- Main feed (combines all sources below)/blog/feed.xml- Blog collection (fromroutes/blog/content/)/changelog/feed.xml- Changelog route (from route'sssr.data)/announcements/feed.xml- Announcements (from layout'sssr.data)
Pattern:
/[collection]/feed.xml- For any content collection/[route]/feed.xml- For any route/layout withssr.data
What Gets Included
All three sources are automatically discovered:
- Content Collections - Items from
routes/[collection]/content/folders- Example: Blog posts with metadata (title, description, date)
- Route SSR Data - Arrays returned by route
ssr.datafunctions- Example: Changelog versions fetched from API
- Must export
ssr.datathat returns object containing arrays
- Layout SSR Data - Arrays returned by layout
ssr.datafunctions- Example: Site-wide announcements fetched in layout
- Must export
ssr.datain[layout].tsxthat returns object containing arrays
Important: For ssr.data discovery, the data must contain arrays. Nested arrays (e.g., { user: { notifications: [...] } }) are automatically found via recursive discovery.
Custom Configuration
rss: {
enabled: true,
baseUrl: 'https://your-domain.com',
title: 'My Blog',
description: 'Latest updates',
limit: 50, // max items per feed (default: 50)
format: 'rss', // 'rss' (RSS 2.0) or 'atom' (Atom 1.0)
// Extract URL segment from ssr.data items
extractUrl: (item, routePattern) => {
// Changelog uses version field
if (routePattern === '/changelog' && item.version) {
return item.version; // /changelog/1.2.0
}
return null; // fall back to defaults (slug, id, name, key)
},
// Extract publication date
extractPubDate: (item) => item.publishedAt || item.date || item.createdAt,
// Extract author
extractAuthor: (item) => item.author || item.authorName,
// Extract full content (optional, for full-text feeds)
extractContent: (item) => item.body || item.content,
}Features
- Multi-source - Combines content collections, route ssr.data, and layout ssr.data
- Per-collection feeds - Each collection gets
/[collection]/feed.xml - Per-route feeds - Routes with ssr.data get
/[route]/feed.xml - RSS & Atom - Choose RSS 2.0 or Atom 1.0 format
- Auto-sorting - Items sorted by pubDate (newest first)
- Smart caching - Generated on first request, cached according to
maxAge - Recursive discovery - Finds arrays even in nested data (e.g.,
user.notifications) - Content-agnostic - Works with any data structure via custom extractors
Discovering Feeds in Your UI
Use the useFeed() hook to automatically discover and link to RSS feeds:
import { useFeed } from 'reroute-js/react';
function Header() {
const feed = useFeed();
return (
<header>
{/* Show collection/route-specific feed if available */}
{feed.hasFeed && (
<a href={feed.feedUrl!} target="_blank">
Subscribe to {feed.collection}
</a>
)}
{/* Main feed always available */}
<a href={feed.mainFeedUrl}>RSS Feed</a>
</header>
);
}Returns:
feedUrl- URL of the specific feed (/blog/feed.xml,/changelog/feed.xml, etc.) ornullmainFeedUrl- Main feed URL (always/feed.xml)collection- Collection/route name ('blog','changelog', etc.) ornullhasFeed- Boolean indicating if current route has a specific feed
SPA Navigation Support
Important: When using layout ssr.data, use <Link> components (not <a> tags) for proper client-side navigation:
import { Link } from 'reroute-js/react';
// ✅ Good - prefetches layout data on hover
<Link to="/announcements">Announcements</Link>
// ❌ Bad - full page reload, no prefetch
<a href="/announcements">Announcements</a>LLM-Friendly Content
Make your site AI-navigable with automatic text/markdown formats and content bundles.
Quick Start
// reroute.config.ts
import { defineConfig } from 'reroute-js/core';
export default defineConfig({
llms: {
enabled: true,
baseUrl: 'https://your-domain.com',
siteName: 'My Blog',
siteDescription: 'Thoughts on web development',
},
});Your site now supports:
.txtand.mdextensions on any route (e.g.,/blog/post.txt,/about.md)- Accept header content negotiation (
Accept: text/plain,Accept: text/markdown) /llms.txt- Index of all content organized by section/llms-full.txt- Complete site content in one file
How It Works
File Extensions:
# Original HTML route
GET /blog/hello-world → HTML page
# Add .txt for plain text
GET /blog/hello-world.txt → Clean text (title + body)
# Add .md for markdown
GET /blog/hello-world.md → Markdown formatAccept Headers (RESTful):
# Same URL, different representations
curl -H "Accept: text/plain" https://example.com/blog/hello-world
curl -H "Accept: text/markdown" https://example.com/blog/hello-world
curl -H "Accept: text/html" https://example.com/blog/hello-world # DefaultFeatures
- Runtime generation - Content extracted on-demand, cached aggressively
- Smart caching - 24h for pages, 7d for full bundle (configurable)
- Multi-format - Extensions AND Accept headers (RESTful)
- Token-efficient - Minimal metadata, maximum useful content
- Flexible exclusions - Strings, RegExp, or custom functions
- Works everywhere - Collections, SSR routes, static pages
- Auto-discovery - All content automatically indexed
Discovering LLM Files in Your UI
Use the useLlms() hook to automatically discover and link to LLM content:
import { useLlms } from 'reroute-js/react';
function Footer() {
const llms = useLlms();
return (
<footer>
{/* Collection/route-specific index if available */}
{llms.collection && (
<a href={llms.llmsUrl!}>
{llms.collection} Index (.txt)
</a>
)}
{/* Full content bundle */}
<a href={llms.fullLlmsUrl}>
Full Content Bundle
</a>
{/* Main site index */}
<a href={llms.mainLlmsUrl}>
Site Index
</a>
</footer>
);
}Returns:
llmsUrl- Collection/route-specific index (/docs/llms.txt) or main index (/llms.txt)fullLlmsUrl- Full content bundle (/docs/llms-full.txtor/llms-full.txt)mainLlmsUrl- Main site index (always/llms.txt)collection- Collection name ('docs','blog') ornullhasLlms- Boolean if current route has specific LLM files
🔍 Content Search
Reroute provides full-text search across your content collections with pagination, client-side and SSR support.
Configuration
// reroute.config.ts
import { defineConfig } from 'reroute-js/core';
export default defineConfig({
search: {
enabled: true,
collections: ['blog', 'docs'], // or omit to index all
includeContent: false, // lightweight (headings only)
metaFields: ['title', 'description', 'excerpt', 'tags'],
defaultPageSize: 20, // results per page (default: 20)
maxPageSize: 100, // max allowed page size (default: 100)
},
});Run reroute gen to generate .reroute/search.json.
Client-Side Search with Pagination
import { useState } from 'react';
import { useSearch, Link } from 'reroute-js/react';
function SearchBox() {
const [query, setQuery] = useState('');
const [page, setPage] = useState(1);
const { results, total, totalPages, isLoading } = useSearch({
query,
page,
pageSize: 10,
collections: ['blog', 'docs'],
});
return (
<div>
<input
type="search"
value={query}
onChange={(e) => {
setQuery(e.target.value);
setPage(1); // reset to first page
}}
placeholder="Search..."
/>
{isLoading && <div>Searching...</div>}
<ul>
{results.map(result => (
<li key={result.id}>
<Link to={result.href}>{result.title}</Link>
{result.excerpt && <p>{result.excerpt}</p>}
</li>
))}
</ul>
{totalPages > 1 && (
<div>
<button onClick={() => setPage(p => p - 1)} disabled={page === 1}>
Previous
</button>
<span>Page {page} of {totalPages}</span>
<button onClick={() => setPage(p => p + 1)} disabled={page === totalPages}>
Next
</button>
</div>
)}
</div>
);
}SSR Search with Pagination
For SEO-friendly search result pages:
// routes/search.tsx
import { collectionSearch, useData, useSearchParams } from 'reroute-js/react';
const ssr = {
async data({ searchParams }) {
const page = Number.parseInt(searchParams?.get('page') || '1');
return await collectionSearch({
searchParams,
page,
pageSize: 20,
});
},
};
function SearchPage() {
const [params] = useSearchParams();
const query = params.get('q') || '';
const data = useData();
return (
<div>
<h1>Search Results for "{query}"</h1>
{data && <p>Found {data.total} results (Page {data.page} of {data.totalPages})</p>}
{data?.results.map(result => (
<div key={result.id}>
<a href={result.href}>{result.title}</a>
<p>{result.excerpt}</p>
</div>
))}
</div>
);
}
export default SearchPage;
export { ssr };Visit /search?q=react&page=1 for server-rendered results.
📑 Table of Contents
Extract and display table of contents from markdown headings.
Basic Usage
// routes/blog/[slug].tsx
import { ContentRoute, Link, useToc, useRouter } from 'reroute-js/react';
function BlogPost() {
return (
<div style={{ display: 'flex', gap: '2rem' }}>
<article style={{ flex: 1 }}>
<ContentRoute collection="blog" />
</article>
<TableOfContents />
</div>
);
}
function TableOfContents() {
const { toc } = useToc(); // Auto-detects collection and slug
const { pathname } = useRouter();
if (toc.length === 0) return null;
return (
<aside style={{ width: '250px', position: 'sticky', top: '2rem' }}>
<h2>On This Page</h2>
<nav>
<ul>
{toc.map((heading) => (
<li
key={heading.slug}
style={{ marginLeft: `${(heading.level - 1) * 0.75}rem` }}
>
<Link to={pathname} fragment={heading.slug}>
{heading.text}
</Link>
</li>
))}
</ul>
</nav>
</aside>
);
}
export default BlogPost;Enable Anchor Scrolling
Configure custom markdown components to add IDs to headings:
// reroute.config.ts
export default defineConfig({
markdown: {
components: './src/client/lib/markdown-components',
},
});// src/client/lib/markdown-components.tsx
import type { StreamdownProps } from 'streamdown';
function slugify(text: string): string {
return text.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.trim();
}
const markdownComponents: Components = {
h2: ({ children, ...props }) => {
const id = slugify(String(children));
return <h2 id={id} {...props}>{children}</h2>;
},
h3: ({ children, ...props }) => {
const id = slugify(String(children));
return <h3 id={id} {...props}>{children}</h3>;
},
// ... h4, h5, h6
};
export default markdownComponents;🎨 OG Image Generation
Auto-generate beautiful Open Graph (social media preview) images using React components.
Quick Start
1. Enable in config:
// reroute.config.ts
import { defineConfig } from 'reroute-js/core';
export default defineConfig({
ogImage: {
enabled: true,
baseUrl: 'https://example.com', // Required for social media sharing
width: 1200, // Default: 1200
height: 630, // Default: 630
maxAge: 3600, // Cache duration in seconds
avatar: 'https://example.com/logo.png', // Optional
siteName: 'My Site', // Optional
},
});2. Create OG templates:
// routes/blog/[og].tsx - OG template for all blog posts
import type { OGImageProps } from 'reroute-js/core';
export default function BlogOGImage({ meta, avatar }: OGImageProps) {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: '80px',
}}>
<h1 style={{ fontSize: '72px', color: '#fff' }}>
{meta.title}
</h1>
{avatar && (
<img src={avatar} width={80} height={80}
style={{ borderRadius: '50%' }} />
)}
</div>
);
}File Structure
src/client/routes/
├── [og].tsx # OG image for /
├── blog/
│ ├── index.tsx
│ ├── [og].tsx # OG image for /blog
│ └── [slug].tsx # Uses blog/[og].tsx
└── about/
└── index.tsx # Uses default template (no [og].tsx)Available Props
interface OGImageProps {
params: Record<string, string>; // Route params { slug: 'my-post' }
meta: any; // Frontmatter + page metadata
avatar?: string; // Avatar URL (config or per-route)
siteName?: string; // Site name (config or per-route)
}Styling
Use inline style prop with full CSS support:
<div style={{
display: 'flex',
background: 'linear-gradient(135deg, #667eea, #764ba2)',
padding: '64px',
}}>
<h1 style={{ fontSize: '72px', fontWeight: 'bold' }}>
{meta.title}
</h1>
</div>Note: tw prop is NOT supported. Use inline styles with full CSS.
Per-Route Customization
Override avatar/siteName per route:
// routes/special-page.tsx
export const meta = {
title: 'Special Page',
};
export const ssr = {
ogImage: {
avatar: 'https://example.com/special-avatar.png',
siteName: 'Special Site',
},
};
export default function SpecialPage() {
return <div>Special content</div>;
}CLI Preview
Preview OG images in your browser:
# Interactive list
reroute og
# Direct preview
reroute og /blog/my-postURLs & Meta Tags
Generated URLs:
/→https://example.com/__reroute_og/index.png/blog→https://example.com/__reroute_og/blog.png/blog/my-post→https://example.com/__reroute_og/blog/my-post.png
Auto-injected meta tags (when baseUrl is configured):
<meta property="og:image" content="https://example.com/__reroute_og/blog/my-post.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://example.com/__reroute_og/blog/my-post.png" />Note: The baseUrl is required for proper social media sharing. Without it, relative URLs will be generated and a warning will be logged.
Default Template
If no [og].tsx exists for a route, uses built-in template with:
- Title from meta
- Description from meta
- Avatar and siteName from config
- Clean black card design
📊 OpenTelemetry
Server Telemetry
Track HTTP requests, errors, and system metrics.
Installation:
# Server telemetry packages
bun add source-map \
@opentelemetry/api \
@opentelemetry/api-logs \
@opentelemetry/exporter-logs-otlp-proto \
@opentelemetry/exporter-metrics-otlp-proto \
@opentelemetry/exporter-trace-otlp-proto \
@opentelemetry/resources \
@opentelemetry/sdk-logs \
@opentelemetry/sdk-metrics \
@opentelemetry/sdk-trace-node \
@opentelemetry/semantic-conventions \
Configuration in reroute.config.ts:
export default defineConfig({
telemetry: {
// Server telemetry
enabled: true,
environment: process.env.NODE_ENV || 'development',
ignoreRoutes: [
'/health',
/^\/assets\//, // Use regex instead of functions (closures don't serialize to browser)
/\.(js|css|png|jpg|svg|ico)$/,
],
sampleRate: 1.0,
capture: [
...RerouteHeaders.http(),
...RerouteHeaders.request(),
...CloudflareHeaders.geo(),
],
// Browser telemetry proxy (avoids CORS and ad blockers)
proxy: {
enabled: true, // Default
pathname: '/__reroute_telemetry', // Default
verbose: false, // Log proxy requests
},
// Browser telemetry
browser: {
enabled: true,
serviceName: 'my-app-browser',
environment: process.env.NODE_ENV || 'development',
otlpEndpoint: '/__reroute_telemetry', // Must match proxy.pathname
enableConsoleCapture: true,
},
},
});Usage - Config loaded automatically:
import { telemetry } from 'reroute-js/telemetry/server';
new Elysia()
.use(telemetry())
.use(reroute())
.listen(3000);Browser telemetry - Track client-side events and errors.
Installation:
# Browser telemetry packages
bun add @opentelemetry/api \
@opentelemetry/auto-instrumentations-web \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/instrumentation \
@opentelemetry/resources \
@opentelemetry/sdk-trace-base \
@opentelemetry/sdk-trace-web \
@opentelemetry/semantic-conventionsUsage:
import { TelemetryProvider } from 'reroute-js/telemetry/react/react';
// Auto-loads from telemetry.browser config
<TelemetryProvider>
<RerouteProvider {...bundle} />
</TelemetryProvider>
// Or override config values
<TelemetryProvider enabled={true} serviceName="custom-name">
<RerouteProvider {...bundle} />
</TelemetryProvider>Custom Instrumentation
Create custom spans for API calls, database queries, or any operation:
import { withSpan } from 'reroute-js/telemetry/server';
// Instrument API endpoints
app.get('/api/products', async () => {
return withSpan('api.get_products', async (span) => {
span.setAttribute('api.operation', 'list_products');
const products = await fetchProducts();
span.setAttribute('api.result_count', products.length);
return products;
});
});
// Nested spans automatically create parent-child relationships
app.get('/api/orders', async () => {
return withSpan('api.get_orders', async (span) => {
span.setAttribute('api.operation', 'list_orders');
// Child span
const user = await withSpan('api.fetch_user', async (childSpan) => {
childSpan.setAttribute('user.id', userId);
return await getUser(userId);
});
return { user, orders: await getOrders(user.id) };
});
});Error Handling:
Errors are automatically tracked and the span is properly closed:
app.get('/api/user/:id', async ({ params }) => {
return withSpan('api.get_user', async (span) => {
span.setAttribute('user.id', params.id);
const user = await getUser(params.id);
if (!user) {
// Error is automatically captured in the span
throw new Error('User not found');
}
return user;
});
});When an error occurs inside a span:
- Error is recorded -
span.recordException()captures the full error details - Status is set - Span status automatically set to
ERRORwith error message - Span is closed - Span properly ended even when errors occur
- Error is re-thrown - Error propagates normally (not swallowed)
This means errors appear in your traces with full context while maintaining normal error handling behavior.
Key Features:
- Automatic error tracking - Errors are captured and recorded in spans
- Nested spans - Child spans automatically linked to parent context
- Zero overhead when disabled - No-op implementation when OpenTelemetry not installed
Reroute SSR Tracing
Built-in tracing for debugging SSR, 404s, and cache behavior. Automatically traces SSR rendering, 404s, data loading, and errors. Filter by reroute.* attributes in SigNoz/Jaeger.
📖 Learn More
- Examples - Example projects to get started
- Packages - Reroute entrypoints
- GitHub - Source code and issues
🏗️ Build & Deployment
Production Build
# Build JavaScript bundle
reroute build
# Build standalone binary
reroute build --compile
# Custom output name
reroute build --compile -o myappWhat Gets Generated?
When you run reroute build, two directories are created:
dist/- Your production server bundle or compiled binary.reroute/- Runtime assets needed by the server (client bundles, content metadata)
Key points:
- ✅ No code duplication (server vs browser targets)
- ✅ Production-ready compression (Brotli + Gzip)
- ✅ Code splitting enabled (lazy-loaded chunks)
- ✅ Only ~58KB JavaScript transferred to browsers
Deployment Checklist
For compiled binary (--compile):
dist/app # Your executable
.reroute/
├── bundles/ # Client-side code
├── collections/ # Content metadata
└── content.ts # Content registry for SSRFor JavaScript bundle (no --compile):
dist/app.js # Server bundle
.reroute/ # Runtime assets (same as above)
node_modules/ # Production dependenciesLearn More
See BUILD.md for detailed documentation on:
- What's in each folder
- How the build process works
- Runtime architecture
- Optimization tips
- Troubleshooting
📝 License
MIT
