Blog Manager — Design Spec
Blog Manager — Design Spec
A lightweight local web app for authoring, managing, and publishing posts to a Jekyll-based GitHub Pages blog at gclinton.com.
Goals
- Replace manual file creation with a clean browser UI at
localhost:9000 - Write in markdown with live preview, drag-and-drop images, category/tag management
- Publish = write a correctly named, front-matter-complete
.markdownfile to_posts/ - Zero build step.
node server.jsand go. - Launchable from macOS dock as a native-feeling app
Non-Goals
- Not a CMS — no user accounts, no database, no auth
- Not a Jekyll build tool — doesn’t run
bundle exec jekyll serve - Not a git client — doesn’t commit or push (user does that manually)
- No deployment pipeline — GitHub Pages handles that
Architecture
blog-manager/
├── server.js # Express server, all API routes
├── package.json # 4 dependencies
├── public/ # Static frontend (served by Express)
│ ├── index.html # Single-page app shell
│ ├── style.css # Custom styles on top of Pico CSS
│ └── app.js # Vanilla JS — UI logic, editor, API calls
└── BlogManager.app/ # macOS Automator wrapper (optional)
Lives inside the blog repo at blog-manager/ so it travels with the project.
Backend (Express)
Single server.js file. All routes prefixed /api/.
Routes:
| Method | Path | Purpose |
|---|---|---|
| GET | /api/posts |
List all posts + drafts with parsed front matter |
| GET | /api/posts/:filename |
Read a single post (front matter + body) |
| POST | /api/posts |
Create a new post (draft or published) |
| PUT | /api/posts/:filename |
Update an existing post |
| DELETE | /api/posts/:filename |
Delete a post |
| POST | /api/posts/:filename/publish |
Move a draft from _drafts/ to _posts/ |
| POST | /api/posts/:filename/unpublish |
Move a published post back to _drafts/ |
| GET | /api/categories |
List all categories found across posts |
| GET | /api/pages |
List pages (about, resources, etc.) |
| GET | /api/pages/:filename |
Read a page |
| PUT | /api/pages/:filename |
Update a page |
| POST | /api/upload |
Upload an image to assets/, return markdown link |
File operations:
- Reads/writes directly to
../_posts/,../_drafts/,../assets/(parent ofblog-manager/) - Uses
gray-matterto parse and serialize YAML front matter - Uses
slugifyto generate filenames from titles - Filename format:
YYYY-MM-DD-slug.markdown - Image uploads handled by
multer, saved to../assets/with slugified names (no spaces)
Config:
- Reads
../_config.ymlonce at startup to extract site metadata (title, description, categories if any) - Port:
9000(hardcoded, no env var complexity)
Frontend
A single-page app with three views, no framework, no build step.
Tech:
- Pico CSS (CDN) — clean, dark-mode-ready styling with zero classes needed
- EasyMDE (CDN) — markdown editor with toolbar, preview, and drag-and-drop
- Vanilla JS — fetch calls to
/api/*, DOM manipulation
Views:
- Dashboard — table of all posts and drafts
- Columns: title, date, categories, status (draft/published)
- Sort by date (newest first)
- Click title to edit
- “New Post” button
- Quick-action buttons: publish/unpublish, delete (with confirm)
- Post Editor — the main authoring view
- Title field (text input)
- Date picker (defaults to today)
- Layout selector (dropdown:
post,page— pre-filled from existing posts) - Categories (multi-select pills + “add new” input)
- EasyMDE markdown editor (full width, tall)
- Drag-and-drop images: intercepted, uploaded via
/api/upload, markdowninserted at cursor - Toolbar: bold, italic, heading, link, image, list, preview, side-by-side, fullscreen
- Drag-and-drop images: intercepted, uploaded via
- Live preview panel (EasyMDE built-in side-by-side mode)
- Action buttons:
- “Save Draft” — writes to
_drafts/ - “Publish” — writes to
_posts/with dated filename - “Back to Dashboard”
- “Save Draft” — writes to
- Pages — simple list of existing pages (about, resources, etc.)
- Click to edit with the same markdown editor
- No create/delete (pages are rarely added)
UI details:
- Light mode by default (Pico CSS default theme)
- Responsive but optimized for desktop (this is a local tool)
- Minimal custom CSS — Pico handles typography, forms, buttons, tables
- Toast notifications for save/publish/error feedback (a small vanilla JS toast, ~20 lines)
Front Matter Generation
When saving/publishing, the app constructs the front matter block:
---
layout: post
title: "The Post Title"
date: 2026-03-26 12:00:00 -0400
categories: AI speculation
---
layout: from the layout dropdown (default:post)title: from the title input, quoteddate: from the date picker + hardcoded-0400timezone offset (matches existing posts)categories: space-separated string from selected categories
The body follows after the closing ---.
Image Handling
- User drags image into EasyMDE editor
- JS intercepts the drop event, POSTs file to
/api/upload - Server saves to
../assets/with a slugified filename (spaces → hyphens, lowercase) - Server returns
{ "url": "/assets/my-image.png" } - JS inserts
at the cursor position in the editor
Supported formats: .png, .jpg, .jpeg, .gif, .webp, .svg
Max size: 10MB (multer limit)
macOS Dock Launcher
A minimal BlogManager.app created via a shell script bundle:
BlogManager.app/
└── Contents/
├── Info.plist
├── MacOS/
│ └── launch.sh # Starts node server, opens browser, traps SIGTERM to kill server
└── Resources/
└── AppIcon.icns # Simple icon
launch.sh does:
cdto theblog-manager/directory- Start
node server.jsin background - Wait 1 second for server to start
open http://localhost:9000waiton the node process (keeps app “running” in dock)- On quit (SIGTERM): kills the node process
The user drags BlogManager.app to their dock. One click to launch.
Dependencies
| Package | Purpose | Size |
|---|---|---|
express |
HTTP server + routing | ~200KB |
multer |
Multipart file upload handling | ~40KB |
gray-matter |
YAML front matter parse/serialize | ~30KB |
slugify |
Title → filename slug | ~10KB |
Frontend (CDN, no install):
- Pico CSS v2
- EasyMDE (includes CodeMirror)
Total: 4 npm dependencies. No dev dependencies. No build step.
File Naming Convention
Posts: YYYY-MM-DD-slugified-title.markdown
- Date from the date picker
- Slug from the title: lowercased, non-alphanumeric → hyphens, consecutive hyphens collapsed
- Extension:
.markdown(matches existing posts)
Example: title “AI is a UI Problem”, date 2024-09-02 → 2024-09-02-ai-is-a-ui-problem.markdown
Error Handling
- File write failures: return 500 with message, show toast in UI
- Duplicate filename: append
-2,-3etc. if slug collision detected - Missing title: client-side validation, prevent save
- Upload failures: show toast, don’t insert broken markdown link
- Server not reachable: show connection banner at top of page
What This Doesn’t Do
- No git operations — user commits and pushes manually
- No Jekyll preview — use
bundle exec jekyll serveseparately if needed - No multi-user — this is a single-user local tool
- No database — the filesystem IS the database
- No auth — localhost only, not exposed to network