Theme Development
Examples

Theme Examples

Complete working theme examples to learn from.

Minimal Blog Theme

A simple, clean blog theme perfect for personal blogs.

File Structure

minimal-blog/
├── theme.json
├── index.js
├── views/
│   ├── layouts/
│   │   └── main.html
│   ├── pages/
│   │   ├── home.html
│   │   ├── post-detail.html
│   │   ├── posts.html
│   │   ├── category-detail.html
│   │   └── error.html
│   └── partials/
│       ├── head.html
│       ├── header.html
│       ├── footer.html
│       └── post-card.html
└── assets/
    ├── css/
    │   └── main.css
    └── js/
        └── main.js

theme.json

{
  "name": "Minimal Blog",
  "version": "1.0.0",
  "description": "A clean, minimal blog theme",
  "author": "Your Name",
  
  "assets": {
    "css": ["css/main.css"],
    "js": ["js/main.js"]
  },
  
  "supports": {
    "customLogo": true,
    "menus": ["primary"],
    "widgets": ["sidebar"],
    "featuredImages": true
  },
  
  "settings": [
    {
      "key": "accentColor",
      "type": "color",
      "label": "Accent Color",
      "default": "#2563eb"
    },
    {
      "key": "showSidebar",
      "type": "boolean",
      "label": "Show Sidebar",
      "default": true
    }
  ]
}

index.js

module.exports = function(sdk) {
  return {
    helpers: {
      formatDate: (date) => {
        return new Date(date).toLocaleDateString('en-US', {
          year: 'numeric',
          month: 'long', 
          day: 'numeric'
        });
      },
      readTime: (content) => {
        const text = content.replace(/<[^>]*>/g, '');
        const words = text.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="main">
    <div class="container">
      {% if (themeSettings.showSidebar) { %}
        <div class="layout-sidebar">
          <div class="content">
            {{{ content }}}
          </div>
          <aside class="sidebar">
            {{{ sdk.ui.widgetArea('sidebar') }}}
          </aside>
        </div>
      {% } else { %}
        {{{ content }}}
      {% } %}
    </div>
  </main>
  
  {{{ include('partials/footer') }}}
  
  <script src="{{ themePath }}/assets/js/main.js"></script>
</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>
<meta name="description" content="{{ metaDescription || settings.siteDescription }}">
 
{% if (canonicalUrl) { %}
<link rel="canonical" href="{{ canonicalUrl }}">
{% } %}
 
<link rel="stylesheet" href="{{ themePath }}/assets/css/main.css">
 
<style>
  :root {
    --color-accent: {{ themeSettings.accentColor || '#2563eb' }};
  }
</style>

views/partials/header.html

<header class="header">
  <div class="container">
    <a href="/" class="logo">
      {% if (settings.logo) { %}
        <img src="{{ settings.logo }}" alt="{{ settings.siteName }}">
      {% } else { %}
        {{ settings.siteName }}
      {% } %}
    </a>
    
    <nav class="nav">
      {{{ sdk.ui.menu('primary') }}}
    </nav>
    
    <button class="mobile-menu-toggle" aria-label="Toggle menu">
      <span></span>
    </button>
  </div>
</header>

views/partials/footer.html

<footer class="footer">
  <div class="container">
    <p>&copy; {{ new Date().getFullYear() }} {{ settings.siteName }}. All rights reserved.</p>
  </div>
</footer>

views/partials/post-card.html

<article class="post-card">
  {% if (post.featuredImage) { %}
    <a href="{{ post.url }}" class="post-card__image">
      <img src="{{ post.featuredImage }}" alt="{{ post.title }}" loading="lazy">
    </a>
  {% } %}
  
  <div class="post-card__content">
    {% if (post.category) { %}
      <a href="{{ post.category.url }}" class="post-card__category">
        {{ post.category.name }}
      </a>
    {% } %}
    
    <h2 class="post-card__title">
      <a href="{{ post.url }}">{{ post.title }}</a>
    </h2>
    
    {% if (post.excerpt) { %}
      <p class="post-card__excerpt">{{ post.excerpt }}</p>
    {% } %}
    
    <div class="post-card__meta">
      <time datetime="{{ post.createdAt }}">
        {{ helpers.formatDate(post.createdAt) }}
      </time>
      <span class="read-time">{{ helpers.readTime(post.content) }} min read</span>
    </div>
  </div>
</article>

views/pages/home.html

<div class="home">
  {% if (featuredPost) { %}
    <section class="featured">
      <article class="featured-post">
        {% if (featuredPost.featuredImage) { %}
          <img src="{{ featuredPost.featuredImage }}" alt="{{ featuredPost.title }}">
        {% } %}
        <div class="featured-post__content">
          <h1><a href="{{ featuredPost.url }}">{{ featuredPost.title }}</a></h1>
          <p>{{ featuredPost.excerpt }}</p>
          <a href="{{ featuredPost.url }}" class="btn">Read More</a>
        </div>
      </article>
    </section>
  {% } %}
  
  <section class="latest-posts">
    <h2>Latest Posts</h2>
    
    {% if (latestPosts && latestPosts.length > 0) { %}
      <div class="posts-grid">
        {% latestPosts.forEach(post => { %}
          {{{ include('partials/post-card', { post }) }}}
        {% }) %}
      </div>
    {% } else { %}
      <p>No posts yet.</p>
    {% } %}
  </section>
</div>

views/pages/post-detail.html

<article class="post">
  <header class="post__header">
    {% if (post.category) { %}
      <a href="{{ post.category.url }}" class="post__category">
        {{ post.category.name }}
      </a>
    {% } %}
    
    <h1 class="post__title">{{ post.title }}</h1>
    
    <div class="post__meta">
      {% if (post.author) { %}
        <span class="post__author">By {{ post.author.name }}</span>
      {% } %}
      <time datetime="{{ post.createdAt }}">
        {{ helpers.formatDate(post.createdAt) }}
      </time>
      <span class="post__read-time">{{ helpers.readTime(post.content) }} min read</span>
    </div>
  </header>
  
  {% if (post.featuredImage) { %}
    <figure class="post__image">
      <img src="{{ post.featuredImage }}" alt="{{ post.title }}">
    </figure>
  {% } %}
  
  <div class="post__content">
    {{{ post.content }}}
  </div>
  
  <footer class="post__footer">
    {{{ sdk.ui.shareButtons(post.url, post.title) }}}
    
    {% if (post.tags && post.tags.length > 0) { %}
      <div class="post__tags">
        {% post.tags.forEach(tag => { %}
          <a href="{{ tag.url }}" class="tag">{{ tag.name }}</a>
        {% }) %}
      </div>
    {% } %}
  </footer>
</article>
 
{% if (post.author) { %}
  <div class="author-box">
    {% if (post.author.avatar) { %}
      <img src="{{ post.author.avatar }}" alt="{{ post.author.name }}" class="author-box__avatar">
    {% } %}
    <div class="author-box__content">
      <h3>{{ post.author.name }}</h3>
      {% if (post.author.bio) { %}
        <p>{{ post.author.bio }}</p>
      {% } %}
    </div>
  </div>
{% } %}
 
{{{ sdk.ui.comments(post.id) }}}

views/pages/posts.html

<div class="archive">
  <h1>All Posts</h1>
  
  {% if (posts && posts.length > 0) { %}
    <div class="posts-grid">
      {% posts.forEach(post => { %}
        {{{ include('partials/post-card', { post }) }}}
      {% }) %}
    </div>
    
    {% if (pagination && pagination.total > 1) { %}
      {{{ sdk.ui.pagination(pagination) }}}
    {% } %}
  {% } else { %}
    <p>No posts found.</p>
  {% } %}
</div>

views/pages/category-detail.html

<div class="archive archive--category">
  <h1>{{ category.name }}</h1>
  
  {% if (category.description) { %}
    <p class="archive__description">{{ category.description }}</p>
  {% } %}
  
  {% if (posts && posts.length > 0) { %}
    <div class="posts-grid">
      {% posts.forEach(post => { %}
        {{{ include('partials/post-card', { post }) }}}
      {% }) %}
    </div>
    
    {% if (pagination && pagination.total > 1) { %}
      {{{ sdk.ui.pagination(pagination) }}}
    {% } %}
  {% } else { %}
    <p>No posts in this category yet.</p>
  {% } %}
</div>

views/pages/error.html

<div class="error-page">
  <h1>{{ error.code || '404' }}</h1>
  <p>{{ error.message || 'Page not found' }}</p>
  <a href="/" class="btn">Back to Home</a>
</div>

assets/css/main.css

/* Reset */
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; }
 
/* Variables */
:root {
  --color-text: #1f2937;
  --color-text-muted: #6b7280;
  --color-bg: #ffffff;
  --color-bg-alt: #f9fafb;
  --color-border: #e5e7eb;
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-serif: Georgia, 'Times New Roman', serif;
}
 
/* Base */
body {
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.6;
  color: var(--color-text);
  background: var(--color-bg);
}
 
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}
 
/* Header */
.header {
  padding: 20px 0;
  border-bottom: 1px solid var(--color-border);
}
 
.header .container {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
 
.logo {
  font-size: 1.5rem;
  font-weight: 700;
  text-decoration: none;
  color: var(--color-text);
}
 
.logo img {
  height: 40px;
}
 
.nav ul {
  display: flex;
  gap: 30px;
  list-style: none;
  margin: 0;
  padding: 0;
}
 
.nav a {
  text-decoration: none;
  color: var(--color-text);
  transition: color 0.2s;
}
 
.nav a:hover {
  color: var(--color-accent);
}
 
/* Layout */
.main {
  padding: 40px 0;
  min-height: 60vh;
}
 
.layout-sidebar {
  display: grid;
  grid-template-columns: 1fr 300px;
  gap: 40px;
}
 
@media (max-width: 768px) {
  .layout-sidebar {
    grid-template-columns: 1fr;
  }
}
 
/* Posts Grid */
.posts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 30px;
}
 
/* Post Card */
.post-card {
  border: 1px solid var(--color-border);
  border-radius: 8px;
  overflow: hidden;
  transition: box-shadow 0.3s;
}
 
.post-card:hover {
  box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
 
.post-card__image img {
  width: 100%;
  height: 200px;
  object-fit: cover;
}
 
.post-card__content {
  padding: 20px;
}
 
.post-card__category {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--color-accent);
  text-decoration: none;
}
 
.post-card__title {
  font-size: 1.25rem;
  margin: 10px 0;
}
 
.post-card__title a {
  text-decoration: none;
  color: var(--color-text);
}
 
.post-card__excerpt {
  color: var(--color-text-muted);
  margin: 0 0 15px;
}
 
.post-card__meta {
  font-size: 0.875rem;
  color: var(--color-text-muted);
  display: flex;
  gap: 15px;
}
 
/* Single Post */
.post__header {
  margin-bottom: 30px;
}
 
.post__category {
  display: inline-block;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--color-accent);
  text-decoration: none;
  margin-bottom: 10px;
}
 
.post__title {
  font-size: 2.5rem;
  line-height: 1.2;
  margin: 0 0 15px;
}
 
.post__meta {
  color: var(--color-text-muted);
  display: flex;
  gap: 20px;
}
 
.post__image {
  margin: 0 0 30px;
}
 
.post__image img {
  width: 100%;
  border-radius: 8px;
}
 
.post__content {
  font-family: var(--font-serif);
  font-size: 1.125rem;
  line-height: 1.8;
}
 
.post__content h2 { font-size: 1.75rem; margin: 40px 0 20px; }
.post__content h3 { font-size: 1.5rem; margin: 30px 0 15px; }
.post__content p { margin: 0 0 20px; }
.post__content img { max-width: 100%; height: auto; border-radius: 8px; }
 
/* Footer */
.footer {
  padding: 40px 0;
  border-top: 1px solid var(--color-border);
  text-align: center;
  color: var(--color-text-muted);
}
 
/* Button */
.btn {
  display: inline-block;
  padding: 10px 20px;
  background: var(--color-accent);
  color: white;
  text-decoration: none;
  border-radius: 5px;
  font-weight: 500;
  transition: opacity 0.2s;
}
 
.btn:hover {
  opacity: 0.9;
}
 
/* Error Page */
.error-page {
  text-align: center;
  padding: 80px 20px;
}
 
.error-page h1 {
  font-size: 6rem;
  margin: 0;
  color: var(--color-accent);
}

This minimal theme provides a solid foundation that you can customize and extend. Copy it, modify the styles, add more features, and make it your own!