Building a No-Database CMS

calculating....


  1. JSON
  2. php
  3. javascript
  4. css

Why I ditched MySQL for a JSON file — and built a lightning-fast CMS that runs on a $2/month hosting. ⚡

The Problem 💡

A client needed a simple blog. Requirements were basic:

  • Easy content updates (no WordPress learning curve)
  • Fast loading & minimal maintenance

Instead of a heavy MySQL setup, I went with JSON as a database. Result? A CMS that loads in under 100ms. 🎯

Project Structure 📁

📁 data/
📄 posts.json <-- our "database" 🗄️
📁 api/
🐘 posts.php <-- PHP endpoints
📁 assets/
🎨 style.css
📁 js/
📜 app.js <-- frontend logic
🌐 index.html <-- single-page app (SPA)

JSON as Database 🗄️

{
    "posts": [
    {
        "id": 1,
        "title": "Why I Switched to CSS Grid",
        "slug": "css-grid-why",
        "excerpt": "Flexbox is great, but Grid solves problems...",
        "content": "<p>For years I only used Flexbox...</p>",
        "tags": ["css", "layout"],
        "date": "2026-05-10",
        "readTime": 5
    }
    ]
}

Why JSON? ✅ Human-readable ✅ Git-friendly backups ✅ No migrations ✅ Works on any hosting

PHP API (No Framework!) 🔧

<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');

$jsonFile = __DIR__ . '/../data/posts.json';
$data = json_decode(file_get_contents($jsonFile), true);

$action = $_GET['action'] ?? 'list';

switch ($action) {
    case 'list':
    $posts = array_map(fn($p) => [
        'id' => $p['id'], 'title' => $p['title'],
        'slug' => $p['slug'], 'excerpt' => $p['excerpt'],
        'date' => $p['date'], 'tags' => $p['tags'],
        'readTime' => $p['readTime']
    ], $data['posts']);

    usort($posts, fn($a, $b) =>
        strtotime($b['date']) - strtotime($a['date']));

    echo json_encode(['posts' => $posts]);
    break;

    case 'single':
    $slug = $_GET['slug'] ?? '';
    $post = array_values(array_filter($data['posts'],
        fn($p) => $p['slug'] === $slug))[0] ?? null;

    echo json_encode(['post' => $post]);
    break;
}

💡 Smart bits: array_map for data projection (no full content in list view), usort for dynamic sorting, simple slug filtering.

Vanilla JS Frontend ⚡

const API_URL = '/blog/api/posts.php';

// Simple router 🗺️
const router = {
    routes: {
    '/': () => renderList(),
    '/post/:slug': (slug) => renderPost(slug)
    },

    navigate(path) {
    history.pushState({}, '', path);
    this.handleRoute();
    },

    handleRoute() {
    const path = location.pathname.replace('/blog', '') || '/';
    if (path.startsWith('/post/')) {
        this.routes['/post/:slug'](path.split('/post/')[1]);
        return;
    }
    (this.routes[path] || this.routes['/'])();
    }
};

// Fetch wrapper 📡
async function fetchJSON(url) {
    try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
    } catch (err) {
    console.error('Fetch error:', err);
    return null;
    }
}

// Render post list 📋
async function renderList() {
    const data = await fetchJSON(`${API_URL}?action=list`);
    const app = document.getElementById('app');

    if (!data?.posts) {
    app.innerHTML = '<p>Error loading posts 😕</p>';
    return;
    }

    app.innerHTML = `<div class="posts-grid">${data.posts.map(p => `
    <article class="post-card">
        <h2><a href="/blog/post/${p.slug}" data-nav>${p.title}</a></h2>
        <div class="meta">📅 ${p.date} · ⏱️ ${p.readTime} min</div>
        <p>${p.excerpt}</p>
        <div>${p.tags.map(t => `<span class="tag">#${t}</span>`).join('')}</div>
    </article>`).join('')}</div>`;

    // Intercept clicks for SPA feel 🖱️
    document.querySelectorAll('[data-nav]').forEach(link =>
    link.addEventListener('click', e => {
        e.preventDefault();
        router.navigate(link.getAttribute('href'));
    })
    );
}

// Render single post 📝
async function renderPost(slug) {
    const data = await fetchJSON(`${API_URL}?action=single&slug=${slug}`);
    const app = document.getElementById('app');

    if (!data?.post) {
    app.innerHTML = '<p>Post not found 🔍</p>';
    return;
    }

    const p = data.post;
    app.innerHTML = `
    <article class="single-post">
        <a href="/blog/" data-nav>← Back</a>
        <h1>${p.title}</h1>
        <div class="meta">📅 ${p.date} · ⏱️ ${p.readTime} min</div>
        <div>${p.tags.map(t => `<span class="tag">#${t}</span>`).join('')}</div>
        <div class="content">${p.content}</div>
    </article>`;

    // Re-attach click handlers
    document.querySelectorAll('[data-nav]').forEach(link =>
    link.addEventListener('click', e => {
        e.preventDefault();
        router.navigate(link.getAttribute('href'));
    })
    );
}

// Init 🚀
window.addEventListener('popstate', () => router.handleRoute());
document.addEventListener('DOMContentLoaded', () => router.handleRoute());

💡 Why vanilla JS? No 200KB framework. Custom router gives SPA feel. Progressive enhancement ready.

CSS — Clean & Light 🎨

/* Light theme, dev-friendly 🌤️ */
:root {
    --bg: #f1f5f7;
    --surface: #ffffff;
    --text: #233742;
    --accent: #76a5af;
    --border: #b4cddd;
}

.posts-grid {
    display: grid;
    gap: 1.5rem;
}

.post-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 1.5rem;
    transition: transform 0.2s;
}

.post-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.tag {
    background: var(--bg);
    color: var(--accent);
    padding: 0.25rem 0.75rem;
    border-radius: 999px;
    font-size: 0.75rem;
}

✅ Pros vs ❌ Cons 🎨

✅ Advantages

  • ⚡ No DB queries = <100ms response
  • 🛡️ No SQL injection possible
  • 💾 Git commit = DB backup
  • 💰 Runs on any $2 hosting
  • 🧠 You understand every part

❌ Limitations

  • 👥 No multi-author support
  • 📊 1000+ posts = slow JSON
  • 🔗 No complex relations
  • 🔒 No built-in auth
  • 📝 Manual content editing

When to Use / Avoid 🎯

Use it for: ✅ Personal blogs ✅ Portfolio sites ✅ Small client projects ✅ Prototyping ✅ Learning

Avoid for: ❌ E-commerce ❌ User-generated content ❌ Multi-user platforms ❌ Complex data relationships

Takeaway 🏆

"The right" tech isn't always the "best" tech. For small projects, simplicity wins. PHP + JSON + vanilla JS = powerful enough for 90% of use cases I encounter. 🚀

🎮 Your Challenge

Try adding these features:

  • 🔍 Client-side search (posts.filter())
  • 📄 Pagination (array_slice() in PHP)
  • ✏️ Admin panel (file_put_contents())
  • 🌙 Light/dark theme toggle

What do you think? 💬

Would you use JSON as a database? Or is this too "hacky"? 🤔

Drop your thoughts below! 👇

📌 Follow me for more no-BS web dev tips!

Dervic is a web developer who believes in keeping things simple. When not coding, he's probably optimizing something that doesn't need optimizing.

Built with HTML, CSS, JS & a lot of ☕. No frameworks were harmed in the making of this blog.

If you liked this post, please LIKE or COMMENT below! 💬✨


Comments (0)

Thank you!