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
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.
Parameterized routes contain one or more parameter segments: const routes : Routes = [
{ path: 'user/:userId' , component: UserComponent },
{ path: 'blog/:slug' , component: BlogPostComponent },
{ path: 'product/:category/:id' , component: ProductComponent }
];
Discovered as unhandled routes: /user/:userId
/blog/:slug
/product/:category/:id
These routes cannot be rendered without additional configuration that tells Scully what values to use for the parameters.
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
Define the route in Angular
const routes : Routes = [
{ path: 'blog/:slug' , component: BlogPostComponent }
];
Create markdown files
blog/
├── my-first-post.md
├── scully-basics.md
└── advanced-features.md
Configure the route in scully.config.ts
export const config : ScullyConfig = {
projectRoot: './src' ,
projectName: 'my-blog' ,
routes: {
'/blog/:slug' : {
type: 'contentFolder' ,
slug: {
folder: './blog'
}
}
}
};
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:
scully.config.ts
API Response
Generated Routes
export const config : ScullyConfig = {
routes: {
'/user/:userId' : {
type: 'json' ,
userId: {
url: 'https://api.example.com/users' ,
property: 'id'
}
}
}
};
Sometimes your application has routes that Scully cannot discover through automatic traversal:
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'
]
};
Hidden or Unlisted Routes
Routes that exist but aren’t linked from anywhere: export const config : ScullyConfig = {
extraRoutes: [
'/secret-page' ,
'/maintenance' ,
'/coming-soon'
]
};
The extraRoutes option accepts multiple formats:
export const config : ScullyConfig = {
extraRoutes: '/single-route'
};
export const config : ScullyConfig = {
extraRoutes: [
'/route-1' ,
'/route-2' ,
'/nested/route-3'
]
};
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 ;
}
export const config : ScullyConfig = {
extraRoutes: [
'/static-route' ,
fetchDynamicRoutes (),
'/another-static'
]
};
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 files If 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 included Scully 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
Content Blog
Product Catalog
Documentation
User Profiles
// Route: /blog/:slug
export const config : ScullyConfig = {
routes: {
'/blog/:slug' : {
type: 'contentFolder' ,
slug: {
folder: './blog'
}
}
}
};
// Route: /product/:id
export const config : ScullyConfig = {
routes: {
'/product/:id' : {
type: 'json' ,
id: {
url: 'https://api.example.com/products' ,
property: 'id'
}
}
}
};
// Route: /docs/:section/:page
export const config : ScullyConfig = {
routes: {
'/docs/:section/:page' : {
type: 'contentFolder' ,
page: {
folder: './docs'
}
}
}
};
// Route: /user/:username
export const config : ScullyConfig = {
routes: {
'/user/:username' : {
type: 'json' ,
username: {
url: 'https://api.example.com/users' ,
property: 'username' ,
resultsHandler : ( data ) => data . users
}
}
}
};
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