My new post script

Projects

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

Replies from Popsicle Boat

jake · edited

This was basically

1- one CLI command
2- edit the boilerplate for the blog post with content
3- git push

This comment section was setup automatically. This comment appears on the actual blog post automatically. Proving the whole thing not only works, but almost completely effortlessly.

Reply

Replies come from Popsicle Boat members. Accounts are required, it’s why there’s no spam. You’ll sign in there (or create an account, it takes a short minute), confirm before it posts, and land right back here after.

Left one of the replies above? It stays yours — you can edit or delete it in the thread, and this page follows on its next build.

512KB Club Orange Team member