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.
The Designer Mode CMS is built on a serverless architecture that leverages GitHub's API as the backend. Here's the high-level architecture:
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;
}
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();
}
}
OAuth 2.0 provides secure authentication without storing user credentials. The flow:
// 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;
}
After three months of production use, here are the actual performance metrics:
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';
}
};
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);
});
}
};
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;
}
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);
});
}
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() });
}
};
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.