Skip to main content

plugin

The plugin schematic (also available as add-plugin) creates a custom Scully plugin for extending Scully’s functionality with router or render plugins.

Usage

Basic Usage

ng generate @scullyio/init:plugin --name=myPlugin

Short Form

ng g @scullyio/init:plugin --name=myPlugin

Using Alias

ng g @scullyio/init:add-plugin --name=customRouter

Interactive Mode

If you don’t provide all required options, you’ll be prompted:
ng g @scullyio/init:plugin
? What name do you want to use for the plugin? myCustomPlugin
? What type of plugin do you want to create?
 router
    render

Options

--name (required)

  • Type: string
  • Description: The name of the plugin
  • Prompt: “What name do you want to use for the plugin?”
The name will be used for:
  • The plugin filename (dasherized)
  • The plugin function name (camelized)
  • Plugin registration identifier
ng g @scullyio/init:plugin --name=customRouter
Creates: scully-plugins/custom-router.plugin.js

--pluginType (required)

  • Type: string
  • Options: "router", "render"
  • Description: The type of plugin to create
  • Prompt: “What type of plugin do you want to create?”
Router Plugin:
ng g @scullyio/init:plugin --name=myRouter --pluginType=router
Creates a plugin that discovers and handles routes. Render Plugin:
ng g @scullyio/init:plugin --name=myRenderer --pluginType=render
Creates a plugin that transforms rendered HTML.

--project

  • Type: string
  • Default: "defaultProject"
  • Description: The project to create the plugin in (for multi-project workspaces)
ng g @scullyio/init:plugin --name=myPlugin --project=my-app

What Gets Created/Modified

The plugin schematic creates a plugin file and registers it in your Scully configuration.

1. Router Plugin

File: [sourceRoot]/scully-plugins/[plugin-name].plugin.js For a router plugin named “customRouter”: File: src/scully-plugins/custom-router.plugin.js
/** import from scully the register plugin function */
const {registerPlugin} = require('@scullyio/scully');

/** import from scully the register plugin function */
const customRouter = async (route, options) => {
  const handleRoute = [];
  /**
  this is a router plugin and this need return a handle route
  the HandleRoute is a type available for you.
  If you will create another type of plugin, you need return HTML.
   * */
  return handleRoute;
};
/**
  You can add extra validator for your custom plugin
*/
const validator = async conf => [];
/**
  registerPlugin(TypeOfPlugin, name of the plugin, plugin function, validator)
*/
registerPlugin('router', 'customRouter', customRouter, validator);

2. Render Plugin

File: [sourceRoot]/scully-plugins/[plugin-name].plugin.js For a render plugin named “customRenderer”: File: src/scully-plugins/custom-renderer.plugin.js
/** import from scully the register plugin function */
const {registerPlugin} = require('@scullyio/scully');

/** create the plugin function */
const customRenderer = (html, route) => {
  const updatedHtml = html;
  /**
    This is a render plugin that needs return the updated HTML
    using a Promise.
   **/
  return Promise.resolve(updatedHtml);
};

/**
  You can add extra validator for your custom plugin
*/
const validator = async conf => [];

/**
  registerPlugin(TypeOfPlugin, name of the plugin, plugin function, validator)
*/
registerPlugin('postProcessByHtml', 'customRenderer', customRenderer, validator);

3. Scully Configuration Update

File: scully.[project].config.ts (or scully.config.ts) The schematic automatically adds a require statement at the top of your Scully config: Before:
import { ScullyConfig } from '@scullyio/scully';

export const config: ScullyConfig = {
  projectRoot: "./src",
  projectName: "my-app",
  outDir: './dist/static',
  routes: {}
};
After:
require('./scully-plugins/custom-router.plugin.js');

import { ScullyConfig } from '@scullyio/scully';

export const config: ScullyConfig = {
  projectRoot: "./src",
  projectName: "my-app",
  outDir: './dist/static',
  routes: {}
};

Plugin Types

Router Plugins

Router plugins discover and generate routes for Scully to render. They return an array of HandledRoute objects. Use cases:
  • Fetching routes from an API
  • Generating routes from a database
  • Creating dynamic route variations
  • Custom route parameter resolution
Example - API Router:
const {registerPlugin} = require('@scullyio/scully');
const fetch = require('node-fetch');

const apiRouter = async (route, options) => {
  const { apiUrl } = options;
  const response = await fetch(apiUrl);
  const items = await response.json();
  
  return items.map(item => ({
    route: `/products/${item.id}`,
    data: item
  }));
};

const validator = async conf => {
  if (!conf.apiUrl) {
    throw new Error('apiRouter plugin requires apiUrl option');
  }
  return [];
};

registerPlugin('router', 'apiRouter', apiRouter, validator);
Usage in config:
routes: {
  '/products/:id': {
    type: 'apiRouter',
    apiUrl: 'https://api.example.com/products'
  }
}

Render Plugins

Render plugins transform the rendered HTML after Scully captures it. They receive HTML as a string and return modified HTML. Use cases:
  • Adding analytics scripts
  • Injecting meta tags
  • Transforming content
  • Adding CSS/JS resources
  • Minifying HTML
Example - Analytics Injector:
const {registerPlugin} = require('@scullyio/scully');

const analyticsInjector = (html, route) => {
  const analyticsScript = `
    <script>
      // Google Analytics code
      window.gtag('config', 'GA_MEASUREMENT_ID');
    </script>
  `;
  
  // Inject before closing </head> tag
  const updatedHtml = html.replace(
    '</head>',
    `${analyticsScript}</head>`
  );
  
  return Promise.resolve(updatedHtml);
};

const validator = async conf => [];

registerPlugin('postProcessByHtml', 'analyticsInjector', analyticsInjector, validator);
Usage in config:
export const config: ScullyConfig = {
  // ... other config
  defaultPostRenderers: ['analyticsInjector']
};

Examples

Creating a Router Plugin

ng g @scullyio/init:plugin --name=jsonRouter --pluginType=router
Implement the plugin:
const {registerPlugin} = require('@scullyio/scully');
const fs = require('fs').promises;
const path = require('path');

const jsonRouter = async (route, options) => {
  const { filename } = options;
  const filePath = path.join(process.cwd(), filename);
  const content = await fs.readFile(filePath, 'utf8');
  const data = JSON.parse(content);
  
  return data.routes.map(item => ({
    route: route.replace(':id', item.id),
    data: item
  }));
};

const validator = async conf => {
  if (!conf.filename) {
    throw new Error('jsonRouter requires filename option');
  }
  return [];
};

registerPlugin('router', 'jsonRouter', jsonRouter, validator);

Creating a Render Plugin

ng g @scullyio/init:plugin --name=metaTags --pluginType=render
Implement the plugin:
const {registerPlugin} = require('@scullyio/scully');

const metaTags = (html, route) => {
  const { title, description, image } = route.data || {};
  
  const metaTags = `
    <meta property="og:title" content="${title || 'Default Title'}">
    <meta property="og:description" content="${description || 'Default Description'}">
    <meta property="og:image" content="${image || '/default-og-image.jpg'}">
    <meta name="twitter:card" content="summary_large_image">
  `;
  
  const updatedHtml = html.replace('</head>', `${metaTags}</head>`);
  return Promise.resolve(updatedHtml);
};

const validator = async conf => [];

registerPlugin('postProcessByHtml', 'metaTags', metaTags, validator);

Multi-Project Workspace

ng g @scullyio/init:plugin --name=customPlugin --project=my-app --pluginType=router

Plugin File Location

Plugins are created in the scully-plugins directory within your project’s source root:
my-app/
├── src/
│   ├── app/
│   ├── scully-plugins/
│   │   ├── custom-router.plugin.js
│   │   ├── custom-renderer.plugin.js
│   │   └── another-plugin.plugin.js
│   └── ...
├── scully.config.ts
└── ...

Naming Conventions

The schematic applies these transformations to your plugin name:
  • File name: Dasherized (kebab-case)
    • Input: myCustomPlugin
    • File: my-custom-plugin.plugin.js
  • Function name: Camelized (camelCase)
    • Input: my-custom-plugin
    • Function: myCustomPlugin
  • Registration name: Camelized
    • Input: MyCustomPlugin
    • Registered as: myCustomPlugin

Validator Function

Both plugin templates include a validator function. Use this to validate plugin configuration:
const validator = async conf => {
  const errors = [];
  
  if (!conf.requiredOption) {
    errors.push('requiredOption is required');
  }
  
  if (conf.numericOption && typeof conf.numericOption !== 'number') {
    errors.push('numericOption must be a number');
  }
  
  return errors;
};
Scully will throw an error if the validator returns any error messages.

Plugin Registration Types

The schematic registers plugins with specific types:
Plugin TypeRegistration TypePurpose
router'router'Route discovery and generation
render'postProcessByHtml'HTML transformation after rendering
Other plugin types (not created by this schematic):
  • 'render' - Render process plugins
  • 'routeProcess' - Route processing plugins
  • 'allDone' - Post-build plugins
  • 'routeDiscoveryDone' - After route discovery plugins

Testing Your Plugin

After creating a plugin:
  1. Implement the plugin logic in the generated file
  2. Configure the plugin in scully.config.ts:
    routes: {
      '/products/:id': {
        type: 'myRouter',  // for router plugins
        // ... options
      }
    }
    
  3. Add to post-renderers (for render plugins):
    export const config: ScullyConfig = {
      defaultPostRenderers: ['myRenderer']
    };
    
  4. Build and run Scully:
    ng build
    npm run scully
    
  5. Check the output in the dist/static directory

Common Issues

Plugin Not Loading

Ensure the require statement is at the top of scully.config.ts:
require('./scully-plugins/my-plugin.plugin.js');
// BEFORE any imports
import { ScullyConfig } from '@scullyio/scully';

Plugin Not Found

Verify the plugin is registered with the correct name:
registerPlugin('router', 'myPlugin', myPlugin, validator);
//                       ^^^^^^^^ - use this name in config

Async Issues

Router plugins must return a Promise or use async/await:
// Good
const myRouter = async (route, options) => {
  const data = await fetchData();
  return data;
};

// Also good
const myRouter = (route, options) => {
  return fetchData().then(data => data);
};
Render plugins must return a Promise:
// Good
const myRenderer = (html, route) => {
  return Promise.resolve(modifyHtml(html));
};

// Bad - missing Promise
const myRenderer = (html, route) => {
  return modifyHtml(html); // ❌
};

See Also

Build docs developers (and LLMs) love