Skip to content

Route Configuration Reference ​

Complete reference for all route-level exports and configuration options.

Route Exports ​

Every route file can export these named exports:

default (Component) ​

The React component that renders for this route.

tsx
export default function Page({ loaderData, params }) {
  return <div>{loaderData.title}</div>
}

Props:

PropTypeDescription
loaderDataTData returned from the loader
paramsRecord<string, string>URL parameters

loader ​

Server-side data fetching function.

tsx
import { createLoader } from '@ereo/data'

export const loader = createLoader(async ({ request, params, context }) => {
  const post = await db.posts.find(params.id)
  return { post }
})

Args:

ArgTypeDescription
requestRequestThe incoming HTTP request
paramsRecord<string, string>URL parameters
contextRequestContextShared request context

action ​

Server-side mutation handler for form submissions.

tsx
import { createAction } from '@ereo/data'

export const action = createAction(async ({ request, params, context }) => {
  const formData = await request.formData()
  // Handle mutation...
  return { success: true }
})

meta ​

Generate document metadata (title, meta tags, etc.).

tsx
import type { MetaFunction } from '@ereo/core'

export const meta: MetaFunction = ({ data, params }) => {
  return [
    { title: data.post.title },
    { name: 'description', content: data.post.excerpt },
    { property: 'og:title', content: data.post.title },
    { property: 'og:image', content: data.post.image }
  ]
}

Args:

ArgTypeDescription
dataTLoader data
paramsRecord<string, string>URL parameters
matchesRouteMatch[]All matched routes

headers ​

Set HTTP response headers.

tsx
import type { HeadersFunction } from '@ereo/core'

export const headers: HeadersFunction = ({ loaderHeaders, actionHeaders, parentHeaders }) => {
  return {
    'Cache-Control': loaderHeaders.get('Cache-Control') || 'max-age=300',
    'X-Custom-Header': 'value'
  }
}

Args:

ArgTypeDescription
loaderHeadersHeadersHeaders from loader response
actionHeadersHeadersHeaders from action response
parentHeadersHeadersHeaders from parent route

handle ​

Custom route metadata accessible via route matching.

tsx
export const handle = {
  breadcrumb: 'Blog Post',
  showSidebar: true,
  permissions: ['read:posts']
}

Access in layouts:

tsx
import { useMatches } from '@ereo/client'

export default function Layout({ children }) {
  const matches = useMatches()
  const breadcrumbs = matches
    .filter(m => m.handle?.breadcrumb)
    .map(m => m.handle.breadcrumb)

  return (
    <div>
      <Breadcrumbs items={breadcrumbs} />
      {children}
    </div>
  )
}

ErrorBoundary ​

Error boundary component for this route.

tsx
import { useRouteError, isRouteErrorResponse } from '@ereo/client'

export function ErrorBoundary() {
  const error = useRouteError()

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    )
  }

  return <h1>Something went wrong</h1>
}

params (Validation) ​

Validate and transform URL parameters.

tsx
export const params = {
  id: {
    parse: (value: string) => {
      const num = parseInt(value, 10)
      if (isNaN(num) || num <= 0) throw new Error('Invalid ID')
      return num
    }
  },
  slug: {
    parse: (value: string) => value.toLowerCase(),
    optional: false
  }
}

searchParams (Validation) ​

Validate and transform query string parameters.

tsx
export const searchParams = {
  page: {
    default: 1,
    validator: {
      parse: (value: string) => Math.max(1, parseInt(value, 10) || 1)
    }
  },
  sort: {
    default: 'date',
    validator: {
      parse: (value: string) => {
        const allowed = ['date', 'title', 'views']
        return allowed.includes(value) ? value : 'date'
      }
    }
  }
}

Route Configuration (config) ​

Export a config object to configure route behavior:

tsx
export const config = {
  // All options documented below
}

render ​

Configure rendering mode and strategy.

tsx
export const config = {
  render: 'ssr'  // Shorthand for { mode: 'ssr' }
}

Or with full options:

tsx
export const config = {
  render: {
    mode: 'ssg',  // 'ssr' | 'ssg' | 'csr' | 'streaming' | 'json' | 'xml' | 'rsc'

    // SSG options
    prerender: {
      enabled: true,
      paths: async () => {
        const posts = await db.posts.findAll()
        return posts.map(p => `/posts/${p.slug}`)
      },
      revalidate: 3600,  // ISR: revalidate every hour
      tags: ['posts'],   // Cache tags for on-demand invalidation
      fallback: 'blocking'  // 'blocking' | 'static' | '404'
    },

    // Streaming options
    streaming: {
      enabled: true,
      suspenseBoundaries: ['comments', 'related']
    },

    // CSR options
    csr: {
      enabled: true,
      clientLoader: async (params) => {
        const res = await fetch(`/api/posts/${params.id}`)
        return res.json()
      }
    }
  }
}

Render Modes:

ModeDescription
'ssr'Server-side rendering (default)
'ssg'Static site generation at build time
'csr'Client-side rendering only
'streaming'SSR with streaming for deferred data
'json'JSON API response
'xml'XML response
'rsc'React Server Component

middleware ​

Route-specific middleware chain.

tsx
export const config = {
  middleware: ['auth', 'rateLimit', customMiddleware]
}

Middleware runs in order before the loader/action.

cache ​

Multi-level caching configuration.

tsx
export const config = {
  cache: {
    // Edge/CDN cache
    edge: {
      maxAge: 3600,
      staleWhileRevalidate: 86400,
      vary: ['Accept-Language', 'Accept-Encoding'],
      keyGenerator: ({ request, params }) => {
        return `${request.url}:${params.locale}`
      }
    },

    // Browser cache
    browser: {
      maxAge: 300,
      private: true  // User-specific content
    },

    // Framework data cache
    data: {
      key: (params) => `post:${params.id}`,
      tags: (params) => ['posts', `post:${params.id}`]
    }
  }
}

Shorthand:

tsx
export const config = {
  cache: {
    maxAge: 3600,
    staleWhileRevalidate: 86400,
    tags: ['posts']
  }
}

islands ​

Selective hydration configuration.

tsx
export const config = {
  islands: {
    // Default strategy for all islands in this route
    defaultStrategy: 'visible',  // 'load' | 'idle' | 'visible' | 'media' | 'none'

    // Per-component overrides
    components: [
      { component: 'CommentForm', strategy: 'load' },
      { component: 'RelatedPosts', strategy: 'idle' },
      { component: 'MobileMenu', strategy: 'media', mediaQuery: '(max-width: 768px)' }
    ],

    // Disable all hydration (fully static page)
    disabled: false
  }
}

Hydration Strategies:

StrategyDescription
'load'Hydrate immediately on page load
'idle'Hydrate during browser idle time
'visible'Hydrate when scrolled into viewport
'media'Hydrate when media query matches
'none'Never hydrate (static only)

progressive ​

Progressive enhancement settings.

tsx
export const config = {
  progressive: {
    // Form behavior
    forms: {
      fallback: 'server',  // 'server' | 'spa'
      redirect: 'follow'   // 'follow' | 'manual'
    },

    // Link prefetching
    prefetch: {
      trigger: 'hover',  // 'hover' | 'visible' | 'intent' | 'never'
      data: true,        // Also prefetch loader data
      ttl: 60000         // Prefetch cache TTL in ms
    }
  }
}

auth ​

Authentication and authorization settings.

tsx
export const config = {
  auth: {
    required: true,
    roles: ['admin', 'editor'],
    permissions: ['posts:write'],
    check: async ({ request, context, params }) => {
      const user = context.get('user')
      const post = await db.posts.find(params.id)
      return post.authorId === user.id
    },
    redirect: '/login?redirect={pathname}',
    unauthorized: {
      status: 403,
      body: { error: 'Forbidden' }
    }
  }
}

route ​

Layout and composition configuration.

tsx
export const config = {
  route: {
    // Layout chain
    layouts: ['root', 'dashboard'],

    // Inherit from parent
    inherit: {
      middleware: true,
      meta: 'merge'  // 'merge' | 'replace' | false
    },

    // Error boundary config
    errorBoundary: {
      capture: 'all'  // 'all' | 'loader' | 'render'
    },

    // Loading UI config
    loading: {
      delay: 200,    // Show loading after 200ms
      timeout: 5000  // Show timeout UI after 5s
    }
  }
}

error ​

Error recovery and resilience.

tsx
export const config = {
  error: {
    // Auto-retry failed loaders
    retry: {
      count: 3,
      delay: 1000  // ms between retries
    },

    // Error handling strategy
    onError: 'boundary',  // 'boundary' | 'toast' | 'redirect' | 'silent'

    // Error reporting
    reportError: (error, context) => {
      console.error(`[${context.route}] ${context.phase}:`, error)
    }
  }
}

runtime ​

Runtime environment configuration.

tsx
export const config = {
  runtime: {
    runtime: 'edge',       // 'node' | 'edge' | 'auto'
    regions: ['iad1'],     // Edge regions
    memory: 256,           // Memory limit (MB)
    timeout: 30            // Timeout (seconds)
  }
}

dev ​

Development mode configuration.

tsx
export const config = {
  dev: {
    // Mock data for development
    mock: {
      enabled: true,
      data: {
        posts: [{ id: 1, title: 'Mock Post' }]
      }
    },

    // Artificial latency
    latency: 500,  // ms

    // Error injection rate (0-1)
    errorRate: 0.1
  }
}

variants ​

Multiple URL patterns for the same component.

tsx
export const config = {
  variants: [
    {
      path: '/blog/:slug',
      params: { slug: 'string' }
    },
    {
      path: '/articles/:id',
      params: { id: 'number' },
      config: {
        cache: { maxAge: 600 }
      }
    }
  ]
}

File Naming Conventions ​

Special file extensions auto-configure render mode:

ExtensionRender Mode
*.ssg.tsxStatic Site Generation
*.server.tsxServer-only (no client hydration)
*.client.tsxClient-side rendering only
*.api.tsxJSON API response
*.rsc.tsxReact Server Component

Example: routes/posts/[slug].ssg.tsx is pre-rendered at build time.

Special Files ​

FilePurpose
_layout.tsxLayout wrapper for child routes
_error.tsxError boundary for child routes
_loading.tsxLoading UI for child routes
_middleware.tsMiddleware for child routes

Complete Example ​

tsx
// routes/posts/[slug].tsx
import { createLoader, createAction, defer } from '@ereo/data'
import type { MetaFunction, HeadersFunction } from '@ereo/core'

// Route configuration
export const config = {
  render: {
    mode: 'ssg',
    prerender: {
      paths: async () => {
        const posts = await db.posts.findPublished()
        return posts.map(p => `/posts/${p.slug}`)
      },
      revalidate: 3600,
      fallback: 'blocking'
    }
  },
  cache: {
    edge: { maxAge: 3600, vary: ['Accept-Language'] },
    data: { tags: (params) => ['posts', `post:${params.slug}`] }
  },
  islands: {
    defaultStrategy: 'visible',
    components: [
      { component: 'CommentForm', strategy: 'load' }
    ]
  },
  middleware: ['analytics']
}

// Parameter validation
export const params = {
  slug: {
    parse: (value) => value.toLowerCase().replace(/[^a-z0-9-]/g, '')
  }
}

// Search param validation
export const searchParams = {
  tab: { default: 'content' }
}

// Data loading
export const loader = createLoader(async ({ params, context }) => {
  const post = await db.posts.findBySlug(params.slug)
  if (!post) throw new Response('Not Found', { status: 404 })

  context.cache.addTags([`author:${post.authorId}`])

  return {
    post,
    comments: defer(db.comments.findByPost(post.id))
  }
})

// Form handling
export const action = createAction(async ({ request, params }) => {
  const formData = await request.formData()
  // Handle comment submission...
})

// Document metadata
export const meta: MetaFunction = ({ data }) => [
  { title: data.post.title },
  { name: 'description', content: data.post.excerpt }
]

// Response headers
export const headers: HeadersFunction = ({ loaderHeaders }) => ({
  'Cache-Control': loaderHeaders.get('Cache-Control') || 'public, max-age=3600'
})

// Route metadata
export const handle = {
  breadcrumb: (data) => data.post.title
}

// Component
export default function PostPage({ loaderData }) {
  return (
    <article>
      <h1>{loaderData.post.title}</h1>
      {/* ... */}
    </article>
  )
}

// Error boundary
export function ErrorBoundary() {
  const error = useRouteError()
  return <ErrorPage error={error} />
}

Released under the MIT License.