Skip to main content

How Scully Works

Scully is a static site generator designed specifically for Angular applications. It takes your Angular app, analyzes its structure, and pre-renders all known routes into static HTML files that can be served without requiring JavaScript to display content.

Core Architecture

Scully operates through a multi-phase build process that transforms your dynamic Angular application into a collection of static files:
1

Configuration Loading

Scully compiles and loads your scully.<projectName>.config.ts file along with any custom plugins defined in the ./scully directory.
2

Route Discovery

The system analyzes your Angular application to identify all routes, including lazy-loaded modules and parameterized routes.
3

Route Enrichment

Each discovered route is enriched with configuration data and transformed from an unhandled route into a handled route with all necessary metadata.
4

Rendering

Each route is rendered using Puppeteer (by default) or a custom render plugin, generating the final HTML output.
5

Post-Processing

Render plugins process the HTML to add additional functionality, optimize content, or inject metadata.
6

Output

The final static files are written to disk, ready to be served by any static file server.

The Build Pipeline

When you run npx scully, the following sequence of operations occurs:
// From libs/scully/src/lib/utils/startup/startup.ts
export const startScully = async (url?: string) => {
  startProgress();
  printProgress(undefined, 'warming up');
  performance.mark('startDuration');
  
  await loadConfig().catch((err) => process.exit(15));
  await handleBeforeAll();
  
  const numberOfRoutesProm = findPlugin(generateAll)(url)
    .then((routes) => {
      printProgress(false, 'calculate timings');
      performance.mark('stopDuration');
      return routes.length;
    })
    .catch(() => 0);
    
  // ... performance measurement and stats
};

Phase 1: Initialization

Scully begins by:
  1. Compiling user plugins - TypeScript files in ./scully/ are compiled using ./scully/tsconfig.json
  2. Loading configuration - The scully.<projectName>.config.ts file is loaded and validated
  3. Running beforeAll plugins - Any registered beforeAll plugins are executed
The beforeAll plugins are useful for setup tasks like database connections, API calls, or environment checks that need to complete before route discovery begins.

Phase 2: Route Discovery

The route discovery process identifies all routes in your application:
// From libs/scully/src/lib/utils/handlers/defaultAction.ts
const unhandledRoutes = await findPlugin(handleTravesal)();
This phase:
  • Parses your Angular routing configuration using the guess-parser library
  • Identifies static routes (e.g., /about, /contact)
  • Discovers parameterized routes (e.g., /user/:id, /blog/:slug)
  • Merges in any extraRoutes defined in your configuration
For detailed information, see Route Discovery.

Phase 3: Route Enrichment

Unhandled routes are transformed into handled routes:
// From libs/scully/src/lib/utils/handlers/defaultAction.ts
const handledRoutes = await routeDiscovery(unhandledRoutes, localBaseFilter);
During enrichment:
  • Router plugins expand parameterized routes into concrete routes
  • Configuration data is attached to each route
  • Route type (content, JSON, etc.) is determined
  • Custom metadata is added from plugins
Learn more about Unhandled Routes and Handled Routes.

Phase 4: Route Processing

Before rendering, routes pass through processing plugins:
const processedRoutes = await findPlugin(processRoutes)(handledRoutes);
Route process plugins can:
  • Filter routes based on custom criteria
  • Modify route metadata
  • Add or remove routes from the list
  • Transform route properties

Phase 5: Rendering

Each route is rendered to generate static HTML:
await findPlugin(renderPlugin)(processedRoutes);
The rendering process is detailed in Rendering Process.

Phase 6: Completion

After all routes are rendered:
  1. The scully.routes.json file is written with metadata for all routes
  2. allDone plugins are executed for post-processing tasks
  3. Performance statistics are calculated and displayed
[
  {
    "route": "/blog/scully-basics",
    "title": "Scully Basics",
    "sourceFile": "blog/scully-basics.md",
    "published": true,
    "lang": "en"
  },
  {
    "route": "/blog/advanced-features",
    "title": "Advanced Features",
    "sourceFile": "blog/advanced-features.md",
    "published": true,
    "lang": "en"
  }
]

Plugin System

Scully’s architecture is highly extensible through its plugin system. There are several types of plugins:

Router Plugins

Transform unhandled routes with parameters into concrete routes

Render Plugins

Control how routes are rendered into HTML

Route Process Plugins

Modify the list of routes before rendering

Post-Render Plugins

Process HTML after initial rendering

Plugin Execution Order

Caching and Performance

Scully implements intelligent caching to improve performance:

Route Caching

Discovered routes are cached to avoid re-scanning the Angular application:
// From libs/scully/src/lib/routerPlugins/traverseAppRoutesPlugin.ts
const routesPath = join(
  scullyConfig.homeFolder,
  'node_modules/.cache/@scullyio',
  `${scullyConfig.projectName}.unhandledRoutes.json`
);

if (forceScan === false && existFolder(routesPath)) {
  const result = JSON.parse(readFileSync(routesPath).toString());
  logWarn('Using stored unhandled routes!');
  logWarn(`To discover new routes use "npx scully --scanRoutes"`);
  return [...new Set([...result, ...extraRoutes]).values()];
}
Use npx scully --scanRoutes to force a fresh scan when you’ve added new routes to your Angular application.

Parallel Rendering

Scully renders routes in parallel using all available CPU cores:
// Routes are processed in parallel for maximum performance
return stringRenders.reduce(async (updatedHTML, { handler, plugin }) => {
  const html = await updatedHTML;
  try {
    return await handler(html, route);
  } catch (e) {
    logError(`Error during content generation with plugin "${plugin}"`);
  }
  return html;
}, Promise.resolve(jsDomHtml || InitialHTML));
This parallelization can process multiple routes per second, depending on your hardware and route complexity.

Background Server

When Scully starts, it automatically launches a background server to serve your Angular application during the rendering process:
The background server runs on the port specified in your scully.config.ts (default: 1668) and serves your pre-built Angular application from the dist folder.
Puppeteer connects to this server to render each route, executing your Angular application in a headless browser and capturing the fully-rendered HTML.

Performance Monitoring

Scully tracks detailed performance metrics throughout the build process:
performance.mark('startDuration');
performanceIds.add('Duration');
performance.mark('startConfigLoad');
performanceIds.add('ConfigLoad');
// ... later
performance.mark('stopConfigLoad');
performance.measure('ConfigLoad', 'startConfigLoad', 'stopConfigLoad');
These metrics help you:
  • Identify bottlenecks in your build process
  • Optimize plugin performance
  • Track improvements over time
Use the --stats flag to generate a detailed scullyStats.json file with comprehensive timing information.

Next Steps

Now that you understand how Scully works, dive deeper into specific concepts:

Route Discovery

Learn how Scully finds all routes in your application

Rendering Process

Understand how routes are transformed into static HTML

Handled Routes

Explore the structure and properties of handled routes

Unhandled Routes

Learn about parameterized and dynamic routes

Build docs developers (and LLMs) love