# My new post script

> Source: <https://jva.lol/weblog/testing-my-new-post-script/>
> Published: 2026-07-25 04:08:48+00:00

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

``` bash
#!/usr/bin/env bash
#
# new-post — scaffold a weblog post interactively.
#
# Prompts for the front matter this blog actually uses (title, slug, bundle,
# description, summary, categories, tags), writes the post with a correct
# RFC3339 date and an empty popsicleboat: line, and leaves the body to you.
# Committing the post later triggers scripts/companion-prompt, which asks for
# the thread's space and wires the discussion thread (spec 0006).

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

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

# Quote a YAML scalar: double quotes unless the value has one, then single.
yaml_quote() {
  case $1 in
    *\"*\'* | *\'*\"*) die "can't hold both quote kinds in front matter: $1" ;;
    *\"*) printf "'%s'" "$1" ;;
    *) printf '"%s"' "$1" ;;
  esac
}

# Comma-separated answer -> YAML list lines ("- item" per entry).
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

``` bash
#!/usr/bin/env bash
#
# companion-prompt — wire a new post's companion thread at commit time.
#
# For each NEW post staged in this commit (content/weblog/*.md or a new
# bundle's index.md) whose `popsicleboat:` field is empty, prompt for the
# thread's space and an optional per-post thread title, write the answers
# into the front matter, run scripts/companion-thread, and re-stage the
# post — so the commit lands with its Discuss thread created and wired.
#
# Never blocks a commit. Degraded paths (no terminal, no POPSICLEBOAT_TOKEN,
# empty space answer, API failure) print how to wire the thread later and let
# the commit through. Skip the hook with COMPANION_PROMPT=0, or --no-verify.
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

# Posts that already carry a thread URL (or no field at all) need no prompt.
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
}

# -r /dev/tty lies without a controlling terminal (macOS: open fails with
# "Device not configured"), so probe by actually opening it.
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 a front-matter line right after the popsicleboat: field.
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.
