Skip to main content

Route Discovery

Route discovery is the process by which Scully identifies all the routes in your Angular application. This is a critical first step that determines which pages will be pre-rendered into static HTML files.

Overview

Scully uses a combination of static analysis and configuration to discover routes:
  1. Automatic traversal of Angular routing modules using the guess-parser library
  2. Manual specification through the extraRoutes configuration option
  3. Plugin-based expansion of parameterized routes into concrete paths
Route discovery happens once per build unless you use the --scanRoutes flag to force a fresh scan.

The Traversal Process

The route traversal is handled by the traverseAppRoutes plugin:
// From libs/scully/src/lib/routerPlugins/traverseAppRoutesPlugin.ts
const plugin = async (forceScan = scanRoutes): Promise<string[]> => {
  const appRootFolder = scullyConfig.projectRoot;
  const routesPath = join(
    scullyConfig.homeFolder,
    'node_modules/.cache/@scullyio',
    `${scullyConfig.projectName}.unhandledRoutes.json`
  );
  const extraRoutes = await addExtraRoutes();
  let routes = [] as string[];

  if (!scullyConfig.bareProject) {
    // Read from cache when exists and not forced to scan
    if (forceScan === false && existFolder(routesPath)) {
      const result = JSON.parse(readFileSync(routesPath).toString());
      logWarn('Using stored unhandled routes!');
      return [...new Set([...result, ...extraRoutes]).values()];
    }
    
    // Parse Angular routes
    routes = parseAngularRoutes(file, excludedFiles).map((r) => r.path);
    
    // Make sure root route is always rendered
    if (routes.findIndex((r) => r.trim() === '' || r.trim() === '/') === -1) {
      routes.push('/');
    }
    
    // Cache the scanned routes
    writeFileSync(routesPath, JSON.stringify(routes));
  }
  
  // De-duplicate routes
  return [...new Set([...routes, ...extraRoutes]).values()];
};

What Gets Discovered

Scully automatically discovers:
Routes with no parameters:
const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent }
];
Discovered routes:
/
/about
/contact

Caching Mechanism

Scully caches discovered routes to improve build performance:
node_modules/.cache/@scullyio/
└── your-project-name.unhandledRoutes.json
When routes are cached, you’ll see this warning:
----------------------------------
Using stored unhandled routes!.
   To discover new routes in the angular app use "npx scully --scanRoutes"
----------------------------------
Always use --scanRoutes after adding new routes to your Angular application to ensure they’re discovered and rendered.

Extra Routes Configuration

Sometimes Scully cannot automatically discover all routes in your application. The extraRoutes configuration allows you to manually specify additional routes:

String Routes

// scully.your-project.config.ts
export const config: ScullyConfig = {
  projectRoot: './src',
  projectName: 'my-app',
  extraRoutes: '/hidden-route'
};

Array of Routes

export const config: ScullyConfig = {
  projectRoot: './src',
  projectName: 'my-app',
  extraRoutes: [
    '/special-page',
    '/legacy-route',
    '/unlisted-content'
  ]
};

Promise-Based Routes

For dynamic route discovery, use a Promise:
export const config: ScullyConfig = {
  projectRoot: './src',
  projectName: 'my-app',
  extraRoutes: fetchRoutesFromAPI()
};

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

Real-World Example

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

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

function cleanupUrls(data: any[]): string[] {
  return data
    .filter(item => item.statuscode === '200')
    .map(item => new URL(item.original).pathname)
    .filter(path => isValidAppRoute(path));
}
The extraRoutes configuration is particularly useful when:
  • Using custom route matchers that can’t be statically analyzed
  • Working with ng-upgrade applications that have AngularJS routes
  • Building non-Angular applications with Scully
  • Dynamically generating routes from external data sources

The Route Discovery Pipeline

Here’s the complete flow of route discovery:

From Discovery to Enrichment

Once routes are discovered, they move to the enrichment phase:
// From libs/scully/src/lib/utils/handlers/routeDiscovery.ts
export async function routeDiscovery(
  unhandledRoutes: string[], 
  localBaseFilter: string
): Promise<HandledRoute[]> {
  performance.mark('startDiscovery');
  performanceIds.add('Discovery');
  printProgress(undefined, 'Pulling in data to create additional routes.');
  
  let handledRoutes = [] as HandledRoute[];
  
  // Apply filters
  const baseFilterRegexs = wildCardStringToRegEx(localBaseFilter, {
    addTrailingStar: true,
  });
  const routeFilterRegexs = wildCardStringToRegEx(routeFilter);
  
  // Transform unhandled to handled routes
  handledRoutes = (
    await addOptionalRoutes(
      unhandledRoutes.filter((r: string) => 
        typeof r === 'string' && 
        baseFilterRegexs.some((reg) => r.match(reg) !== null)
      )
    )
  ).filter(
    (r) =>
      !r.route.endsWith('*') &&
      (routeFilter === '' || routeFilterRegexs.some((reg) => r.route.match(reg) !== null))
  );
  
  performance.mark('stopDiscovery');
  return handledRoutes;
}
This enrichment process:
  1. Filters routes based on command-line arguments
  2. Invokes router plugins to expand parameterized routes
  3. Transforms unhandled routes into handled routes with full metadata

Filtering Routes

You can limit which routes are processed using command-line filters:
# Only process routes starting with /blog
npx scully --baseFilter /blog
Filters are useful during development to quickly test changes on a subset of routes without building the entire site.

Troubleshooting Route Discovery

Routes Not Found

Problem: Routes in lazy-loaded modules aren’t being found.Solution: Ensure your tsconfig.app.json is in the expected location and includes all module files. Check the Scully logs for parsing errors.
// Verify your tsconfig.app.json includes:
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": []
  },
  "files": [
    "src/main.ts",
    "src/polyfills.ts"
  ],
  "include": [
    "src/**/*.d.ts"
  ]
}
Problem: Routes using custom UrlMatcher functions aren’t discovered.Solution: Custom matchers can’t be statically analyzed. Add these routes manually via extraRoutes:
export const config: ScullyConfig = {
  projectRoot: './src',
  projectName: 'my-app',
  extraRoutes: [
    '/custom-matched-route-1',
    '/custom-matched-route-2'
  ]
};
Problem: Routes appear in the cache but don’t generate HTML files.Solution: Parameterized routes need router plugin configuration. See Handled Routes for configuration examples.
export const config: ScullyConfig = {
  routes: {
    '/blog/:slug': {
      type: 'contentFolder',
      slug: {
        folder: './blog'
      }
    }
  }
};

Parser Errors

If you encounter parser errors:
We encountered a problem while reading the routes from your applications source.
This might happen when there are lazy-loaded routes, that are not loaded,
Or when there are paths we can not resolve statically.
Use --showGuessError to see detailed error information:
npx scully --showGuessError

Performance Considerations

Route discovery timing depends on:
  • Application size: More routes and modules take longer to analyze
  • Lazy loading: Multiple lazy-loaded modules increase parsing time
  • Cache status: Cached routes are retrieved in milliseconds
Typical performance:
  • Small app (< 20 routes): 500ms - 1s
  • Medium app (20-100 routes): 1s - 3s
  • Large app (> 100 routes): 3s - 10s
After the initial scan, subsequent builds use the cache and complete in under 100ms.

Next Steps

Unhandled Routes

Learn what happens to routes with parameters

Handled Routes

Understand how routes are enriched with metadata

Router Plugins

Create plugins to handle custom route types

Configuration

Configure route discovery settings

Build docs developers (and LLMs) love