My new post script
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!
New Post
#!/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
#!/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