#!/bin/bash # WordPress posting script # config: EDITOR=vim TRANSFORM=() # empty or a subset of: 'title' 'markdown' USER="" # WP user to create the post PASSWORD="" # application password generated for your WP user SERVER="" # server hostname, optionally with subdirectories STATUS="draft" # one of publish,future,draft,pending,private CATEGORIES="" # comma separated integer IDs of categories TAGS="" # comma separated integer IDs of tags TMPFILE=/tmp/wordpress-post.txt # location of a temporary file with the post text USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) wordpress-rest-curl/1.0" # some hosts block curl's default UA as bot traffic SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/config.sh" # CLI options: # --site NAME after config.sh, also source $CONFIG_DIR/NAME.sh, whose # values override config.sh's (lets you keep just the # per-instance bits — USER/PASSWORD/SERVER — in each site # file, and shared defaults in config.sh) # --edit POST_ID update an existing post instead of creating a new one # --publish shortcut for STATUS=publish # --file PATH use PATH as the post source instead of opening $EDITOR # --date DATE backdate the post to DATE, an ISO 8601 UTC timestamp # (e.g. 2026-07-27T10:00:00Z, matching a Gitea commit # date) — sent as the post's date_gmt # --category NAME attach category NAME, looking up its ID or creating it # if it doesn't exist yet (repeatable) # --tag NAME same as --category, for tags (repeatable) SITE="" POST_ID="" SOURCE_FILE="" PUBLISH=0 DATE="" CATEGORY_NAMES=() TAG_NAMES=() while [[ $# -gt 0 ]]; do case "$1" in --site) SITE="$2"; shift 2 ;; --edit) POST_ID="$2"; shift 2 ;; --publish) PUBLISH=1; shift ;; --file) SOURCE_FILE="$2"; shift 2 ;; --date) DATE="$2"; shift 2 ;; --category) CATEGORY_NAMES+=("$2"); shift 2 ;; --tag) TAG_NAMES+=("$2"); shift 2 ;; *) echo "Unknown argument: $1" >&2 exit 1 ;; esac done wp_load_config [[ "$PUBLISH" -eq 1 ]] && STATUS="publish" # let the user provide the post content if [[ -n "$SOURCE_FILE" ]]; then cp "$SOURCE_FILE" "$TMPFILE" || exit 1 else $EDITOR $TMPFILE || exit 1 fi [[ -e $TMPFILE ]] || exit 1 # transformations cp $TMPFILE $TMPFILE.trans for T in "${TRANSFORM[@]}"; do if [[ "$T" == "title" ]]; then python3 >$TMPFILE.trans2 <$TMPFILE.trans2 < 0) | tonumber' | jq -s '.') fi for NAME in "${CATEGORY_NAMES[@]}"; do ID=$(wp_resolve_term "/wp-json/wp/v2/categories" "$NAME") || exit 1 CATEGORIES_JSON=$(jq -c --argjson id "$ID" '. + [$id] | unique' <<<"$CATEGORIES_JSON") done TAGS_JSON="[]" if [[ -n "$TAGS" ]]; then TAGS_JSON=$(echo "$TAGS" | tr ',' '\n' | jq -R 'select(length > 0) | tonumber' | jq -s '.') fi for NAME in "${TAG_NAMES[@]}"; do ID=$(wp_resolve_term "/wp-json/wp/v2/tags" "$NAME") || exit 1 TAGS_JSON=$(jq -c --argjson id "$ID" '. + [$id] | unique' <<<"$TAGS_JSON") done PAYLOAD=$(jq -n \ --arg title "$TITLE" \ --arg content "$CONTENT" \ --arg status "$STATUS" \ --argjson categories "$CATEGORIES_JSON" \ --argjson tags "$TAGS_JSON" \ --arg date_gmt "${DATE%Z}" \ '{title: $title, content: $content, status: $status, categories: $categories, tags: $tags} + (if $date_gmt != "" then {date_gmt: $date_gmt} else {} end)') POST_PATH="/wp-json/wp/v2/posts/" [[ -n "$POST_ID" ]] && POST_PATH="/wp-json/wp/v2/posts/$POST_ID" # push the post! BODY=$(wp_api_call POST "$POST_PATH" -H "Content-Type: application/json" --data "$PAYLOAD") || exit 1 echo "$BODY" # backup the posted data (temporarily until it is auto-removed) mv $TMPFILE $TMPFILE.posted