Chris Ellis
Posted on March 30, 2021
This post was originally posted at chrisellis.dev
I've taken chrisellis.dev headless. When I started my website, I chose Ghost because of its amazing publishing experience. It's pretty great.
Unfortunately, I found the theme development experience a little bit clunky. Especially when you compare it to something like Gatsby or NextJS.
So I decided to try out Headless Ghost. Let's see how you can do this.
How to do this
- Create your NextJS App
- Prepare your Ghost Server
- Import use the Content API JavaScript Client to import posts
- Host it on a Serverless Platform
Create your Next App
The first step is to create your NextJS app. You can find more detailed instructions for doing this at NextJS, but the just of it is to do the following:
# Create your site
yarn create next-app yourdomain.com
cd yourdomain.com
# Start your server
yarn dev
That's it.
Prepare your Ghost Server
This step is a bit more involved. Login to your Ghost site backend.
We need to get a Content API Key to be able to pull content from our Ghost Site.
- Click Integrations at the bottom of your Ghost Backend
- Then Add custom integration at the bottom of your Integrations page
- Enter a decent name
- Make note of your Content API Key
The Content API JavaScript Client
Ok, let's start getting fancy and adding a dependency. First, we need to import the Content API.
yarn add @tryghost/content-api
This API is used for fetching content. You can use the Admin API to do more complex things like reading, writing, and editing.
I've chosen to extract this out to a /lib folder for reusability. Create a file /lib/posts.js
. We also need to create a .env.local file at the root of your project which NextJS will use for development.
// lib/posts.js
import GhostContentAPI from "@tryghost/content-api";
// Create API instance with site credentials
const api = new GhostContentAPI({
url: process.env.BLOG_URL,
key: process.env.API_KEY,
version: "v3",
});
// .env.local
BLOG_URL="https://yourdomain.com"
API_KEY="f77b5e81e07899d45850bbf248"
Great, now our app will connect to our Ghost Instance.
To start off with, let's fetch all of the posts so we can list the posts on the index page.
// lib/posts.js
export async function getPosts() {
let posts = await api.posts
.browse({
limit: "all"
})
.catch((err) => {
console.error(err);
});
return posts;
}
Now in pages/index.js
, we need to import our getPosts() and call it in a getStaticProps. In NextJS, getStaticProps is used to prerender the page at buildtime. Blog posts are a perfect use case for this as they don't change names and slugs very often.
// pages/index.js
// top o' the file
...
import { getPosts } from "../lib/posts";
export const getStaticProps = async () => {
const posts = await getPosts();
if (!posts) {
return {
notFound: true,
};
}
return {
props: {
posts,
},
};
};
export default function Home({ posts }) {
console.log(posts);
...
If you've set everything up correctly, you should now have a list of all of your published posts in your console from your Ghost site. Switch over to your terminal and you will notice that the posts are logged there as well. This demonstrates that getStaticProps is run during the pre-render at build time.
Let's display these posts in a list so we can navigate to them. We've already destructured the posts in our Home component so all we have to do is use plain old React patterns to display. Remove everything but the heading inside of . I'll throw mine in an unordered list.
// pages/index.js
import Link from "next/link";
...
<main className={styles.main}>
<h1 className={styles.title}>Posts</h1>
<ul>
{posts.map((post) => {
return (
<li key={post.slug}>
<Link
href="/posts/[slug]"
as={`/posts/${post.slug}`}>
<a>{post.title}</a>
</Link>
</li>
);
})}
</ul>
</main>
...
Note we've also imported the Link component to take advantage of NextJS's routing. Onto the post pages.
You may have noticed the href and as in the Link component. As is a decorator on the Link and href is the actual link. Documentation
We need to create another async function to get a single post by its slug from our Ghost site. Head back over to lib/posts
and add the following code.
// lib/posts.js
export async function getSinglePost(postSlug) {
return await api.posts
.read({
slug: postSlug,
})
.catch((err) => {
console.error(err);
});
}
Remember, Next uses file-based routing so let's create a posts folder with a file called [slug].js
in it. This is called a dynamic route in NextJS.
Next.js provides dynamic routes for pages that don’t have a fixed URL / slug. The name of the js file will be the variable, in this case the post slug, wrapped in square brackets – [slug].js.
// pages/posts/[slug].js
import { getSinglePost, getPosts } from "../../lib/posts";
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(context) {
const post = await getSinglePost(context.params.slug);
if (!post) {
return {
notFound: true,
};
}
return {
props: { post },
};
}
const PostPage = ({ post }) => {
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
);
};
export default PostPage;
Walking you through it, we import our data fetching functions.
Next, we use getStaticPaths()
to get the list of posts that match the given slug. Hint, should only be one.
Having matched the paths, we pass the matching paths into getStaticProps()
via context so Next knows to render this post at build time.
Then we just render it like we would in any other React application. Simple, right? 😕
I've added some styling in the css. You can see the repo at https://github.com/csellis/headless-ghost-next.
It's all downhill from here.
Host it on a Serverless Platform
The easiest way to manage this is using Vercel's own hosting platform. Vercel owns NextJS so you get to take advantage of the tight integrations.
I like to use the Vercel CLI as I tend to throw up a lot of small projects. I'll let you follow their install instructions and we'll just go through uploading it to their domain.
Once you have the CLI setup and logged in, literally just type vercel
and enter a bunch of times.
And you'll be greeted with an amazing deploy error. I'm so excited.
Following the link, you'll discover we're missing the environment variables we have in our .env.local
file. Since we're going all CLI here, let's take advantage of the vercel env part of it.
➜ yourdomain.com git:(main) vercel env add
Vercel CLI 21.3.3
? Which type of Environment Variable do you want to add? Plaintext
? What’s the name of the variable? API_KEY
? What’s the value of API_KEY? f77b5e81e07899d45850bbf248
? Add API_KEY to which Environments (select multiple)? Production, Preview, Development
✅ Added Environment Variable API_KEY to Project yourdomain-com [592ms]
Adding API_KEY
Do the same for both BLOG_URL and API_KEY.
This time to redeploy we need to run vercel --prod
. Et voilà !
Check it out https://yourdomain-com.vercel.app/
We've successfully uploaded our Headless Ghost site with NextJS to Vercel. Now you can test the site out before making a full switch to Headless Ghost.
Next steps
From here you have several options. What I chose to do was host this site on a subdomain while I was working on it over several weekends. You could still visit chrisellis.dev and get to Genuine GhostJS experience.
When I was happy with the Headless site, I swapped the subdomains around. Reach out to me if you'd like to read about that experience.
If you want to get more of these posts as soon as they come out, check out chrisellis.dev.
Posted on March 30, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.