Plugin Development
Hooks & Filters

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

ActionParametersDescription
init-CMS initialized
post.createdpostPost created
post.updatedpost, changesPost updated
post.deletedpostIdPost deleted
post.publishedpostPost published
category.createdcategoryCategory created
category.updatedcategoryCategory updated
category.deletedcategoryIdCategory deleted
media.uploadedmediaMedia file uploaded
media.deletedmediaIdMedia file deleted
user.loginuserUser logged in
user.logoutuserIdUser logged out
user.registereduserNew 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

FilterParametersDescription
post.titletitle, postModify post title
post.contentcontent, postModify post content
post.excerptexcerpt, postModify post excerpt
post.urlurl, postModify post URL
posts.queryqueryModify posts query
category.namename, categoryModify category name
category.urlurl, categoryModify category URL
media.urlurl, mediaModify media URL
search.resultsresults, queryModify search results
template.datadata, templateModify template data
seo.titletitle, contextModify SEO title
seo.descriptiondescription, contextModify 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 });
});