Skip to main content
The routeRenderer constant identifies Scully’s primary route rendering plugin. It orchestrates the entire rendering pipeline, executing render plugins and DOM transformations to generate static HTML.

Overview

routeRenderer is a special plugin identifier used to find and invoke Scully’s main rendering system. While you typically don’t call it directly, understanding how it works helps you integrate with Scully’s rendering pipeline.
export const routeRenderer = 'routeRenderer' as const;

The Rendering Pipeline

When Scully renders a route, it follows this sequence:
  1. Pre-render hooks - Execute any preRenderer function defined in route config
  2. Initial render - Use the configured render plugin (or default routeRenderer)
  3. DOM plugins - Execute postProcessByDom plugins on the JSDOM
  4. HTML plugins - Execute postProcessByHtml plugins on the HTML string
  5. Write to disk - Save the final HTML to the output directory

Using routeRenderer

Finding the Renderer

Access the route renderer plugin using findPlugin:
import { findPlugin, routeRenderer } from '@scullyio/scully';

const renderer = findPlugin(routeRenderer);

Custom Render Plugins

You can specify an alternative render plugin per route:
import { ScullyConfig } from '@scullyio/scully';

export const config: ScullyConfig = {
  routes: {
    '/special/:id': {
      type: 'json',
      // Use a custom renderer for these routes
      renderPlugin: 'customRenderer'
    }
  }
};

The renderRoute Function

The renderRoute function implements the rendering pipeline. It’s registered as a system plugin and handles:
import { renderRoute } from '@scullyio/scully';

// Render a single route
const html = await renderRoute(route);

How renderRoute Works

const executePluginsForRoute = async (route: HandledRoute) => {
  // 1. Collect all handlers for this route
  const handlers = [
    route.type,
    ...(route.postRenderers || scullyConfig.defaultPostRenderers)
  ].filter(Boolean);

  // 2. Execute preRenderer if defined
  if (route.config?.preRenderer) {
    const result = await route.config.preRenderer(route);
    if (result === false) {
      return ''; // Skip rendering
    }
  }

  // 3. Get initial HTML from render plugin
  const renderPlugin = route.renderPlugin || routeRenderer;
  const InitialHTML = await findPlugin(renderPlugin)(route);

  // 4. Execute DOM plugins
  let processedHTML = InitialHTML;
  const domPlugins = handlers
    .map(name => findPlugin(name, 'postProcessByDom', false))
    .filter(Boolean);
  
  if (domPlugins.length > 0) {
    let dom = await toJSDOM(InitialHTML);
    for (const plugin of domPlugins) {
      dom = await plugin(dom, route);
    }
    processedHTML = await fromJSDOM(dom);
  }

  // 5. Execute HTML plugins
  const htmlPlugins = handlers
    .map(name => findPlugin(name, 'postProcessByHtml', false))
    .filter(Boolean);
  
  for (const plugin of htmlPlugins) {
    processedHTML = await plugin(processedHTML, route);
  }

  return processedHTML;
};

Render Plugin Interface

A render plugin must accept a HandledRoute and return the rendered HTML:
import { registerPlugin, HandledRoute } from '@scullyio/scully';

const customRenderer = async (route: HandledRoute): Promise<string> => {
  // Your custom rendering logic
  const html = await fetchRenderedHTML(route);
  return html;
};

registerPlugin('scullySystem', 'customRenderer', customRenderer);

Post-Render Plugins

Plugins execute in the order they’re defined in the route configuration:
import { ScullyConfig } from '@scullyio/scully';

export const config: ScullyConfig = {
  // Default post-renderers for all routes
  defaultPostRenderers: ['seoHeuristic', 'critical-css'],
  
  routes: {
    '/blog/:slug': {
      type: 'contentFolder',
      slug: { folder: './blog' },
      // Additional post-renderers for blog routes
      postRenderers: ['addMetaTags', 'optimizeImages']
    }
  }
};

Execution Order

  1. Default post-renderers from defaultPostRenderers
  2. Route-specific post-renderers from route.postRenderers
  3. DOM plugins execute before HTML plugins
  4. Plugins execute in array order

Pre-Render Hooks

Control whether a route should be rendered using a preRenderer function:
import { ScullyConfig, HandledRoute } from '@scullyio/scully';

const skipDrafts = async (route: HandledRoute) => {
  if (route.data?.draft) {
    console.log(`Skipping draft: ${route.route}`);
    return false; // Skip this route
  }
  // Perform setup tasks
  return true; // Continue rendering
};

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

Error Handling

The rendering system catches errors in plugins and logs them:
try {
  const html = await plugin(dom, route);
  return html;
} catch (error) {
  console.error(`Error in plugin "${pluginName}" for ${route.route}`);
  console.error(error.message);
  // Return original HTML or reset to initial state
  return originalHTML;
}
When a plugin throws an error:
  • The error is logged to the console
  • The route continues rendering with the previous HTML state
  • The build does not fail (fail-safe behavior)

Manual Rendering

For advanced use cases, you can manually invoke the rendering system:
import { findPlugin, routeRenderer, HandledRoute } from '@scullyio/scully';

const customBuildProcess = async () => {
  const routes: HandledRoute[] = [
    { route: '/home', type: 'default' },
    { route: '/about', type: 'default' }
  ];

  const renderer = findPlugin(routeRenderer);

  for (const route of routes) {
    const html = await renderer(route);
    // Do something with the HTML
    console.log(`Rendered ${route.route}`);
  }
};

Performance Considerations

DOM plugins are slower because they parse HTML into JSDOM:
  • Use postProcessByHtml for simple string replacements
  • Use postProcessByDom for complex DOM manipulations
// Fast: String-based plugin
const addBanner = async (html) => {
  return html.replace('<body>', '<body><div class="banner">...</div>');
};

// Slower but more powerful: DOM-based plugin
const restructureHTML = async (dom) => {
  const document = dom.window.document;
  const main = document.querySelector('main');
  // Complex DOM operations
  return dom;
};
Plugins execute sequentially, so the order affects performance:
// Good: Fast plugins first, slow plugins last
defaultPostRenderers: [
  'removeComments',        // Fast
  'minifyCSS',            // Medium
  'critical-css'          // Slow (analyzes CSS usage)
]
Only include plugins that are needed for each route:
routes: {
  '/blog/:slug': {
    postRenderers: ['syntax-highlight'] // Only for blog posts
  },
  '/': {
    postRenderers: [] // Homepage needs minimal processing
  }
}

Source Reference

  • Constant definition: libs/scully/src/lib/utils/config.ts:12
  • Rendering implementation: libs/scully/src/lib/renderPlugins/executePlugins.ts:13

HandledRoute

Route object structure and properties

Render Plugins

Learn about creating render plugins

Post-Process Plugins

Transform HTML after rendering

Rendering Process

Understanding how Scully renders pages

Build docs developers (and LLMs) love