{"slug": "my-new-post-script", "title": "My new post script", "summary": "A developer published a blog post and two shell scripts, new-post and companion-prompt, that automate scaffolding Hugo weblog posts and wiring comment threads on Popsicle Boat. The scripts prompt for front matter, generate RFC3339 dates, and integrate with a commit-time hook to create discussion threads per spec 0006.", "body_md": "I asked AI to draft this for me. I've edited it. Any surviving AI smells are my oversight.\n\nWell, that did something.Keep going.Nothing is hidden on this page, but some Weblog pages have letters that burn brighter than others.Congratulations! Hope you enjoyed the fire! 🔥 The fire isn't everything, though. One more click starts it over. When you've found and solved the real puzzle, you'll know.\n\nNot a lot to it, just scripts/new-post and follow the prompts to setup the boilerplate. Then write the content. Then to wire up comments on Popsicle Boat, scripts/companion-prompt That’s it!\n\nBoilerplate, fresh. Kind of like making waffles. Get it? Hehe.\n\nNew Post\n\n``` bash\n#!/usr/bin/env bash\n#\n# new-post — scaffold a weblog post interactively.\n#\n# Prompts for the front matter this blog actually uses (title, slug, bundle,\n# description, summary, categories, tags), writes the post with a correct\n# RFC3339 date and an empty popsicleboat: line, and leaves the body to you.\n# Committing the post later triggers scripts/companion-prompt, which asks for\n# the thread's space and wires the discussion thread (spec 0006).\n\nset -euo pipefail\ncd \"$(git rev-parse --show-toplevel)\"\n\ndie() { echo \"new-post: $1\" >&2; exit 1; }\n\n# Quote a YAML scalar: double quotes unless the value has one, then single.\nyaml_quote() {\n  case $1 in\n    *\\\"*\\'* | *\\'*\\\"*) die \"can't hold both quote kinds in front matter: $1\" ;;\n    *\\\"*) printf \"'%s'\" \"$1\" ;;\n    *) printf '\"%s\"' \"$1\" ;;\n  esac\n}\n\n# Comma-separated answer -> YAML list lines (\"- item\" per entry).\nyaml_list() {\n  local IFS=',' item\n  for item in $1; do\n    item=$(printf '%s' \"$item\" | sed -E 's/^ +| +$//g')\n    [ -n \"$item\" ] && printf -- \"- %s\\n\" \"$item\"\n  done\n}\n\nread -r -p \"Title: \" title\n[ -n \"$title\" ] || die \"a post needs a title\"\n\ndefault_slug=$(printf '%s' \"$title\" |\n  tr '[:upper:]' '[:lower:]' |\n  sed -E \"s/'//g; s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//\")\nread -r -p \"Slug [$default_slug]: \" slug\nslug=${slug:-$default_slug}\n[ -n \"$slug\" ] || die \"a post needs a slug\"\n\nread -r -p \"Page bundle, for images alongside the post? [y/N]: \" bundle\ncase $bundle in\n  [yY]*) post=\"content/weblog/$slug/index.md\" ;;\n  *) post=\"content/weblog/$slug.md\" ;;\nesac\n[ -e \"$post\" ] && die \"$post already exists\"\n\nread -r -p \"Description (meta/OG line; blank to fill in later): \" description\nread -r -p \"Summary (list-page line; blank to fill in later): \" summary\nread -r -p \"Categories, comma-separated [Projects]: \" categories\ncategories=${categories:-Projects}\nread -r -p \"Tags, comma-separated (e.g. popsicleboat, hugo): \" tags\n\nts=$(date \"+%Y-%m-%dT%H:%M:%S%z\")\nts=\"${ts:0:22}:${ts:22}\" # RFC3339 wants a colon in the offset\n\nmkdir -p \"$(dirname \"$post\")\"\n{\n  echo \"---\"\n  echo \"title: $(yaml_quote \"$title\")\"\n  echo \"description: $(yaml_quote \"$description\")\"\n  echo \"date: $ts\"\n  echo \"summary: $(yaml_quote \"$summary\")\"\n  echo 'popsicleboat: \"\" # wired at commit by scripts/companion-prompt'\n  echo \"categories:\"\n  yaml_list \"$categories\"\n  if [ -n \"$tags\" ]; then\n    echo \"tags:\"\n    yaml_list \"$tags\"\n  fi\n  echo \"---\"\n  echo \"\"\n} > \"$post\"\n\necho \"\"\necho \"Created $post — write the essay.\"\necho \"Committing it will prompt for its companion thread (empty space answer skips).\"\n```\n\nCompanion Prompt\n\n``` bash\n#!/usr/bin/env bash\n#\n# companion-prompt — wire a new post's companion thread at commit time.\n#\n# For each NEW post staged in this commit (content/weblog/*.md or a new\n# bundle's index.md) whose `popsicleboat:` field is empty, prompt for the\n# thread's space and an optional per-post thread title, write the answers\n# into the front matter, run scripts/companion-thread, and re-stage the\n# post — so the commit lands with its Discuss thread created and wired.\n#\n# Never blocks a commit. Degraded paths (no terminal, no POPSICLEBOAT_TOKEN,\n# empty space answer, API failure) print how to wire the thread later and let\n# the commit through. Skip the hook with COMPANION_PROMPT=0, or --no-verify.\nset -uo pipefail\ncd \"$(git rev-parse --show-toplevel)\"\n\n[ \"${COMPANION_PROMPT:-1}\" = \"0\" ] && exit 0\n\nnew_posts=$(git diff --cached --name-only --diff-filter=A -- \\\n  'content/weblog/*.md' 'content/weblog/*/index.md' | grep -v '_index\\.md$' || true)\n[ -z \"$new_posts\" ] && exit 0\n\n# Posts that already carry a thread URL (or no field at all) need no prompt.\nneeds_thread() {\n  grep -qE '^popsicleboat: *(\"\")? *(#.*)?$' \"$1\"\n}\n\npending=()\nwhile IFS= read -r post; do\n  [ -f \"$post\" ] && needs_thread \"$post\" && pending+=(\"$post\")\ndone <<< \"$new_posts\"\n[ \"${#pending[@]}\" -eq 0 ] && exit 0\n\nlater() { # how to finish the job after a degraded path\n  echo \"companion-prompt: wire it later with: scripts/companion-thread $1 --space <space>\" >&2\n}\n\n# -r /dev/tty lies without a controlling terminal (macOS: open fails with\n# \"Device not configured\"), so probe by actually opening it.\nif ! { : < /dev/tty; } 2>/dev/null; then\n  echo \"companion-prompt: no terminal — committing without companion threads.\" >&2\n  for post in \"${pending[@]}\"; do later \"$post\"; done\n  exit 0\nfi\n\nif [ -z \"${POPSICLEBOAT_TOKEN:-}\" ]; then\n  echo \"companion-prompt: POPSICLEBOAT_TOKEN not set — committing without companion threads.\" >&2\n  for post in \"${pending[@]}\"; do later \"$post\"; done\n  exit 0\nfi\n\n# Insert a front-matter line right after the popsicleboat: field.\ninsert_after_popsicleboat() {\n  local file=$1 line=$2\n  awk -v add=\"$line\" '{ print } !done && /^popsicleboat:/ { print add; done = 1 }' \\\n    \"$file\" > \"$file.tmp\" && mv \"$file.tmp\" \"$file\"\n}\n\nfor post in \"${pending[@]}\"; do\n  echo \"\"\n  echo \"New post: $post\"\n  read -r -p \"  PopsicleBoat space for its thread (empty to skip): \" space < /dev/tty\n  if [ -z \"$space\" ]; then\n    later \"$post\"\n    continue\n  fi\n\n  read -r -p \"  Thread title override (empty for the template default): \" title < /dev/tty\n\n  insert_after_popsicleboat \"$post\" \"popsicleboat_space: \\\"$space\\\"\"\n  if [ -n \"$title\" ]; then\n    case $title in\n      *\\\"*\\'* | *\\'*\\\"*)\n        echo \"  title has both quote kinds — front matter can't hold that; using the template default.\" >&2\n        ;;\n      *\\\"*)\n        insert_after_popsicleboat \"$post\" \"popsicleboat_title: '$title'\"\n        ;;\n      *)\n        insert_after_popsicleboat \"$post\" \"popsicleboat_title: \\\"$title\\\"\"\n        ;;\n    esac\n  fi\n\n  if scripts/companion-thread \"$post\"; then\n    git add \"$post\"\n  else\n    echo \"companion-prompt: thread creation failed — committing the post unwired.\" >&2\n    later \"$post\"\n    git add \"$post\" # keep the space/title answers we wrote either way\n  fi\ndone\n```\n\nYour reply is in the thread. It’ll appear right here in about a minute.", "url": "https://wpnews.pro/news/my-new-post-script", "canonical_source": "https://jva.lol/weblog/testing-my-new-post-script/", "published_at": "2026-07-25 04:08:48+00:00", "updated_at": "2026-08-17 04:41:32.526154+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Hugo", "Popsicle Boat"], "alternates": {"html": "https://wpnews.pro/news/my-new-post-script", "markdown": "https://wpnews.pro/news/my-new-post-script.md", "text": "https://wpnews.pro/news/my-new-post-script.txt", "jsonld": "https://wpnews.pro/news/my-new-post-script.jsonld"}}