Hooks & Filters
Extend and modify CMS behavior with hooks and filters.
Overview
- Actions: Execute code at specific points (e.g., after post saved)
- Filters: Modify data as it passes through (e.g., change post content)
Actions
sdk.hooks.addAction(name, callback, priority)
Register an action callback.
// Basic action
sdk.hooks.addAction('post.created', (post) => {
console.log('New post created:', post.title);
});
// With priority (lower = runs earlier, default is 10)
sdk.hooks.addAction('post.created', (post) => {
// This runs first
}, 5);
sdk.hooks.addAction('post.created', (post) => {
// This runs second
}, 15);Available Actions
| Action | Parameters | Description |
|---|---|---|
init | - | CMS initialized |
post.created | post | Post created |
post.updated | post, changes | Post updated |
post.deleted | postId | Post deleted |
post.published | post | Post published |
category.created | category | Category created |
category.updated | category | Category updated |
category.deleted | categoryId | Category deleted |
media.uploaded | media | Media file uploaded |
media.deleted | mediaId | Media file deleted |
user.login | user | User logged in |
user.logout | userId | User logged out |
user.registered | user | New user registered |
theme_head | - | Inside <head> tag |
theme_footer | - | Before </body> tag |
admin_head | - | Admin <head> tag |
admin_footer | - | Admin before </body> |
Action Examples
// Send notification when post is published
sdk.hooks.addAction('post.published', async (post) => {
await notifySubscribers(post);
});
// Log user activity
sdk.hooks.addAction('user.login', async (user) => {
await sdk.storage.set(`lastLogin:${user.id}`, new Date().toISOString());
});
// Add tracking code
sdk.hooks.addAction('theme_footer', () => {
return `
<script>
// Analytics code here
</script>
`;
});
// Add custom CSS
sdk.hooks.addAction('theme_head', () => {
return `
<style>
.my-plugin-class { color: red; }
</style>
`;
});Filters
sdk.hooks.addFilter(name, callback, priority)
Register a filter callback. Must return the (modified) value.
// Modify post title
sdk.hooks.addFilter('post.title', (title) => {
return title.toUpperCase();
});
// Add prefix to all titles
sdk.hooks.addFilter('post.title', (title, post) => {
if (post.category?.slug === 'news') {
return '📰 ' + title;
}
return title;
});Available Filters
| Filter | Parameters | Description |
|---|---|---|
post.title | title, post | Modify post title |
post.content | content, post | Modify post content |
post.excerpt | excerpt, post | Modify post excerpt |
post.url | url, post | Modify post URL |
posts.query | query | Modify posts query |
category.name | name, category | Modify category name |
category.url | url, category | Modify category URL |
media.url | url, media | Modify media URL |
search.results | results, query | Modify search results |
template.data | data, template | Modify template data |
seo.title | title, context | Modify SEO title |
seo.description | description, context | Modify meta description |
Filter Examples
// Add reading time to post content
sdk.hooks.addFilter('post.content', (content, post) => {
const words = content.replace(/<[^>]*>/g, '').split(/\s+/).length;
const readTime = Math.ceil(words / 200);
const badge = `<div class="read-time">${readTime} min read</div>`;
return badge + content;
});
// Add UTM parameters to external links
sdk.hooks.addFilter('post.content', (content) => {
return content.replace(
/href="(https?:\/\/[^"]+)"/g,
(match, url) => {
if (!url.includes(window.location.host)) {
const separator = url.includes('?') ? '&' : '?';
return `href="${url}${separator}utm_source=mysite"`;
}
return match;
}
);
});
// Modify posts query to exclude category
sdk.hooks.addFilter('posts.query', (query) => {
query.filter = query.filter || {};
query.filter.category = { $ne: 'hidden-category' };
return query;
});
// Custom SEO title format
sdk.hooks.addFilter('seo.title', (title, context) => {
if (context.post) {
return `${context.post.title} | ${context.settings.siteName}`;
}
return title;
});Removing Hooks
sdk.hooks.removeAction(name, callback)
function myHandler(post) {
console.log(post);
}
// Add
sdk.hooks.addAction('post.created', myHandler);
// Remove
sdk.hooks.removeAction('post.created', myHandler);sdk.hooks.removeFilter(name, callback)
function myFilter(content) {
return content.toUpperCase();
}
sdk.hooks.addFilter('post.content', myFilter);
sdk.hooks.removeFilter('post.content', myFilter);Async Hooks
Both actions and filters support async callbacks:
// Async action
sdk.hooks.addAction('post.created', async (post) => {
await sendEmailNotification(post);
await updateSearchIndex(post);
});
// Async filter
sdk.hooks.addFilter('post.content', async (content, post) => {
const relatedPosts = await sdk.content.getPosts({
filter: { category: post.category },
limit: 3
});
const html = renderRelatedPosts(relatedPosts);
return content + html;
});Priority Order
Lower priority numbers run first:
// Runs first (priority 5)
sdk.hooks.addFilter('post.content', (content) => {
return content + '<p>Added first</p>';
}, 5);
// Runs second (default priority 10)
sdk.hooks.addFilter('post.content', (content) => {
return content + '<p>Added second</p>';
});
// Runs third (priority 20)
sdk.hooks.addFilter('post.content', (content) => {
return content + '<p>Added third</p>';
}, 20);Practical Examples
Auto-generate Excerpts
sdk.hooks.addFilter('post.excerpt', (excerpt, post) => {
if (excerpt) return excerpt;
// Generate from content
const text = post.content
.replace(/<[^>]*>/g, '')
.trim();
return text.substring(0, 160) + '...';
});Add Social Meta Tags
sdk.hooks.addAction('theme_head', async (context) => {
if (!context.post) return '';
const post = context.post;
return `
<meta property="og:title" content="${post.title}">
<meta property="og:description" content="${post.excerpt || ''}">
<meta property="og:image" content="${post.featuredImage || ''}">
<meta property="og:type" content="article">
<meta name="twitter:card" content="summary_large_image">
`;
});Content Transformations
// Auto-embed YouTube links
sdk.hooks.addFilter('post.content', (content) => {
const youtubeRegex = /https?:\/\/(?:www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)/g;
return content.replace(youtubeRegex, (match, videoId) => {
return `<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}" frameborder="0" allowfullscreen></iframe>`;
});
});
// Lazy load images
sdk.hooks.addFilter('post.content', (content) => {
return content.replace(
/<img([^>]*)src="([^"]+)"([^>]*)>/g,
'<img$1src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-src="$2"$3 loading="lazy">'
);
});Audit Logging
const logActivity = async (action, data) => {
const logs = (await sdk.storage.get('activity_logs')) || [];
logs.unshift({
action,
data,
timestamp: new Date().toISOString()
});
// Keep last 1000 entries
await sdk.storage.set('activity_logs', logs.slice(0, 1000));
};
sdk.hooks.addAction('post.created', (post) => {
logActivity('post.created', { id: post.id, title: post.title });
});
sdk.hooks.addAction('post.deleted', (postId) => {
logActivity('post.deleted', { id: postId });
});
sdk.hooks.addAction('user.login', (user) => {
logActivity('user.login', { id: user.id, email: user.email });
});