Skip to content

feat: add Ghost publishing provider#1663

Open
onyedikachi-david wants to merge 3 commits into
gitroomhq:mainfrom
onyedikachi-david:feat/ghost-provider
Open

feat: add Ghost publishing provider#1663
onyedikachi-david wants to merge 3 commits into
gitroomhq:mainfrom
onyedikachi-david:feat/ghost-provider

Conversation

@onyedikachi-david

@onyedikachi-david onyedikachi-david commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

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:

image image

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

    • Added support for publishing to Ghost as a new integration option.
    • Added Ghost-specific settings for post title, status, canonical URL, cover image, tags, newsletter targeting, and email-only posting.
  • Bug Fixes

    • Integration lists now skip unavailable providers instead of showing broken entries.
    • Ghost settings are now validated and recognized throughout the app, improving setup and publishing reliability.

@postiz-contribution postiz-contribution Bot added the contribution:approved Approved contributor label Jul 1, 2026
@postiz-agent

postiz-agent Bot commented Jul 1, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@nafaya77

nafaya77 commented Jul 1, 2026

Copy link
Copy Markdown

je n'arrive pas a le faire

@onyedikachi-david

Copy link
Copy Markdown
Contributor Author

please review @nevo-david @egelhaus

@egelhaus egelhaus marked this pull request as ready for review July 1, 2026 10:36
Comment on lines +194 to +195
],
}),

This comment was marked as outdated.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Ghost Provider Integration

Layer / File(s) Summary
Ghost settings DTO and provider registry
libraries/nestjs-libraries/src/dtos/posts/providers-settings/ghost.settings.dto.ts, libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts
Adds GhostSettingsDto with validation for title, main_image, canonical, tags, newsletter, emailSegment, emailOnly, status; registers it in AllProvidersSettings and allProviders().
GhostProvider backend implementation
libraries/nestjs-libraries/src/integrations/social/ghost.provider.ts
Implements GhostProvider with authenticate() (SSRF-safe fetch, JWT-based admin token, credential base64-encoding), post() (creates Ghost posts with newsletter/canonical/tags/feature image), and helpers for credential parsing, tag parsing, and media URL resolution.
Register GhostProvider in IntegrationManager
libraries/nestjs-libraries/src/integrations/integration.manager.ts
Adds GhostProvider to socialIntegrationList; reformats extensionCookies conditional spread.
Ghost settings UI component
apps/frontend/src/components/new-launch/providers/ghost/ghost.provider.tsx
Implements GhostSettings form component and default withProvider<GhostSettingsDto> export with post/character limits.
Register GhostProvider in frontend providers list
apps/frontend/src/components/new-launch/providers/show.all.providers.tsx
Adds ghost entry mapping to GhostProvider in Providers.
Filter unregistered providers in integration list
apps/backend/src/api/routes/integrations.controller.ts
Refactors getIntegrationList to log and skip providers with no registered social integration, filtering results with filter(Boolean).

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
Loading

Possibly related PRs

  • gitroomhq/postiz-app#511: Registers a new social provider (Lemmy) into the same socialIntegrationList in integration.manager.ts.
  • gitroomhq/postiz-app#578: Wires a new social integration (Nostr) into the same IntegrationManager and ShowAllProviders registries.
  • gitroomhq/postiz-app#706: Extends the same socialIntegrationList and frontend Providers mapping to add a new integration (Vk).
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding Ghost as a publishing provider.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

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 value

Select default relies on option order, not register's value option.

register('status', { value: 'published' }) and register('emailSegment', { value: 'all' }) set the internal RHF field value, but register's returned props (name, onChange, onBlur, ref) don't include value, 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 value

Consider NestJS Logger instead of console.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 raw console.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

📥 Commits

Reviewing files that changed from the base of the PR and between 8943fea and 7d2467d.

⛔ Files ignored due to path filters (1)
  • apps/frontend/public/icons/platforms/ghost.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • apps/backend/src/api/routes/integrations.controller.ts
  • apps/frontend/src/components/new-launch/providers/ghost/ghost.provider.tsx
  • apps/frontend/src/components/new-launch/providers/show.all.providers.tsx
  • libraries/nestjs-libraries/src/dtos/posts/providers-settings/all.providers.settings.ts
  • libraries/nestjs-libraries/src/dtos/posts/providers-settings/ghost.settings.dto.ts
  • libraries/nestjs-libraries/src/integrations/integration.manager.ts
  • libraries/nestjs-libraries/src/integrations/social/ghost.provider.ts

Comment on lines +73 to +76
key: 'siteUrl',
label: 'Site URL',
validation: `/^https?:\\/\\/(?:www\\.)?[\\w\\-]+(\\.[\\w\\-]+)+([\\/?#][^\\s]*)?$/`,
type: 'text' as const,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Comment on lines +217 to +225
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(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution:approved Approved contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants