Projects About me Blog FAQ Contact me

JSON as a Headless CMS: Building My Content Pipeline,...

How I use JSON files, Git, and a simple PHP admin panel as a free alternative to Contentful/Strapi.

April 15, 2026 · BLOG
JSON as a Headless CMS: Building My Content Pipeline,...

JSON as a Headless CMS

I pay $0/month for my CMS. No WordPress, no Contentful, no Strapi server. Just JSON files and Git.

The Setup

/data
  ├── posts.json      # Blog content
  ├── projects.json   # Portfolio items
  └── settings.json   # Site config

The Schema

{
  "posts": [
    {
      "id": 7,
      "slug": "my-new-post",
      "title": "My New Post",
      "excerpt": "Short description...",
      "content": "# Markdown here...",
      "date": "2026-04-15",
      "tags": ["php", "css"],
      "image": "assets/img/blog-7.svg",
      "published": true,
      "views": 0
    }
  ]
}

The Admin Panel (Pure PHP)

<?php // admin/edit-post.php ?>
<?php
session_start();
require_once '../core/JsonStore.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $store = new JsonStore('posts.json');
    $posts = $store->all();
    
    $id = (int)$_POST['id'];
    $key = array_search($id, array_column($posts, 'id'));
    
    // Update fields
    $posts[$key]['title'] = $_POST['title'];
    $posts[$key]['content'] = $_POST['content'];
    $posts[$key]['published'] = isset($_POST['published']);
    $posts[$key]['date'] = $_POST['date'];
    
    // Auto-generate excerpt if empty
    if (empty($_POST['excerpt'])) {
        $posts[$key]['excerpt'] = substr(strip_tags($_POST['content']), 0, 150) . '...';
    }
    
    if ($store->save($posts)) {
        // Git commit automatically
        exec('cd ' . __DIR__ . '/.. && git add data/posts.json && git commit -m "Update: ' . escapeshellarg($_POST['title']) . '"');
        
        header('Location: /admin?success=1');
        exit;
    }
}
?>

<!DOCTYPE html>
<html>
<head>
    <title>Edit Post</title>
    <link rel="stylesheet" href="/css/admin.css">
</head>
<body>
    <form method="POST" class="editor">
        <input type="hidden" name="id" value="<?= $post['id'] ?>">
        
        <input type="text" name="title" value="<?= htmlspecialchars($post['title']) ?>" 
               class="editor__title" placeholder="Post title...">
        
        <textarea name="content" class="editor__content" rows="20"><?= htmlspecialchars($post['content']) ?></textarea>
        
        <div class="editor__meta">
            <input type="date" name="date" value="<?= $post['date'] ?>">
            <label>
                <input type="checkbox" name="published" <?= $post['published'] ? 'checked' : '' ?>>
                Published
            </label>
        </div>
        
        <button type="submit" class="btn btn--primary">Save Post</button>
    </form>
    
    <!-- Live preview with JS -->
    <div id="preview" class="preview"></div>
    
    <script>
        // Simple markdown preview
        const textarea = document.querySelector('.editor__content');
        const preview = document.querySelector('#preview');
        
        textarea.addEventListener('input', () => {
            preview.innerHTML = simpleMarkdown(textarea.value);
        });
        
        function simpleMarkdown(text) {
            return text
                .replace(/^# (.*$)/gim, '<h1>$1</h1>')
                .replace(/^## (.*$)/gim, '<h2>$1</h2>')
                .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
                .replace(/\n/g, '<br>');
        }
    </script>
</body>
</html>

The Frontend Rendering

<?php // post.php ?>
<?php
$store = new JsonStore('posts.json');
$post = $store->findBySlug($_GET['slug']);

if (!$post || !$post['published']) {
    http_response_code(404);
    include '404.php';
    exit;
}

// Parse markdown (simple version)
$content = $post['content'];
$content = preg_replace('/^# (.*$)/m', '<h1>$1</h1>', $content);
$content = preg_replace('/^## (.*$)/m', '<h2>$1</h2>', $content);
$content = preg_replace('/\*\*(.*?)\*\*/s', '<strong>$1</strong>', $content);
$content = nl2br($content);
?>

<article class="post">
    <h1><?= htmlspecialchars($post['title']) ?></h1>
    <div class="post__meta">
        <time><?= date('F j, Y', strtotime($post['date'])) ?></time>
        <span><?= $post['views'] ?> views</span>
    </div>
    <div class="post__content">
        <?= $content ?>
    </div>
</article>

The Git Workflow

1. Edit in admin panel → saves to JSON

2. PHP executes git commit automatically

3. Push to GitHub (backup + history)

4. Deploy via git pull on server

Free backups forever.

When to Upgrade

Move to a real database when you need:

  • Multi-user editing (conflict resolution)
  • Full-text search (use SQLite FTS or Meilisearch)
  • Complex relationships (use PostgreSQL)

Until then? JSON is perfect.

Your CMS doesn't need to be a server. Sometimes it's just a well-structured file.

More from the blog