Automate Blog Publishing with Claude Code Commands


Publishing a blog post requires the same steps every time: summarize findings, write frontmatter, SSH to server, copy the file, run the build, verify. Repeating that manually is friction that delays publishing.

Claude Code custom commands eliminate that friction. This post covers building a system-wide /user:publish-blog command that converts any conversation into a published post on an Astro blog. You’ll learn the command architecture, the deploy script design, and how the two components stay cleanly separated.

Architecture

Two components handle this workflow.

The command file (~/.claude/commands/publish-blog.md) is a prompt Claude Code loads when you invoke /user:publish-blog. It instructs Claude to review the conversation, draft a post in your preferred style, confirm metadata with you, then call the deploy script.

The deploy script (~/Scripts/Blogging/publish-blog-post.sh) handles mechanics: SSH to the server, write the .md file, run the Astro build, verify HTTP 200. It accepts a slug as $1 and reads markdown content from stdin.

Separation matters. The command focuses on content generation. The script focuses on deployment. Each is independently testable.

The command file

Claude Code commands are Markdown files stored in ~/.claude/commands/ for system-wide scope or .claude/commands/ for project scope. The filename determines the invocation: publish-blog.md/user:publish-blog.

The prompt instructs Claude to follow a specific workflow:

1. Review the conversation for publishable technical content
2. Draft a post following the Blogger output style
3. Present title, slug, description, tags, full content for review
4. On confirmation, pipe content through the deploy script
5. Report success or failure with URL

The $ARGUMENTS variable captures anything after the command name, enabling /user:publish-blog specific topic to focus the post on a subtopic.

The deploy script

The script receives the full markdown file (frontmatter + body) on stdin and a slug as the first argument.

#!/usr/bin/env bash
set -euo pipefail

readonly SLUG="${1:-}"
readonly SERVER="trzeci"
readonly POST_DIR="~/astro-blog/src/content/blog"

# Validate inputs
[[ -z "${SLUG}" ]] && { echo "ERROR: slug required" >&2; exit 1; }
[[ "${SLUG}" =~ [^a-z0-9-] ]] && { echo "ERROR: invalid slug chars" >&2; exit 1; }

CONTENT="$(cat)"
[[ -z "${CONTENT}" ]] && { echo "ERROR: no content on stdin" >&2; exit 1; }
[[ "${CONTENT}" != ---* ]] && { echo "ERROR: missing frontmatter" >&2; exit 1; }

# Write, build, verify
echo "${CONTENT}" | ssh "${SERVER}" "cat > ${POST_DIR}/${SLUG}.md"
ssh "${SERVER}" "~/publish-blog.sh"

HTTP_STATUS="$(curl -s -o /dev/null -w '%{http_code}' https://blog.stonith.pl)"
[[ "${HTTP_STATUS}" -ne 200 ]] && { echo "ERROR: HTTP ${HTTP_STATUS}" >&2; exit 1; }

echo "Published: https://blog.stonith.pl"

Three validation checks run before any SSH connection: slug present, slug format valid, content non-empty with frontmatter. Fail fast, fail loudly.

The SSH commands run sequentially with set -euo pipefail propagating any failure up. The build step calls the server’s existing ~/publish-blog.sh, which runs npm run build and sudo cp to the Apache document root.

Calling the script from the command

Claude constructs the publish call using a heredoc to avoid escaping issues with the post content:

cat <<'BLOG_POST_EOF' | ~/Scripts/Blogging/publish-blog-post.sh my-post-slug
---
title: 'Post Title'
pubDate: 2026-03-07
description: 'Post description for SEO.'
author: 'Artur Kaminski'
tags: ["tag1", "tag2"]
---

Post body here.