npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

next-slug-splitter

v5.3.0

Published

Build-time Next.js route handler generation and rewrite integration for large content trees.

Readme

next-slug-splitter

Next.js route handler generation with build-time rewrites and dev-time request routing

A configuration-driven package for analyzing content page trees, generating route-specific handlers, and routing traffic into those handlers — either through build-time rewrites (production) or a request-time proxy (development).

Table of Contents

  1. Overview
  2. Getting Started
  3. Quick Start
  4. Usage
  5. Operation Modes
  6. Configuration Reference
  7. Architecture
  8. Capabilities
  9. Next.js Integration Points

Overview

Features

  • Two Operation Modes: Rewrite mode for production builds, proxy mode for development — each optimized for its environment.
  • Config-Driven Targets: Declare one or more route spaces such as docs and blog with app-level and target-level settings.
  • Build-Time Generation: Discover content pages, resolve component metadata, classify heavy routes, and emit dedicated handler pages before the app build.
  • Dev-Time Proxy Routing: A generated proxy.ts intercepts requests and delegates heavy/light routing decisions into a long-lived worker session.
  • Lazy Discovery and Reuse: In proxy mode, heavy routes are classified on first request and cached under .next/cache for fast subsequent requests and dev restarts.
  • Optional Startup Prewarm: Opt into app.routing.workerPrewarm: 'instrumentation' to bootstrap the dev worker during Next startup instead of waiting for the first proxied request.
  • Locale-Aware Routing: Support for locale detection based on filenames or a default-locale routing model.
  • Multi-Target: Support multi-target setups such as docs plus blog in one configuration file.

Why Use It?

Content-heavy route spaces such as docs and blogs often benefit from splitting "heavy" routes (pages with interactive components) from "light" routes (pages with only standard markdown elements). next-slug-splitter manages that split:

  • In production: next build generates dedicated handler pages for heavy routes and installs rewrites that route matching traffic into those handlers.
  • In development: A proxy discovers heavy routes lazily on first request, reuses cached route-capture facts across dev restarts, and can optionally prewarm its worker session during startup.

The configuration lives in one app-owned file, the integration is a single withSlugSplitter(...) wrapper, and the routing strategy adapts automatically to the current Next.js phase.

Limitations

  • MDX only — content pages must be .mdx files. Standard .tsx / .jsx pages are not analyzed. Support for non-MDX content sources may be added later.
  • Pages Router — currently has the fuller feature set, including the existing dev proxy path and the getStaticProps / getStaticPaths-based integration under pages/.
  • App Router — catch-all page routes under app/ support build-time generation plus rewrite-based routing in production, and proxy-based lazy routing in development through the same worker architecture as the Pages Router path. See docs/architecture/app-router-boundary-files.md. For a current Pages-vs-App behavior comparison around dev proxy quirks and safeguards, see docs/architecture/router-behavior-matrix.md.

Getting Started

Installation

npm install next-slug-splitter next
# or
pnpm add next-slug-splitter next

next-slug-splitter integrates with Next.js entirely through the stable top-level adapterPath option, available since Next.js 16.2.0 — the minimum version the package supports.

Quick Start

1. Wrap the Next Config

import { withSlugSplitter } from 'next-slug-splitter/next';

const nextConfig = {
  i18n: {
    locales: ['en', 'de'],
    defaultLocale: 'en'
  }
};

export default withSlugSplitter(nextConfig, {
  configPath: './route-handlers-config.mjs'
});

For single-locale Pages Router setups, omit Next i18n entirely. The library normalizes the missing i18n block into its internal single-locale LocaleConfig automatically.

2. Declare Route Targets

// route-handlers-config.mjs
// @ts-check

import process from 'node:process';
import path from 'node:path';
import {
  createCatchAllRouteHandlersPreset,
  relativeModule
} from 'next-slug-splitter/next';
import { routeHandlerBindings } from 'site-route-handlers/config';

const rootDir = process.cwd();

/** @type {import('next-slug-splitter/next').DynamicRouteParam} */
const docsRouteParam = {
  name: 'slug',
  kind: 'catch-all'
};

/** @type {import('next-slug-splitter/next').DynamicRouteParam} */
const blogRouteParam = {
  name: 'slug',
  kind: 'single'
};

/** @type {import('next-slug-splitter/next').RouteHandlersConfig} */
export const routeHandlersConfig = {
  routerKind: 'pages',
  app: {
    rootDir,
    routing: {
      // Default: 'proxy' in development, rewrites in production
      development: 'proxy'
    }
  },
  targets: [
    createCatchAllRouteHandlersPreset({
      routeSegment: 'docs',
      handlerRouteParam: docsRouteParam,
      contentDir: path.resolve(rootDir, 'docs/src/pages'),
      routeContract: relativeModule('pages/docs/[...slug]'),
      handlerBinding: routeHandlerBindings.docs
    }),
    createCatchAllRouteHandlersPreset({
      routeSegment: 'blog',
      handlerRouteParam: blogRouteParam,
      contentDir: path.resolve(rootDir, 'blog/src/pages'),
      routeContract: relativeModule('pages/blog/[slug]'),
      contentLocaleMode: 'default-locale',
      handlerBinding: routeHandlerBindings.blog
    })
  ]
};

Each preset resolves generatedRootDir for you from its routeSegment, then the library appends the canonical generated-handlers/ leaf during target resolution.

For Pages Router targets, point routeContract at the catch-all page module itself, for example pages/docs/[...slug].tsx, because generated heavy handler pages reuse that page's getStaticProps contract rather than introducing a second data-loading entrypoint. Route enumeration still stays on that catch-all page through getStaticPaths.

No separate generation command is required for the standard integration path. next build runs route-handler generation automatically through the installed adapter.

App Router Catch-All Targets

Use createAppCatchAllRouteHandlersPreset(...) when the public catch-all route lives under app/ and you want the current App Router path.

// route-handlers-config.mjs
// @ts-check

import process from 'node:process';
import path from 'node:path';
import {
  createAppCatchAllRouteHandlersPreset,
  relativeModule
} from 'next-slug-splitter/next';
import { routeHandlerBindings } from 'site-route-handlers/config';

const rootDir = process.cwd();

/** @type {import('next-slug-splitter/next').DynamicRouteParam} */
const docsRouteParam = {
  name: 'slug',
  kind: 'catch-all'
};

/** @type {import('next-slug-splitter/next').RouteHandlersConfig} */
export const routeHandlersConfig = {
  routerKind: 'app',
  app: {
    rootDir
  },
  targets: [
    createAppCatchAllRouteHandlersPreset({
      routeSegment: 'docs',
      handlerRouteParam: docsRouteParam,
      contentDir: path.resolve(rootDir, 'content/pages'),
      contentLocaleMode: 'default-locale',
      routeContract: relativeModule('app/docs/[...slug]/route-contract'),
      handlerBinding: {
        ...routeHandlerBindings.docs,
        pageDataCompilerImport: relativeModule(
          'config-variants/javascript/content-compiler.mjs'
        )
      }
    })
  ]
};

This preset resolves generatedRootDir for you from routeSegment, and the library later resolves the final generated-handlers/ directory under that root.

For single-locale App Router targets, the preset derives a conventional output root such as app/docs. For multi-locale App Router targets with app.localeConfig, the preset derives a locale-subtree output root such as app/[locale]/docs so generated heavy pages inherit the same physical locale layout subtree as the public catch-all page.

Unlike the Pages Router path, App Router usually keeps the route contract in a dedicated sibling file:

  1. Single-locale trees usually use app/docs/[...slug]/route-contract.ts.
  2. Multi-locale trees usually use app/[locale]/docs/[...slug]/route-contract.ts.

The public page and the generated heavy pages both call into that one contract module, and that same file also owns route enumeration through getStaticParams.

The App-specific fields are:

  • routeContract — the dedicated App route-contract module imported by the light page and generated heavy pages
  • handlerBinding.pageDataCompilerImport — the app-owned compiler module that the library executes in an isolated worker for page-data compilation
  • app.localeConfig — optional multi-locale semantics used for App-side worker routing, static-param filtering, and default-locale URL normalization
  • app.localeRouteParamName — optional physical App route-param name for the locale segment; defaults to locale when app.localeConfig is configured

app.localeConfig is a library routing contract, not a direct mirror of Next.js i18n settings:

  • omit app.localeConfig for single-locale App Router setups
  • provide app.localeConfig only for multi-locale App Router setups
  • locales lists every locale identity the library should reason about
  • defaultLocale must be included in locales

app.localeRouteParamName describes the filesystem segment name, not a public URL prefix:

  • omit it for the default App shape app/[locale]/...
  • set it to a bare name such as lang for app/[lang]/...
  • do not include brackets or slashes; use locale, not [locale]
  • do not configure it without app.localeConfig

App static-param filtering reads the configured locale route param when returned from getStaticParams; the default param name is locale, and a custom app.localeRouteParamName changes the field the library reads. Params without that field fall back to the configured default locale. In multi-locale App setups, unprefixed default-locale light routes are rewritten internally to the physical locale route, so /docs/foo serves the same App page as /en/docs/foo without generating a handler.

If the App tree needs route groups or another custom filesystem placement for generated handlers, use manual target config and set generatedRootDir directly there. The catch-all preset intentionally stays on the library's conventional App placement: app/<routeSegment> for single-locale targets and app/[<localeRouteParamName>]/<routeSegment> for multi-locale targets.

Concrete comparison:

| Aspect | Pages Router | App Router | | -------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | Public light route file | pages/docs/[...slug].tsx | app/docs/[...slug]/page.tsx or app/[locale]/docs/[...slug]/page.tsx | | Route contract location | Usually the catch-all page module itself | Usually a dedicated sibling file such as app/[locale]/docs/[...slug]/route-contract.ts | | Route enumeration | getStaticPaths stays on the catch-all page | getStaticParams lives on the dedicated route contract | | Shared page-data contract | Generated heavy handlers reuse the catch-all page's getStaticProps | The light page and generated heavy pages share loadPageProps from the route contract | | Optional metadata / revalidation | Follows normal Pages Router page exports around the catch-all page surface | generatePageMetadata and revalidate live on the dedicated route contract | | Locale semantics | Uses normal Pages Router i18n when multi-locale | Uses app.localeConfig for multi-locale App routing semantics |

Usage

Module References for Shared Packages

If your processors, factories, or component registries already live behind package exports, use packageModule(...) early instead of building manual filesystem paths into your route-handler config.

This is especially useful in processors:

  • the package manager and Node resolve the shared package through node_modules
  • the processor does not need to maintain app-specific absolute or relative paths for shared component modules
  • the same package export can be reused from both handlerBinding and componentImport.source

For workspace packages, packageModule(...) only works when the package is reachable through the app's node_modules resolution path, which usually means declaring it in the root app package.json so the workspace package is hoisted or otherwise installed where Node can resolve it. A complete processor example appears in Step 3 below.

1. Wrap the Next Config

withSlugSplitter(nextConfigExport, options) resolves the app-owned route handlers config and installs the adapter entry into adapterPath.

Two registration modes:

// File-based (recommended for most apps)
withSlugSplitter(nextConfig, {
  configPath: './route-handlers-config.mjs'
});

// Direct object (useful for monorepos or programmatic setups)
withSlugSplitter(nextConfig, {
  routeHandlersConfig: myConfig
});

File-based registration is the most predictable option for development proxy mode because the proxy worker can reload the app-owned config module in a fresh process. Direct object registration is still useful for adapter startup, but the proxy worker later needs an importable config path of its own. If you use a direct object with proxy mode, keep a conventional root config file such as route-handlers-config.ts or route-handlers-config.mjs beside the Next config.

Composing an Existing Adapter

Next supports a single adapterPath, and withSlugSplitter(...) claims it for the slug-splitter adapter entry. Apps that already rely on another Next adapter (a deployment adapter, for example) pass it through the optional adapter option instead of setting adapterPath themselves:

import deploymentAdapter from 'my-deployment-adapter';

withSlugSplitter(nextConfig, {
  routeHandlersConfig: myConfig,
  adapter: deploymentAdapter
});

Composition guarantees:

  • modifyConfig runs the provided adapter first, so slug-splitter derives locale semantics from the already-adapted config and layers its rewrites on top of it.
  • onBuildComplete is only exposed to Next when the provided adapter implements it. Without one, Next skips its adapter build-output collection entirely, so builds that do not pass an adapter keep their current cost.

Composition covers the two adapter hooks Next currently defines, modifyConfig and onBuildComplete.

2. Declare Route Targets

The route handlers config is the app-owned source of truth for route handler generation. A target typically describes:

  • the public route segment such as docs or blog
  • the dynamic route parameter kind
  • the content page directory
  • the binding that provides the processor module for route planning

createCatchAllRouteHandlersPreset(...) is the shortest way to configure catch-all targets without hand-assembling all path values.

3. Wire handlerBinding and the Processor

The handler binding tells the library which processor module to load:

{
  handlerBinding: {
    processorImport: relativeModule('lib/handler-processor');
  }
}

The processor is the single source of truth for component imports and factory selection. It is exported from the module referenced by processorImport.

This is also a common place to use packageModule(...) for shared component registries or UI packages, because the processor can rely on package exports instead of maintaining manual filesystem paths to those component modules.

// route-handlers-config.ts
import path from 'node:path';
import process from 'node:process';
import {
  createCatchAllRouteHandlersPreset,
  packageModule,
  relativeModule
} from 'next-slug-splitter/next';

const rootDir = process.cwd();

export const routeHandlersConfig = {
  routerKind: 'pages',
  app: {
    rootDir
  },
  targets: [
    createCatchAllRouteHandlersPreset({
      routeSegment: 'docs',
      handlerRouteParam: { name: 'slug', kind: 'catch-all' },
      contentDir: path.join(rootDir, 'content/pages'),
      routeContract: relativeModule('pages/docs/[...slug]'),
      handlerBinding: {
        processorImport: packageModule('site-route-handlers/docs/processor')
      }
    })
  ]
};

Then the referenced processor module can reuse package exports for component imports:

// site-route-handlers/docs/processor.ts
import { packageModule, relativeModule } from 'next-slug-splitter/next';

const componentsModule = packageModule('@site/components');

export const routeHandlerProcessor = {
  resolve({ capturedComponentKeys }) {
    // Gather what you need — registry lookups, metadata, etc. — and return
    // the final generation plan directly.
    const componentEntriesByKey = resolveComponentsByCapturedKey(
      capturedComponentKeys
    );

    return {
      factoryImport: relativeModule('lib/handler-factory/runtime'),
      components: capturedComponentKeys.map(key => {
        const entry = componentEntriesByKey[key];
        return {
          key,
          componentImport: {
            source: componentsModule,
            kind: 'named',
            importedName: entry.exportName
          }
        };
      })
    };
  }
};
  • resolve — produce the generation plan for one heavy route. Implementations can still use private local helpers to gather registry data, metadata, or config before returning the final plan.

A TypeScript helper defineRouteHandlerProcessor(...) is available for type inference:

import { defineRouteHandlerProcessor } from 'next-slug-splitter/next';

export const routeHandlerProcessor = defineRouteHandlerProcessor({
  resolve({ capturedComponentKeys, route }) { ... }
});

Captured Components and MDX Scope

This section distinguishes two related surfaces:

  • Captured components are custom component names found while analyzing the MDX source graph.
  • MDX scope is the component map available when the compiled MDX renders.

capturedComponentKeys contains every custom MDX component name found in the source graph. The processor's components array describes which of those keys should become imports emitted into generated handlers.

For example, a page may reference both a lightweight Callout component and a heavyweight FlowComposer component:

<Callout />

<FlowComposer />

The capture step reports both names:

capturedComponentKeys = ['Callout', 'FlowComposer'];

If Callout is already available in the MDX component scope, the processor result can include only the generated handler entry for FlowComposer:

components = [
  {
    key: 'FlowComposer',
    componentImport: ...
  }
];

In that case, Callout renders through the existing MDX component scope. If that scope does not provide it, rendering fails through the normal MDX application path.

The validation is one-way: the processor result may contain fewer keys than were captured, but every returned key must have been captured from the MDX graph.

4. Generate or Analyze (Optional CLI)

The standalone CLI generates handler artifacts or runs analysis only. Unlike the Next adapter, it does not derive inputs from a discovered Next config. Pass the route-handlers config module path plus explicit locale semantics.

Required flags:

  • --route-handlers-config-path — path to the route-handlers config module
  • --locales — comma-separated locale list
  • --default-locale — default locale and member of --locales

Optional flags:

  • --analyze-only — skip handler emission and report what would be generated
  • --json — emit a machine-readable array of per-target results
pnpm exec tsx ./node_modules/next-slug-splitter/dist/cli.js \
  --route-handlers-config-path ./route-handlers-config.ts \
  --locales en,de \
  --default-locale en \
  --analyze-only

node ./node_modules/next-slug-splitter/dist/cli.js \
  --route-handlers-config-path ./route-handlers-config.mjs \
  --locales en,de \
  --default-locale en \
  --json

The human-readable output prints one summary line per configured target. --json emits the same per-target results as an array.

  1. Use tsx when the route-handlers config module is TypeScript, for example .ts.
  2. Use plain node when the route-handlers config module is JavaScript, for example .mjs.

In development with proxy mode, the CLI step is not needed. The proxy discovers routes on demand.

Operation Modes

Rewrite Mode (Production Default)

Used during PHASE_PRODUCTION_BUILD and PHASE_PRODUCTION_SERVER.

  1. The build analyzes content pages and generates dedicated handler page files
  2. The adapter injects phase-aware rewrites into the Next config
  3. Next.js routes matching traffic to the generated handler pages

All routes are resolved upfront at build time. The generated handler pages and rewrites are static artifacts.

Build rewrites are split by Next phase: generated-handler public guards and exact heavy-route rewrites run in beforeFiles; App Router default-locale normalization runs later in afterFiles when multi-locale App routing needs it.

Proxy Mode (Development Default)

Used during PHASE_DEVELOPMENT_SERVER.

  1. The adapter generates a thin proxy.ts file at the app root
  2. It also writes a structural worker bootstrap manifest to .next/cache/route-handlers-worker-bootstrap.json
  3. proxy.ts intercepts page requests matching configured route base paths
  4. A long-lived worker session classifies unknown routes on demand
  5. Heavy routes are rewritten to their generated handler pages; light routes pass through to the catch-all page
  6. Stage 1 route-capture facts are cached per target under .next/cache/route-handlers-lazy-single-routes/

Benefits over rewrite mode in development:

  • Instant startup — no upfront generation pass
  • On-demand discovery — only routes actually visited are classified
  • Cross-restart reuse — emitted handlers and lazy route-capture facts can be reused across dev restarts while development remains the owning phase

Optional Worker Prewarm

When development routing uses 'proxy', you can ask the library to bootstrap the long-lived worker session during Next startup:

// In route-handlers-config.mjs
export const routeHandlersConfig = {
  routerKind: 'pages',
  app: {
    routing: {
      development: 'proxy',
      workerPrewarm: 'instrumentation'
    }
  },
  targets: [...]
};

When enabled, next-slug-splitter generates a tiny root instrumentation.ts file that imports prewarmRouteHandlerProxyWorker from next-slug-splitter/next/instrumentation. This is a best-effort startup prewarm of the current worker session only. It does not classify routes, emit handlers, or warm specific pages ahead of traffic.

If your app already owns instrumentation.ts or instrumentation.js at the root or under src/, the library refuses to overwrite it. Leave workerPrewarm set to 'off' in that case.

Dev-Mode Cold-Start Behavior

The lazy proxy path is self-healing in development:

  1. The worker determines whether the requested route is light or heavy.
  2. If the route is heavy, it checks whether the emitted handler file already exists.
  3. If the file already exists, it is reused and not regenerated.
  4. If the file is missing, the worker emits that single handler on demand.
  5. The rewrite is then resolved within the same request cycle.

This means a missing heavy-route handler can be recreated on first request without a separate generation step, while an existing handler is reused as-is.

Handler generation is therefore self-healing, but development can still hit a narrow Next/Turbopack warm-up window where the proxy already knows the correct handler destination while the emitted page is not fully ready yet. During that window, the browser can briefly land on a transient 404 for a catch-all route.

To smooth that development-only case, install the router-specific not-found retry helper in the not-found boundary that owns your catch-all route.

Pages Router uses pages/404.* and imports the helper from next-slug-splitter/next/pages/proxy/not-found-retry:

// pages/404.tsx
import type { NextPage } from 'next';

import { useSlugSplitterNotFoundRetry } from 'next-slug-splitter/next/pages/proxy/not-found-retry';

const CATCH_ALL_ROUTE_SEGMENTS = ['docs'];

const NotFound: NextPage = () => {
  const isNotFoundConfirmed = useSlugSplitterNotFoundRetry({
    catchAllRouteSegments: CATCH_ALL_ROUTE_SEGMENTS
  });

  if (!isNotFoundConfirmed) {
    return null;
  }

  return <h1>Page Not Found</h1>;
};

export default NotFound;

App Router uses app/not-found.* and imports the helper from next-slug-splitter/next/app/proxy/not-found-retry:

// app/not-found.tsx
'use client';

import { useSlugSplitterNotFoundRetry } from 'next-slug-splitter/next/app/proxy/not-found-retry';

const CATCH_ALL_ROUTE_SEGMENTS = ['docs'];

export default function NotFound() {
  const isNotFoundConfirmed = useSlugSplitterNotFoundRetry({
    catchAllRouteSegments: CATCH_ALL_ROUTE_SEGMENTS
  });

  if (!isNotFoundConfirmed) {
    return null;
  }

  return <h1>Page Not Found</h1>;
}

This hook is a no-op outside development, so production builds still render their normal 404 page immediately.

Configuring the Routing Policy

The development routing mode defaults to 'proxy', and worker prewarm defaults to 'off'. To override:

// In route-handlers-config.mjs
export const routeHandlersConfig = {
  routerKind: 'pages',
  app: {
    routing: {
      development: 'rewrites',
      workerPrewarm: 'off'
    }
  },
  targets: [...]
};

Environment variable override (takes precedence over config):

NEXT_SLUG_SPLITTER_DEV_ROUTING=proxy     # Force proxy mode
NEXT_SLUG_SPLITTER_DEV_ROUTING=rewrites  # Force rewrite mode

NEXT_SLUG_SPLITTER_DEV_ROUTING controls only the development routing mode. workerPrewarm accepts 'off' or 'instrumentation' and only applies when development routing resolves to 'proxy'.

Configuration Reference

withSlugSplitter(nextConfigExport, options)

Wrap one Next config export and register the route handlers config.

| Option | Description | | --------------------- | ---------------------------------------------------- | | configPath | Path to the app-owned route-handlers-config module | | routeHandlersConfig | Direct config object (alternative to configPath) | | adapter | Optional Next adapter composed before slug-splitter |

RouteHandlersConfig

Top-level configuration shape.

| Property | Description | | --------------------------- | -------------------------------------------------------------------------- | | routerKind | Required router family: 'pages' or 'app' | | app.rootDir | Application root directory | | app.localeConfig | App-only multi-locale routing semantics; omit for single-locale App setups | | app.localeRouteParamName | Bare App locale route-param name, defaulting to locale when configured | | app.routing.development | Development routing mode: 'proxy' (default) or 'rewrites' | | app.routing.workerPrewarm | Dev-only worker startup strategy: 'off' (default) or 'instrumentation' | | app.prepare | Optional TypeScript prepare step or steps run before route planning | | targets | Array of target configurations |

When a processor or registry needs a local TypeScript build before runtime loading, configure app.prepare as one object or an ordered array of objects:

app: {
  rootDir,
  prepare: [
    {
      tsconfigPath: relativeModule('tsconfig.processor.json')
    }
  ]
}

If you only need one prepare step, a single object is also accepted. If no pre-build is needed, omit app.prepare.

app.routing.workerPrewarm only affects development proxy mode. When set to 'instrumentation', the library generates a root instrumentation.ts bridge that prewarms the current proxy worker session. Existing app-owned instrumentation.ts / instrumentation.js files at the root or under src/ are treated as conflicts and are never overwritten.

createCatchAllRouteHandlersPreset(options)

Create one catch-all target with normalized route and path values.

| Option | Description | | ------------------- | -------------------------------------------------------------------------------------------- | | routeSegment | Public route segment (e.g. 'docs', 'blog') | | handlerRouteParam | Dynamic route parameter configuration | | contentDir | Directory containing content pages | | routeContract | Pages route contract module, typically the catch-all page such as pages/docs/[...slug].tsx | | handlerBinding | Binding with processor module for route planning | | contentLocaleMode | Locale detection mode (see below) |

The preset derives generatedRootDir from routeSegment. Set generatedRootDir only when writing a manual target config instead of using the preset.

createAppCatchAllRouteHandlersPreset(options)

Create one App Router catch-all target with normalized public route values and App-specific route-module inputs.

| Option | Description | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | routeSegment | Public route segment (e.g. 'docs') | | handlerRouteParam | Dynamic route parameter configuration | | contentDir | Directory containing content pages | | handlerBinding | Binding with processor module for route planning | | handlerBinding.pageDataCompilerImport | Optional App page-data compiler module executed through the library-owned isolated worker | | routeContract | Dedicated App route-contract module that owns getStaticParams, loadPageProps, optional metadata, and revalidate | | contentLocaleMode | Locale detection mode (see below) |

The preset derives generatedRootDir from the resolved App route base and the App locale route-param policy:

  1. Without app.localeConfig, routeSegment: 'docs' derives app/docs.
  2. With app.localeConfig, the same target derives app/[locale]/docs.
  3. With app.localeConfig and app.localeRouteParamName: 'lang', it derives app/[lang]/docs.
  4. Set generatedRootDir manually only when writing a low-level target config instead of using the preset.

DynamicRouteParam

Supported kind values:

  • single — matches a single path segment
  • catch-all — matches one or more path segments
  • optional-catch-all — matches zero or more path segments

Module References

Several config fields use module-reference helpers instead of raw strings.

| Helper | Use when | | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | relativeModule('lib/handler-processor') | The file lives under the app root and should resolve relative to app.rootDir | | packageModule('site-route-handlers/docs/processor') | The module is exposed through package exports in node_modules, including hoisted workspace packages | | absoluteModule('/abs/path/to/module') | The file lives outside the app root and outside reachable package exports |

See the Usage section above for a complete packageModule(...) example in both handlerBinding.processorImport and processor-side component imports.

handlerBinding

The handler binding tells the library which processor module to load.

{
  processorImport: relativeModule('lib/handler-processor');
}

See the Usage section above for a worked processor example.

Processor (RouteHandlerProcessor)

A processor is a route-local transformer the library calls once per heavy route. It is exported from the module referenced by processorImport.

  • resolve — produce the generation plan for one heavy route. Implementations can still use private local helpers to gather registry data, metadata, or config before returning the final plan.

A TypeScript helper defineRouteHandlerProcessor(...) is available for type inference:

import { defineRouteHandlerProcessor } from 'next-slug-splitter/next';

export const routeHandlerProcessor = defineRouteHandlerProcessor({
  resolve({ capturedComponentKeys, route }) { ... }
});

contentLocaleMode

Supported modes:

  • filename — locale is encoded in the content file naming scheme
  • default-locale — the default locale omits the locale prefix in the public route space

Architecture

Adapter

The adapter (adapterPath, available since Next.js 16.2.0) is the entry point for Next.js integration and the foundation the whole library builds on. It runs during the relevant Next.js phases and coordinates:

  • Routing strategy selection (rewrite vs. proxy)
  • App-owned preparation and config resolution
  • Phase-local artifact ownership for dev versus build
  • Rewrite injection or generated proxy.ts / instrumentation.ts bridges
  • Composition of an optional app-provided adapter ahead of its own hooks

When an app passes its own adapter through withSlugSplitter(..., { adapter }), the installed entry composes it ahead of the built-in adapter: modifyConfig pipes the config through the app adapter first, and onBuildComplete is only exposed to Next when the app adapter implements it, so builds without one keep Next's fast path.

Runtime Reuse

The runtime keeps reuse narrowly scoped to the artifacts it actually owns:

  • Phase ownership record.next/cache/route-handlers-phase-owner.json separates dev-owned and build-owned generated state so the two phases do not trust each other's handlers or caches
  • Proxy bootstrap manifest.next/cache/route-handlers-worker-bootstrap.json persists only the structural target data the parent proxy runtime and worker need to share
  • Lazy single-route cache.next/cache/route-handlers-lazy-single-routes/ stores per-target Stage 1 MDX capture facts via file-entry-cache, enabling safe cross-restart reuse in development

Proxy File Lifecycle

In proxy mode, the adapter generates a thin proxy.ts bridge file at the app root. This file:

  • Imports the library-owned proxy runtime
  • Embeds static matchers for configured route base paths and locales
  • Is automatically created when entering proxy mode and cleaned up when leaving

The generated file is marked with an ownership marker so it can be distinguished from user-authored proxy files.

Existing app-owned proxy.ts, proxy.js, middleware.ts, or middleware.js files at the root or under src/ are treated as hard conflicts. The library does not overwrite framework-owned routing entrypoints.

Instrumentation File Lifecycle

When app.routing.workerPrewarm === 'instrumentation' and development uses proxy mode, the adapter also generates a tiny root instrumentation.ts bridge. That file is removed again when prewarm is turned off or proxy mode is no longer active. Existing app-owned instrumentation files are treated as hard conflicts and are never overwritten or deleted.

Worker Process

In proxy mode, route classification happens in a child worker process. This is necessary because the proxy runtime environment cannot dynamically import app-owned configuration modules. The parent process keeps only lightweight bootstrap state and route-base matchers; the worker reconstructs planner state from the persisted bootstrap manifest, reuses one long-lived session per bootstrap generation, and returns lazy route classifications on demand.

Capabilities

  • Two operation modes optimized for their respective environments
  • Standalone CLI with explicit route-handlers config and locale semantics
  • Lazy on-demand route discovery in development
  • Cross-restart dev reuse through persisted bootstrap and lazy single-route caches under .next/cache
  • Optional dev-only instrumentation.ts worker prewarm
  • Phase-local artifact ownership that avoids dev/build cache contamination
  • Install rewrite integration without mutating the incoming Next config object
  • Resolve app-level and target-level route handler config in one shared shape
  • Discover content pages and generate handler artifacts per target
  • Reuse handler bindings for processor-driven route planning
  • Support multi-target setups such as docs plus blog
  • Locale-aware routing with configurable detection modes
  • Phase-aware behavior — only active during development, build, and production server phases

Next.js Integration Points

| Next.js API | Purpose | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | adapterPath | Adapter entry point — hooks into Next.js config resolution; stable since Next.js 16.2.0 | | rewrites() | Installs library rewrites into the correct Next rewrite phases | | proxy.ts (root file) | Intercepts and classifies requests on demand in development; existing proxy.* or middleware.* files at the root or under src/ are treated as conflicts | | instrumentation.ts (root file) | Optional dev-only worker-session prewarm when workerPrewarm: 'instrumentation' is enabled | | Phase constants | Selects rewrite mode (build/serve) or proxy mode (dev) |

The library-owned rewrites() entries are phase-aware:

  1. beforeFiles blocks direct generated-handler URLs.
  2. beforeFiles routes exact heavy-page traffic to generated handlers.
  3. afterFiles installs App default-locale normalization when multi-locale App routing needs it.