Skip to content

WordPress/Ghost publishers, media providers, and deploy hooks#9

Closed
bnz183 wants to merge 1 commit into
split/07-git-publishersfrom
split/08-wp-ghost-media
Closed

WordPress/Ghost publishers, media providers, and deploy hooks#9
bnz183 wants to merge 1 commit into
split/07-git-publishersfrom
split/08-wp-ghost-media

Conversation

@bnz183

@bnz183 bnz183 commented Jun 9, 2026

Copy link
Copy Markdown
Owner

PR 8 of 11. WordPress/Ghost CMS, Cloudinary/S3 media providers, deploy hooks.

.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}`)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +386 to +390
} 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() || "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +198 to +203
const result = await ghostRequest(
"PUT",
`/ghost/api/admin/posts/${encodeURIComponent(remoteId)}/?source=html`,
"update",
{ posts: [postPayload] },
remoteId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +89 to +95
const post: Record<string, unknown> = {
title: article.title,
slug: article.slug,
html: article.body,
status,
excerpt: article.description,
tags: article.tags.map((name) => ({ name })),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map Ghost publication dates

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@bnz183

bnz183 commented Jun 9, 2026

Copy link
Copy Markdown
Owner Author

Closing retroactive split-stack review PR. Continuing from protected main with scoped feature PRs.

@bnz183 bnz183 closed this Jun 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants