cd /news/developer-tools/my-new-post-script · home topics developer-tools article
[ARTICLE · art-99349] src=jva.lol ↗ pub= topic=developer-tools verified=true sentiment=· neutral

My new post script

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.

read5 min views2 publishedJul 25, 2026
My new post script
Image: Jva (auto-discovered)

I asked AI to draft this for me. I've edited it. Any surviving AI smells are my oversight.

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

Not 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!

Boilerplate, fresh. Kind of like making waffles. Get it? Hehe.

New Post

#!/usr/bin/env bash
#
#

set -euo pipefail
cd "$(git rev-parse --show-toplevel)"

die() { echo "new-post: $1" >&2; exit 1; }

yaml_quote() {
  case $1 in
    *\"*\'* | *\'*\"*) die "can't hold both quote kinds in front matter: $1" ;;
    *\"*) printf "'%s'" "$1" ;;
    *) printf '"%s"' "$1" ;;
  esac
}

yaml_list() {
  local IFS=',' item
  for item in $1; do
    item=$(printf '%s' "$item" | sed -E 's/^ +| +$//g')
    [ -n "$item" ] && printf -- "- %s\n" "$item"
  done
}

read -r -p "Title: " title
[ -n "$title" ] || die "a post needs a title"

default_slug=$(printf '%s' "$title" |
  tr '[:upper:]' '[:lower:]' |
  sed -E "s/'//g; s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//")
read -r -p "Slug [$default_slug]: " slug
slug=${slug:-$default_slug}
[ -n "$slug" ] || die "a post needs a slug"

read -r -p "Page bundle, for images alongside the post? [y/N]: " bundle
case $bundle in
  [yY]*) post="content/weblog/$slug/index.md" ;;
  *) post="content/weblog/$slug.md" ;;
esac
[ -e "$post" ] && die "$post already exists"

read -r -p "Description (meta/OG line; blank to fill in later): " description
read -r -p "Summary (list-page line; blank to fill in later): " summary
read -r -p "Categories, comma-separated [Projects]: " categories
categories=${categories:-Projects}
read -r -p "Tags, comma-separated (e.g. popsicleboat, hugo): " tags

ts=$(date "+%Y-%m-%dT%H:%M:%S%z")
ts="${ts:0:22}:${ts:22}" # RFC3339 wants a colon in the offset

mkdir -p "$(dirname "$post")"
{
  echo "---"
  echo "title: $(yaml_quote "$title")"
  echo "description: $(yaml_quote "$description")"
  echo "date: $ts"
  echo "summary: $(yaml_quote "$summary")"
  echo 'popsicleboat: "" # wired at commit by scripts/companion-prompt'
  echo "categories:"
  yaml_list "$categories"
  if [ -n "$tags" ]; then
    echo "tags:"
    yaml_list "$tags"
  fi
  echo "---"
  echo ""
} > "$post"

echo ""
echo "Created $post — write the essay."
echo "Committing it will prompt for its companion thread (empty space answer skips)."

Companion Prompt

#!/usr/bin/env bash
#
#
#
set -uo pipefail
cd "$(git rev-parse --show-toplevel)"

[ "${COMPANION_PROMPT:-1}" = "0" ] && exit 0

new_posts=$(git diff --cached --name-only --diff-filter=A -- \
  'content/weblog/*.md' 'content/weblog/*/index.md' | grep -v '_index\.md$' || true)
[ -z "$new_posts" ] && exit 0

needs_thread() {
  grep -qE '^popsicleboat: *("")? *(#.*)?$' "$1"
}

pending=()
while IFS= read -r post; do
  [ -f "$post" ] && needs_thread "$post" && pending+=("$post")
done <<< "$new_posts"
[ "${#pending[@]}" -eq 0 ] && exit 0

later() { # how to finish the job after a degraded path
  echo "companion-prompt: wire it later with: scripts/companion-thread $1 --space <space>" >&2
}

if ! { : < /dev/tty; } 2>/dev/null; then
  echo "companion-prompt: no terminal — committing without companion threads." >&2
  for post in "${pending[@]}"; do later "$post"; done
  exit 0
fi

if [ -z "${POPSICLEBOAT_TOKEN:-}" ]; then
  echo "companion-prompt: POPSICLEBOAT_TOKEN not set — committing without companion threads." >&2
  for post in "${pending[@]}"; do later "$post"; done
  exit 0
fi

insert_after_popsicleboat() {
  local file=$1 line=$2
  awk -v add="$line" '{ print } !done && /^popsicleboat:/ { print add; done = 1 }' \
    "$file" > "$file.tmp" && mv "$file.tmp" "$file"
}

for post in "${pending[@]}"; do
  echo ""
  echo "New post: $post"
  read -r -p "  PopsicleBoat space for its thread (empty to skip): " space < /dev/tty
  if [ -z "$space" ]; then
    later "$post"
    continue
  fi

  read -r -p "  Thread title override (empty for the template default): " title < /dev/tty

  insert_after_popsicleboat "$post" "popsicleboat_space: \"$space\""
  if [ -n "$title" ]; then
    case $title in
      *\"*\'* | *\'*\"*)
        echo "  title has both quote kinds — front matter can't hold that; using the template default." >&2
        ;;
      *\"*)
        insert_after_popsicleboat "$post" "popsicleboat_title: '$title'"
        ;;
      *)
        insert_after_popsicleboat "$post" "popsicleboat_title: \"$title\""
        ;;
    esac
  fi

  if scripts/companion-thread "$post"; then
    git add "$post"
  else
    echo "companion-prompt: thread creation failed — committing the post unwired." >&2
    later "$post"
    git add "$post" # keep the space/title answers we wrote either way
  fi
done

Your reply is in the thread. It’ll appear right here in about a minute.

── more in #developer-tools 4 stories · sorted by recency
── more on @hugo 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/my-new-post-script] indexed:0 read:5min 2026-07-25 ·