Skip to main content

Gatsby and Ghost

Gatsby works well with Ghost when Ghost is the CMS and Gatsby is the static front-end. Editors keep writing in Ghost Admin. Gatsby reads published content from the Content API at build time, then creates pages from that content. For most Gatsby sites, use the official JavaScript Content API client. It is maintained as part of the TryGhost SDK and works in Node.js during Gatsby builds. Use the Admin API only in private server-side code, such as an import script or publishing workflow. Do not expose an Admin API key in Gatsby browser code.

Prerequisites

This configuration requires basic knowledge of JavaScript and React. You will also need:
  • A running Ghost site, either self-hosted or on Ghost(Pro)
  • A custom integration in Ghost Admin so you can copy the Content API URL and key
  • A Gatsby project
Create the Content API key from Settings -> Integrations in Ghost Admin. For more detail, see Content API authentication.

Start from a new Gatsby site

If you already have a Gatsby site, skip to the Content API setup. Otherwise, create a new Gatsby project with the current Gatsby tooling:
npm init gatsby my-gatsby-site
cd my-gatsby-site
npm run develop
Gatsby also documents manual setup in the official Gatsby docs.

Install the Ghost Content API client

Install the Ghost Content API client in the Gatsby project:
npm install @tryghost/content-api dotenv
Add your Ghost credentials to Gatsby environment files. Keep these files out of source control.
# .env.development
GHOST_API_URL=https://demo.ghost.io
GHOST_CONTENT_API_KEY=22444f78447824223cefc48062
GHOST_API_VERSION=v6.0
SITE_URL=http://localhost:8000

# .env.production
GHOST_API_URL=https://your-site.example.com
GHOST_CONTENT_API_KEY=your_content_api_key
GHOST_API_VERSION=v6.0
SITE_URL=https://www.example.com
For Ghost(Pro), the URL is usually your .ghost.io URL. For self-hosted Ghost, use the public URL for your Ghost install. Gatsby loads .env.development and .env.production, but Gatsby’s Node APIs need dotenv to be configured in gatsby-config.js:
// gatsby-config.js
require("dotenv").config({
  path: `.env.${process.env.NODE_ENV || "development"}`,
});

module.exports = {
  siteMetadata: {
    siteUrl: process.env.SITE_URL || process.env.GHOST_API_URL,
  },
};

Fetch all posts during the Gatsby build

Gatsby runs gatsby-node.js during builds. Use it to fetch Ghost posts and pages, then create static pages from that content.
// gatsby-node.js
const path = require("path");
const GhostContentAPI = require("@tryghost/content-api");

const api = new GhostContentAPI({
  url: process.env.GHOST_API_URL,
  key: process.env.GHOST_CONTENT_API_KEY,
  version: process.env.GHOST_API_VERSION || "v6.0",
});

async function browseAll(resource, options = {}) {
  const items = [];
  let page = 1;

  while (page) {
    const response = await resource.browse({
      ...options,
      limit: 100,
      page,
    });

    items.push(...response);
    page = response.meta.pagination.next || null;
  }

  return items;
}

function ghostPath(resource) {
  return new URL(resource.url).pathname;
}

exports.createPages = async ({ actions, reporter }) => {
  const { createPage } = actions;
  const postTemplate = path.resolve("./src/templates/post.js");
  const pageTemplate = path.resolve("./src/templates/page.js");

  const [posts, pages] = await Promise.all([
    browseAll(api.posts, {
      include: "tags,authors",
      order: "published_at DESC",
    }),
    browseAll(api.pages, {
      include: "tags,authors",
    }),
  ]);

  posts.forEach((post) => {
    createPage({
      path: ghostPath(post),
      component: postTemplate,
      context: { post },
    });
  });

  pages.forEach((page) => {
    createPage({
      path: ghostPath(page),
      component: pageTemplate,
      context: { page },
    });
  });

  reporter.info(`Created ${posts.length} Ghost posts and ${pages.length} Ghost pages`);
};
Ghost 6.0 and later limit each browse request to 100 records, so static builds need pagination. The helper above follows the meta.pagination.next value until there are no more pages. See pagination for building static sites and Content API pagination for the underlying API behavior.

Render a post template

Create a Gatsby template that reads the Ghost post from pageContext.
// src/templates/post.js
import * as React from "react";

export default function GhostPost({ pageContext }) {
  const { post } = pageContext;

  return (
    <article>
      <h1>{post.title}</h1>
      <p>
        {post.primary_author?.name}
        {post.published_at ? ` / ${new Date(post.published_at).toLocaleDateString()}` : null}
      </p>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  );
}

export function Head({ pageContext }) {
  const { post } = pageContext;
  const description = post.meta_description || post.excerpt;

  return (
    <>
      <title>{post.meta_title || post.title}</title>
      {description ? <meta name="description" content={description} /> : null}
      <link rel="canonical" href={post.canonical_url || post.url} />
    </>
  );
}
Ghost returns post HTML from content written in Ghost Admin. Rendering it with dangerouslySetInnerHTML is normal for a trusted CMS source, but do not mix this with untrusted user-submitted HTML.

Render a page template

The gatsby-node.js example also creates pages, so add a matching page template:
// src/templates/page.js
import * as React from "react";

export default function GhostPage({ pageContext }) {
  const { page } = pageContext;

  return (
    <article>
      <h1>{page.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: page.html }} />
    </article>
  );
}

export function Head({ pageContext }) {
  const { page } = pageContext;
  const description = page.meta_description || page.excerpt;

  return (
    <>
      <title>{page.meta_title || page.title}</title>
      {description ? <meta name="description" content={description} /> : null}
      <link rel="canonical" href={page.canonical_url || page.url} />
    </>
  );
}

Add pages, tags, authors, and settings

The Content API exposes the resources Gatsby usually needs for a headless site:
const [posts, pages, tags, authors, settings] = await Promise.all([
  browseAll(api.posts, { include: "tags,authors" }),
  browseAll(api.pages, { include: "tags,authors" }),
  browseAll(api.tags, { include: "count.posts" }),
  browseAll(api.authors, { include: "count.posts" }),
  api.settings.browse(),
]);
Use createPage() for any resource that needs its own route, such as posts, pages, tag archives, or author archives. Use Gatsby’s built-in Head API for page metadata, and the official gatsby-plugin-sitemap package if you need a generated sitemap. If you prefer Gatsby’s GraphQL data layer, you can use Gatsby’s sourceNodes API to create nodes from these same Content API responses.

Use the Admin API from private scripts

The Admin API can create, update, and publish content. It is useful for migrations or custom editorial tooling, but it is not needed to render a public Gatsby front-end. Install the Admin API client only where the key stays private:
npm install @tryghost/admin-api
// scripts/create-draft.js
const GhostAdminAPI = require("@tryghost/admin-api");

const admin = new GhostAdminAPI({
  url: process.env.GHOST_API_URL,
  key: process.env.GHOST_ADMIN_API_KEY,
  version: "v6.0",
});

admin.posts.add(
  {
    title: "Draft from a private script",
    html: "<p>This draft was created through the Admin API.</p>",
    status: "draft",
  },
  { source: "html" }
);
Run Admin API scripts on your own machine, in CI, or on a server. Never ship GHOST_ADMIN_API_KEY to the browser.

Next steps

Read the Content API JavaScript client docs for available endpoints and options. Gatsby’s Node API documentation explains how createPages and sourceNodes fit into the Gatsby build. Gatsby output is static, so new Ghost content appears after the next Gatsby build. Most hosting providers let you trigger a rebuild from a Ghost webhook or build hook.