Skip to main content

Overview

Scully’s plugin system is incredibly powerful and allows you to extend its functionality at various points in the build pipeline. Whether you need to fetch data for routes, transform HTML output, handle custom file types, or perform post-build operations, Scully’s plugin system has you covered.

Plugin Types

Scully supports several types of plugins, each designed for a specific purpose in the build pipeline:

Core Plugin Types

  • router - Teach Scully how to fetch data needed to pre-render pages from route parameters
  • render/postProcessByHtml - Transform the rendered HTML content after Angular renders
  • postProcessByDom - Transform HTML using JSDOM for DOM manipulation
  • fileHandler - Process custom file types (markdown, AsciiDoc, etc.)
  • routeProcess - Modify the route array before rendering starts
  • routeDiscoveryDone - Execute after all routes are discovered
  • allDone - Execute after Scully completes all processes
  • beforeAll - Execute before Scully starts processing

Plugin Structure

All Scully plugins follow a similar structure:

1. Plugin Function

Every plugin is an async function that returns a Promise:
const myPlugin = async (/* parameters */) => {
  // Plugin logic here
  return result;
};

2. Plugin Registration

After creating the function, register it with Scully:
import { registerPlugin } from '@scullyio/scully';

registerPlugin('pluginType', 'pluginName', myPlugin);

3. Plugin Usage

Depending on the plugin type, you’ll either:
  • Add it to your Scully config file
  • Use it automatically (like routeDiscoveryDone)
  • Reference it in route configurations

Basic Example

Here’s a simple postProcessByHtml plugin that adds a custom meta tag:
// scully/plugins/custom-meta.plugin.ts
import { registerPlugin, HandledRoute } from '@scullyio/scully';

const customMetaPlugin = async (
  html: string,
  route: HandledRoute
): Promise<string> => {
  // Add a custom meta tag
  const metaTag = '<meta name="custom-tag" content="Scully rocks!">';
  return html.replace('</head>', `${metaTag}</head>`);
};

// Register the plugin
registerPlugin('postProcessByHtml', 'customMeta', customMetaPlugin);

export { customMetaPlugin };
Then use it in your config:
// scully.config.ts
import { ScullyConfig } from '@scullyio/scully';
import './scully/plugins/custom-meta.plugin';

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

Plugin Location

When you initialize Scully with the Angular schematic, a scully/plugins folder is created in your project. This is the recommended location for your custom plugins.
my-project/
├── scully/
│   ├── plugins/
│   │   ├── my-router-plugin.ts
│   │   ├── my-render-plugin.ts
│   │   └── my-file-handler.ts
│   └── scully.config.ts
├── src/
└── ...

Plugin Return Values

All plugins return a Promise wrapping their result:
  • Router plugins: Promise<HandledRoute[]>
  • Render plugins: Promise<string>
  • DOM plugins: Promise<JSDOM>
  • File handler plugins: Promise<string>
  • Lifecycle plugins: Promise<void> or Promise<HandledRoute[]>
Always remember to await plugin function calls or chain .then() when invoking them manually.

Plugin Configuration

Plugins can have their own configuration using Scully’s config system:
import { getMyConfig, setMyConfig } from '@scullyio/scully';

const myPlugin = async (html: string) => {
  const config = getMyConfig(myPlugin);
  // Use config.someOption
  return html;
};

// Set default configuration
setMyConfig(myPlugin, {
  someOption: 'default-value',
});

registerPlugin('postProcessByHtml', 'myPlugin', myPlugin);
Users can then override configuration in their Scully config:
import { setPluginConfig } from '@scullyio/scully';

setPluginConfig('myPlugin', {
  someOption: 'custom-value',
});

Finding Plugins Programmatically

Use the findPlugin function to access registered plugins:
import { findPlugin } from '@scullyio/scully';

// Find by name (if unique)
const myPlugin = findPlugin('myPluginName');

// Find by type and name
const myRouter = findPlugin('router', 'myRouterPlugin');

// Optional: don't throw if not found
const optionalPlugin = findPlugin('pluginName', undefined, false);

Best Practices

1. Use TypeScript

TypeScript provides type safety and better IDE support:
import { HandledRoute } from '@scullyio/scully';

const myPlugin = async (
  html: string,
  route: HandledRoute
): Promise<string> => {
  // TypeScript will catch errors
  return html;
};

2. Error Handling

Always handle errors gracefully:
import { logError } from '@scullyio/scully';

const myPlugin = async (html: string) => {
  try {
    // Plugin logic
    return processedHtml;
  } catch (error) {
    logError('myPlugin encountered an error:', error);
    return html; // Return original on error
  }
};

3. Don’t Export Plugin Functions

Export plugin names, not functions, to avoid bleeding into other parts:
// Good
export const myPluginName = 'myPlugin';

// Avoid
export const myPluginFunction = async () => { /* ... */ };

4. Use Symbols for Unique Names (Optional)

While symbols were traditionally used to prevent name collisions, modern Scully recommends using string constants instead:
// Recommended
export const myPlugin = 'myPlugin' as const;

// Legacy (still supported)
export const myPlugin = Symbol('myPlugin');

Testing Plugins

Always test your plugins thoroughly:
import { findPlugin } from '@scullyio/scully';
import './my-plugin';

describe('myPlugin', () => {
  it('should transform HTML correctly', async () => {
    const plugin = findPlugin('myPlugin');
    const html = '<html><head></head><body></body></html>';
    const result = await plugin(html, mockRoute);
    
    expect(result).toContain('expected-content');
  });
});

Next Steps

Now that you understand the basics, dive into creating specific plugin types:

Community Plugins

Explore existing community plugins for inspiration:

Build docs developers (and LLMs) love