Plugin Development
Examples

Examples

Complete working examples of themes and plugins.

Theme Examples

Minimal Blog Theme

A simple, clean blog theme:

minimal-theme/
├── theme.json
├── index.js
├── views/
│   ├── layouts/main.html
│   ├── pages/
│   │   ├── home.html
│   │   ├── post-detail.html
│   │   ├── posts.html
│   │   └── error.html
│   └── partials/
│       ├── head.html
│       ├── header.html
│       ├── footer.html
│       └── post-card.html
└── assets/
    └── css/main.css
theme.json
{
  "name": "Minimal Theme",
  "version": "1.0.0",
  "description": "A clean, minimal blog theme",
  "author": "Your Name",
  "assets": {
    "css": ["css/main.css"]
  },
  "supports": {
    "customLogo": true,
    "menus": ["primary"]
  }
}
index.js
module.exports = function(sdk) {
  return {
    helpers: {
      readTime: (content) => {
        const words = content.replace(/<[^>]*>/g, '').split(/\s+/).length;
        return Math.ceil(words / 200);
      }
    }
  };
};
views/layouts/main.html
<!DOCTYPE html>
<html lang="{{ settings.language || 'en' }}">
<head>
  {{{ include('partials/head') }}}
</head>
<body>
  {{{ include('partials/header') }}}
  
  <main class="container">
    {{{ content }}}
  </main>
  
  {{{ include('partials/footer') }}}
</body>
</html>
views/partials/head.html
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ pageTitle || settings.siteName }}</title>
<link rel="stylesheet" href="/themes/minimal-theme/assets/css/main.css">
views/partials/header.html
<header class="header">
  <div class="container">
    <a href="/" class="logo">{{ settings.siteName }}</a>
    <nav>
      {{{ sdk.ui.menu('primary') }}}
    </nav>
  </div>
</header>
views/pages/home.html
<div class="home">
  <h1>Welcome to {{ settings.siteName }}</h1>
  
  {% if (latestPosts && latestPosts.length > 0) { %}
    <section class="posts">
      <h2>Latest Posts</h2>
      <div class="posts-grid">
        {% latestPosts.forEach(post => { %}
          {{{ include('partials/post-card', { post }) }}}
        {% }) %}
      </div>
    </section>
  {% } %}
</div>
views/partials/post-card.html
<article class="post-card">
  {% if (post.featuredImage) { %}
    <img src="{{ post.featuredImage }}" alt="{{ post.title }}" loading="lazy">
  {% } %}
  <div class="post-card__body">
    <h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
    <p>{{ post.excerpt }}</p>
    <time>{{ sdk.utils.formatDate(post.createdAt) }}</time>
  </div>
</article>
assets/css/main.css
* { box-sizing: border-box; margin: 0; padding: 0; }
 
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  line-height: 1.6;
  color: #333;
}
 
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}
 
.header {
  padding: 20px 0;
  border-bottom: 1px solid #eee;
}
 
.header .container {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
 
.logo {
  font-size: 1.5rem;
  font-weight: bold;
  text-decoration: none;
  color: inherit;
}
 
.posts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 30px;
  margin-top: 30px;
}
 
.post-card {
  border: 1px solid #eee;
  border-radius: 8px;
  overflow: hidden;
}
 
.post-card img {
  width: 100%;
  height: 200px;
  object-fit: cover;
}
 
.post-card__body {
  padding: 20px;
}
 
.post-card h3 {
  margin-bottom: 10px;
}
 
.post-card h3 a {
  text-decoration: none;
  color: inherit;
}
 
.post-card time {
  color: #666;
  font-size: 0.9rem;
}

Plugin Examples

Social Share Plugin

Add social sharing buttons to posts:

plugin.json
{
  "name": "Social Share",
  "version": "1.0.0",
  "description": "Add social sharing buttons to posts",
  "author": "Your Name",
  "main": "index.js",
  "permissions": ["hooks", "shortcodes"],
  "settings": [
    {
      "key": "platforms",
      "type": "multiselect",
      "label": "Platforms",
      "options": ["facebook", "twitter", "pinterest", "linkedin", "email"],
      "default": ["facebook", "twitter", "pinterest"]
    },
    {
      "key": "position",
      "type": "select",
      "label": "Position",
      "options": [
        { "value": "before", "label": "Before content" },
        { "value": "after", "label": "After content" },
        { "value": "both", "label": "Both" }
      ],
      "default": "after"
    }
  ]
}
index.js
module.exports = async function(sdk) {
  
  // Get plugin settings
  const settings = await sdk.storage.get('settings') || {
    platforms: ['facebook', 'twitter', 'pinterest'],
    position: 'after'
  };
  
  // Generate share buttons HTML
  function getShareButtons(url, title) {
    const encodedUrl = encodeURIComponent(url);
    const encodedTitle = encodeURIComponent(title);
    
    const buttons = {
      facebook: `<a href="https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}" target="_blank" rel="noopener" class="share-btn share-btn--facebook">Facebook</a>`,
      twitter: `<a href="https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}" target="_blank" rel="noopener" class="share-btn share-btn--twitter">Twitter</a>`,
      pinterest: `<a href="https://pinterest.com/pin/create/button/?url=${encodedUrl}&description=${encodedTitle}" target="_blank" rel="noopener" class="share-btn share-btn--pinterest">Pinterest</a>`,
      linkedin: `<a href="https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}" target="_blank" rel="noopener" class="share-btn share-btn--linkedin">LinkedIn</a>`,
      email: `<a href="mailto:?subject=${encodedTitle}&body=${encodedUrl}" class="share-btn share-btn--email">Email</a>`
    };
    
    let html = '<div class="share-buttons">';
    html += '<span class="share-label">Share:</span>';
    settings.platforms.forEach(platform => {
      if (buttons[platform]) {
        html += buttons[platform];
      }
    });
    html += '</div>';
    
    return html;
  }
  
  // Register shortcode
  sdk.shortcodes.register('share', (attrs) => {
    const url = attrs.url || '';
    const title = attrs.title || '';
    return getShareButtons(url, title);
  });
  
  // Add to post content via filter
  sdk.hooks.addFilter('post.content', (content, post) => {
    if (!post || !post.url) return content;
    
    const buttons = getShareButtons(post.url, post.title);
    
    if (settings.position === 'before') {
      return buttons + content;
    } else if (settings.position === 'after') {
      return content + buttons;
    } else {
      return buttons + content + buttons;
    }
  }, 100);
  
  // Add styles
  sdk.hooks.addAction('theme_head', () => {
    return `
      <style>
        .share-buttons {
          display: flex;
          gap: 10px;
          align-items: center;
          margin: 20px 0;
          padding: 15px;
          background: #f5f5f5;
          border-radius: 8px;
        }
        .share-label {
          font-weight: bold;
        }
        .share-btn {
          padding: 8px 16px;
          border-radius: 4px;
          text-decoration: none;
          color: white;
          font-size: 14px;
        }
        .share-btn--facebook { background: #1877f2; }
        .share-btn--twitter { background: #1da1f2; }
        .share-btn--pinterest { background: #e60023; }
        .share-btn--linkedin { background: #0077b5; }
        .share-btn--email { background: #666; }
      </style>
    `;
  });
  
  return {
    activate: async () => {
      console.log('Social Share plugin activated');
    },
    deactivate: async () => {
      console.log('Social Share plugin deactivated');
    }
  };
};

Related Posts Widget

Display related posts in sidebar:

plugin.json
{
  "name": "Related Posts Widget",
  "version": "1.0.0",
  "description": "Show related posts in widget areas",
  "author": "Your Name",
  "main": "index.js",
  "permissions": ["content:read", "widgets"]
}
index.js
module.exports = async function(sdk) {
  
  sdk.widgets.registerWidget('related-posts', {
    name: 'Related Posts',
    description: 'Display posts from the same category',
    icon: '📝',
    
    settings: [
      { 
        key: 'title', 
        type: 'text', 
        label: 'Widget Title', 
        default: 'Related Posts' 
      },
      { 
        key: 'count', 
        type: 'number', 
        label: 'Number of posts', 
        default: 5 
      },
      { 
        key: 'showImage', 
        type: 'boolean', 
        label: 'Show thumbnails', 
        default: true 
      }
    ],
    
    render: async (settings, context) => {
      // Get current post's category if on a post page
      let categorySlug = null;
      if (context && context.post && context.post.category) {
        categorySlug = context.post.category.slug;
      }
      
      // Fetch posts
      let posts;
      if (categorySlug) {
        const result = await sdk.content.getPosts({
          filter: { category: categorySlug },
          limit: settings.count + 1 // Get extra in case current post is included
        });
        // Filter out current post
        posts = result.items.filter(p => 
          !context.post || p.id !== context.post.id
        ).slice(0, settings.count);
      } else {
        posts = await sdk.content.getLatestPosts(settings.count);
      }
      
      if (posts.length === 0) {
        return '';
      }
      
      let html = `
        <div class="widget-related-posts">
          <h3 class="widget-title">${settings.title}</h3>
          <ul class="related-posts-list">
      `;
      
      posts.forEach(post => {
        html += `
          <li class="related-post">
            ${settings.showImage && post.featuredImage ? 
              `<img src="${post.featuredImage}" alt="${post.title}" class="related-post__image">` 
              : ''
            }
            <a href="${post.url}" class="related-post__link">${post.title}</a>
          </li>
        `;
      });
      
      html += `
          </ul>
        </div>
        <style>
          .related-posts-list {
            list-style: none;
            padding: 0;
            margin: 0;
          }
          .related-post {
            display: flex;
            gap: 10px;
            margin-bottom: 15px;
            align-items: center;
          }
          .related-post__image {
            width: 60px;
            height: 60px;
            object-fit: cover;
            border-radius: 4px;
          }
          .related-post__link {
            font-size: 14px;
          }
        </style>
      `;
      
      return html;
    }
  });
  
  return {
    activate: async () => {},
    deactivate: async () => {}
  };
};

Table of Contents Plugin

Auto-generate table of contents for posts:

plugin.json
{
  "name": "Table of Contents",
  "version": "1.0.0",
  "description": "Auto-generate table of contents from headings",
  "author": "Your Name",
  "main": "index.js",
  "permissions": ["hooks", "shortcodes"]
}
index.js
module.exports = async function(sdk) {
  
  function generateTOC(content) {
    const headingRegex = /<h([2-4])[^>]*>(.*?)<\/h\1>/gi;
    const headings = [];
    let match;
    
    while ((match = headingRegex.exec(content)) !== null) {
      const level = parseInt(match[1]);
      const text = match[2].replace(/<[^>]*>/g, ''); // Strip HTML
      const id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-');
      headings.push({ level, text, id });
    }
    
    if (headings.length === 0) {
      return { toc: '', content };
    }
    
    // Generate TOC HTML
    let toc = '<nav class="table-of-contents"><h4>Table of Contents</h4><ul>';
    headings.forEach(h => {
      const indent = (h.level - 2) * 20;
      toc += `<li style="margin-left: ${indent}px"><a href="#${h.id}">${h.text}</a></li>`;
    });
    toc += '</ul></nav>';
    
    // Add IDs to headings in content
    let modifiedContent = content;
    headings.forEach(h => {
      const regex = new RegExp(`(<h${h.level}[^>]*)>`, 'i');
      modifiedContent = modifiedContent.replace(regex, `$1 id="${h.id}">`);
    });
    
    return { toc, content: modifiedContent };
  }
  
  // Shortcode to insert TOC
  sdk.shortcodes.register('toc', () => {
    return '<!-- TOC_PLACEHOLDER -->';
  });
  
  // Filter to process content
  sdk.hooks.addFilter('post.content', (content) => {
    const { toc, content: modifiedContent } = generateTOC(content);
    
    // Replace placeholder with TOC
    if (modifiedContent.includes('<!-- TOC_PLACEHOLDER -->')) {
      return modifiedContent.replace('<!-- TOC_PLACEHOLDER -->', toc);
    }
    
    return modifiedContent;
  }, 5);
  
  // Add styles
  sdk.hooks.addAction('theme_head', () => {
    return `
      <style>
        .table-of-contents {
          background: #f9f9f9;
          border: 1px solid #eee;
          border-radius: 8px;
          padding: 20px;
          margin: 20px 0;
        }
        .table-of-contents h4 {
          margin: 0 0 15px 0;
        }
        .table-of-contents ul {
          list-style: none;
          padding: 0;
          margin: 0;
        }
        .table-of-contents li {
          margin: 8px 0;
        }
        .table-of-contents a {
          color: #333;
          text-decoration: none;
        }
        .table-of-contents a:hover {
          color: #0066cc;
        }
      </style>
    `;
  });
  
  return {
    activate: async () => {},
    deactivate: async () => {}
  };
};

Use in content with [toc] shortcode to place the table of contents.