Skip to content

@ereo/router-conventions

File-based routing conventions for the EreoJS framework. This package provides automatic route configuration based on file naming patterns.

Import

ts
import {
  parseConvention,
  conventionToRouteConfig,
  hasConvention,
  stripConvention,
  applyConventionConfig,
  getConventionPatterns,
  integrateConventions,
  generateRouteId,
  isApiRoute,
  isIslandComponent,
  getEffectiveRenderMode,
  CONVENTION_SUFFIXES
} from '@ereo/router-conventions'

Overview

The @ereo/router-conventions package enables convention-based routing configuration. Instead of manually configuring each route's render mode, you can use file naming conventions to automatically determine how routes should be rendered.

File Naming Conventions

Render Mode Suffixes

SuffixRender ModeDescription
.ssg.tsxSSGStatic Site Generation (pre-rendered at build)
.server.tsxSSRServer-side only (no client JavaScript)
.client.tsxCSRClient-side rendering only
.api.tsxAPIAPI endpoint (JSON response)
.rsc.tsxRSCReact Server Component

Examples

app/routes/
  index.tsx           # Default SSR
  about.ssg.tsx       # Static generation
  dashboard.client.tsx # Client-only rendering
  api/
    users.api.tsx     # JSON API endpoint
  blog/
    [slug].ssg.tsx    # Dynamic SSG with params
    preview.server.tsx # Server-only, no JS bundle

Directory Structure Patterns

Standard Routes

app/routes/
  index.tsx              # /
  about.tsx              # /about
  contact.tsx            # /contact

Nested Routes

app/routes/
  blog/
    index.tsx            # /blog
    [slug].tsx           # /blog/:slug
    categories/
      index.tsx          # /blog/categories
      [category].tsx     # /blog/categories/:category

Dynamic Routes

app/routes/
  posts/
    [id].tsx             # /posts/:id (single param)
    [id]/
      comments.tsx       # /posts/:id/comments
  docs/
    [...slug].tsx        # /docs/* (catch-all)

Special File Types

Layout Files

Layout files wrap child routes with shared UI:

app/routes/
  _layout.tsx            # Root layout
  dashboard/
    _layout.tsx          # Dashboard-specific layout
    index.tsx            # Wrapped by dashboard layout
    settings.tsx         # Also wrapped by dashboard layout
tsx
// app/routes/dashboard/_layout.tsx
export default function DashboardLayout({ children }) {
  return (
    <div className="dashboard">
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}

Island Components

Components in _islands directories are automatically extracted as interactive islands:

app/routes/
  _islands/
    Counter.tsx          # Auto-extracted island
    SearchBar.tsx        # Auto-extracted island
  blog/
    _islands/
      CommentForm.tsx    # Blog-specific island

Features Handled by @ereo/router

The following file patterns and conventions are NOT handled by @ereo/router-conventions. They are implemented in @ereo/router:

  • Error Boundaries (_error.tsx) - Error files that catch and display errors for their route segment
  • Loading States (_loading.tsx) - Loading files shown during route transitions
  • Middleware Files (_middleware.ts) - Middleware files that run before route handlers
  • Route Groups ((marketing)/) - Organizational folders that don't affect URL structure
  • Parallel Routes (@analytics/) - Multiple pages rendered simultaneously in the same layout

See the @ereo/router documentation for details on these features.

Convention Detection Rules

Suffix Matching Behavior

Convention suffixes are matched against the filename (without extension) at the end of the name:

  • First match wins: The parser iterates through suffixes and uses the first match. Suffix stacking (e.g., .ssg.server.tsx) is not supported.
  • Case sensitive: Suffixes must be lowercase. .SSG.tsx or .Ssg.tsx will NOT be recognized.
  • Extension agnostic: Conventions work with .tsx, .ts, .jsx, and .js files.

Island Detection

A file is detected as an island component if:

  • The path contains /_islands/ (e.g., blog/_islands/Counter.tsx)
  • The path starts with _islands/ (e.g., _islands/Counter.tsx)

Layout Detection

A file is detected as a layout if the filename (without extension):

  • Ends with /_layout (e.g., dashboard/_layout)
  • Equals _layout (e.g., _layout)

Edge Cases

Multiple Dots in Filename

Files with multiple dots are handled correctly. The convention suffix is identified by checking if the filename (minus the final extension) ends with a known suffix:

my.component.name.ssg.tsx  -> renderMode: 'ssg', basePath: 'my.component.name'
some.api.service.api.tsx   -> isApi: true, basePath: 'some.api.service'

Files Without Extensions

Files without extensions are parsed without error. The entire filename is treated as the name to check for convention suffixes.

Streaming Auto-Enabled for Server Routes

When using .server.tsx convention, streaming is automatically enabled:

ts
// page.server.tsx generates:
{
  render: {
    mode: 'ssr',
    streaming: { enabled: true }
  },
  islands: { disabled: true, defaultStrategy: 'none' }
}

Islands Disabled for Server-Only Routes

Routes using .server.tsx convention automatically have islands disabled since server-only routes don't include client JavaScript.

API Reference

parseConvention

Parses a filename to extract convention information.

Signature

ts
function parseConvention(filename: string): ConventionInfo

Returns

ts
interface ConventionInfo {
  basePath: string       // Path without convention suffix
  renderMode?: RenderMode // Detected render mode
  isApi: boolean         // Whether this is an API route
  isIsland: boolean      // Whether this is an island component
  isLayout: boolean      // Whether this is a layout file
  filename: string       // Original filename
  extension: string      // File extension
}

Example

ts
import { parseConvention } from '@ereo/router-conventions'

const info = parseConvention('blog/[slug].ssg.tsx')
// {
//   basePath: 'blog/[slug]',
//   renderMode: 'ssg',
//   isApi: false,
//   isIsland: false,
//   isLayout: false,
//   filename: 'blog/[slug].ssg.tsx',
//   extension: '.tsx'
// }

conventionToRouteConfig

Generates route configuration from convention info.

Signature

ts
function conventionToRouteConfig(info: ConventionInfo): Partial<RouteConfig>

Example

ts
import { parseConvention, conventionToRouteConfig } from '@ereo/router-conventions'

const info = parseConvention('posts/[id].ssg.tsx')
const config = conventionToRouteConfig(info)
// {
//   render: {
//     mode: 'ssg',
//     streaming: { enabled: false },
//     prerender: { enabled: true, fallback: 'blocking' }
//   }
// }

hasConvention

Checks if a filename uses any convention pattern.

Signature

ts
function hasConvention(filename: string): boolean

Example

ts
import { hasConvention } from '@ereo/router-conventions'

hasConvention('page.ssg.tsx')  // true
hasConvention('page.tsx')      // false
hasConvention('_islands/Button.tsx') // true

stripConvention

Removes convention suffix from a route path.

Signature

ts
function stripConvention(routePath: string): string

Example

ts
import { stripConvention } from '@ereo/router-conventions'

stripConvention('blog/[slug].ssg')  // 'blog/[slug]'
stripConvention('api/users.api')    // 'api/users'

applyConventionConfig

Applies convention-based configuration to a route. Explicit config replaces convention config at the top level for render, islands, and cache properties (no deep merging).

Signature

ts
function applyConventionConfig(
  routePath: string,
  explicitConfig?: Partial<RouteConfig>
): Partial<RouteConfig>

Merge Behavior

Important: This function performs top-level replacement, NOT deep merging:

ts
// If convention provides:
{ render: { mode: 'ssg', streaming: { enabled: false } } }

// And explicit config provides:
{ render: { prerender: { paths: ['/a', '/b'] } } }

// Result is (explicit REPLACES convention for 'render'):
{ render: { prerender: { paths: ['/a', '/b'] } } }
// Note: mode and streaming are LOST because the entire render object was replaced

The replacement logic:

ts
render: explicitConfig?.render ?? conventionConfig.render,
islands: explicitConfig?.islands ?? conventionConfig.islands,
cache: explicitConfig?.cache ?? conventionConfig.cache,

If you need to preserve convention defaults while adding explicit config, you must include all properties in your explicit config.

Example

ts
import { applyConventionConfig } from '@ereo/router-conventions'

// Explicit config completely replaces convention config for 'render'
const config = applyConventionConfig('posts/[id].ssg.tsx', {
  render: {
    mode: 'ssg',  // Must re-specify if you want to keep it
    prerender: {
      enabled: true,
      paths: ['/posts/1', '/posts/2']
    }
  }
})

getConventionPatterns

Returns all supported convention patterns (useful for documentation).

Signature

ts
function getConventionPatterns(): string[]

Example

ts
import { getConventionPatterns } from '@ereo/router-conventions'

const patterns = getConventionPatterns()
// [
//   '*.ssg.tsx - Static Site Generation (pre-rendered at build)',
//   '*.server.tsx - Server-side only (no client JavaScript)',
//   '*.client.tsx - Client-side rendering only',
//   '*.api.tsx - API endpoint (JSON response)',
//   '*.rsc.tsx - React Server Component',
//   '_islands/*.tsx - Auto-extracted island components',
//   '_layout.tsx - Nested layout wrapper'
// ]

integrateConventions

Integrates conventions with discovered routes from the file router.

Signature

ts
function integrateConventions(
  routes: Route[],
  options?: ConventionIntegrationOptions
): Route[]

Options

ts
interface ConventionIntegrationOptions {
  enabled?: boolean              // Enable convention parsing (default: true)
  customSuffixes?: Record<string, string> // Custom convention suffixes
  islandDirs?: string[]          // Directories to scan for islands
}

Example

ts
import { createFileRouter } from '@ereo/router'
import { integrateConventions } from '@ereo/router-conventions'

const router = await createFileRouter()
const routes = router.getRoutes()

// Apply conventions to all routes
const configuredRoutes = integrateConventions(routes, {
  enabled: true,
  islandDirs: ['_islands', 'components/_islands']
})

customSuffixes Example

Add custom convention suffixes for your project:

ts
const configuredRoutes = integrateConventions(routes, {
  customSuffixes: {
    '.static': 'ssg',      // .static.tsx treated as SSG
    '.dynamic': 'ssr',     // .dynamic.tsx treated as SSR
    '.edge': 'edge',       // .edge.tsx for edge runtime
  }
})

islandDirs Example

Specify multiple directories to scan for island components:

ts
const configuredRoutes = integrateConventions(routes, {
  islandDirs: [
    '_islands',                    // Default location
    'components/_islands',         // Shared components
    'features/dashboard/_islands', // Feature-specific islands
  ]
})

generateRouteId

Strips convention suffixes from file paths to generate clean route IDs.

Signature

ts
function generateRouteId(filePath: string): string

Example

ts
import { generateRouteId } from '@ereo/router-conventions'

generateRouteId('blog/[slug].ssg.tsx')  // 'blog/[slug]'
generateRouteId('api/users.api.tsx')    // 'api/users'
generateRouteId('page.tsx')             // 'page'

isApiRoute

Checks if a file path represents an API route.

Signature

ts
function isApiRoute(filePath: string): boolean

Example

ts
import { isApiRoute } from '@ereo/router-conventions'

isApiRoute('api/users.api.tsx')  // true
isApiRoute('api/users.tsx')      // false
isApiRoute('page.ssg.tsx')       // false

isIslandComponent

Checks if a file path is an island component.

Signature

ts
function isIslandComponent(filePath: string): boolean

Example

ts
import { isIslandComponent } from '@ereo/router-conventions'

isIslandComponent('_islands/Counter.tsx')       // true
isIslandComponent('blog/_islands/Comments.tsx') // true
isIslandComponent('components/Button.tsx')      // false

getEffectiveRenderMode

Determines the effective render mode for a route. Priority: explicit mode > convention > default ('ssr').

Signature

ts
function getEffectiveRenderMode(
  filePath: string,
  explicitMode?: string
): string

Example

ts
import { getEffectiveRenderMode } from '@ereo/router-conventions'

// Convention-based
getEffectiveRenderMode('page.ssg.tsx')         // 'ssg'
getEffectiveRenderMode('api/users.api.tsx')    // 'json'

// Explicit mode takes priority
getEffectiveRenderMode('page.tsx', 'ssg')      // 'ssg'
getEffectiveRenderMode('page.ssg.tsx', 'csr')  // 'csr' (explicit overrides)

// Default fallback
getEffectiveRenderMode('page.tsx')             // 'ssr'

Route Configuration Override

Explicit configuration in route files takes precedence over conventions:

tsx
// app/routes/posts/[id].ssg.tsx

// Convention says SSG, but explicit config can override
export const config = {
  render: {
    mode: 'ssg',
    prerender: {
      paths: async () => {
        const posts = await getPosts()
        return posts.map(p => `/posts/${p.id}`)
      }
    }
  }
}

export async function loader({ params }) {
  return { post: await getPost(params.id) }
}

export default function PostPage({ data }) {
  return <article>{data.post.content}</article>
}

CONVENTION_SUFFIXES

The mapping of file suffixes to render modes:

ts
const CONVENTION_SUFFIXES = {
  '.ssg': 'ssg',
  '.server': 'ssr',
  '.client': 'csr',
  '.api': 'api',
  '.rsc': 'rsc',
}

Released under the MIT License.