# Ghost

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 45 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Ghost using the Content API for a read-only reader app — it authenticates with a simple key query parameter and is safe to call directly from FlutterFlow. For publishing or member management, the Admin API requires JWT signing with an Admin key and must be proxied through a Firebase Cloud Function or Supabase Edge Function. Most FlutterFlow mobile apps need only the Content API.

## Two Doors Into Ghost — Pick the Right One for Your App

Ghost provides two API tiers with fundamentally different security models. The Content API is read-only and authenticates with a simple ?key= query parameter — because the key is read-only, it is acceptable to include it directly in FlutterFlow API Calls without a server proxy. The Admin API requires a HS256-signed JWT with a 5-minute expiry, generated from the id:secret Admin Integration key. Generating this JWT in Dart on the client would expose the Admin secret in the compiled app binary, so any write operations must go through a Firebase Cloud Function or Supabase Edge Function proxy.

Most FlutterFlow blog reader or newsletter companion apps need only the Content API: it exposes /posts, /pages, /tags, /authors, and /tiers with rich filtering and pagination. Ghost's pagination is body-based (not header-based like Canvas): the response includes a meta.pagination object with page, pages, limit, total, next, and prev fields — all parseable by FlutterFlow's JSON paths without any custom Dart code.

Ghost post content (the feature field) is HTML. Plain FlutterFlow Text widgets will display raw tags; you need an HTML/Markdown widget or a WebView to render it properly. Ghost also supports a lexical field (Lexical editor JSON) in recent versions — the feature field provides the HTML rendition for display purposes.

## Before you start

- Ghost site either self-hosted (with admin access) or on Ghost(Pro) at ghost.org/pricing
- Ghost Admin access to create a Custom Integration and copy the Content API key (and Admin API key if publishing)
- Your Ghost site's URL — either your custom domain or yoursite.ghost.io
- FlutterFlow project open at app.flutterflow.io
- Firebase or Supabase project (needed only if you want to use the Admin API for writing content)

## Step-by-step guide

### 1. Create a Ghost Custom Integration and copy your API keys

Open your Ghost Admin panel in your browser — for a self-hosted site this is at https://yourdomain.com/ghost, for Ghost(Pro) it is at https://yoursite.ghost.io/ghost. In the left navigation sidebar click Settings (the gear icon at the bottom), then scroll to the Advanced section and click Integrations. On the Integrations page, scroll past the built-in integrations to the Custom Integrations section and click Add Custom Integration. Give it a descriptive name such as FlutterFlow Reader and click Create.

Ghost will display the integration detail page showing two key values. The Content API Key is a long hex string — copy it. This is the key you will use directly in your FlutterFlow API Calls as a query parameter. Below it you will see the Admin API Key in the format id:secret (a hex ID, a colon, then a longer hex secret). Copy this as well — you will need it only if you plan to implement Admin API write operations.

Also note the API URL shown on this page: Ghost displays the exact base URL to use for your site, typically https://yourdomain.com or https://yoursite.ghost.io. The Content API paths will be appended to this as /ghost/api/content. Do not include the trailing slash. For Ghost(Pro) the format may be slightly different depending on your plan — use the exact URL shown in your integration page.

**Expected result:** You have the Content API Key (a long hex string) and the Admin API Key (in id:secret format if you need write access) copied from Ghost Admin → Settings → Integrations → Custom Integration.

### 2. Create a FlutterFlow API Group for the Ghost Content API

In FlutterFlow, click API Calls in the left navigation bar, then click + Add → Create API Group. Name it Ghost Content API. Set the Base URL to your Ghost site's Content API root: https://yourdomain.com/ghost/api/content (replace yourdomain.com with your actual domain, or yoursite.ghost.io for Ghost Pro). No authentication headers are needed for the Content API — authentication is via query parameter.

Add an API Call inside this group named listPosts, method GET, endpoint /posts. In the Query Parameters section add the following variables:
- key: set to your Content API Key as a constant string.
- limit: add as a variable named limit (String), default 15.
- page: add as a variable named page (String), default 1.
- include: set to 'authors,tags' as a constant (this tells Ghost to include nested author and tag objects in the response).
- filter: optionally add as a variable named filter (String) to allow tag filtering, e.g. 'tag:news'.

In the Response & Test tab, click Test API Call to run it against your live Ghost site. Paste the returned JSON into the Sample JSON field and click Generate JSON Paths. Key paths: $.posts[0].title, $.posts[0].slug, $.posts[0].excerpt, $.posts[0].feature_image, $.posts[0].html, $.posts[0].published_at, $.meta.pagination.page, $.meta.pagination.pages, $.meta.pagination.total, $.meta.pagination.next.

```
// Ghost Content API Call config reference
{
  "method": "GET",
  "base_url": "https://yourdomain.com/ghost/api/content",
  "endpoint": "/posts",
  "query_params": {
    "key": "YOUR_CONTENT_API_KEY",
    "limit": "{{limit}}",
    "page": "{{page}}",
    "include": "authors,tags",
    "filter": "{{filter}}"
  }
}
// Key JSON paths:
// $.posts[0].title
// $.posts[0].slug
// $.posts[0].excerpt
// $.posts[0].feature_image
// $.posts[0].html         ← render with HTML widget, not Text
// $.posts[0].published_at
// $.meta.pagination.page
// $.meta.pagination.pages
// $.meta.pagination.next  ← next page number (null if last page)
```

**Expected result:** The Ghost Content API group shows listPosts and getPostBySlug API Calls. Testing listPosts returns your actual Ghost blog posts with JSON Paths auto-generated, including the meta.pagination object.

### 3. Build a posts ListView with pagination

Create a posts list screen in FlutterFlow. Add a Column widget containing: a search or filter Row at the top (optional), a ListView for the posts, and a Row with Previous/Next buttons at the bottom for pagination.

For the ListView: set its data source to a Backend Query calling Ghost Content API → listPosts with limit=15 and page=1. Store the page number in a page-level Integer variable named currentPage, starting at 1. Map the response posts array to a list page variable named posts. Map $.meta.pagination.pages to a variable named totalPages and $.meta.pagination.next to a variable named nextPage.

Inside each ListView item, add: a NetworkImage widget bound to $.posts[i].feature_image, a Text widget for $.posts[i].title, a Text widget for $.posts[i].excerpt truncated to 2-3 lines, and a Text widget showing the formatted $.posts[i].published_at date. Add a tap action that navigates to a PostDetail screen, passing the post's slug as a navigation parameter.

For the Next button: wire it to increment currentPage by 1 (or use $.meta.pagination.next if it is not null) and re-call listPosts with the updated page number. Hide the Next button when nextPage is null (you are on the last page). For the Previous button, decrement currentPage and re-call, hiding the button when currentPage equals 1. This implements simple page-based navigation that requires no custom Dart code.

**Expected result:** The posts list screen shows Ghost blog posts in a scrollable ListView with featured images and excerpts. Next/Previous buttons update the page and reload the correct posts. The pagination buttons hide correctly on the first and last pages.

### 4. Render Ghost HTML post content in a detail screen

Create a PostDetail screen with a slug parameter. On page load, call Ghost Content API → getPostBySlug with the passed slug, and map the response to a page variable named post.

The key rendering challenge: Ghost's post content comes back in the html field as a string of HTML markup. If you bind this to a plain FlutterFlow Text widget, users will see raw HTML tags like <p>, <h2>, <blockquote> instead of formatted text. You have two options.

Option 1 — FlutterFlow HTML Widget: in the widget palette search for 'HTML' or look under Rich Text. Some FlutterFlow versions include an HTML renderer widget. Bind the widget's content property to $.post.html. This renders HTML inline within the Flutter layout with basic styling support.

Option 2 — WebView Custom Widget: for full Ghost HTML rendering including styles, embeds, and responsive images, create a Custom Widget using the webview_flutter pub.dev package. Set the widget's initialUrl to a data URI constructed from the HTML string: 'data:text/html;charset=utf-8,' + Uri.encodeComponent(post.html). This renders the full Ghost post HTML in a native WebView. Note: webview_flutter requires platform-specific setup (Android: add internet permission in FlutterFlow's Permissions settings; iOS: add NSAppTransportSecurity to Info.plist via Permissions settings).

Above the HTML content, display the post's title, feature image, author name (from $.post.authors[0].name), and published_at date in standard FlutterFlow widgets for a polished header layout.

```
// Custom Widget Dart snippet for Ghost HTML rendering
// Add pub.dev dependency: webview_flutter (^4.4.0)
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class GhostPostViewer extends StatefulWidget {
  final String htmlContent;
  const GhostPostViewer({Key? key, required this.htmlContent})
      : super(key: key);
  @override
  State<GhostPostViewer> createState() => _GhostPostViewerState();
}

class _GhostPostViewerState extends State<GhostPostViewer> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..loadHtmlString(widget.htmlContent);
  }

  @override
  Widget build(BuildContext context) {
    return WebViewWidget(controller: _controller);
  }
}
```

**Expected result:** The PostDetail screen displays the post title, featured image, and author info in standard Flutter widgets above a rendered HTML body. The HTML content shows formatted text, headings, and block quotes rather than raw tags.

### 5. Deploy an Admin API proxy for write operations (optional)

If your app needs to publish posts, update drafts, or manage newsletter members, you need the Ghost Admin API. The Admin API key is in the format id:secret where id is a 24-character hex string and secret is a 64-character hex string separated by a colon. To call the Admin API, you must sign a JWT using HS256 with: the header {alg: 'HS256', typ: 'JWT', kid: id}, a payload {iat: now, exp: now+5min, aud: '/admin/'}, and the secret as the signing key (hex-decoded to bytes).

This JWT must be generated server-side. Create a Firebase Cloud Function or Supabase Edge Function that accepts the API endpoint path and request body from FlutterFlow, generates the JWT, and proxies the call to Ghost Admin API, returning the result. Store the Admin key id and secret in your Firebase/Supabase environment variables — never in FlutterFlow.

In FlutterFlow, create a second API Group named Ghost Admin Proxy pointing to your Cloud Function URL. Add API Calls for each Admin operation you need (create post, update post, etc.), passing the path and body as variables. The Cloud Function handles the JWT signing and forwards the request to https://yourdomain.com/ghost/api/admin.

For most FlutterFlow reader and companion apps, this step is not needed — the Content API covers all reading use-cases. Add the Admin proxy only when your app genuinely requires writing content back to Ghost.

```
// Firebase Cloud Function — Ghost Admin API proxy (Node.js)
const functions = require('firebase-functions');
const axios = require('axios');
const jwt = require('jsonwebtoken');

exports.ghostAdmin = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') { res.status(204).send(''); return; }

  const adminKey = process.env.GHOST_ADMIN_KEY; // format: id:secret
  const ghostUrl = process.env.GHOST_URL;       // e.g. https://yourdomain.com
  const [id, secret] = adminKey.split(':');

  const token = jwt.sign({}, Buffer.from(secret, 'hex'), {
    keyid: id,
    algorithm: 'HS256',
    expiresIn: '5m',
    audience: '/admin/',
  });

  const { path, method = 'GET', body } = req.body;
  try {
    const resp = await axios({
      method,
      url: `${ghostUrl}/ghost/api/admin${path}`,
      headers: {
        Authorization: `Ghost ${token}`,
        'Content-Type': 'application/json',
      },
      data: body,
    });
    res.json(resp.data);
  } catch (err) {
    res.status(err.response?.status || 500)
       .json({ error: err.message });
  }
});
```

**Expected result:** For Content-API-only apps, no proxy is needed. If the Admin proxy was deployed, calling your Cloud Function from FlutterFlow with a post creation payload successfully creates a draft in Ghost Admin.

## Best practices

- Use only the Content API for read-only apps — it is simpler, safer, and sufficient for blogs, newsletters, and knowledge base apps without any server proxy.
- Never include the Admin API secret in FlutterFlow API Call parameters or Dart code — proxy all Admin operations through a Firebase Cloud Function or Supabase Edge Function.
- Render Ghost's html field with an HTML/Markdown widget or WebView, never a plain Text widget — raw HTML tags in a Text widget are a common source of app store rejection for poor content display.
- Use the include=authors,tags query parameter on Content API list calls to get nested author and tag data in one request rather than making separate calls.
- Implement pagination using Ghost's meta.pagination.next field — when it is null, hide the Next button to signal the last page without any custom logic.
- Confirm your Ghost URL before building — Ghost(Pro) sites use yoursite.ghost.io while self-hosted sites use your custom domain; mixing these up is the most common setup mistake.
- Add a 404 handler for post detail screens: if getPostBySlug returns an empty response (unpublished or deleted post), show a graceful 'Post not found' screen rather than an empty layout.
- For self-hosted Ghost, ensure your Ghost version is v5.0 or later — the Content API and Admin API paths changed in major versions; older v3/v4 sites use slightly different URL structures.

## Use cases

### Ghost-powered mobile blog reader app

A FlutterFlow app displays the latest posts from a Ghost blog in a ListView with title, featured image, and excerpt. Tapping a post navigates to a detail screen that renders the full HTML content in an HTML/Markdown widget or WebView. Users can filter by tag and browse paginated post lists. The app is read-only and uses only the Content API.

Prompt example:

```
Build a mobile blog reader app connected to a Ghost blog that shows a paginated list of posts with featured images and excerpts, and opens full post HTML content on tap.
```

### Newsletter companion app with member content gating

A FlutterFlow newsletter app authenticates subscribers via Ghost's member tiers, fetching the user's access level from the Content API and showing or hiding premium posts accordingly. Free members see public posts; paying members see paid-tier posts. The app serves as a mobile companion to the Ghost web experience.

Prompt example:

```
Create a newsletter app that fetches Ghost posts filtered by member access tier and displays public content to free users while showing premium posts only to members who log in with their email.
```

### CMS-backed FAQ or knowledge base app

A FlutterFlow app uses Ghost Pages (not Posts) as a headless CMS for a structured FAQ or knowledge base. Each page represents a help article with title and HTML body. The app fetches pages filtered by tag to build category navigation, then displays article content in a formatted detail screen using an HTML renderer.

Prompt example:

```
Build a help center app that reads Ghost Pages tagged with categories, displays a category list, and shows full article HTML content when a user selects an article.
```

## Troubleshooting

### Ghost Content API returns 401 Unauthorized

Cause: The key query parameter is missing, incorrect, or belongs to a different Ghost site. Also check that you are using the Content API key, not the Admin API key.

Solution: Open your Ghost Admin → Settings → Integrations → your Custom Integration and confirm the Content API Key. Verify it is appended as ?key=... in your API Call query parameters. Test the full URL directly in your browser: https://yourdomain.com/ghost/api/content/posts/?key=YOUR_KEY should return JSON.

### Post HTML content displays raw tags in a FlutterFlow Text widget

Cause: Ghost's html field returns HTML markup that a plain Text widget renders as literal text, not as formatted content.

Solution: Use FlutterFlow's HTML/Markdown widget to render the html field, or implement the WebView Custom Widget approach described in Step 4. Do not bind Ghost's html field to a plain Text widget.

### Ghost API returns 404 for the /ghost/api/content path

Cause: The Base URL in FlutterFlow is incorrect. Common issues: wrong domain, missing /ghost/api/content path segment, or a self-hosted Ghost site where the API is served at a non-standard path.

Solution: Verify the exact API URL in Ghost Admin → Settings → Integrations → your integration — Ghost shows the correct Content API URL. Test the URL directly in your browser. For Ghost(Pro), the URL is typically https://yoursite.ghost.io/ghost/api/content, not https://ghost.io/yoursite/...

### Admin API calls return 401 with 'Token Validation Failed'

Cause: The JWT is either being generated with an incorrect algorithm, the wrong key format (the secret must be hex-decoded to bytes before signing, not used as a UTF-8 string), or the token's audience claim does not match '/admin/'.

Solution: Confirm your Cloud Function decodes the secret with Buffer.from(secret, 'hex') before passing it to jwt.sign(). Verify the audience is exactly '/admin/' and the keyid matches the id portion of the Admin key. Ghost's JWT requirement is very specific — use the proxy code provided in Step 5 as a reference.

## Frequently asked questions

### Do I need the Admin API to build a basic Ghost blog reader app?

No. The Content API is fully sufficient for a read-only blog reader, newsletter companion, or knowledge base app. It exposes posts, pages, tags, authors, and tiers. You only need the Admin API when your app needs to create or update content, manage member subscriptions, or perform other write operations in Ghost.

### Why can't I just put the Admin API key in a FlutterFlow header?

Ghost's Admin API key contains a secret component (the id:secret pair) that is used to sign JWTs. If you include the secret in a FlutterFlow API Call header or Dart code, it compiles into the app binary and can be extracted by anyone who downloads your app. With the Admin secret, an attacker could create, modify, or delete all content on your Ghost site. Always sign the JWT server-side and keep the secret in Firebase/Supabase environment variables.

### How is Ghost different from WordPress as a CMS backend for FlutterFlow?

Ghost has a cleaner two-tier Content/Admin API split with less configuration overhead than WordPress's REST API. Ghost's Content API returns HTML directly in the html field; WordPress returns content in a different structure with Gutenberg blocks. Ghost natively includes membership and newsletter tier data; WordPress requires plugins for similar features. The auth model also differs: Ghost Content uses a key query param, while WordPress typically uses application passwords or nonces.

### Can Ghost's Content API return member-only or paid content?

No. The Content API only returns publicly accessible posts and pages — content gated to paid or member tiers is not returned. To show tiered content to authenticated members in a FlutterFlow app, you need to implement Ghost's member authentication flow (email magic link or portal), which requires the Admin API or a Ghost member session. This is an advanced integration beyond the scope of a simple Content API setup.

### What is the difference between a Ghost self-hosted and Ghost(Pro) API URL?

Self-hosted Ghost uses your custom domain: https://yourblog.com/ghost/api/content. Ghost(Pro) uses your site's ghost.io subdomain: https://yoursite.ghost.io/ghost/api/content (unless you have connected a custom domain, in which case use that domain). Always check the exact URL shown in Ghost Admin → Settings → Integrations → your Custom Integration, as Ghost displays the correct URL for your specific setup.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/ghost
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/ghost
