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.csstheme.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.