Returnspace
A personal blog for sharing long-form thoughts via direct links. Posts use random slugs for privacy-by-obscurity while keeping an RSS feed available for those who discover it.
Live site: https://returnspace.net/
Table of Contents
- Quick Start
- Requirements
- Testing (TUI-first)
- Philosophy
- Publishing Workflows
- Working with Images
- Management Commands
- Complete Examples
- Technical Details
- Cross-Platform Support
- Web Admin Panel
- Troubleshooting
Quick Start
First time setup (1 minute):
# After cloning the repo
cd ~/Git/returnspace
./scripts/bootstrap.sh
# Then reload your shell config
source ~/.zshrc
Simplest way to post:
blog-quick my-post "Post Title"
# Opens editor → write → close → auto-published!
See EASY-MODE.md for all shortcuts.
Requirements
Required
Hugo (static site generator)
- macOS:
brew install hugo - Linux/WSL:
sudo apt install hugoor download from gohugo.io - Version 0.111.3 or later recommended
- macOS:
Git (version control)
- Usually pre-installed on macOS/Linux
- WSL:
sudo apt install git
Required for Web Admin Panel (Cloudflare Worker)
The worker/ directory contains a Cloudflare Worker for mobile/remote posting. These are only needed if you want to deploy or develop the worker.
Node.js (v18 or later) and npm
- macOS:
brew install node - Linux/WSL:
sudo apt install nodejs npmor use nvm - Required to install worker dependencies and run Wrangler
- macOS:
Wrangler (Cloudflare’s CLI — installed automatically via npm)
cd worker npm install # installs wrangler and TypeScript locallyCloudflare account — free tier is sufficient
- Sign up at cloudflare.com
- Domain must be active in your Cloudflare account for custom domain routing
GitHub Personal Access Token (PAT)
- GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)
- Required scope:
repo - Used by the worker to commit posts via the GitHub Contents API
After deploying the worker, set secrets:
cd worker npx wrangler secret put GITHUB_PAT # GitHub token npx wrangler secret put AUTH_USER # Basic auth username npx wrangler secret put AUTH_PASS # Basic auth password
Optional but Recommended
Vale (prose linting)
- macOS:
brew install vale - Linux/WSL: Download from vale.sh
- Checks writing quality, catches typos
- macOS:
ImageMagick (image optimization)
- macOS:
brew install imagemagick - Linux/WSL:
sudo apt install imagemagick - Only needed if you optimize images with
optimize-images.sh
- macOS:
ShellCheck (shell script linting)
- macOS:
brew install shellcheck - Linux/WSL:
sudo apt install shellcheck - Used by
./scripts/pre-commit-check.shto lint shell scripts
- macOS:
fswatch (macOS) or inotify-tools (Linux/WSL) - for image watcher
- macOS:
brew install fswatch - Linux/WSL:
sudo apt install inotify-tools - Only needed for auto-optimizing images with
watch-images.sh
- macOS:
Enhanced TUI Experience (Highly Recommended)
These tools dramatically improve the interactive experience:
fzf (fuzzy finder for interactive selection)
- macOS:
brew install fzf - Linux/WSL:
sudo apt install fzf - Enables fuzzy search post selection with live previews
- Arrow key navigation instead of numbered menus
- macOS:
gum (beautiful TUI components)
- macOS:
brew install gum - Linux/WSL:
brew install gum(via Homebrew on Linux) - Alternative: Download from github.com/charmbracelet/gum
- Provides polished prompts and confirmations
- macOS:
bat (syntax-highlighted file previews)
- macOS:
brew install bat - Linux/WSL:
sudo apt install bat - Shows beautiful code-highlighted previews in fzf
- macOS:
Install all three at once:
# macOS
brew install fzf gum bat
# Linux/WSL (Ubuntu/Debian)
sudo apt install fzf bat
brew install gum # gum via Homebrew on Linux
Scripts automatically detect these tools and provide enhanced interfaces when available, with graceful fallback to basic bash menus if not installed.
Supported Platforms
- ✅ macOS (Apple Silicon and Intel)
- ✅ Linux (Ubuntu, Debian, etc.)
- ✅ Windows Subsystem for Linux (WSL2)
Scripts automatically detect the platform and use appropriate commands.
Testing (TUI-first)
The project uses a two-tier TUI test strategy:
Fast smoke suite (
./scripts/test-tui-smoke.sh)- Runs quickly and is safe for frequent use
- Covers core menu rendering, navigation, blank/cancel guards, and non-destructive safety checks
- Used by pre-commit fast mode
Integration suite (
./scripts/tests/tui-integration.sh)- Uses reusable stubs/fixtures to exercise deeper TUI flows without side effects
- Covers publish/snippet success and failure paths, deploy prompt flow, preview modes, analytics, and post-management success branches
- Used by pre-commit full mode
Recommended commands
# Fast local check (recommended before most commits)
./scripts/pre-commit-check.sh --fast
# Full confidence pass (runs TUI integration + full smoke in sandbox)
./scripts/pre-commit-check.sh --full
# Run just TUI suites directly
./scripts/test-tui-smoke.sh
./scripts/tests/tui-integration.sh
# Run full smoke directly in isolated sandbox
./scripts/tests/smoke-test.sh --sandbox
Pre-commit behavior
--fast: shellcheck + TUI smoke--full: shellcheck + TUI smoke + TUI integration + sandboxed full smoke (scripts/tests/smoke-test.sh --sandbox)
If running inside git hooks, full mode auto-downgrades to fast mode unless RETURNSPACE_ALLOW_FULL_IN_HOOK=1 is set.
Philosophy
This isn’t a traditional blog. There’s no browse-by-date, no categories, no homepage listing, and no navigation between posts. Posts are completely isolated and only accessible via direct links.
Use case: Write something → share the direct link via text/Instagram/etc. → only people with the link can read it.
Someone technical could find the RSS feed, but there’s no way to browse posts through the site itself.
Publishing Workflows
Choose based on your needs:
1. Quick Post (Simple, No Images)
When to use: Short posts, quick thoughts, no images needed
# Write draft
code drafts/my-post.md
# Publish when ready
./scripts/publish.sh my-post.md "The Post Title"
# Preview locally
hugo server
# Deploy
git add content/p/<slug>
git commit -m "Add: The Post Title"
git push github master
# Get URL to share
./scripts/url.sh <slug>
2. Draft-First Workflow (Best for Posts with Images)
When to use: You want to add images while writing, before making the post public
Why this exists: Images can only be added to published posts (in content/p/<slug>/), not drafts. This workflow creates the post structure with draft = true so it exists on the server but isn’t publicly visible.
# 1. Write your draft
code drafts/sleep.md
# Write as much as you want, save often
# 2. Publish as HIDDEN draft (creates structure but not publicly visible)
./scripts/publish-draft.sh sleep.md "Sleep Has Her House"
# Output: "Slug: abc123def456..."
# Copy this slug!
# 3. Edit the post and add images
./scripts/edit.sh abc123def456
# In VSCode:
# - Write more content
# - Copy screenshot → Cmd+Alt+V → enter filename → image saved!
# - Repeat as needed
# - Save when done
# 4. Preview locally (including drafts)
hugo server -D
# Visit http://localhost:1313/p/abc123def456/
# Your post is visible in preview, images work
# 5. Keep editing as long as you want
./scripts/edit.sh abc123def456
# Add more images, write more, etc.
# 6. When ready to make PUBLIC
./scripts/make-live.sh abc123def456
# This changes draft = true to draft = false
# 7. Deploy to production
git add content/p/abc123def456
git commit -m "Add: Sleep Has Her House"
git push github master # GitHub Actions auto-deploys to DigitalOcean
# GitHub Actions (.github/workflows/deploy.yml) handles:
# - Pushing to prod (DigitalOcean)
# - Running Hugo build
# - Deploying passwords
# - Restarting Caddy
# 8. Get URL to share
./scripts/url.sh abc123def456
Key concept: draft = true posts exist in your repo and show in local preview (hugo server -D) but are NOT served on the live site. When you’re ready, flip to draft = false and deploy.
3. Web Admin Panel (From Any Device)
When to use: You’re away from your computer and want to post from your phone, tablet, or any browser
Visit https://post.returnspace.net in any browser. Authenticate, then use the dashboard to create, edit, toggle, or delete posts. The editor saves drafts to localStorage so you won’t lose work if the page refreshes.
Changes commit directly to GitHub via the Contents API, triggering GitHub Actions to deploy automatically.
See Web Admin Panel below and worker/README.md for setup.
4. Ultra-Quick (All-in-One)
When to use: Very short posts you can write in one sitting
blog-quick lunch "Great Lunch Spot"
# Opens editor, you write, close when done
# Auto-publishes and deploys
# URL copied to clipboard
This is an alias that combines write→publish→deploy into one command. See EASY-MODE.md.
Working with Images
Adding Images to Posts
Important: Images only work in published posts (in content/p/<slug>/), not in drafts. Use the Draft-First Workflow if you need images.
Method 1: Paste in VSCode (Easiest)
Requires the “Paste Image” extension (already configured in .vscode/settings.json).
While editing a post’s index.md in VSCode:
- Copy an image to clipboard
- Take a screenshot (
Cmd+Shift+4on Mac,Win+Shift+Son Windows) - Or copy an image from your browser
- Or copy from Photos/Finder
- Take a screenshot (
- Position cursor where you want the image
- Press
Cmd+Alt+V(Mac) orCtrl+Alt+V(Windows/Linux) - Dialog appears: enter a filename (e.g., “screenshot” or “photo1”)
- Image automatically saves to
content/p/<slug>/filename.png - Markdown inserted automatically:

Tip: The extension is configured to prefix filenames with “index-” by default. You can delete this prefix in the dialog if you want cleaner names.
Method 2: Copy Existing Files
./scripts/add-images.sh <slug> ~/Pictures/photo1.jpg ~/Desktop/screenshot.png
Copies images to the post directory. Then reference them in your markdown:

Optimizing Images
Large photos from phones can be 5MB+. Compress them:
# Install ImageMagick (first time only)
brew install imagemagick
# Optimize all images in a post
./scripts/optimize-images.sh <slug>
Reduces file sizes by ~80% with no visible quality loss. Great for mobile readers.
Management Commands
All Available Scripts
Located in scripts/ directory:
| Script | Usage | What it does |
|---|---|---|
snippet.sh | echo "content" | ./scripts/snippet.sh "Title" | Quick publish one-liner posts/snippets |
deploy.sh | ./scripts/deploy.sh <slug> "Title" | Deploy post to production (git add/commit/push) |
publish.sh | ./scripts/publish.sh draft.md "Title" | Publish a draft as live post immediately |
publish-draft.sh | ./scripts/publish-draft.sh draft.md "Title" | Publish as hidden draft (for adding images) |
make-live.sh | ./scripts/make-live.sh <slug> | Make hidden draft publicly visible |
edit.sh | ./scripts/edit.sh <slug> | Edit a published post in VSCode |
list.sh | ./scripts/list.sh | List all published posts with URLs |
url.sh | ./scripts/url.sh <slug> | Copy post URL to clipboard |
add-images.sh | ./scripts/add-images.sh <slug> image.jpg | Copy images to post directory |
optimize-images.sh | ./scripts/optimize-images.sh <slug> | Compress all images in a post |
preview.sh | ./scripts/preview.sh <slug> | Preview a specific post locally |
stats.sh | ./scripts/stats.sh | Open GoatCounter analytics dashboard |
doctor.sh | ./scripts/doctor.sh | Check required/recommended environment dependencies |
archive-draft.sh | ./scripts/archive-draft.sh draft.md | Move draft to archive after publishing |
unpublish.sh | ./scripts/unpublish.sh <slug> [--delete] [--dry-run] | Hide or remove a post (emergency) |
delete.sh | ./scripts/delete.sh <slug> [--dry-run] | Permanently delete a post |
change-password.sh | ./scripts/change-password.sh <slug> [new-password] [--dry-run] | Change/add password protection |
Quick Aliases (After Setup)
If you’ve added .blog-aliases to your shell:
blog # Jump to blog directory
draft # Open drafts folder
blog-write my-post # Start new draft
blog-publish-draft draft.md "T" # Publish as hidden
blog-make-live <slug> # Make draft public
blog-edit <slug> # Edit and deploy
blog-list # List all posts
blog-url <slug> # Copy URL
blog-stats # Show statistics
blog-doctor # Environment readiness check
blog-preview # Preview site
blog-preview-drafts # Preview with drafts visible
blog-quick name "Title" # Write→publish→deploy in one
See EASY-MODE.md for complete alias reference.
Canonical cross-interface mapping lives in docs/COMMAND-MATRIX.md.
Complete Examples
Example 1: Simple Text Post (No Images)
# Write
code drafts/thoughts-on-movies.md
# (Write your content, save, close)
# Publish
./scripts/publish.sh thoughts-on-movies.md "My Favorite Films of 2025"
# Output: Created slug abc123def456...
# Preview
hugo server
# Visit http://localhost:1313/p/abc123def456/
# Deploy
git add content/p/abc123def456
git commit -m "Add: My Favorite Films of 2025"
git push github master
# Share
./scripts/url.sh abc123def456
# Copies: https://returnspace.net/p/abc123def456/
# Send to friends via text/Instagram
Example 2: Post with Images (Draft-First)
# Start draft
code drafts/vancouver-trip.md
# (Write initial content, save)
# Publish as hidden draft
./scripts/publish-draft.sh vancouver-trip.md "Vancouver Film Festival"
# Output: Slug: xyz789abc012...
# Add images and keep writing
./scripts/edit.sh xyz789abc012
# In VSCode:
# 1. Write more
# 2. Copy screenshot → Cmd+Alt+V → type "theater" → saves as theater.png
# 3. Copy photo → Cmd+Alt+V → type "hotel-view" → saves as hotel-view.png
# 4. Keep writing
# 5. Save and close
# Preview with drafts
hugo server -D
# Check http://localhost:1313/p/xyz789abc012/
# Compress images (optional)
./scripts/optimize-images.sh xyz789abc012
# Edit more if needed
./scripts/edit.sh xyz789abc012
# Add more content/images, save
# Ready to publish? Make it live
./scripts/make-live.sh xyz789abc012
# Deploy
git add content/p/xyz789abc012
git commit -m "Add: Vancouver Film Festival"
git push github master
# Share
./scripts/url.sh xyz789abc012
# Clean up
./scripts/archive-draft.sh vancouver-trip.md
Example 3: Emergency Takedown
# List posts to find the one
./scripts/list.sh
# Quick hide (sets draft=true, easy to undo)
./scripts/unpublish.sh abc123def456
# Or complete removal (moves back to drafts)
./scripts/unpublish.sh abc123def456 --delete
# Preview either mode without changing files/git
./scripts/unpublish.sh abc123def456 --dry-run
./scripts/unpublish.sh abc123def456 --delete --dry-run
Example 4: Fixing a Typo
# Find the post
./scripts/list.sh
# Edit it
./scripts/edit.sh abc123def456
# Fix typo, save, close
# Deploy
git add content/p/abc123def456
git commit -m "Fix: Typo in Post Title"
git push github master
Technical Details
Infrastructure
- Hosting: DigitalOcean droplet ($4/month) at 143.110.130.232
- CDN: Cloudflare (free tier, proxying enabled)
- Deployment: Git post-receive hook triggers Hugo rebuild in Docker
- Web server: Caddy
- Static site generator: Hugo v0.111.3
- Theme: Shibui theme (git submodule)
- Mobile posting: Cloudflare Worker at
post.returnspace.net
Git Remotes
# Production server
prod: [email protected]:/srv/returnspace/repo.git
# GitHub backup (private)
github: https://github.com/vacuumboots/returnspace.git
Deploying:
git push github master # Primary deploy path (GitHub Actions)
git push prod master # Manual fallback if Actions is unavailable
Site Configuration
Key settings in hugo.toml:
contentTypeName = "p"- custom content type for postsdisableKinds = ["section", "taxonomy", "term"]- disables standard Hugo organization- Custom permalink:
/p/:slug/ - RSS feed limited to 50 items
Custom Layouts
layouts/index.html- Homepage (no post listing, just explanation)layouts/_default/single.html- Post template (no navigation between posts)layouts/partials/extended_head.html- GoatCounter analytics (privacy-friendly)
Directory Structure
returnspace/
├── content/p/ # Published posts
│ └── <slug>/
│ ├── index.md # Post content with front matter
│ └── *.jpg # Images for this post
├── drafts/ # Unpublished writing
│ └── archive/ # Old drafts after publishing
├── scripts/ # Management scripts
├── layouts/ # Custom Hugo templates
├── worker/ # Cloudflare Worker (mobile posting form)
│ └── src/ # TypeScript source
├── themes/shibui/ # Theme (git submodule)
├── docs/ # Setup guides and references
│ ├── COMMAND-MATRIX.md # Canonical command reference (TUI/alias/script)
│ ├── ANALYTICS.md
│ └── SOCIAL-PREVIEWS.md
├── .blog-aliases # Shell aliases for convenience
├── .vscode/settings.json # VSCode config (Paste Image)
├── .vale.ini # Prose linting config
└── hugo.toml # Site configuration
Post Front Matter
+++
title = "Post Title"
date = 2026-01-31T12:00:00-07:00
slug = "abc123def456789012345678"
draft = false
+++
Post content here...
draft = true- Post exists but not served on live sitedraft = false- Post is publicslug- Random 24-char hex, used in URL
Analytics
GoatCounter is configured at https://vacuumboots.goatcounter.com
- Privacy-friendly, no cookies
- View counts per post
- Only runs on production (not local preview)
Writing Quality
Vale prose linter checks writing style:
vale . # Check all markdown
vale drafts/my-post.md # Check specific file
Configuration in .vale.ini. Some rules disabled for personal blog style (first person allowed, contractions OK).
Cross-Platform Support
The blog system works seamlessly across macOS, Linux, and Windows (via WSL2).
Platform Detection
Scripts automatically detect your platform and use the correct commands:
- File watching:
fswatch(macOS) vsinotifywait(Linux/WSL) - File operations: BSD vs GNU utilities
- Package managers: Suggests
brew installon macOS,apt installon Linux
Line Endings
The .gitattributes file enforces LF line endings for all text files:
- Prevents issues with shell scripts in WSL
- Ensures consistent behavior across platforms
- No manual configuration needed
WSL-Specific Notes
Repo location: The blog directory can be anywhere. Scripts use dynamic path detection:
# Works from any location
source ~/Git/returnspace/.blog-aliases
# Or
source /mnt/c/Users/YourName/Git/returnspace/.blog-aliases
File permissions: Make sure scripts are executable:
chmod +x scripts/*.sh
Windows paths: Avoid spaces in the path to your repo (e.g., don’t put it in “My Documents”).
Web Admin Panel
A Cloudflare Worker at post.returnspace.net serves a full lightweight CMS for managing posts from any device. It uses the GitHub Contents API for all operations, triggering the existing GitHub Actions deploy pipeline.
Browser -> post.returnspace.net
-> HTTP Basic Auth (username/password)
-> Cloudflare Worker (multi-page app)
-> GitHub Contents API (CRUD on content/p/<slug>/index.md)
-> GitHub Actions (auto-deploys to DigitalOcean)
Features:
- Dashboard — lists all posts with title, date, and status badges (live/draft/password-protected)
- Create — compose form with markdown body and localStorage draft saving
- View — read post content with action bar
- Edit — edit title, body, and draft status (SHA-based conflict detection)
- Toggle — flip draft status with one click
- Delete — delete with confirmation (warns if post has images)
- Mobile-responsive, touch-friendly (44px tap targets)
- Ctrl/Cmd+Enter keyboard shortcut to submit forms
Setup: See worker/README.md for deployment and configuration.
Troubleshooting
Can’t find a post
./scripts/list.sh
# Shows all posts with slugs and URLs
Hugo server won’t start (port in use)
pkill hugo
hugo server
Post has wrong date
./scripts/edit.sh <slug>
# Change the date field in front matter
git add content/p/<slug> && git commit -m "Fix date" && git push github master
Images not showing up locally
Make sure you’re editing a published post, not a draft:
# Wrong: code drafts/my-post.md
# Right: ./scripts/edit.sh <slug>
For drafts with images, use the draft-first workflow.
Images not showing after deploy
Check the image paths in your markdown. They should be relative:
# Correct

# Wrong


Want to see drafts in preview
hugo server -D
# The -D flag shows draft posts
Accidentally published something private
# Quick hide (easiest to undo)
./scripts/unpublish.sh <slug>
# Or complete removal
./scripts/unpublish.sh <slug> --delete
# Or preview first with no changes
./scripts/unpublish.sh <slug> --dry-run
./scripts/delete.sh <slug> --dry-run
VSCode paste image not working
- Check the extension is installed: “Paste Image” by mushan
- Check
.vscode/settings.jsonexists with proper config - Make sure you’re editing
content/p/<slug>/index.md, not a draft - Try the keyboard shortcut:
Cmd+Alt+V(Mac) orCtrl+Alt+V(Windows)
Need to clone repo on another machine
git clone <repo-url>
cd returnspace
git submodule update --init --recursive
Theme needs updating
git submodule update --remote themes/terminal
Next Steps
- Use it! Start with
blog-quickfor simple posts - Add images using the draft-first workflow
- Check analytics at https://vacuumboots.goatcounter.com
- Read EASY-MODE.md for all the shortcuts
- See FUTURE-IMPROVEMENTS.md for ideas on what to add next
The setup is intentionally simple and works well for sharing long-form thoughts via direct links. Only add features if you actually need them.