- Update README with gopher/phlog section, publishing commands, URLs, and first-time SDF setup instructions - Add gopher workflow documentation to execution-notes.txt - Include planning docs in docs/ directory
25 KiB
Gopher Integration Plan
Status: Planning Created: 2025-01-13 Goal: Publish Hugo blog posts to SDF gopherspace with
phlog: truefrontmatter flag
Overview
Enhance the marcus-web project to deploy selected blog posts to both the Hugo site (mnw.sdf.org) and the SDF gopherspace (gopher://sdf.org/1/users/mnw/). Posts are opted-in via frontmatter, converted from markdown to gopher-friendly plain text, and organized by series/category.
Current State
Hugo Site (mnw.sdf.org)
- Series: "Frank's Couch" (movies), "Fun Center" (tech), beercall content
- Frontmatter: title, date, series, summary, tags, draft
- Deploy:
remote_publish.sh→ SSH to SDF → git pull && hugo && mkhomepg -p
Existing Gopherspace (/sdf/arpa/gm/m/mnw/gopher)
- Rich structure with ASCII art headers and footers
- Categories as directories:
journal/,older/,LISA19/,hope_2024/, etc. - Dynamic CGI: latest phlog, user info via
=echodirectives - Template style established in
templates/phlogpost.txt
Design Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Opt-in mechanism | phlog: true frontmatter |
Explicit control per post |
| Category mapping | Hugo series → gopher directories | Natural organization |
| Beercall handling | One gopher file per year | Matches existing yearly logs |
| Movie poster | Link to web blog post | Gopher can't display images |
| Movie metadata | ASCII table | Old-school aesthetic |
| ASCII art headers | Category-specific art | Visual distinction, same size as current |
| Root gophermap | Script-managed with makeover | Cohesive, automated experience |
| Sync strategy | rsync with --delete | Git-tracked, reversible |
Directory Structure
On SDF (~/gopher aka /sdf/arpa/gm/m/mnw/gopher)
~/gopher/
├── gophermap # Root menu (UPDATED by script)
├── journal/ # Existing daily phlogs
├── older/ # Existing historical
├── LISA19/ # Existing conference notes
├── hope_2024/ # Existing conference notes
├── blog/ # NEW: Hugo-sourced content
│ ├── gophermap # Blog index
│ ├── franks-couch/ # Movie reviews
│ │ ├── gophermap # Category index with summaries
│ │ ├── megalopolis.txt
│ │ ├── joker-folie-a-deux.txt
│ │ └── ...
│ ├── fun-center/ # Tech posts
│ │ ├── gophermap
│ │ ├── is-it-me.txt
│ │ └── ...
│ └── beercalls/ # Yearly beer logs
│ ├── gophermap
│ ├── 2023.txt
│ └── 2024.txt
└── templates/ # Existing templates
Local Build Directory
marcus-web/
├── gopher_build/ # Staging area (gitignored)
│ └── blog/
│ ├── gophermap
│ ├── franks-couch/
│ ├── fun-center/
│ └── beercalls/
Frontmatter Extension
Add phlog field to any post:
---
title: 'Megalopolis'
date: 2024-10-05T00:03:15Z
draft: false
series: "Frank's Couch"
summary: "A sprawling mess of ambition"
phlog: true # <-- NEW: publish to gopher
imdb: "tt10128846"
poster: "/images/posters/megalopolis.jpg"
year: 2024
runtime: 138
director: "Francis Ford Coppola"
genres:
- Drama
- Sci-Fi
tags:
- alamo-drafthouse
---
Behavior:
phlog: true→ Convert and deploy to gopherphlog: falseor absent → Hugo only (web exclusive)
Content Conversion
Markdown → Gopher Text
| Markdown | Gopher |
|---|---|
# Heading |
═══════════════════════════════════════ HEADING═══════════════════════════════════════ |
## Subheading |
--- Subheading --- |
**bold** |
*bold* |
*italic* |
_italic_ |
[text](url) |
text [1] + links section at bottom |
{{< imdbposter >}} |
ASCII metadata table (see below) |
| Code blocks | Preserve, indent 4 spaces |
--- (hr) |
──────────────────────────────────────── |
Line Wrapping
- Wrap at 70 characters
- Preserve code block formatting
- Handle URLs gracefully (collect for footer)
Movie Metadata Table
Convert the {{< imdbposter >}} shortcode to ASCII table:
┌─────────────────────────────────────────────────────────────────┐
│ MEGALOPOLIS (2024) │
├─────────────────────────────────────────────────────────────────┤
│ Director: Francis Ford Coppola │
│ Runtime: 138 minutes │
│ Genres: Drama, Sci-Fi │
│ │
│ View on web: https://mnw.sdf.org/posts/megalopolis/ │
└─────────────────────────────────────────────────────────────────┘
ASCII Art Headers
Each category gets unique ASCII art. Size constraint: No larger than current template (~12 lines max).
Fun Center (Tech)
___ ___ _
| __| _ _ _ / __|___ _ _| |_ ___ _ _
| _| || | ' \ | (__/ -_) ' \ _/ -_) '_|
|_| \_,_|_||_| \___\___|_||_\__\___|_|
Tech posts from the Double Lunch Dispatch
Frank's Couch (Movies)
___ _ _ ___ _
| __| _ __ _ _ _| |_( )___ / __|___ _ _ __| |_
| _| '_/ _` | ' \ / /|_-< | (__/ _ \ || (_-< ' \
|_||_| \__,_|_||_\_\ /__/ \___\___/\_,_/__/_||_|
Movie reviews from the couch
Beercalls (Beer)
___ _ _
| _ ) ___ ___ _ _ __ __ _| | |___
| _ \/ -_) -_) '_/ _/ _` | | (_-<
|___/\___\___|_| \__\__,_|_|_/__/
Thursday night adventures in Austin
Blog Index
___ _ ___ _ _
| _ )| |___ __ | __|_ _ | |_ _ _ (_)___ ___
| _ \| / _ \/ _|| _|| ' \| _| '_|| / -_|_-<
|___/|_\___/\__||___|_||_|\__|_| |_\___/__/
Posts from mnw.sdf.org
Gophermap Formats
Category Gophermap (e.g., blog/franks-couch/gophermap)
i ___ _ _ ___ _ fake (NULL) 0
i | __| _ __ _ _ _| |_( )___ / __|___ _ _ __| |_ fake (NULL) 0
i | _| '_/ _` | ' \ / /|_-< | (__/ _ \ || (_-< ' \ fake (NULL) 0
i |_||_| \__,_|_||_\_\ /__/ \___\___/\_,_/__/_||_| fake (NULL) 0
i fake (NULL) 0
i Movie reviews from the couch fake (NULL) 0
i fake (NULL) 0
i──────────────────────────────────────────────────── fake (NULL) 0
i fake (NULL) 0
0[2024-10-05] Megalopolis megalopolis.txt
i A sprawling mess of ambition fake (NULL) 0
i fake (NULL) 0
0[2024-10-07] Joker: Folie a Deux joker-folie-a-deux.txt
i Not what you'd expect fake (NULL) 0
i fake (NULL) 0
0[2024-10-12] Terrifier 3 terrifier-3.txt
i Art the Clown returns fake (NULL) 0
i fake (NULL) 0
i──────────────────────────────────────────────────── fake (NULL) 0
1Back to Blog Index ../
1Back to Main Menu /users/mnw/
Blog Index Gophermap (blog/gophermap)
i ___ _ ___ _ _ fake (NULL) 0
i | _ )| |___ __ | __|_ _ | |_ _ _ (_)___ ___ fake (NULL) 0
i | _ \| / _ \/ _|| _|| ' \| _| '_|| / -_|_-< fake (NULL) 0
i |___/|_\___/\__||___|_||_|\__|_| |_\___/__/ fake (NULL) 0
i fake (NULL) 0
i Posts from mnw.sdf.org fake (NULL) 0
i The Double Lunch Dispatch fake (NULL) 0
i fake (NULL) 0
i──────────────────────────────────────────────── fake (NULL) 0
i fake (NULL) 0
1Frank's Couch - Movie Reviews franks-couch/
i X posts about films watched from the couch fake (NULL) 0
i fake (NULL) 0
1Fun Center - Tech Posts fun-center/
i X posts about technology and open source fake (NULL) 0
i fake (NULL) 0
1Beercalls - Thursday Night Adventures beercalls/
i Yearly logs of Austin beer adventures fake (NULL) 0
i fake (NULL) 0
i──────────────────────────────────────────────── fake (NULL) 0
hView on the web URL:https://mnw.sdf.org/posts/
1Back to Main Menu /users/mnw/
Root Gophermap Makeover
Update the main gophermap to incorporate blog content while preserving existing sections.
Proposed Structure
i fake (NULL) 0
i ''~`` fake (NULL) 0
i ( o o ) fake (NULL) 0
i+-------------------.oooO--(_)--Oooo.---------------+ fake (NULL) 0
i| | fake (NULL) 0
i| THE DOUBLE LUNCH DISPATCH | fake (NULL) 0
i| mnw @ sdf | fake (NULL) 0
i| | fake (NULL) 0
i+---------------------------------------------------+ fake (NULL) 0
i fake (NULL) 0
imnw(at)sdf.org | SDF VOIP Ext. 1908 fake (NULL) 0
i fake (NULL) 0
i═══════════════════════════════════════════════════ fake (NULL) 0
i DAILY PHLOGS fake (NULL) 0
i═══════════════════════════════════════════════════ fake (NULL) 0
i fake (NULL) 0
1Daily Journal journal/
=echo "0Latest Entry journal/`ls -t journal/ | grep -v gophermap | head -1`"
1Historical Phlogs (2018-2019) older/
i fake (NULL) 0
i═══════════════════════════════════════════════════ fake (NULL) 0
i FROM THE WEB fake (NULL) 0
i═══════════════════════════════════════════════════ fake (NULL) 0
i fake (NULL) 0
iPosts from mnw.sdf.org fake (NULL) 0
i fake (NULL) 0
1Blog Categories blog/
=echo "0Latest Blog Post `find ~/gopher/blog -name '*.txt' -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | awk '{print $2}' | sed 's|.*/gopher/||'`"
i fake (NULL) 0
i═══════════════════════════════════════════════════ fake (NULL) 0
i CONFERENCE NOTES fake (NULL) 0
i═══════════════════════════════════════════════════ fake (NULL) 0
i fake (NULL) 0
iPhlogs from conferences (rough notes) fake (NULL) 0
i fake (NULL) 0
1HOPE 2024 hope_2024/
1FOSSY 2024 fossy_2024/
1LISA 2019 LISA19/
1SeaGL 2019 seagl_2019/
1Texas Linux Fest 2019 txlf_2019/
1HOPE 2018 hope_2018/
1SeaGL 2018 seagl_2018/
i fake (NULL) 0
i═══════════════════════════════════════════════════ fake (NULL) 0
i OTHER THINGS fake (NULL) 0
i═══════════════════════════════════════════════════ fake (NULL) 0
i fake (NULL) 0
1Comedy comedy/
1Short Stories stories/
i fake (NULL) 0
i .oooO fake (NULL) 0
i ( ) Oooo. fake (NULL) 0
i+-------------------\ (----( )-----------------+ fake (NULL) 0
i \_) ) / fake (NULL) 0
i (_/ fake (NULL) 0
i fake (NULL) 0
iMy SDF User Info fake (NULL) 0
=echo "`/usr/pkg/bin/uinfo mnw | tr -d '\-=><'`"
i fake (NULL) 0
i __^__ __^__ fake (NULL) 0
i( ___ )--------------------------------------------( ___ ) fake (NULL) 0
i | / | This gopher space proudly hosted by SDF | \ | fake (NULL) 0
i | / | mnw on pixelfed.social and tilde.zone | \ | fake (NULL) 0
i |___| Web: mnw.sdf.org |___| fake (NULL) 0
i(_____)--------------------------------------------(_____) fake (NULL) 0
i fake (NULL) 0
iThanks for reading! Enjoy an SDF oneliner: fake (NULL) 0
=echo "`/usr/pkg/bin/oneliner | tr -d '\-=><'`"
Post Template
Each converted post follows this structure:
___ ___ ___
/\ \ /\ \ /\ \
|::\ \ \:\ \ _\:\ \
|:|:\ \ \:\ \ /\ \:\ \
__|:|\:\ \ _____\:\ \ _\:\ \:\ \
/::::|_\:\__\ /::::::::\__\ /\ \:\ \:\__\
\:\~~\ \/__/ \:\~~\~~\/__/ \:\ \:\/:/ /
\:\ \ \:\ \ \:\ \::/ /
\:\ \ \:\ \ \:\/:/ /
\:\__\ \:\__\ \::/ /
\/__/ \/__/ \/__/
mnw(at)sdf.org | SDF VOIP Ext. 1908
────────────────────────────────────────────────────────────────────
2024-11-19 | Fun Center
────────────────────────────────────────────────────────────────────
IS IT ME?
Marcus shares his blog writing process.
────────────────────────────────────────────────────────────────────
I've always loved talking to people. Whether it's at work, at
home, or out with friends, connecting with others and hearing
about their passions brings me so much joy. There's nothing
better than a conversation with someone excited to share what
they care about.
[... content continues, wrapped at 70 characters ...]
═══════════════════════════════════════════════════════════════════
LINKS
═══════════════════════════════════════════════════════════════════
[1] https://example.com/macwhisper
[2] https://openai.com/chatgpt
__^__ __^__
( ___ )------------------------------------------------------( ___ )
| / | | \ |
| / | This gopher space proudly hosted by SDF | \ |
| \ | mnw on pixelfed.social and tilde.zone | / |
| \ | | / |
(_____)------------------------------------------------------(_____)
Web version: https://mnw.sdf.org/posts/is-it-me/
New Scripts
1. scripts/gopher/convert_to_gopher.py
Purpose: Convert Hugo markdown to gopher-formatted text
Usage:
python scripts/gopher/convert_to_gopher.py content/posts/blog-posting.md
python scripts/gopher/convert_to_gopher.py --all
python scripts/gopher/convert_to_gopher.py --all --output gopher_build/
Responsibilities:
- Parse YAML frontmatter
- Check
phlog: trueflag - Determine output directory from series
- Convert markdown syntax
- Handle
{{< imdbposter >}}→ ASCII table - Word wrap to 70 chars
- Apply header/footer templates
- Collect and format links section
2. scripts/gopher/generate_gophermaps.py
Purpose: Generate all gophermap files
Usage:
python scripts/gopher/generate_gophermaps.py
python scripts/gopher/generate_gophermaps.py --output gopher_build/
Responsibilities:
- Scan converted posts by category
- Generate category gophermaps with titles + summaries
- Generate blog index gophermap
- Count posts per category
- Sort by date (newest first)
3. scripts/gopher/ascii_art.py
Purpose: ASCII art assets and helper functions
Contents:
- Category header art (Fun Center, Frank's Couch, Beercalls)
- Post header template (MNW owl)
- Footer template (SDF box)
- Section dividers
- ASCII table generator for movie metadata
4. scripts/gopher/update_root_gophermap.py
Purpose: Update the main gophermap with blog section
Usage:
python scripts/gopher/update_root_gophermap.py
Responsibilities:
- Read existing root gophermap (preserve conference notes, etc.)
- Insert/update "FROM THE WEB" section
- Update dynamic CGI for latest blog post
- Optionally apply full makeover template
Modified Scripts
scripts/remote_publish.sh
Add gopher deployment support:
#!/bin/bash
# Remote publish script for marcus-web
GOPHER=false
GOPHER_ONLY=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--gopher)
GOPHER=true
shift
;;
--gopher-only)
GOPHER_ONLY=true
GOPHER=true
shift
;;
*)
shift
;;
esac
done
# ... existing git/commit logic ...
if [[ "$GOPHER_ONLY" != "true" ]]; then
# Existing Hugo deployment
ssh "$SDF_HOST" "cd $REMOTE_DIR && git pull && hugo && mkhomepg -p"
echo "Site is live at: https://mnw.sdf.org/"
fi
if [[ "$GOPHER" == "true" ]]; then
echo -e "${GREEN}=== Deploying to Gopher ===${NC}"
# Build gopher content locally
python scripts/gopher/convert_to_gopher.py --all --output gopher_build/blog/
python scripts/gopher/generate_gophermaps.py --output gopher_build/blog/
# Sync to SDF (--delete removes posts where phlog was set to false)
rsync -avz --delete gopher_build/blog/ "$SDF_HOST:~/gopher/blog/"
# Fix permissions on SDF
ssh "$SDF_HOST" "find ~/gopher/blog -type f -exec chmod 644 {} \; && find ~/gopher/blog -type d -exec chmod 755 {} \;"
echo "Gopher updated at: gopher://sdf.org/1/users/mnw/blog/"
fi
scripts/new_techpost.py
Add phlog prompt:
# After getting summary...
phlog_input = input("\nPublish to gopher phlog? (y/N): ").strip().lower()
phlog = phlog_input == 'y'
# In frontmatter:
content = f'''---
title: '{title}'
date: {now}
draft: true
series: "Fun Center"
summary: "{summary}"
phlog: {str(phlog).lower()}
tags:
{tags_yaml}
---
'''
scripts/new_movie.py / scripts/import_letterboxd.py
Same pattern - add phlog prompt after other inputs.
Deployment Workflow
Standard Deploy (Hugo only)
./scripts/remote_publish.sh
Deploy Hugo + Gopher
./scripts/remote_publish.sh --gopher
Deploy Gopher Only
./scripts/remote_publish.sh --gopher-only
One-time Root Gophermap Update
# After first blog/ deployment
python scripts/gopher/update_root_gophermap.py
scp gopher_build/gophermap mnw@sdf.org:~/gopher/gophermap
Implementation Order
Phase 1: Core Infrastructure
- Create
scripts/gopher/directory - Create
scripts/gopher/ascii_art.py- All ASCII art assets - Create
scripts/gopher/convert_to_gopher.py- Markdown conversion - Add
gopher_build/to.gitignore
Phase 2: Gophermap Generation
- Create
scripts/gopher/generate_gophermaps.py- Category + index maps - Create
scripts/gopher/update_root_gophermap.py- Root gophermap
Phase 3: Deployment Integration
- Modify
scripts/remote_publish.sh- Add --gopher flags - Test with single post locally
Phase 4: Post Creation Updates
- Modify
scripts/new_techpost.py- Add phlog prompt - Modify
scripts/import_letterboxd.py- Add phlog prompt - Modify
scripts/new_movie.py- Add phlog prompt (if separate)
Phase 5: Content & Testing
- Add
phlog: trueto existing posts to backfill - Run full deployment to SDF
- Verify gopher content via
gopher gopher://sdf.org/1/users/mnw/blog/ - Update root gophermap on SDF
Phase 6: Polish
- Fine-tune ASCII art sizing
- Adjust line wrapping edge cases
- Document in README or execution-notes.txt
File Checklist
New Files
scripts/gopher/ascii_art.pyscripts/gopher/convert_to_gopher.pyscripts/gopher/generate_gophermaps.pyscripts/gopher/update_root_gophermap.pygopher_build/directory (gitignored)
Modified Files
scripts/remote_publish.shscripts/new_techpost.pyscripts/import_letterboxd.py.gitignore(add gopher_build/)
Content Files (add phlog: true)
content/posts/blog-posting.mdcontent/posts/recovering-failed-ubuntu-upgrade.md- Movie posts as desired
- Beercall yearly files as desired
URLs
After deployment:
| Type | URL |
|---|---|
| Gopher root | gopher://sdf.org/1/users/mnw/ |
| Blog index | gopher://sdf.org/1/users/mnw/blog/ |
| Fun Center | gopher://sdf.org/1/users/mnw/blog/fun-center/ |
| Frank's Couch | gopher://sdf.org/1/users/mnw/blog/franks-couch/ |
| Beercalls | gopher://sdf.org/1/users/mnw/blog/beercalls/ |
| Web proxy | https://gopher.floodgap.com/gopher/gw?a=gopher://sdf.org/1/users/mnw/ |
References
- SDF Gopher Setup: https://wiki.sdf.org/doku.php?id=gopher_site_setup_and_hosting_features
- SDF Web Spaces: https://wiki.sdf.org/doku.php?id=accessing_web_spaces#gopher
- Tilde.town Gopher Guide: https://tilde.town/wiki/editing-your-homepage/gopher.html
- Floodgap Gopher Proxy: https://gopher.floodgap.com/gopher/gw
Gophermap Quick Reference
From tilde.town wiki - item types we'll use:
| Code | Type | Usage |
|---|---|---|
0 |
Text file | Blog posts (.txt) |
1 |
Directory | Category folders, navigation |
i |
Inline text | ASCII art, descriptions, spacing |
h |
HTML link | Link to web version (prefix with URL:) |
7 |
Search | Future: search posts (CGI) |
Syntax: {type}{description}\t{path}\t{host}\t{port}
- Tab character separates columns
- Host/port optional for local resources
- For inline text (
i): usefakeas path,(NULL)as host,0as port - For HTML links: path should be
URL:https://...
Example entries:
i fake (NULL) 0
0[2024-10-05] Megalopolis megalopolis.txt
1Back to Blog Index ../
hView on web URL:https://mnw.sdf.org/posts/
Notes
- Gophermaps must NOT have execute permission (causes errors)
- Files need 644, directories need 755
- CGI scripts (=echo) must use full paths on SDF
- ASCII art should stay under 12 lines for headers
- The
--deleteflag on rsync will remove posts wherephlogwas changed tofalse - All work is git-tracked, so deletions are recoverable