Skip to main content

Unhandled Routes

An unhandled route is a route path discovered during route traversal that contains parameters or dynamic segments. Before Scully can render these routes, they must be transformed into concrete paths through router plugins.

What is an Unhandled Route?

When you look at your browser’s address bar, you see a complete URL:
http://localhost:4200/docs/concepts/unhandled-routes
The path portion is: /docs/concepts/unhandled-routes This is a concrete path. However, in your Angular routing configuration, this might be defined as:
const routes: Routes = [
  {
    path: 'docs',
    children: [
      { path: 'concepts/:topic', component: ConceptComponent }
    ]
  }
];
Here, /docs/concepts/:topic is an unhandled route because :topic is a parameter that needs to be resolved.

Static vs. Parameterized Routes

Static routes have no parameters and are immediately ready for rendering:
const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },
  { path: 'privacy', component: PrivacyComponent }
];
Discovered routes:
/
/about
/contact
/privacy
These routes can be rendered immediately without additional configuration.

Angular Routing Example

Consider a typical Angular application with lazy-loaded modules:
// app-routing.module.ts
const routes: Routes = [
  {
    path: 'user',
    loadChildren: () => import('./user/user.module').then(m => m.UserModule)
  }
];

// user-routing.module.ts
const routes: Routes = [
  { path: '', component: UsersComponent },
  {
    path: ':userId',
    component: UserComponent,
    children: [
      { path: '', component: PostsComponent, pathMatch: 'full' },
      { path: 'friend/:friendCode', component: FriendComponent },
      { path: 'post/:postId', component: PostComponent }
    ]
  }
];
Scully’s route traversal discovers these unhandled routes:
[
  '/user',                           // ✓ Static route
  '/user/:userId',                   // ✗ Unhandled (needs userId values)
  '/user/:userId/friend/:friendCode', // ✗ Unhandled (needs userId & friendCode)
  '/user/:userId/post/:postId'       // ✗ Unhandled (needs userId & postId)
]
Scully automatically traverses lazy-loaded modules to discover all routes, regardless of module boundaries.

The Problem with Parameters

Parameters in routes represent dynamic data that can have infinite possible values:
  • /user/:userId could be /user/1, /user/2, /user/alice, /user/bob123, etc.
  • /blog/:slug could be /blog/my-post, /blog/another-article, etc.
Scully cannot guess what values to use. Without configuration:
Routes with dynamic parameters but no configuration will NOT be rendered.This means there will be no static files for routes with dynamic data unless you configure them.

Configuring Unhandled Routes

To render parameterized routes, you must configure a router plugin that provides the parameter values:

Example: Blog Posts with ContentFolder Plugin

1

Define the route in Angular

const routes: Routes = [
  { path: 'blog/:slug', component: BlogPostComponent }
];
2

Create markdown files

blog/
├── my-first-post.md
├── scully-basics.md
└── advanced-features.md
3

Configure the route in scully.config.ts

export const config: ScullyConfig = {
  projectRoot: './src',
  projectName: 'my-blog',
  routes: {
    '/blog/:slug': {
      type: 'contentFolder',
      slug: {
        folder: './blog'
      }
    }
  }
};
4

Scully generates routes

The contentFolder plugin reads the folder and creates:
[
  { route: '/blog/my-first-post', type: 'contentFolder' },
  { route: '/blog/scully-basics', type: 'contentFolder' },
  { route: '/blog/advanced-features', type: 'contentFolder' }
]

Example: User Pages with JSON Plugin

For data from an API or JSON file:
export const config: ScullyConfig = {
  routes: {
    '/user/:userId': {
      type: 'json',
      userId: {
        url: 'https://api.example.com/users',
        property: 'id'
      }
    }
  }
};

Extra Routes for Special Cases

Sometimes your application has routes that Scully cannot discover through automatic traversal:

Use Cases for Extra Routes

Angular’s custom UrlMatcher functions cannot be statically analyzed:
// Angular routing with custom matcher
const routes: Routes = [
  {
    matcher: (segments) => {
      if (segments.length === 2 && segments[0].path === 'custom') {
        return { consumed: segments };
      }
      return null;
    },
    component: CustomComponent
  }
];

// Add manually to Scully config
export const config: ScullyConfig = {
  extraRoutes: [
    '/custom/page-1',
    '/custom/page-2',
    '/custom/special'
  ]
};
When using ng-upgrade, some routes might still be handled by AngularJS:
export const config: ScullyConfig = {
  extraRoutes: [
    '/legacy/dashboard',
    '/legacy/settings',
    '/legacy/profile'
  ]
};
Using Scully with non-Angular applications:
export const config: ScullyConfig = {
  bareProject: true,
  extraRoutes: [
    '/',
    '/about',
    '/services',
    '/contact'
  ]
};
Routes that exist but aren’t linked from anywhere:
export const config: ScullyConfig = {
  extraRoutes: [
    '/secret-page',
    '/maintenance',
    '/coming-soon'
  ]
};

Extra Routes Configuration

The extraRoutes option accepts multiple formats:

String Format

export const config: ScullyConfig = {
  extraRoutes: '/single-route'
};

Array Format

export const config: ScullyConfig = {
  extraRoutes: [
    '/route-1',
    '/route-2',
    '/nested/route-3'
  ]
};

Promise Format

export const config: ScullyConfig = {
  extraRoutes: fetchDynamicRoutes()
};

async function fetchDynamicRoutes(): Promise<string[]> {
  const response = await fetch('https://api.example.com/routes');
  const data = await response.json();
  return data.routes;
}

Mixed Format

export const config: ScullyConfig = {
  extraRoutes: [
    '/static-route',
    fetchDynamicRoutes(),
    '/another-static'
  ]
};

Real-World Extra Routes Example

Fetching archived URLs from the Wayback Machine:
import { httpGetJson } from './http-utils';

export const config: ScullyConfig = {
  projectRoot: './src',
  projectName: 'my-site',
  extraRoutes: httpGetJson(
    'http://web.archive.org/cdx/search/cdx?url=scully.io*&output=json'
  ).then(processWaybackData)
};

function processWaybackData(data: any[]): string[] {
  return data
    // Filter successful responses
    .filter(item => item.statuscode === '200')
    // Extract pathname from URL
    .map(item => {
      try {
        return new URL(item.original).pathname;
      } catch {
        return null;
      }
    })
    // Remove invalid paths
    .filter(path => path && isValidAppRoute(path))
    // Remove duplicates
    .filter((path, index, self) => self.indexOf(path) === index);
}

function isValidAppRoute(path: string): boolean {
  // Ensure the path is a valid route in your app
  const validPrefixes = ['/blog', '/docs', '/about'];
  return validPrefixes.some(prefix => path.startsWith(prefix));
}
Always validate and sanitize routes from external sources to ensure they’re valid paths in your application.

Route Discovery Summary

Here’s what happens when Scully discovers routes:

Important Notes

Routes without configuration are not lost!Static routes without parameters are handled by the default router plugin automatically. Only parameterized routes require explicit configuration.
Dynamic routes without config = No static filesIf you have /user/:id in your Angular app but no configuration in scully.config.ts, Scully will discover the route but will not render any pages for it.
The root route is always includedScully ensures the root route / is always in the unhandled routes list, even if it’s not explicitly defined in your routing configuration.

Debugging Unhandled Routes

You can inspect discovered routes by checking the cache:
cat node_modules/.cache/@scullyio/your-project-name.unhandledRoutes.json
Example output:
[
  "/",
  "/about",
  "/contact",
  "/blog/:slug",
  "/user/:userId",
  "/product/:category/:id"
]
Routes with : are unhandled and need configuration.

Common Patterns

// Route: /blog/:slug
export const config: ScullyConfig = {
  routes: {
    '/blog/:slug': {
      type: 'contentFolder',
      slug: {
        folder: './blog'
      }
    }
  }
};

Next Steps

Handled Routes

Learn how unhandled routes become handled routes

Router Plugins

Explore available router plugins

Route Discovery

Deep dive into how routes are discovered

Configuration

Complete configuration reference

Build docs developers (and LLMs) love