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:
Explore existing community plugins for inspiration: