Plugin Authoring Guide
PocketPages plugins are powerful tools that can extend and modify the behavior of your application. This guide will help you understand how to create plugins and use the various hooks available.
Plugin Structure
A plugin is a factory function that takes configuration options and returns an object with hook methods. Here's the basic structure:
module.exports = (config, options) => {
// Initialize plugin with config and options
return {
// Hook methods
onRequest(context) {
/* ... */
},
onRender(context) {
/* ... */
},
handles(context) {
/* ... */
},
onExtendContextApi(context) {
/* ... */
},
onResponse(context) {
/* ... */
},
}
}
Configuration Parameters
The plugin factory receives two parameters:
config: PluginFactoryConfig
- Core configuration object containing:pagesRoot
- Root directory of pagesconfig
- Global PocketPages configurationglobal
- Global context APIroutes
- Array of route definitionsdbg
- Debug logging function
options: PluginOptionsBase
- Plugin-specific options including:debug
- Enable debug mode- Any custom options defined by your plugin
Available Hooks
Plugins can implement any of these hooks to modify application behavior:
onRequest(context: RequestContext)
Called at the start of each request, before any processing begins.
onRequest({ request, response }) {
// Modify request/response
// Add headers, check auth, etc.
}
onRender(context: RenderContext): string
Transform content during the rendering process.
onRender({ api, content, filePath, plugins }) {
// Transform content
// Process templates
// Add features
return modifiedContent;
}
handles(context: HandlesContext): boolean
Determine if this plugin should handle a specific file.
handles({ route, filePath }) {
return filePath.endsWith('.md'); // Handle markdown files
}
onExtendContextApi(context: ExtendContextApiContext)
Add methods to the context API available to pages.
onExtendContextApi({ api }) {
api.myHelper = () => {
// Add custom functionality
};
}
onResponse(context: ResponseContext): boolean
Final processing before sending the response.
onResponse({ api, content }) {
// Modify final output
// Add headers
// Handle caching
return false; // Continue processing
}
Best Practices
Early Returns: Use early returns for cleaner code flow
handles({ filePath }) { if (!filePath.endsWith('.md')) return false; return true; }
Factory Pattern: Return an API object instead of using classes
module.exports = (config, options) => { const api = { onRender: () => { /* ... */ }, // other methods } return api }
Debug Logging: Use the provided debug function
module.exports = ({ dbg }, options) => { dbg('Initializing plugin', { options }) // ... }
Example Plugins
Content Transformer
module.exports = (config, options) => {
return {
handles: ({ filePath }) => filePath.endsWith('.custom'),
onRender: ({ content }) => {
return content.toUpperCase() // Transform to uppercase
},
}
}
API Extension
module.exports = (config, options) => {
return {
onExtendContextApi: ({ api }) => {
api.formatDate = (date) => {
return new Date(date).toLocaleDateString()
}
},
}
}
Extending Global API
Plugins can extend the global API during initialization by modifying the global
object in the factory config. This makes functionality available across all pages and other plugins.
The global API includes core utilities like:
url()
- URL parsingstringify()
- JSON stringificationenv()
- Environment variable accessstore()
- Global state management- Logging functions (
dbg
,info
,warn
,error
)
Example: Adding Global Functions
Here's how the JS SDK plugin extends the global API with a PocketBase client:
const jsSdkPluginFactory = (config, extra) => {
const { global } = config
const host = extra?.host ?? `http://localhost:8090`
// Store PocketBase instance
let pb = null
// Add pb() to global API
global.pb = () => {
if (pb) return pb
pb = new PocketBase(host)
return pb
}
return {}
}
This makes pb()
available everywhere through the global API:
// In any page or plugin
const pb = pb()
const records = pb.collection('posts').getList()
Best Practices for Global API Extensions
Lazy Initialization: Initialize expensive resources only when first requested
let instance = null global.myApi = () => { if (instance) return instance instance = new ExpensiveResource() return instance }
Namespacing: Use clear naming to avoid conflicts
global.myPlugin = { helper1: () => { /* ... */ }, helper2: () => { /* ... */ }, }
Type Safety: Define types for your extensions
declare module 'pocketpages' { interface PagesGlobalContext { myApi: () => MyApiType } }
Documentation: Document the API you're exposing
/** * Returns configured API client * @param {object} options - Optional configuration * @returns {ApiClient} Configured client instance */ global.myApi = (options) => { // Implementation }