Open Stack Blog Workflow

blog

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

  1. Project Genesis and Development Timeline
  2. Technical Architecture
  3. Asset Pipeline and Publication Flow
  4. Installation Guide
  5. Daily Usage Workflow
  6. Maintenance Without AI Assistance
  7. Hosting Services and Credentials
  8. Limitations and Costs
  9. 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?

TechnologyRationale
ElectronCross-platform desktop app with web technologies; single codebase for Pi and Mac
React + ViteFast development iteration; familiar component model; hot module replacement
ExpressLightweight API layer; runs inside Electron process; no separate server needed
Tailwind CSSRapid UI development; consistent styling; small production bundle
gray-matterStandard library for parsing markdown frontmatter; used by Astro itself
CloudinaryGenerous free tier; automatic image optimization; global CDN; video support
AstroStatic 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:                  │
              │  ![Image](/media/photo.jpg) │
              └─────────────────────────────┘

Publication Flow (Publish Button)

┌─────────────────────────────────────────────────────────────────┐
│                    PUBLISH WORKFLOW                              │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ STEP 1: Scan Content for Media References                       │
│                                                                  │
│ Patterns matched:                                                │
│ - ![...](/media/path)        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:                                                          │
│ ![Photo](/media/photography/sunset.jpg)                          │
│                                                                  │
│ After:                                                           │
│ ![Photo](https://res.cloudinary.com/******/photography/sunset.jpg)          │
│                                                                  │
│ 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

  1. Click content type in sidebar (Posts, Projects, Pieces, Notes)
  2. Click + New button
  3. Fill in frontmatter fields (title, date, tags, etc.)
  4. Write markdown content
  5. Press Ctrl+S to save

Inserting Media

  1. Click the Image icon in the toolbar (or Camera icon in frontmatter)
  2. Select Media Library tab
  3. Browse images/videos from ~/Media/ folder
  4. Click to insert
    • Images insert as: ![Image](/media/path/to/file.jpg)
    • Videos insert as: <video src="/media/path/to/file.mp4" controls></video>

Publishing

  1. Make edits and save (Ctrl+S)
  2. Sidebar shows git status (files changed, commits ahead)
  3. Click Publish button
  4. Wait for:
    • Media sync to Cloudinary
    • URL rewriting
    • Git commit and push
  5. 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:

FilePurposeWhen to Edit
electron/server/index.tsAPI routesAdding new endpoints
electron/server/media-sync.tsUpload logicChanging CDN behavior
electron/main.tsWindow/menu managementApp chrome changes
src/components/Editor.tsxMain editing UIEditor features
src/components/Sidebar.tsxNavigation/publishSidebar changes

Common Maintenance Tasks

Adding a new content type:

  1. Edit src/config.ts - add to content types array
  2. Edit src/components/Editor.tsx - add template in templates object
  3. Create folder: ~/Website/site/src/content/newtype/
  4. Update Astro content config if needed

Changing file type support:

  1. Edit electron/server/index.ts - update extension arrays (lines ~192, ~221)
  2. Edit electron/server/media-sync.ts - update VIDEO_EXTENSIONS or add new category
  3. Edit src/components/Editor.tsx - update isVideoFile function 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:

  1. ~/Website/site/ - The actual content (also on GitHub)
  2. ~/Media/ - Source media files (not uploaded anywhere automatically)
  3. ~/.config/casey-editor-desktop/config.json - Local configuration
  4. .env file - API credentials

Hosting Services and Credentials {#hosting-services}

Services Used

ServicePurposeLogin URLCost
GitHubGit hosting, CI triggersgithub.comFree
CloudinaryImage/video CDNcloudinary.comFree tier
Netlify (or Vercel)Site hostingnetlify.comFree 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: .env file 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:

  1. Log in to cloudinary.com
  2. Go to Settings → Access Keys
  3. Generate new key pair
  4. Update .env file or environment variables
  5. 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

LimitationImpactWorkaround
Single userNo collaboration featuresUse git branches for multi-user
No scheduled postsPublishes immediatelyManually hold commits
Basic markdown onlyNo MDX componentsEdit MDX files in VS Code
No image editingRaw files onlyEdit in external app first

Cloudinary Free Tier Limits

ResourceFree LimitTypical Usage
Storage25 GB~10,000 high-res photos
Bandwidth25 GB/month~100,000 image views
Transformations25,000/monthAutomatic optimization
Video processing25 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

ServiceMonthly CostAnnual 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

  1. Old Jekyll blog - Markdown files with different frontmatter schema
  2. WordPress exports - XML/HTML content
  3. Flickr archive - Photos with metadata in JSON sidecar files
  4. 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

  1. Copy all legacy images to ~/Media/legacy/
  2. Update paths in migrated content
  3. Run publish to upload to Cloudinary
  4. 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

PhaseDurationDependencies
Inventory1-2 hoursAccess to all archives
Schema mapping2-4 hoursUnderstanding of both schemas
Migration scripts4-8 hoursNode.js scripting
Media migration2-4 hoursSufficient Cloudinary quota
Validation2-4 hoursTest 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