WordPress/Ghost publishers, media providers, and deploy hooks#9
Conversation
| .map((key) => `${key}=${params[key]}`) | ||
| .join("&"); | ||
|
|
||
| return createHash("sha1").update(`${serialized}${apiSecret}`).digest("hex"); |
| return { | ||
| ok: true, | ||
| config: { | ||
| endpoint: endpoint.replace(/\/+$/, ""), |
| accessKeyId, | ||
| secretAccessKey, | ||
| ...(input.publicBaseUrl?.trim() | ||
| ? { publicBaseUrl: input.publicBaseUrl.trim().replace(/\/+$/, "") } |
| assert.equal(decodedPayload.aud, "/admin/"); | ||
|
|
||
| const expectedSignature = createHmac("sha256", Buffer.from(TEST_SECRET_HEX, "hex")) | ||
| .update(`${header}.${payload}`) |
| }), | ||
| ); | ||
| const signature = createHmac("sha256", parsed.secret) | ||
| .update(`${header}.${payload}`) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 15e6ba0df0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } else if (publisher === "wordpress") { | ||
| owner = process.env.WORDPRESS_API_URL?.trim() || ""; | ||
| branch = project.defaultBranch; | ||
| } else if (publisher === "ghost") { | ||
| owner = process.env.GHOST_ADMIN_URL?.trim() || ""; |
There was a problem hiding this comment.
Enable remote CMS publish readiness
When CMS_PUBLISHER is wordpress or ghost, this public config leaves repo empty, but the Studio still computes githubReady as owner && repo and PublishGate disables publishing whenever that is false. A fully configured WordPress/Ghost user therefore sees the GitHub setup message and cannot click Publish at all; expose/use a publisher-ready flag or make the frontend treat remote CMS targets as ready without a repo.
Useful? React with 👍 / 👎.
| created: boolean; | ||
| sha: string; | ||
| commitSha: string; | ||
| remoteId?: string; |
There was a problem hiding this comment.
Persist remote IDs for CMS updates
The WordPress/Ghost response can now include remoteId, but the frontend only stores result.path and publishArticle() only sends sourcePath, so the server never receives body.remoteId on a subsequent publish. For CMS publishers that makes every re-publish of an already-created post call the create endpoint again, producing duplicate posts instead of updates; store the returned remote ID with the editor state and include it in the publish payload.
Useful? React with 👍 / 👎.
| title: article.title, | ||
| slug: article.slug, | ||
| description: article.description, | ||
| body: article.body, |
There was a problem hiding this comment.
Render Markdown before sending CMS content
The Studio editor body is Markdown/MDX, but this shared CMS payload passes the raw Markdown through; the new WordPress publisher assigns it to REST content and the Ghost publisher assigns it to Admin API html. Articles with headings, links, images, or formatting will render as literal Markdown in those CMSes rather than formatted content, so the CMS payload needs an HTML-rendered body (or publisher-specific Markdown conversion) before it is sent.
Useful? React with 👍 / 👎.
| const result = await ghostRequest( | ||
| "PUT", | ||
| `/ghost/api/admin/posts/${encodeURIComponent(remoteId)}/?source=html`, | ||
| "update", | ||
| { posts: [postPayload] }, | ||
| remoteId, |
There was a problem hiding this comment.
Include current updated_at for Ghost edits
When remoteId is supplied, this sends the edit payload without first loading Ghost's current updated_at value; the Ghost Admin API update docs list updated_at as required for edits. Any caller that does manage to pass a Ghost remoteId will hit update-collision/validation failures unless it already has the exact latest timestamp, so fetch the post and include that server timestamp before issuing the PUT.
Useful? React with 👍 / 👎.
| const post: Record<string, unknown> = { | ||
| title: article.title, | ||
| slug: article.slug, | ||
| html: article.body, | ||
| status, | ||
| excerpt: article.description, | ||
| tags: article.tags.map((name) => ({ name })), |
There was a problem hiding this comment.
For Ghost publishes with draft: false, this payload never uses SourceDraft's required pubDate, so Ghost will date the post at the time the API call runs rather than at the article's intended publication date. Ghost posts expose and accept published_at in the Admin/Content API examples, so include article.pubDate (and scheduling semantics when applicable) instead of dropping that metadata.
Useful? React with 👍 / 👎.
| const publicPath = joinPublicMediaPath(env.publicMediaPath, repoFilename); | ||
|
|
||
| const publisher = createPublisherFromEnv(env); | ||
| const mediaProvider = createMediaProviderFromEnv(env); |
There was a problem hiding this comment.
Return JSON errors for Cloudinary misconfiguration
When CMS_MEDIA_PROVIDER=cloudinary but any required CLOUDINARY_* variable is missing, createMediaProviderFromEnv() throws here before the handler reaches the !result.ok branch. With no catch in this route, Express returns its generic 500 response instead of the structured upload error the client expects, so the user sees an unreachable/server failure rather than the actionable missing-variable message.
Useful? React with 👍 / 👎.
|
Closing retroactive split-stack review PR. Continuing from protected main with scoped feature PRs. |
PR 8 of 11. WordPress/Ghost CMS, Cloudinary/S3 media providers, deploy hooks.