feat: add Ghost publishing provider#1663
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
|
je n'arrive pas a le faire |
|
please review @nevo-david @egelhaus |
📝 WalkthroughWalkthroughAdds a new Ghost social integration provider: backend GhostProvider implementing authentication and posting via Ghost Admin API, a GhostSettingsDto with validation, frontend Ghost settings UI, registry wiring in both backend and frontend, plus a controller fix filtering out unregistered social integrations. ChangesGhost Provider Integration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant GhostSettingsUI
participant Backend
participant GhostProvider
participant GhostAdminAPI
User->>GhostSettingsUI: Configure title, status, canonical, tags, newsletter
GhostSettingsUI->>Backend: Submit post with GhostSettingsDto
Backend->>GhostProvider: post(id, accessToken, postDetails, integration)
GhostProvider->>GhostProvider: parseCredentials(accessToken)
GhostProvider->>GhostProvider: adminToken() (JWT sign)
GhostProvider->>GhostAdminAPI: POST /posts/ (HTML, newsletter, canonical, tags, image)
GhostAdminAPI-->>GhostProvider: created post id
GhostProvider-->>Backend: PostResponse (id, release URL)
Backend-->>User: Post published confirmation
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed: private package registry requires authentication. Disable ESLint in CodeRabbit settings or use public packages. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
apps/frontend/src/components/new-launch/providers/ghost/ghost.provider.tsx (1)
26-32: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueSelect default relies on option order, not
register'svalueoption.
register('status', { value: 'published' })andregister('emailSegment', { value: 'all' })set the internal RHF field value, butregister's returned props (name,onChange,onBlur,ref) don't includevalue, so it isn't spread onto the native<select>. The visually selected option currently only matches because'published'/'all'happen to be the first<option>— this works today but is fragile to reordering or adding new options later.♻️ More robust default via defaultValue on the select
<Select label="Status" - {...form.register('status', { value: 'published' })} + defaultValue="published" + {...form.register('status')} >Also applies to: 55-62
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/frontend/src/components/new-launch/providers/ghost/ghost.provider.tsx` around lines 26 - 32, The default selected value for the native Select fields is currently relying on option order instead of an explicit select default, affecting both the status and emailSegment inputs in GhostProvider. Update the Select usage in GhostProvider so the default is set on the select itself rather than through register’s value option, and keep using react-hook-form register for the field wiring. Make sure the unique Select instances for status and emailSegment remain aligned with their intended defaults regardless of option ordering.apps/backend/src/api/routes/integrations.controller.ts (1)
99-101: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider NestJS
Loggerinstead ofconsole.warn.Using the framework's
Logger(with a context/tag) keeps log output consistent with the rest of the Nest application and integrates with log level configuration, rather than rawconsole.warn.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/api/routes/integrations.controller.ts` around lines 99 - 101, Replace the raw console.warn call in integrations.controller with NestJS Logger usage so the warning is emitted through the framework’s logging system. Update the relevant controller logic to use a Logger instance with a clear context/tag, and preserve the existing message details about the integration id and missing providerIdentifier registration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@libraries/nestjs-libraries/src/integrations/social/ghost.provider.ts`:
- Around line 73-76: The Ghost `siteUrl` field validation in `ghost.provider.ts`
is too regex-based and can accept malformed admin/dashboard URLs while rejecting
valid URLs with ports. Update the `siteUrl` handling in the provider
configuration and `apiUrl()` flow to parse the input with `new URL()`, normalize
it by removing any `search` and `hash`, and validate the protocol and host from
that normalized URL before building the `/ghost/api/admin` endpoint.
- Around line 217-225: parseCredentials currently assumes the token always
decodes to valid JSON with string siteUrl and adminApiKey, which can throw on
malformed input. Update GhostProvider.parseCredentials to validate the base64
decode and JSON shape before calling JSON.parse() and .trim(), and convert any
failure into a controlled provider error. Make sure authenticate() and post()
can surface that handled error instead of crashing, using the existing
GhostProvider/parseCredentials flow to locate the fix.
---
Nitpick comments:
In `@apps/backend/src/api/routes/integrations.controller.ts`:
- Around line 99-101: Replace the raw console.warn call in
integrations.controller with NestJS Logger usage so the warning is emitted
through the framework’s logging system. Update the relevant controller logic to
use a Logger instance with a clear context/tag, and preserve the existing
message details about the integration id and missing providerIdentifier
registration.
In `@apps/frontend/src/components/new-launch/providers/ghost/ghost.provider.tsx`:
- Around line 26-32: The default selected value for the native Select fields is
currently relying on option order instead of an explicit select default,
affecting both the status and emailSegment inputs in GhostProvider. Update the
Select usage in GhostProvider so the default is set on the select itself rather
than through register’s value option, and keep using react-hook-form register
for the field wiring. Make sure the unique Select instances for status and
emailSegment remain aligned with their intended defaults regardless of option
ordering.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ac71391c-a37d-4b62-b353-21dfb0b5e356
⛔ Files ignored due to path filters (1)
apps/frontend/public/icons/platforms/ghost.pngis excluded by!**/*.png
📒 Files selected for processing (7)
apps/backend/src/api/routes/integrations.controller.tsapps/frontend/src/components/new-launch/providers/ghost/ghost.provider.tsxapps/frontend/src/components/new-launch/providers/show.all.providers.tsxlibraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.tslibraries/nestjs-libraries/src/dtos/posts/providers-settings/ghost.settings.dto.tslibraries/nestjs-libraries/src/integrations/integration.manager.tslibraries/nestjs-libraries/src/integrations/social/ghost.provider.ts
| key: 'siteUrl', | ||
| label: 'Site URL', | ||
| validation: `/^https?:\\/\\/(?:www\\.)?[\\w\\-]+(\\.[\\w\\-]+)+([\\/?#][^\\s]*)?$/`, | ||
| type: 'text' as const, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Normalize siteUrl instead of validating it with this regex alone.
Line 75 rejects valid public Ghost URLs with explicit ports, but still accepts query/hash suffixes. Because apiUrl() later concatenates the raw value at Line 259, a pasted admin/dashboard URL can validate here and still produce a broken /ghost/api/admin endpoint. Parse with new URL(), drop search/hash, and validate protocol/host from the normalized value instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@libraries/nestjs-libraries/src/integrations/social/ghost.provider.ts` around
lines 73 - 76, The Ghost `siteUrl` field validation in `ghost.provider.ts` is
too regex-based and can accept malformed admin/dashboard URLs while rejecting
valid URLs with ports. Update the `siteUrl` handling in the provider
configuration and `apiUrl()` flow to parse the input with `new URL()`, normalize
it by removing any `search` and `hash`, and validate the protocol and host from
that normalized URL before building the `/ghost/api/admin` endpoint.
| private parseCredentials(token: string): GhostCredentials { | ||
| const body = JSON.parse(Buffer.from(token, 'base64').toString()) as { | ||
| siteUrl: string; | ||
| adminApiKey: string; | ||
| }; | ||
|
|
||
| return { | ||
| siteUrl: body.siteUrl.trim().replace(/\/+$/, ''), | ||
| adminApiKey: body.adminApiKey.trim(), |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Handle malformed encoded credentials before JSON.parse() and .trim().
Line 218 assumes every token is base64-encoded JSON with string siteUrl and adminApiKey fields. A malformed connect request or a corrupted stored token will throw here and take both authenticate() (Line 93) and post() (Line 153) down with a 500 instead of a controlled provider error.
Proposed fix
private parseCredentials(token: string): GhostCredentials {
- const body = JSON.parse(Buffer.from(token, 'base64').toString()) as {
- siteUrl: string;
- adminApiKey: string;
- };
-
- return {
- siteUrl: body.siteUrl.trim().replace(/\/+$/, ''),
- adminApiKey: body.adminApiKey.trim(),
- };
+ try {
+ const body = JSON.parse(
+ Buffer.from(token, 'base64').toString()
+ ) as Partial<GhostCredentials>;
+
+ if (
+ typeof body.siteUrl !== 'string' ||
+ typeof body.adminApiKey !== 'string'
+ ) {
+ throw new Error('Invalid credential payload');
+ }
+
+ return {
+ siteUrl: body.siteUrl.trim().replace(/\/+$/, ''),
+ adminApiKey: body.adminApiKey.trim(),
+ };
+ } catch {
+ throw new Error('Invalid Ghost credentials payload.');
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private parseCredentials(token: string): GhostCredentials { | |
| const body = JSON.parse(Buffer.from(token, 'base64').toString()) as { | |
| siteUrl: string; | |
| adminApiKey: string; | |
| }; | |
| return { | |
| siteUrl: body.siteUrl.trim().replace(/\/+$/, ''), | |
| adminApiKey: body.adminApiKey.trim(), | |
| private parseCredentials(token: string): GhostCredentials { | |
| try { | |
| const body = JSON.parse( | |
| Buffer.from(token, 'base64').toString() | |
| ) as Partial<GhostCredentials>; | |
| if ( | |
| typeof body.siteUrl !== 'string' || | |
| typeof body.adminApiKey !== 'string' | |
| ) { | |
| throw new Error('Invalid credential payload'); | |
| } | |
| return { | |
| siteUrl: body.siteUrl.trim().replace(/\/+$/, ''), | |
| adminApiKey: body.adminApiKey.trim(), | |
| }; | |
| } catch { | |
| throw new Error('Invalid Ghost credentials payload.'); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@libraries/nestjs-libraries/src/integrations/social/ghost.provider.ts` around
lines 217 - 225, parseCredentials currently assumes the token always decodes to
valid JSON with string siteUrl and adminApiKey, which can throw on malformed
input. Update GhostProvider.parseCredentials to validate the base64 decode and
JSON shape before calling JSON.parse() and .trim(), and convert any failure into
a controlled provider error. Make sure authenticate() and post() can surface
that handled error instead of crashing, using the existing
GhostProvider/parseCredentials flow to locate the fix.
What kind of change does this PR introduce?
Feature: adds Ghost as a publishing provider.
Why was this change needed?
Postiz already supports several long-form publishing platforms, but Ghost was missing as a newsletter/publication CMS provider.
This change adds Ghost channel support using the official Ghost Admin API. Users can connect a Ghost site with a Site URL and Admin API key, then create draft or published Ghost posts from Postiz. The provider supports title, HTML content, canonical URL, tags, cover image, newsletter slug, email segment, and email-only publishing.
This also hardens the integration list endpoint so stale or unknown provider rows do not crash the calendar channel list.
Other information:
Verified locally:
Connected a Ghost site successfully.
Created a Ghost draft from Postiz.
Published a Ghost post from Postiz.
Published with a cover image.
Confirmed the Ghost channel remains visible in Postiz after posting.
Ran Prettier successfully.
Ran frontend TypeScript successfully.
I have read the CONTRIBUTING guide.
I have signed the Contributor License Agreement (CLA) (ICLA for individuals, CCLA for entities).
I confirm I have not used AI to submit this PR or generate code for it.
I checked that there were no similar issues or PRs already open for this.
This PR fixes just ONE issue
Summary by CodeRabbit
New Features
Bug Fixes