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

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

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.

These tools dramatically improve the interactive experience:

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

Scripts automatically detect the platform and use appropriate commands.


Testing (TUI-first)

The project uses a two-tier TUI test strategy:

# 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

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:

  1. Copy an image to clipboard
    • Take a screenshot (Cmd+Shift+4 on Mac, Win+Shift+S on Windows)
    • Or copy an image from your browser
    • Or copy from Photos/Finder
  2. Position cursor where you want the image
  3. Press Cmd+Alt+V (Mac) or Ctrl+Alt+V (Windows/Linux)
  4. Dialog appears: enter a filename (e.g., “screenshot” or “photo1”)
  5. Image automatically saves to content/p/<slug>/filename.png
  6. Markdown inserted automatically: ![filename.png](filename.png)

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:

![Description of the photo](photo1.jpg)

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:

ScriptUsageWhat it does
snippet.shecho "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.shList 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.jpgCopy 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.shOpen GoatCounter analytics dashboard
doctor.sh./scripts/doctor.shCheck required/recommended environment dependencies
archive-draft.sh./scripts/archive-draft.sh draft.mdMove 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

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:

Custom Layouts

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...

Analytics

GoatCounter is configured at https://vacuumboots.goatcounter.com

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:

Line Endings

The .gitattributes file enforces LF line endings for all text files:

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:

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
![Description](photo.jpg)

# Wrong
![Description](/p/abc123/photo.jpg)
![Description](./photo.jpg)

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

  1. Check the extension is installed: “Paste Image” by mushan
  2. Check .vscode/settings.json exists with proper config
  3. Make sure you’re editing content/p/<slug>/index.md, not a draft
  4. Try the keyboard shortcut: Cmd+Alt+V (Mac) or Ctrl+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

The setup is intentionally simple and works well for sharing long-form thoughts via direct links. Only add features if you actually need them.