Add an image field, required for WordPress posts
Posts get a nullable image_url. WordPressPublisher now downloads it and uploads it to WordPress's media endpoint before creating the post, setting it as featured_media — and rejects publishing (without touching status) if a blog post has no image set, mirroring the existing title requirement. Every other network treats it as optional. Not enforced at save time, same as title — you can still draft an incomplete WordPress post, it just can't be published until an image is set. Exposed through the UI form (with a thumbnail/""missing"" indicator in the table), the JSON API, and the Python CLI wrapper. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,8 @@ 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.
|
||||
└─ 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
|
||||
@@ -145,6 +147,7 @@ Body:
|
||||
"title": "Optional title",
|
||||
"body": "Required content",
|
||||
"target": null,
|
||||
"image_url": null,
|
||||
"status": "draft",
|
||||
"scheduled_at": null
|
||||
}
|
||||
@@ -153,8 +156,10 @@ Validation: `site_id` required, must exist. `social_account_id` nullable,
|
||||
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`. `status` defaults to
|
||||
`draft` if omitted. `scheduled_at` nullable datetime.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -192,7 +197,7 @@ who/what is driving:
|
||||
- **Manually**, for anything without a wired-in publisher yet: do it
|
||||
yourself and `PATCH` the result back (see below).
|
||||
|
||||
Note the asymmetry: WordPress is synchronous (a single HTTP call, fast
|
||||
Note the asymmetry: WordPress is synchronous (a few plain HTTP calls, fast
|
||||
enough to run inline), Reddit is not (it's real browser automation via
|
||||
Playwright, tens of seconds) — which is exactly why the UI path is
|
||||
queue-backed rather than a direct synchronous call.
|
||||
@@ -212,17 +217,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,
|
||||
`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. POSTs to `{site.url}/wp-json/wp/v2/posts` using HTTP Basic Auth with
|
||||
the site's WordPress Application Password, `status: publish` (goes live
|
||||
immediately, not a WP draft).
|
||||
4. On success: sets `Post.status = published`, `Post.published_url` to the
|
||||
`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.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.
|
||||
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.
|
||||
5. On success: sets `Post.status = published`, `Post.published_url` to the
|
||||
new post's permalink, prints `{"success": true, "url": "..."}`, exits 0.
|
||||
5. On failure (bad credentials, unreachable site, WordPress error): sets
|
||||
`Post.status = failed`, prints `{"success": false, "error": "..."}`,
|
||||
exits 1.
|
||||
6. On failure at either HTTP step (bad credentials, unreachable site,
|
||||
unreachable/rejected image, WordPress error): sets `Post.status = failed`,
|
||||
prints `{"success": false, "error": "..."}`, exits 1.
|
||||
|
||||
This is a **shell command**, not an HTTP endpoint — run it from wherever
|
||||
you have shell access to this app (e.g. `php artisan posts:publish 42`).
|
||||
|
||||
Reference in New Issue
Block a user