Open Stack Blog Workflow
Non Technical preamble
Problem statement
This project aims to create a new content management and editing tool to solve a persistent problem with the fragmentation of my work. My digital resources are currently scattered across WordPress, GitHub Pages, and a legacy caseyclan.ie website. Additionally, the assets required for these projects are distributed across numerous folders on multiple computers. The effort required to pull these disparate pieces together is substantial. Because of this high logistical overhead, I am frequently failing to record my activity and ultimately losing work.
Compounding this organizational issue are the limitations of my current infrastructure. The costs of my hosting setup are not insignificant, yet I am not getting enough out of it. I specifically need the ability to host single-page web applications and single-page sites generated by AI tools. I had been using GitHub for this purpose, but GitHub Pages cannot manage the large file sizes required for image and video uploads.
Finally, this project is driven by what I consider the “iWeb lesson.” I previously built a site using Apple’s iWeb, only to have the software unceremoniously discontinued. I have also invested in tools that eventually became unsupported due to operating system updates, only to face massive price hikes for the newer versions. To avoid repeating this experience, my new solution must run locally, with local backups of all content stored in an open and easily transferable format.
A low friction workflow in day to day use is the primary design goal of the project and so the tool is designed to focus strictly on content, relying on automatic layouts and clean, simple formatting.
The following technical record of the project is written by Claude code with which the application was developed.
Casey Editor Desktop: A Technical Deep Dive
This document provides comprehensive technical documentation for Casey Editor Desktop, a custom content management system built to streamline publishing to an Astro-based static site. It covers the development journey, architectural decisions, installation procedures, and long-term maintenance considerations.
Table of Contents
- Project Genesis and Development Timeline
- Technical Architecture
- Asset Pipeline and Publication Flow
- Installation Guide
- Daily Usage Workflow
- Maintenance Without AI Assistance
- Hosting Services and Credentials
- Limitations and Costs
- Future Development: Legacy Migration
Project Genesis and Development Timeline {#project-genesis}
The Problem
Managing content for an Astro static site required:
- Opening VS Code or another editor
- Navigating to markdown files in
src/content/ - Manually writing frontmatter YAML
- Uploading images to a CDN separately
- Copying URLs back into content
- Running git commands to publish
For quick notes or blog posts, this friction accumulated into a significant barrier to writing.
Development Phases
Phase 1: Core Editor (Day 1)
- Electron shell with React frontend
- Express API server for file operations
- Basic markdown editing with live preview
- Frontmatter form generation from templates
Phase 2: Media Integration (Day 1-2)
- Media library browser pointing to local
~/Media/folder - Image picker modal with grid view
- Cloudinary SDK integration for uploads
- URL rewriting from local paths to CDN URLs
Phase 3: Git Integration (Day 2)
- Git status display in sidebar
- One-click publish: sync media → rewrite URLs → commit → push
- Pull functionality for multi-device sync
Phase 4: Platform Deployment (Day 2-3)
- Initial development on Raspberry Pi 5
- Cross-platform deployment to Mac Mini
- SSH authentication setup for git operations
- Bug fixes for edge cases (empty commits, existing unpushed changes)
Phase 5: Video Support (Day 3)
- Extended media pipeline to handle video files
- Dynamic Cloudinary resource type detection
- Video preview and insertion in editor
Why These Technologies?
| Technology | Rationale |
|---|---|
| Electron | Cross-platform desktop app with web technologies; single codebase for Pi and Mac |
| React + Vite | Fast development iteration; familiar component model; hot module replacement |
| Express | Lightweight API layer; runs inside Electron process; no separate server needed |
| Tailwind CSS | Rapid UI development; consistent styling; small production bundle |
| gray-matter | Standard library for parsing markdown frontmatter; used by Astro itself |
| Cloudinary | Generous free tier; automatic image optimization; global CDN; video support |
| Astro | Static site output; content collections; excellent performance |
Technical Architecture {#technical-architecture}
System Overview
┌─────────────────────────────────────────────────────────────────┐
│ Casey Editor Desktop │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Electron Main Process ││
│ │ ┌─────────────────┐ ┌──────────────────────────────────┐ ││
│ │ │ Window Manager │ │ Express API Server │ ││
│ │ │ - BrowserWindow│ │ (localhost:3001) │ ││
│ │ │ - App Menu │ │ ┌────────────────────────────┐ │ ││
│ │ │ - Context Menu │ │ │ Routes: │ │ ││
│ │ └─────────────────┘ │ │ /api/content/:type/:file │ │ ││
│ │ │ │ /api/media-library │ │ ││
│ │ │ │ /api/git/status|publish │ │ ││
│ │ │ │ /api/media/sync|publish │ │ ││
│ │ │ └────────────────────────────┘ │ ││
│ │ │ ┌────────────────────────────┐ │ ││
│ │ │ │ Media Sync Module │ │ ││
│ │ │ │ - Cloudinary uploads │ │ ││
│ │ │ │ - URL rewriting │ │ ││
│ │ │ │ - Cache management │ │ ││
│ │ │ └────────────────────────────┘ │ ││
│ │ └──────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Electron Renderer Process ││
│ │ ┌─────────────────────────────────────────────────────────┐││
│ │ │ React Application (Vite) │││
│ │ │ ┌───────────┐ ┌───────────┐ ┌───────────────────────┐ │││
│ │ │ │ Dashboard │ │ File List │ │ Editor │ │││
│ │ │ │ - Stats │ │ - Posts │ │ - Markdown textarea │ │││
│ │ │ │ - Quick │ │ - Projects│ │ - Live preview │ │││
│ │ │ │ actions │ │ - Pieces │ │ - Frontmatter form │ │││
│ │ │ │ │ │ - Notes │ │ - Media picker modal │ │││
│ │ │ └───────────┘ └───────────┘ └───────────────────────┘ │││
│ │ │ ┌─────────────────────────────────────────────────────┐│││
│ │ │ │ Sidebar ││││
│ │ │ │ - Navigation - Git status - Publish button ││││
│ │ │ └─────────────────────────────────────────────────────┘│││
│ │ └─────────────────────────────────────────────────────────┘││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
Directory Structure
casey-editor-desktop/
├── electron/
│ ├── main.ts # Electron main process, window creation
│ ├── preload.ts # Context bridge for IPC
│ ├── config-store.ts # Persistent configuration (paths, credentials)
│ └── server/
│ ├── index.ts # Express routes and middleware
│ └── media-sync.ts # Cloudinary upload and URL rewriting logic
├── src/
│ ├── main.tsx # React entry point
│ ├── App.tsx # Router setup
│ ├── config.ts # API URL and constants
│ └── components/
│ ├── Dashboard.tsx # Home screen with stats
│ ├── FileList.tsx # Content type listing
│ ├── Editor.tsx # Main editing interface
│ └── Sidebar.tsx # Navigation and git controls
├── dist/ # Vite production build output
├── dist-electron/ # Compiled Electron files
├── package.json
├── vite.config.ts
├── tailwind.config.js
└── tsconfig.json
External Dependencies
~/Website/site/ # Astro site repository
├── src/content/
│ ├── posts/ # Blog posts (*.md)
│ ├── projects/ # Active projects
│ ├── pieces/ # Finished works
│ └── notes/ # Reference material
├── public/
│ ├── images/ # Site-hosted images
│ └── tools/ # Static web apps
└── .upload-cache.json # Media sync state
~/Media/ # Local media library
├── photography/
├── artwork/
├── uncategorized/
└── *.mp4, *.jpg, etc.
Asset Pipeline and Publication Flow {#asset-pipeline}
Content Creation Flow
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Create/ │ │ Edit │ │ Save │
│ Open Post │ ──▶ │ Content │ ──▶ │ (Ctrl+S) │
└──────────────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ Insert Media │
│ from Library │
└──────────────┘
│
Uses /media/ paths
│
▼
┌─────────────────────────────┐
│ Content saved to disk: │
│ ~/Website/site/src/content │
│ /posts/my-post.md │
│ │
│ Contains: │
│  │
└─────────────────────────────┘
Publication Flow (Publish Button)
┌─────────────────────────────────────────────────────────────────┐
│ PUBLISH WORKFLOW │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ STEP 1: Scan Content for Media References │
│ │
│ Patterns matched: │
│ -  Markdown images │
│ - image: /media/path Frontmatter │
│ - src="/media/path" HTML attributes │
│ - <video src="/media/..."> Video elements │
│ │
│ Output: List of referenced media files │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ STEP 2: Upload to Cloudinary │
│ │
│ For each referenced file: │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Check cache │───▶│ Hash changed?│───▶│ Upload to │ │
│ │ (.upload- │ │ │ │ Cloudinary CDN │ │
│ │ cache.json)│ │ Yes ──────────────▶│ │ │
│ │ │ │ │ │ resource_type: │ │
│ │ │ │ No ─▶ Skip │ │ image | video │ │
│ └─────────────┘ └──────────────┘ └────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ Save to cache: │ │
│ │ - MD5 hash │ │
│ │ - Cloudinary URL │ │
│ │ - public_id │ │
│ │ - timestamp │ │
│ └────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ STEP 3: Rewrite URLs in Content │
│ │
│ Before: │
│  │
│ │
│ After: │
│  │
│ │
│ Files modified in place on disk │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ STEP 4: Git Commit and Push │
│ │
│ Commands executed: │
│ $ git add -A │
│ $ git commit -m "Content update 2026-02-14" │
│ $ git push │
│ │
│ Pushed to: git@github.com:username/site-repo.git │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ STEP 5: Deployment (External) │
│ │
│ GitHub webhook triggers Netlify/Vercel build │
│ Astro builds static HTML │
│ Site deployed to CDN edge locations │
└─────────────────────────────────────────────────────────────────┘
Cache File Structure
.upload-cache.json (stored in site repo root):
{
"photography/sunset.jpg": {
"hash": "*******",
"url": "https://res.cloudinary.com/*****/casey-site/photography/sunset.jpg",
"public_id": "casey-site/photography/sunset",
"uploaded_at": "2026-02-14T12:00:00.000Z"
},
"videos/demo.mp4": {
"hash": "f6e5d4c3b2a1...",
"url": "https://res.cloudinary.com/******/casey-site/videos/demo.mp4",
"public_id": "casey-site/videos/demo",
"uploaded_at": "2026-02-14T12:05:00.000Z"
}
}
Installation Guide {#installation-guide}
Prerequisites
- Node.js 18+ (LTS recommended)
- Git with SSH key configured for GitHub
- Astro site repository cloned locally
- Cloudinary account (free tier sufficient)
Step 1: Clone the Repository
cd ~/Website
git clone git@github.com:username/casey-editor-desktop.git
cd casey-editor-desktop
Step 2: Install Dependencies
npm install
On some systems, native modules may need rebuilding:
npm rebuild
Step 3: Configure Environment
Create .env file in the project root:
CLOUDINARY_API_KEY=your_api_key_here
CLOUDINARY_API_SECRET=your_api_secret_here
Alternatively, add to shell profile (~/.bashrc or ~/.zshrc):
export CLOUDINARY_API_KEY="your_api_key_here"
export CLOUDINARY_API_SECRET="your_api_secret_here"
Step 4: Ensure Directory Structure
# Astro site repo
ls ~/Website/site/src/content/
# Should show: posts/ projects/ pieces/ notes/
# Media library
mkdir -p ~/Media
Step 5: Configure Git SSH (if not already done)
# Generate key if needed
ssh-keygen -t ed25519 -C "your-email@example.com"
# Copy public key
cat ~/.ssh/*****.pub
# Add to GitHub → Settings → SSH Keys
# Test connection
ssh -T git@github.com
# Ensure site repo uses SSH, not HTTPS
cd ~/Website/site
git remote set-url origin git@github.com:username/site-repo.git
Step 6: First Run
cd ~/Website/casey-editor-desktop
npx vite build && npx electron .
If a setup wizard appears, configure:
- Site Path:
/home/username/Website/site - Media Path:
/home/username/Media - Cloudinary Cloud Name:
*******(or your cloud name)
Step 7: Create Launch Alias (Optional)
Add to ~/.bashrc or ~/.zshrc:
alias casey='cd ~/Website/casey-editor-desktop && npx vite build && npx electron .'
Then simply run:
casey
Manual Configuration (if wizard fails)
Create config file manually:
Linux: ~/.config/casey-editor-desktop/config.json
macOS: ~/Library/Application Support/casey-editor-desktop/config.json
{
"sitePath": "/home/username/Website/site",
"mediaPath": "/home/username/Media",
"cloudinaryCloudName": "****"
}
Daily Usage Workflow {#daily-usage}
Starting the Application
cd ~/Website/casey-editor-desktop
npx vite build && npx electron .
Or with alias: casey
Creating Content
- Click content type in sidebar (Posts, Projects, Pieces, Notes)
- Click + New button
- Fill in frontmatter fields (title, date, tags, etc.)
- Write markdown content
- Press Ctrl+S to save
Inserting Media
- Click the Image icon in the toolbar (or Camera icon in frontmatter)
- Select Media Library tab
- Browse images/videos from
~/Media/folder - Click to insert
- Images insert as:
 - Videos insert as:
<video src="/media/path/to/file.mp4" controls></video>
- Images insert as:
Publishing
- Make edits and save (Ctrl+S)
- Sidebar shows git status (files changed, commits ahead)
- Click Publish button
- Wait for:
- Media sync to Cloudinary
- URL rewriting
- Git commit and push
- Site rebuilds automatically via CI/CD
Syncing Across Devices
If you have the editor on multiple machines:
# On the machine with changes
# (Publish normally through the app)
# On other machines
cd ~/Website/site
git pull
cd ~/Website/casey-editor-desktop
git pull
npm install # if dependencies changed
Maintenance Without AI Assistance {#maintenance}
This section provides guidance for maintaining the system if AI coding assistance is unavailable.
Understanding the Codebase
Key files to understand:
| File | Purpose | When to Edit |
|---|---|---|
electron/server/index.ts | API routes | Adding new endpoints |
electron/server/media-sync.ts | Upload logic | Changing CDN behavior |
electron/main.ts | Window/menu management | App chrome changes |
src/components/Editor.tsx | Main editing UI | Editor features |
src/components/Sidebar.tsx | Navigation/publish | Sidebar changes |
Common Maintenance Tasks
Adding a new content type:
- Edit
src/config.ts- add to content types array - Edit
src/components/Editor.tsx- add template intemplatesobject - Create folder:
~/Website/site/src/content/newtype/ - Update Astro content config if needed
Changing file type support:
- Edit
electron/server/index.ts- update extension arrays (lines ~192, ~221) - Edit
electron/server/media-sync.ts- updateVIDEO_EXTENSIONSor add new category - Edit
src/components/Editor.tsx- updateisVideoFilefunction if needed
Debugging API issues:
# Check if server is running
curl http://localhost:3001/
# Check configuration
curl http://localhost:3001/api/debug
# View git status
curl http://localhost:3001/api/git/status
Rebuilding after code changes:
npx vite build && npx electron .
Log Files and Debugging
Electron logs appear in the terminal where you launched the app. For more verbose output:
DEBUG=* npx electron .
Dependency Updates
# Check for outdated packages
npm outdated
# Update all (may introduce breaking changes)
npm update
# Update specific package
npm install package-name@latest
# Always rebuild and test after updates
npx vite build && npx electron .
Backup Strategy
Critical files to back up:
~/Website/site/- The actual content (also on GitHub)~/Media/- Source media files (not uploaded anywhere automatically)~/.config/casey-editor-desktop/config.json- Local configuration.envfile - API credentials
Hosting Services and Credentials {#hosting-services}
Services Used
| Service | Purpose | Login URL | Cost |
|---|---|---|---|
| GitHub | Git hosting, CI triggers | github.com | Free |
| Cloudinary | Image/video CDN | cloudinary.com | Free tier |
| Netlify (or Vercel) | Site hosting | netlify.com | Free tier |
Credential Locations
GitHub SSH Key:
- Private key:
~/.ssh/**** - Public key:
~/.ssh/******.pub - Registered at: GitHub → Settings → SSH and GPG Keys
Cloudinary API:
- Dashboard: cloudinary.com → Settings → Access Keys
- Stored locally in:
.envfile or shell environment - Required: API Key, API Secret, Cloud Name
Netlify (if used):
- Dashboard: app.netlify.com
- Build triggered automatically via GitHub webhook
- No local credentials needed
Rotating Credentials
Cloudinary API Key rotation:
- Log in to cloudinary.com
- Go to Settings → Access Keys
- Generate new key pair
- Update
.envfile or environment variables - Restart the application
GitHub SSH Key rotation:
# Generate new key
ssh-keygen -t ed25519 -C "email@example.com" -f ~/.ssh/*******_new
# Add to GitHub (Settings → SSH Keys)
cat ~/.ssh/******_new.pub
# Update SSH config
echo "Host github.com
IdentityFile ~/.ssh/*******_new" >> ~/.ssh/config
# Test
ssh -T git@github.com
# Remove old key from GitHub
Limitations and Costs {#limitations-and-costs}
Current Limitations
| Limitation | Impact | Workaround |
|---|---|---|
| Single user | No collaboration features | Use git branches for multi-user |
| No scheduled posts | Publishes immediately | Manually hold commits |
| Basic markdown only | No MDX components | Edit MDX files in VS Code |
| No image editing | Raw files only | Edit in external app first |
Cloudinary Free Tier Limits
| Resource | Free Limit | Typical Usage |
|---|---|---|
| Storage | 25 GB | ~10,000 high-res photos |
| Bandwidth | 25 GB/month | ~100,000 image views |
| Transformations | 25,000/month | Automatic optimization |
| Video processing | 25 seconds/month | ~5 short clips |
Monitoring usage: cloudinary.com → Dashboard → Usage
If limits exceeded:
- Optimize images before upload
- Reduce video resolution
- Upgrade to paid plan (~$99/month for Plus)
GitHub Free Tier Limits
- Unlimited public repositories
- Unlimited private repositories
- 500 MB package storage
- 2,000 Actions minutes/month
For a content site, you’ll never approach these limits.
Netlify Free Tier Limits
- 100 GB bandwidth/month
- 300 build minutes/month
- 1 concurrent build
Sufficient for most personal sites. Vercel has similar limits.
Cost Summary
| Service | Monthly Cost | Annual Cost |
|---|---|---|
| GitHub | $0 | $0 |
| Cloudinary | $0 (free tier) | $0 |
| Netlify | $0 (free tier) | $0 |
| Domain (optional) | ~$1 | ~$12 |
| Total | $0-1 | $0-12 |
Future Development: Legacy Migration {#future-development}
Current Legacy Content Sources
- Old Jekyll blog - Markdown files with different frontmatter schema
- WordPress exports - XML/HTML content
- Flickr archive - Photos with metadata in JSON sidecar files
- Joplin notes - Markdown with internal links
Migration Strategy
Phase 1: Inventory
Document all legacy sources:
- File formats and locations
- Metadata schemas
- Image/attachment handling
- Internal link formats
Phase 2: Schema Mapping
Map old schemas to Astro content collections:
# Old Jekyll frontmatter
layout: post
title: "Old Post"
date: 2020-01-15
categories: [tech, programming]
# Becomes Astro schema
title: "Old Post"
date: 2020-01-15
tags: [tech, programming]
description: ""
image: ""
Phase 3: Migration Scripts
Create Node.js scripts for each source:
// Example: jekyll-migrate.js
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const JEKYLL_DIR = '~/old-blog/_posts';
const ASTRO_DIR = '~/Website/site/src/content/posts';
fs.readdirSync(JEKYLL_DIR).forEach(file => {
const content = fs.readFileSync(path.join(JEKYLL_DIR, file), 'utf-8');
const { data, content: body } = matter(content);
// Transform frontmatter
const newFrontmatter = {
title: data.title,
date: data.date,
tags: data.categories || [],
description: data.excerpt || '',
image: ''
};
// Transform content (fix image paths, etc.)
let newBody = body;
newBody = newBody.replace(/\{\{ site\.baseurl \}\}/g, '');
// Write to Astro
const output = matter.stringify(newBody, newFrontmatter);
fs.writeFileSync(path.join(ASTRO_DIR, file), output);
});
Phase 4: Media Migration
- Copy all legacy images to
~/Media/legacy/ - Update paths in migrated content
- Run publish to upload to Cloudinary
- Verify all images resolve
Phase 5: Validation
- Build site locally:
npm run build - Check for broken links:
npm run check-links - Review in browser
- Fix issues iteratively
Estimated Timeline
| Phase | Duration | Dependencies |
|---|---|---|
| Inventory | 1-2 hours | Access to all archives |
| Schema mapping | 2-4 hours | Understanding of both schemas |
| Migration scripts | 4-8 hours | Node.js scripting |
| Media migration | 2-4 hours | Sufficient Cloudinary quota |
| Validation | 2-4 hours | Test environment |
Total: 1-2 weekends of focused work
Risk Mitigation
- Always work on copies - never modify original legacy files
- Version control everything - commit migration scripts
- Incremental migration - migrate one source at a time
- Keep originals - don’t delete legacy content until fully verified
Conclusion
Casey Editor Desktop demonstrates that a custom CMS doesn’t require complex infrastructure. By combining Electron for cross-platform distribution, a local Express server for API operations, and Cloudinary for media delivery, the entire system runs on commodity hardware with zero recurring costs.
The architecture prioritizes:
- Simplicity - Everything runs locally; no cloud dependencies for core function
- Ownership - Content stays in git; media originals stay on your disk
- Portability - Standard markdown files work with any static site generator
- Maintainability - Clear separation between editor, API, and site
Last updated: February 2026 Version: 1.0