Make the image optional for WordPress posts too

Some blog posts just don't have one. WordPressPublisher no longer
rejects publishing a type=blog post without image_url — it uploads
and attaches it as the featured image when present, and simply skips
that step (no featured_media set) otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
m105
2026-07-21 20:07:47 +03:00
co-authored by Claude Sonnet 5
parent 42abc658f2
commit 4b3af4cc89
5 changed files with 70 additions and 45 deletions
+15 -12
View File
@@ -37,8 +37,9 @@ SocialAccount (login for a social network)
Post (one piece of content, belongs to a Site, optionally a SocialAccount)
└─ type: same vocabulary as SocialAccount.network, plus "blog"
└─ target: nullable, network-specific destination (e.g. a subreddit for type=reddit)
└─ image_url: nullable URL. Required to publish type=blog (WordPress) —
used as the featured image. Optional for every other type.
└─ image_url: nullable URL, optional for every type. If set on a
type=blog post, WordPressPublisher uploads it and uses it as the
featured image — but it's fine to publish a blog post without one.
└─ status: draft -> scheduled -> publishing -> published | failed
└─ published_url: set once it's actually live somewhere
└─ publish_error: set to the failure reason when status=failed (or when a
@@ -157,9 +158,9 @@ must exist if given. `type` required, one of `twitter`, `instagram`,
`facebook`, `reddit`, `tiktok`, `linkedin`, `youtube`, `blog`, `other`.
`body` required. `title` nullable. `target` nullable — a network-specific
destination, e.g. the subreddit name for `type=reddit`. `image_url` nullable
URL — not enforced at save time (you can draft a `type=blog` post without one),
but `WordPressPublisher` will reject publishing it until it's set. `status`
defaults to `draft` if omitted. `scheduled_at` nullable datetime.
URL, optional for every type — if set on a `type=blog` post it's uploaded to
WordPress as the featured image, but publishing works fine without one.
`status` defaults to `draft` if omitted. `scheduled_at` nullable datetime.
Note: `type` and the linked Social Account's `network` are **not**
cross-validated — you can link a `type=blog` post to a Twitter account, or
@@ -217,18 +218,20 @@ php artisan posts:publish {post_id}
What it does:
1. Loads the Post and its Site.
2. Validates: `post.type` must be `blog`, `post.title` must be set,
`post.image_url` must be set, `site.cms_type` must be `wordpress`, and
the site must have `wp_username`/`wp_app_password` set. Any failure here
exits 1 with a descriptive message and **does not touch** `Post.status`.
3. Downloads `post.image_url`, then POSTs it to
`site.cms_type` must be `wordpress`, and the site must have
`wp_username`/`wp_app_password` set. Any failure here exits 1 with a
descriptive message and **does not touch** `Post.status`.
3. If `post.image_url` is set: downloads it, then POSTs it to
`{site.url}/wp-json/wp/v2/media` (HTTP Basic Auth with the site's
WordPress Application Password) to upload it as media and get back a
media id.
media id. Skipped entirely if there's no image — that's fine, the post
just won't have a featured image.
4. POSTs to `{site.url}/wp-json/wp/v2/posts` with `status: publish` (goes
live immediately, not a WP draft) and `featured_media` set to that id.
live immediately, not a WP draft), and `featured_media` set to the
uploaded media id if there was one.
5. On success: sets `Post.status = published`, `Post.published_url` to the
new post's permalink, prints `{"success": true, "url": "..."}`, exits 0.
6. On failure at either HTTP step (bad credentials, unreachable site,
6. On failure at any HTTP step (bad credentials, unreachable site,
unreachable/rejected image, WordPress error): sets `Post.status = failed`,
prints `{"success": false, "error": "..."}`, exits 1.