Plugin Development
Widgets

Widgets

Create reusable UI blocks for sidebars and widget areas.

Registering Widgets

sdk.widgets.registerWidget('my-widget', {
  name: 'My Widget',
  description: 'Description of what this widget does',
  icon: '📦',
  
  settings: [
    { key: 'title', type: 'text', label: 'Title', default: 'My Widget' },
    { key: 'count', type: 'number', label: 'Number of items', default: 5 }
  ],
  
  render: async (settings, context) => {
    return `
      <div class="widget-my-widget">
        <h3>${settings.title}</h3>
        <p>Showing ${settings.count} items</p>
      </div>
    `;
  }
});

Widget Configuration

Basic Properties

PropertyTypeRequiredDescription
namestringYesDisplay name
descriptionstringNoBrief description
iconstringNoEmoji or icon class
settingsarrayNoConfigurable options
renderfunctionYesReturns HTML

Settings Types

settings: [
  // Text input
  {
    key: 'title',
    type: 'text',
    label: 'Widget Title',
    placeholder: 'Enter title...',
    default: 'My Widget'
  },
  
  // Number with range
  {
    key: 'count',
    type: 'number',
    label: 'Number of items',
    min: 1,
    max: 20,
    default: 5
  },
  
  // Toggle
  {
    key: 'showImages',
    type: 'boolean',
    label: 'Show thumbnails',
    default: true
  },
  
  // Dropdown
  {
    key: 'layout',
    type: 'select',
    label: 'Layout',
    options: [
      { value: 'list', label: 'List' },
      { value: 'grid', label: 'Grid' }
    ],
    default: 'list'
  },
  
  // Multi-select
  {
    key: 'categories',
    type: 'multiselect',
    label: 'Categories',
    options: [
      { value: 'news', label: 'News' },
      { value: 'tech', label: 'Technology' },
      { value: 'lifestyle', label: 'Lifestyle' }
    ]
  },
  
  // Image upload
  {
    key: 'backgroundImage',
    type: 'image',
    label: 'Background Image'
  },
  
  // Color picker
  {
    key: 'accentColor',
    type: 'color',
    label: 'Accent Color',
    default: '#007bff'
  },
  
  // Multi-line text
  {
    key: 'customHtml',
    type: 'textarea',
    label: 'Custom HTML',
    rows: 5
  },
  
  // Category selector
  {
    key: 'category',
    type: 'category',
    label: 'Select Category'
  }
]

Render Function

The render function receives settings and context:

render: async (settings, context) => {
  // settings - configured values
  // context.post - current post (on post pages)
  // context.category - current category (on category pages)
  // context.user - logged in user
  // context.request - request object
  
  return '<div>Widget HTML</div>';
}

Widget Examples

Recent Posts Widget

sdk.widgets.registerWidget('recent-posts', {
  name: 'Recent Posts',
  description: 'Display latest posts',
  icon: '📰',
  
  settings: [
    { key: 'title', type: 'text', label: 'Title', default: 'Recent Posts' },
    { key: 'count', type: 'number', label: 'Number of posts', min: 1, max: 20, default: 5 },
    { key: 'showThumbnail', type: 'boolean', label: 'Show thumbnails', default: true },
    { key: 'showDate', type: 'boolean', label: 'Show date', default: true }
  ],
  
  render: async (settings) => {
    const posts = await sdk.content.getLatestPosts(settings.count);
    
    let html = `
      <div class="widget-recent-posts">
        <h3 class="widget-title">${settings.title}</h3>
        <ul class="recent-posts-list">
    `;
    
    posts.forEach(post => {
      html += `
        <li class="recent-post-item">
          ${settings.showThumbnail && post.featuredImage 
            ? `<img src="${post.featuredImage}" alt="${post.title}" class="recent-post-thumb">`
            : ''
          }
          <div class="recent-post-info">
            <a href="${post.url}" class="recent-post-title">${post.title}</a>
            ${settings.showDate 
              ? `<time class="recent-post-date">${sdk.utils.formatDate(post.createdAt)}</time>`
              : ''
            }
          </div>
        </li>
      `;
    });
    
    html += '</ul></div>';
    return html;
  }
});

Categories Widget

sdk.widgets.registerWidget('categories', {
  name: 'Categories',
  description: 'List all categories',
  icon: '📁',
  
  settings: [
    { key: 'title', type: 'text', label: 'Title', default: 'Categories' },
    { key: 'showCount', type: 'boolean', label: 'Show post count', default: true },
    { key: 'orderBy', type: 'select', label: 'Order by', options: [
      { value: 'name', label: 'Name' },
      { value: 'count', label: 'Post count' }
    ], default: 'name' }
  ],
  
  render: async (settings) => {
    const categories = await sdk.content.getCategories();
    
    if (settings.orderBy === 'count') {
      categories.sort((a, b) => b.postCount - a.postCount);
    } else {
      categories.sort((a, b) => a.name.localeCompare(b.name));
    }
    
    let html = `
      <div class="widget-categories">
        <h3 class="widget-title">${settings.title}</h3>
        <ul class="categories-list">
    `;
    
    categories.forEach(cat => {
      html += `
        <li>
          <a href="${cat.url}">${cat.name}</a>
          ${settings.showCount ? `<span class="count">(${cat.postCount})</span>` : ''}
        </li>
      `;
    });
    
    html += '</ul></div>';
    return html;
  }
});

About Widget

sdk.widgets.registerWidget('about', {
  name: 'About',
  description: 'Display about section',
  icon: '👤',
  
  settings: [
    { key: 'title', type: 'text', label: 'Title', default: 'About' },
    { key: 'image', type: 'image', label: 'Image' },
    { key: 'content', type: 'textarea', label: 'Content', rows: 4 },
    { key: 'buttonText', type: 'text', label: 'Button Text' },
    { key: 'buttonUrl', type: 'text', label: 'Button URL' }
  ],
  
  render: async (settings) => {
    return `
      <div class="widget-about">
        <h3 class="widget-title">${settings.title}</h3>
        ${settings.image ? `<img src="${settings.image}" alt="${settings.title}" class="about-image">` : ''}
        <p class="about-content">${settings.content || ''}</p>
        ${settings.buttonText && settings.buttonUrl 
          ? `<a href="${settings.buttonUrl}" class="about-button">${settings.buttonText}</a>`
          : ''
        }
      </div>
    `;
  }
});

Social Links Widget

sdk.widgets.registerWidget('social-links', {
  name: 'Social Links',
  description: 'Display social media links',
  icon: '🔗',
  
  settings: [
    { key: 'title', type: 'text', label: 'Title', default: 'Follow Us' },
    { key: 'facebook', type: 'text', label: 'Facebook URL' },
    { key: 'twitter', type: 'text', label: 'Twitter URL' },
    { key: 'instagram', type: 'text', label: 'Instagram URL' },
    { key: 'youtube', type: 'text', label: 'YouTube URL' },
    { key: 'linkedin', type: 'text', label: 'LinkedIn URL' },
    { key: 'style', type: 'select', label: 'Style', options: [
      { value: 'icons', label: 'Icons only' },
      { value: 'text', label: 'Text only' },
      { value: 'both', label: 'Icons & Text' }
    ], default: 'icons' }
  ],
  
  render: async (settings) => {
    const networks = [
      { key: 'facebook', icon: '📘', name: 'Facebook' },
      { key: 'twitter', icon: '🐦', name: 'Twitter' },
      { key: 'instagram', icon: '📷', name: 'Instagram' },
      { key: 'youtube', icon: '📺', name: 'YouTube' },
      { key: 'linkedin', icon: '💼', name: 'LinkedIn' }
    ];
    
    let html = `
      <div class="widget-social">
        <h3 class="widget-title">${settings.title}</h3>
        <ul class="social-links style-${settings.style}">
    `;
    
    networks.forEach(network => {
      if (settings[network.key]) {
        html += `
          <li>
            <a href="${settings[network.key]}" target="_blank" rel="noopener">
              ${settings.style !== 'text' ? `<span class="icon">${network.icon}</span>` : ''}
              ${settings.style !== 'icons' ? `<span class="name">${network.name}</span>` : ''}
            </a>
          </li>
        `;
      }
    });
    
    html += '</ul></div>';
    return html;
  }
});

Newsletter Widget

sdk.widgets.registerWidget('newsletter', {
  name: 'Newsletter',
  description: 'Email subscription form',
  icon: '📧',
  
  settings: [
    { key: 'title', type: 'text', label: 'Title', default: 'Newsletter' },
    { key: 'description', type: 'textarea', label: 'Description', default: 'Subscribe for updates' },
    { key: 'buttonText', type: 'text', label: 'Button Text', default: 'Subscribe' },
    { key: 'buttonColor', type: 'color', label: 'Button Color', default: '#007bff' }
  ],
  
  render: async (settings) => {
    return `
      <div class="widget-newsletter">
        <h3 class="widget-title">${settings.title}</h3>
        <p class="newsletter-desc">${settings.description}</p>
        <form action="/api/newsletter/subscribe" method="POST" class="newsletter-form">
          <input type="email" name="email" placeholder="Your email" required>
          <button type="submit" style="background-color: ${settings.buttonColor}">
            ${settings.buttonText}
          </button>
        </form>
      </div>
    `;
  }
});

Custom HTML Widget

sdk.widgets.registerWidget('custom-html', {
  name: 'Custom HTML',
  description: 'Add custom HTML content',
  icon: '🔧',
  
  settings: [
    { key: 'title', type: 'text', label: 'Title (optional)' },
    { key: 'content', type: 'textarea', label: 'HTML Content', rows: 10 }
  ],
  
  render: async (settings) => {
    return `
      <div class="widget-custom-html">
        ${settings.title ? `<h3 class="widget-title">${settings.title}</h3>` : ''}
        <div class="custom-content">${settings.content || ''}</div>
      </div>
    `;
  }
});

Using Widgets in Themes

Render widget areas in your templates:

<aside class="sidebar">
  {{{ sdk.ui.widgetArea('sidebar') }}}
</aside>
 
<footer>
  <div class="footer-widgets">
    <div class="footer-col">{{{ sdk.ui.widgetArea('footer-1') }}}</div>
    <div class="footer-col">{{{ sdk.ui.widgetArea('footer-2') }}}</div>
    <div class="footer-col">{{{ sdk.ui.widgetArea('footer-3') }}}</div>
  </div>
</footer>

Unregistering Widgets

sdk.widgets.unregisterWidget('my-widget');