Building a Modern Blog with Ghost CMS and Next.js

As a full-stack developer, I‘m always exploring different technology combinations to create powerful, easy-to-use web applications. One area where we‘ve seen rapid innovation recently is the JAMstack – JavaScript, APIs and Markup. By combining modular, specialized tools, developers can create websites and apps that are fast, scalable and easy to maintain.

One popular JAMstack setup for building blogs is using Ghost CMS as a headless backend with Next.js on the front-end. In this in-depth guide, I‘ll show you exactly how to create a modern, responsive blog with this tech stack.

Why Ghost and Next.js?

Before we dive into the tutorial, let‘s examine what makes Ghost and Next.js a strong combination.

Ghost is a popular open-source CMS designed for publishing content online. While it works great as a standalone blogging platform, Ghost also offers a powerful Content API, allowing you to use it as a headless CMS. This means you write your content in Ghost‘s intuitive editor, but that content can be pulled into any front-end you want via API.

On the front-end side, Next.js has quickly become one of the go-to React frameworks for building websites. Developed by Vercel, Next offers an impressive array of features out-of-the-box, including server-side rendering, static site generation, TypeScript support, smart code bundling, and route pre-fetching. It‘s no surprise that large companies like Netflix, Uber, Starbucks and Twilio have adopted Next.js for major projects.

Plugging these tools together, you get some key benefits:

  1. The writing and admin experience of a purpose-built blogging platform (Ghost)
  2. The design flexibility and interactive capabilities of a modern web framework (Next.js)
  3. Excellent performance from static site generation and code splitting
  4. A fully decoupled architecture that lets you swap out components in the future

In essence, Ghost operates as a content repository and editing interface, while Next.js provides the presentation layer that users interact with. It‘s a slick, modern approach to building a blog.

Setting up Ghost locally

For this tutorial, we‘ll start by running Ghost locally to keep things simple. Down the road, you can look into managed Ghost hosting or deploying to your own server.

First, make sure you have Node.js installed (v12 or newer). Then install the Ghost CLI:

npm install ghost-cli@latest -g

Create a new empty directory for your Ghost install, open a terminal there, and run:

ghost install local

This command downloads and installs Ghost. You‘ll be guided through a series of prompts – for this local install you can choose SQLite as your database and accept the default port. Once installation finishes, start Ghost in development mode:

ghost start

Navigate to http://localhost:2368/ghost in your browser and you‘ll see the Ghost Admin interface. On first run, you‘ll be prompted to create an admin account and do some basic setup. Once you‘re logged in, you‘re ready to start creating content!

Connecting to the Ghost Content API

To pull your Ghost content into a front-end app, you‘ll use the Ghost Content API. This is a read-only JSON API that returns your published content. To access it, you need to create a "Custom Integration" in Ghost Admin and get a Content API key.

In the Ghost Admin, click the "Integrations" link in the left-hand nav. Then click "Add custom integration". Name your integration (e.g. "Next.js Blog") and click "Create". You‘ll then see an "API Key" for your new integration – copy this somewhere safe as you‘ll need it later.

Fetching content in Next.js

Now let‘s set up a new Next.js project and fetch some Ghost content!

If you don‘t have a Next.js project yet, create one with:

npx create-next-app my-blog

Change into the new directory and launch the dev server:

cd my-blog
npm run dev

Visit http://localhost:3000 and you‘ll see the default Next.js starter page.

Next, install the @tryghost/content-api client library which makes hitting the Ghost API easier:

npm install @tryghost/content-api

To securely store your Ghost API url and key, create a .env.local file in the root of your Next project:

GHOST_API_URL=http://localhost:2368
GHOST_CONTENT_API_KEY=YOUR_API_KEY

Be sure to replace YOUR_API_KEY with your actual key.

Now let‘s create a lib/ghost.js file to contain all our API logic:

const GhostContentAPI = require(‘@tryghost/content-api‘);

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

export async function getPosts() {
  return await api.posts
    .browse({
      limit: "all", 
      include: ["tags", "authors"]
    })
    .catch(err => console.error(err));
}

export async function getPost(postSlug) {
  return await api.posts
    .read({
      slug: postSlug
    })
    .catch(err => console.error(err));
}

Here we initialize the GhostContentAPI client with our URL and key from the .env file. We then export two functions:

  1. getPosts fetches a list of all posts, including associated tags and authors
  2. getPost fetches a single post by its slug

On the front-end, we can now import these functions to get real data from Ghost. Open the pages/index.js file and replace its contents with:

import { getPosts } from ‘../lib/ghost‘
import Link from ‘next/link‘

export default function Home({ posts }) {
  return (
    <div>

      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <Link href={`/${post.slug}`}>
              <a>{post.title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  )
}

export async function getStaticProps() {
  const posts = await getPosts()
  return {
    props: { posts },
    revalidate: 60
  }
}

This exports a standard React component for our homepage. Inside the component, we map over the posts array (passed in as a prop) and render a list of links to each post.

The getStaticProps function is where the magic happens. Next will automatically call this function at build time, passing its return value into our Home component as props. Here we use the getPosts function from our API library to fetch a list of posts from Ghost. Returning revalidate: 60 tells Next to re-generate this page if it‘s older than 60 seconds.

To render individual post pages, create a pages/[slug].js file:

import { getPost, getPosts } from ‘../lib/ghost‘

export default function PostPage({ post }) {
  return (
    <div>

      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </div>
  )
}

export async function getStaticPaths() {
  const posts = await getPosts()
  const paths = posts.map(post => ({
    params: { slug: post.slug }
  }))
  return { paths, fallback: false }
}

export async function getStaticProps({ params }) {
  const post = await getPost(params.slug)
  return { props: { post }, revalidate: 60 }
}

This file contains a few key pieces:

  1. The PostPage component which renders the post title and content (the dangerouslySetInnerHTML is necessary to render the HTML content from Ghost)
  2. getStaticPaths which generates a list of post slugs to render as static pages
  3. getStaticProps which fetches the data for a single post based on the slug parameter

Restart your Next dev server and navigate to http://localhost:3000. You should now see your Ghost posts rendered by Next! Clicking a post link should take you to a dedicated post page.

Generating a static site

One of Next‘s most powerful features is static site generation. With your Ghost content available at build time, Next can generate a complete static HTML website. This provides top-tier performance and allows you to host your blog on any static file host (Netlify, Vercel, GitHub Pages, S3, etc).

To generate a static version of your site, run:

npm run build
npm run export

The first command does a production build of your Next app, optimizing assets and creating static pages for each route. The second exports the build to static HTML and assets.

After the export completes, you‘ll have a new out directory containing your full static site. You can test it locally with a tool like http-server or live-server, or deploy it straight to your static host.

Adding styles and interactivity

With the basic Ghost integration in place, you can now focus on making your blog look great and adding some interactive features using React.

For general page layout and styling, I‘d recommend using a CSS-in-JS library like styled-components or emotion. These let you write CSS directly in your React components, keeping styles localized and making dynamic styling based on props or state a breeze. As an example, here‘s how you might style the post list from our homepage:

import styled from ‘@emotion/styled‘

const PostList = styled.ul`
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;

  @media (min-width: 768px) {
    grid-template-columns: repeat(2, 1fr);
  }
`

const PostListItem = styled.li`
  background-color: white;
  border-radius: 5px; 
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
  overflow: hidden;

  a {
    display: block;
    padding: 2rem;
    text-decoration: none;
    color: #333;
  }

  h2 {
    margin-top: 0;
  }
`

export default function Home({ posts }) {
  return (
    <div>

      <PostList>
        {posts.map(post => (
          <PostListItem key={post.id}>
            <Link href={`/${post.slug}`}>
              <a>
                <h2>{post.title}</h2>
                <p>{post.excerpt}</p>
              </a>
            </Link>
          </PostListItem>
        ))}
      </PostList>
    </div>
  )
}

This adds some basic card styling to each post link and uses CSS Grid to create a responsive layout. You have full control over your markup and styles.

Some other ideas for features you could add to your blog:

  • Dark mode toggle using Next themes
  • Client-side search by indexing your posts with a library like Lunr
  • Newsletter signup form posting to an API like SendGrid or Mailchimp
  • Social media share buttons for posts
  • "Related posts" list at the bottom of each post page

The beauty of this setup is that you have the full power of React and the Next ecosystem at your fingertips! You can make your blog as dynamic and feature-rich as you want. The only limit is your imagination (and maybe your free time).

Deployment options

When it‘s time to share your new blog with the world, you have some great deployment options thanks to the static export feature of Next.

My top recommendations would be:

  1. Vercel – This is the company behind Next.js and their platform is tailor-made for Next apps. You get continuous deployment, custom domains, CDN, and preview deployments. They also have a generous free tier.
  2. Netlify – Another great choice for static sites. Simple setup, continuous deployment, and a developer-focused feature set. Also provides a free tier for personal projects.
  3. GitHub Pages – If you want a simple, free option and are okay with hosting source code in a public repo, GitHub Pages is a solid choice. You can set up GitHub Actions to automatically build and deploy your Next app whenever you push.

Whichever option you choose, you‘ll enjoy lightning-fast page loads thanks to your statically generated site. And you can still easily trigger new deploys when you publish content in Ghost thanks to webhooks that these platforms provide.

Conclusion

We‘ve covered a lot of ground in this guide! I hope it‘s given you a solid foundation for building modern, performant blogs using Ghost and Next.js.

To recap, here are the key steps:

  1. Set up Ghost locally (or choose a managed host)
  2. Create a custom integration to access the Ghost API
  3. Fetch your content in getStaticProps using @tryghost/content-api
  4. Use the returned data to statically generate pages at build time
  5. Style and enhance your front-end with React
  6. Generate a static production build and deploy to your host of choice

The JAMstack approach of combining specialized tools is a powerful way to build websites. With Ghost as your publishing platform and Next.js as your front-end, you have the foundation of a modern, scalable, developer-friendly blog.

From here, the possibilities are endless. You can customize your design, add dynamic features, and really make the site your own. And you can do it all with the confidence that your blog is fast, secure, and easy to maintain.

I hope this guide has been informative and maybe even a little inspiring. Happy blogging!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *