← Back to LOONIX

Building the Designer Mode CMS

A Technical Deep-Dive into Zero-Infrastructure Content Management

The Problem: Traditional CMS platforms are bloated, expensive, and over-engineered for simple websites. They require databases, server-side rendering, authentication systems, and ongoing maintenance. For a personal portfolio or blog, this is overkill.

The Solution: A designer-mode CMS that runs entirely in the browser, uses GitHub as a database, requires zero infrastructure, and costs nothing to operate. This article explores how I built it, the architectural decisions, and the lessons learned along the way.

Architecture Overview

The Designer Mode CMS is built on a serverless architecture that leverages GitHub's API as the backend. Here's the high-level architecture:

┌─────────────────────────────────────────────────────────────┐ │ BROWSER (Client) │ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │ │ Visual │ │ Markdown │ │ GitHub OAuth │ │ │ │ Editor │→ │ Parser │ │ Authentication│ │ │ └─────────────┘ └──────────────┘ └─────────────────┘ │ └─────────────────────────────┬───────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────────────┐ │ GITHUB API (Backend) │ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │ │ Content │ │ Images │ │ Webhooks │ │ │ │ (Files) │ │ (Assets) │ │ (Optional) │ │ │ └─────────────┘ └──────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────┘

Core Components

1. Visual Editor (WYSIWYG)

The visual editor provides a what-you-see-is-what-you-get interface for content creation. It's built on top of contenteditable and includes:

// Visual Editor Initialization
const editor = document.getElementById('visual-editor');
editor.contentEditable = true;

// Auto-save functionality
let autoSaveTimer;
editor.addEventListener('input', () => {
    clearTimeout(autoSaveTimer);
    autoSaveTimer = setTimeout(saveContent, 2000);
});

// Convert to Markdown
function convertToMarkdown(html) {
    // Custom conversion logic
    let markdown = html
        .replace(/<h1>(.*?)<\/h1>/g, '# $1\n\n')
        .replace(/<strong>(.*?)<\/strong>/g, '**$1**')
        .replace(/<em>(.*?)<\/em>/g, '*$1*');
    return markdown;
}
        

2. GitHub Integration

The CMS uses GitHub's REST API for all backend operations. This includes:

// GitHub API Client
class GitHubClient {
    constructor(token, repo) {
        this.token = token;
        this.repo = repo;
        this.baseUrl = 'https://api.github.com';
    }

    async createFile(path, content, message) {
        const url = `${this.baseUrl}/repos/${this.repo}/contents/${path}`;
        const base64Content = btoa(unescape(encodeURIComponent(content)));

        const response = await fetch(url, {
            method: 'PUT',
            headers: {
                'Authorization': `token ${this.token}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                message: message,
                content: base64Content
            })
        });

        return response.json();
    }

    async updateFile(path, content, message, sha) {
        const url = `${this.baseUrl}/repos/${this.repo}/contents/${path}`;
        const base64Content = btoa(unescape(encodeURIComponent(content)));

        const response = await fetch(url, {
            method: 'PUT',
            headers: {
                'Authorization': `token ${this.token}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                message: message,
                content: base64Content,
                sha: sha
            })
        });

        return response.json();
    }
}
        

3. Authentication System

OAuth 2.0 provides secure authentication without storing user credentials. The flow:

  1. User clicks "Login with GitHub"
  2. Redirect to GitHub authorization page
  3. Grant permissions (repo access)
  4. Receive authorization code
  5. Exchange code for access token
  6. Store token securely (sessionStorage)
// OAuth Configuration
const config = {
    clientId: 'YOUR_CLIENT_ID',
    redirectUri: window.location.origin,
    scope: 'repo'
};

// Initiate OAuth Flow
function login() {
    const authUrl = `https://github.com/login/oauth/authorize?` +
        `client_id=${config.clientId}&` +
        `redirect_uri=${config.redirectUri}&` +
        `scope=${config.scope}`;
    window.location.href = authUrl;
}

// Exchange Code for Token
async function getToken(code) {
    const response = await fetch('/api/auth', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ code })
    });

    const { token } = await response.json();
    sessionStorage.setItem('github_token', token);
    return token;
}
        

Performance Metrics

After three months of production use, here are the actual performance metrics:

98% Lighthouse Score
0.8s Average Load Time
$0 Monthly Cost
100% Uptime

Key Features

Designer Mode Toggle

The signature feature is the designer mode toggle, which switches between:

// Designer Mode Toggle
const designerMode = {
    active: false,

    toggle() {
        this.active = !this.active;
        document.body.classList.toggle('designer-mode', this.active);

        if (this.active) {
            this.enableEditing();
        } else {
            this.disableEditing();
        }
    },

    enableEditing() {
        // Make elements editable
        document.querySelectorAll('[data-editable]').forEach(el => {
            el.setAttribute('contenteditable', 'true');
            el.classList.add('editing');
        });

        // Show admin panel
        document.getElementById('admin-panel').style.display = 'block';
    },

    disableEditing() {
        // Disable editing
        document.querySelectorAll('[data-editable]').forEach(el => {
            el.removeAttribute('contenteditable');
            el.classList.remove('editing');
        });

        // Hide admin panel
        document.getElementById('admin-panel').style.display = 'none';
    }
};
        

Real-Time Preview

Changes are reflected immediately in the preview panel:

// Real-time Preview
const preview = {
    update(content) {
        const markdown = marked.parse(content);
        document.getElementById('preview').innerHTML = markdown;
    },

    init() {
        const editor = document.getElementById('editor');
        editor.addEventListener('input', (e) => {
            this.update(e.target.innerHTML);
        });
    }
};
        

Challenges and Solutions

Challenge 1: Rate Limiting

GitHub API has rate limits (60 requests/hour for unauthenticated, 5000/hour for authenticated).

Solution: Implement aggressive caching and batch requests:

// Cache Implementation
const cache = {
    data: {},
    ttl: 5 * 60 * 1000, // 5 minutes

    get(key) {
        const item = this.data[key];
        if (!item) return null;

        if (Date.now() - item.timestamp > this.ttl) {
            delete this.data[key];
            return null;
        }

        return item.value;
    },

    set(key, value) {
        this.data[key] = {
            value,
            timestamp: Date.now()
        };
    }
};

// Cached GitHub Request
async function cachedRequest(url) {
    const cached = cache.get(url);
    if (cached) return cached;

    const response = await fetch(url);
    const data = await response.json();
    cache.set(url, data);

    return data;
}
        

Challenge 2: File Uploads

Storing images in Git repositories isn't ideal (large files, version control bloat).

Solution: Client-side optimization + GitHub Pages:

// Image Optimization
async function optimizeImage(file) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const img = new Image();

    return new Promise((resolve) => {
        img.onload = () => {
            // Resize if too large
            const maxWidth = 1920;
            const scale = maxWidth / img.width;
            canvas.width = maxWidth;
            canvas.height = img.height * scale;

            ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

            // Convert to WebP
            canvas.toBlob((blob) => {
                resolve(new File([blob], file.name.replace(/\.[^.]+$/, '.webp')));
            }, 'image/webp', 0.8);
        };
        img.src = URL.createObjectURL(file);
    });
}
        

Challenge 3: Offline Editing

Users want to edit content without internet connection.

Solution: Service Worker + IndexedDB:

// Service Worker Registration
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
        .then(reg => console.log('SW registered'))
        .catch(err => console.error('SW registration failed', err));
}

// IndexedDB Storage
const db = {
    async init() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open('cms-db', 1);

            request.onerror = () => reject(request.error);
            request.onsuccess = () => resolve(request.result);

            request.onupgradeneeded = (event) => {
                const db = event.target.result;
                db.createObjectStore('drafts', { keyPath: 'id' });
            };
        });
    },

    async saveDraft(id, content) {
        const db = await this.init();
        const tx = db.transaction('drafts', 'readwrite');
        tx.objectStore('drafts').put({ id, content, timestamp: Date.now() });
    }
};
        

Pros and Cons

✓ Advantages

  • Zero Cost: No hosting fees, free GitHub Pages
  • Zero Maintenance: No server updates or security patches
  • Version Control: Git provides built-in versioning
  • Collaboration: Pull requests for content review
  • Performance: Static site, CDN delivery
  • Security: No database to hack
  • Portability: Own your data, easy migration

✗ Limitations

  • Rate Limits: GitHub API throttling
  • File Size: 100MB limit per file
  • Learning Curve: Requires Git knowledge
  • Dynamic Features: Limited server-side processing
  • Multi-user: Concurrent editing conflicts
  • Build Time: Large sites take longer to generate

Future Improvements

  1. Webhook Integration: Auto-deploy on content changes
  2. Multi-site Support: Manage multiple repos from one UI
  3. Advanced Analytics: Built-in visitor tracking
  4. AI Assistant: Content suggestions and SEO optimization
  5. Collaborative Editing: Real-time multi-user editing

Conclusion

Building a zero-infrastructure CMS is not only possible but practical for many use cases. By leveraging GitHub as a backend, we eliminate the need for traditional servers while providing a powerful, version-controlled content management system.

The Designer Mode CMS proves that less is more. No database, no server, no maintenance—just pure client-side code and GitHub API integration. The result is a faster, cheaper, and simpler alternative to traditional CMS platforms.

Key Takeaway: Don't overengineer solutions. Modern browser APIs and platform services can replace entire backend stacks if you think creatively about architecture.