Building a Git-based CMS Workflow for Laravel
The traditional CMS pattern involves a database, an admin panel, a WYSIWYG editor, and user authentication. It’s heavy.
For a developer blog, the Git-based CMS pattern is superior.
- Drafting: Write locally in VS Code (or Obsidian/Notion).
- Versioning: Git handles history, diffs, and backups.
- Publishing:
git push origin main. - Deployment: Automated webhook pulls changes.
Here is how to set this up for a Laravel application.
1. The Content Repository
You have two choices:
- Monorepo: Keep content inside the Laravel
content/folder (Simpler, what this blog uses). - Content Repo: A separate repo just for Markdown files (Better for separation of concerns).
If you use a separate repo, you can add it as a Git Submodule or clone it during deployment.
2. The Deployment Script
You don't need complex CI/CD for a personal blog. A simple Laravel Envoy script or a bash script works wonders.
Here is a simple robust bash strategy (deploy.sh):
#!/bin/bash
set -e
echo "🚀 Starting deployment..."
# 1. Pull latest code
git pull origin main
# 2. Install dependencies (if composer.lock changed)
composer install --no-dev --optimize-autoloader
# 3. Build assets (if designing)
npm run build
# 4. Clear and Cache Config
php artisan config:cache
php artisan route:cache
php artisan view:cache
# 5. Clear Application Cache (Critical for file-based blogs)
php artisan cache:clear
echo "✅ Deployment finished!"
3. Automating with Webhooks
To make this "Git Push = Publish", you need a webhook listener.
GitHub Actions (Push-Model)
Create a workflow file .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to VPS
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/my-blog
./deploy.sh
Webhook Route (Pull-Model)
Alternatively, create a hidden route in Laravel that GitHub calls.
// routes/api.php
Route::post('/deploy-webhook', function (Request $request) {
$signature = $request->header('X-Hub-Signature-256');
// Verify signature logic here...
// Trigger deployment process (e.g., dispatch a Job)
\App\Jobs\DeploySite::dispatch();
return response()->json(['status' => 'deploying']);
});
Security Note: Always verify the GitHub webhook signature secret!
4. Handling Cache Invalidation
Since our blog uses file-based caching (parsing Markdown is expensive), the cache must be cleared when content updates.
In our simpler setup, php artisan cache:clear in the deployment script is a sledgehammer approach that works fine for small blogs.
For larger sites, you might want to only clear specific keys. You can listen for file changes if using a daemon, but that's over-engineering.
5. The Writing Workflow
My personal workflow looks like this:
- Create a branch
post/new-feature. - Write
content/posts/YYYY-MM-DD-feature.md. - Set
draft: truein frontmatter while working. - Run
php artisan serveto preview locally. - Commit and Push.
- Merge to
mainwhen ready to go live.
Advantages
- Zero Admin Panel: Less code to maintain, smaller attack surface.
- Offline Writing: Write on a plane, commit later.
- Ownership: Your content is in flat files, readable 50 years from now.
- Speed: No database queries to render a post (if cached properly).
This architecture brings the "Developer Experience" to blogging.