Ghost post preview button

Post previews with Ghost, Eleventy & Netlify

A while back, I updated my Eleventy plugin for Ghost so you get more data from your Ghost instance, including the ability to retrieve draft posts. Here's how I used it to preview draft posts using Eleventy and Netlify.

The main drawback to using Ghost as a headless CMS is that you effectively opt out of a bunch of features that are built in for free. What's worse is the controls for those features continue to show themselves in the UI, a bit like those faux buttons you get in cars when it doesn't have all the add-ons. In this case, it's the draft preview button in the Ghost post editor UI. In this article, I'll share how I rewired that feature using my Eleventy plugin and Netlify.

Prerequisites

If you want to follow along with me, you'll need your own Ghost instance - it can be self hosted or hosted on Ghost(Pro), in addition to an Eleventy site hosted on Netlify. I'll (somewhat) briefly explain how to install my plugin; however, the installation instructions are more in-depth, if you want to check them out.

eleventy-plugin-ghost
Access the Ghost Content API in Eleventy 👻🎛. Latest version: 2.1.0, last published: 8 months ago. Start using eleventy-plugin-ghost in your project by running `npm i eleventy-plugin-ghost`. There are no other projects in the npm registry using eleventy-plugin-ghost.

Setup

Here's the abridged version of installing my Eleventy plugin for Ghost: in your Eleventy project, install the plugin using npm:

npm install eleventy-plugin-ghost

Then, add the plugin to your .eleventy.js file, making sure to supply it with your Ghost site URL, your Content API key and (for this use case) your Admin API key.

const pluginGhost = require("eleventy-plugin-ghost");

require("dotenv").config();
const {
  GHOST_URL,
  GHOST_ADMIN_KEY,
  GHOST_KEY,
} = process.env;

module.exports = (eleventyConfig) => {
  eleventyConfig.addPlugin(pluginGhost, {
    url: GHOST_URL,
    key: GHOST_KEY,
    apiKey: GHOST_ADMIN_KEY
  });
};

.eleventy.js example

GHOST_URL=https://demo.ghost.io
GHOST_KEY=22444f78447824223cefc48062
GHOST_ADMIN_KEY=22444f78447824223cefc4806222444f78447824223cefc48062

.env file example

Note that I'm using enviroment variables inside a .env file. This is important, as later on these variables will need to be configurable via Netlify settings. When you spin up your Eleventy site, you'll now have access to a ghost global data object to get data, like site info, pages, posts, and most importantly, draft posts.

If you're wondering how to use the ghost global data object in your Eleventy project, I've added a demo project that demonstrates how to render posts, pages and more:

eleventy-plugin-ghost/demo at main · daviddarnes/eleventy-plugin-ghost
Access the Ghost API in Eleventy 👻🎛. Contribute to daviddarnes/eleventy-plugin-ghost development by creating an account on GitHub.

Generating and deploying preview posts

Currently, my Eleventy site is generating posts using the ghost.posts data point. Ideally, the draft posts would also be in that data point, too; however, instead of being rendered and shown on my live site, it's on a cloned version of the site that's hidden from public view.

When I updated my plugin so it could use the Admin API, I built it in such a way that if you didn't provide the Admin API key, but still gave the URL and Content API keys, it would return the regular public Content API data. So, with a single environment variable, I can flag Eleventy to generate - or not generate - draft posts.

module.exports = (eleventyConfig) => {
  eleventyConfig.addPlugin(pluginGhost, {
    url: GHOST_URL,
    key: GHOST_KEY,
    apiKey: GHOST_ADMIN_KEY // If this doesn't exist only public content will come back
  });
};

This, coupled with Netlify's somewhat recent updates to environment variables configuration, means I can spin up our draft posts preview site with minimal work.

Ghost Admin key applied in the Netlify admin as a branch deploy environment variable, but not a production variable

The above image shows the environment variables interface within Netlify. Here is where variables can be applied globally or per context. I've marked the important parts with red arrows.

The first is the production key for the Admin API key that's left empty, so the live site will fall back to the Content API, and therefore no drafts. The second highlighted key is the Admin API key in branch deploys. It's hidden, but a value has been applied. This means any branch deployment of the site can use this key and gain access to Admin API content, and use draft posts.

After all of this is configured, a new fixed branch deployment can be set up. I ended up making a branch of my site and letting Netlify know about it. It would be really nice if Netlify allowed for 'mirrors' of a single branch. With the current setup, I have to keep merging development changes from main into my preview branch so it stays up to date.

Wiring up preview buttons

Ghost preview button within the post editing UI

Having previews of draft posts render is all well and good, but the preview button within Ghost needs to, like, actually work. After a little reverse engineering, I found that the preview button shows an iframe containing an unlisted render of the draft post.

However, the draft post doesn't use the post slug for the URL. Instead, it uses the post's UUID, a unique identifier that can be found in the Ghost API. Perfect! Knowing this means I can marry up the URL with the relevant post within the API using URL redirects. Ghost previews always follow the same URL pattern:

https://mydomain.com/p/12345678-90ab-cdef-ghij-klmnopqrstuv/

Example preview URL

Note the /p/ section: this is unique to Ghost preview URLs and is something we can use to target in the Ghost redirects file.

Redirects in Ghost are a bit of a pain to work with, mostly due to their lack of documentation. What makes things slightly more difficult is that they switched from using JSON to YAML, making not only my example out of date, but also other peoples' within the Ghost community. You can still use JSON, but I'd recommend converting my example code using their guide to YAML to 'future-proof' yourself.

[
  { "from": "/p/(.*)", "to": "https://previewdomain.com/$1" }
]

The above redirect, when added into a JSON file and uploaded to Ghost admin (Settings > Labs), will redirect preview URLs to the preview domain, but will maintain the slug part containing the UUID.

Next, those redirects need to be caught and redirected again from the site hosted on Netlify. I could generate draft posts using the UUID and show them on the expected redirect, but my existing Eleventy site is set up to generate posts on the post slug and I'd rather not add any unique code for this feature. I quite like that my draft posts behave the same as normal posts, and that my previews can be torn down without losing lots of unique code.

Not only does Netlify allow redirects to be listed in a _redirects text file, but it's also possible to add it to your build step, so Eleventy can generate all the redirects using template code. This can be really useful if you need to do some kind of mass migration of URLs and don't want to painstakingly list them all out.

Instead of a _redirects file in my Eleventy project, I have a redirects.njk with the permalink set to _redirects, as Netlify expects it to be. Within that, I have the following:

---
permalink: _redirects
---

{%- for post in ghost.posts %}
/{{ post.uuid }}/ /{{ post.slug }}/
{%- endfor -%}

The result will be a list of internal site redirects coming from each post's UUID slug to its equivilant post slug. Once all configured and deployed, the redirects all happen in an instant, resulting in a preview button that reveals a modal showing the Eleventy generated post of the draft posts 🎉.

Redeploying on change

⚠️
Be warned, configuring your Netlify site to rebuild every time you change a draft will cause a lot of rebuilds and might chomp right through your build minutes on Netlify. I recommending testing this before commiting to it.

Geting your site to rebuild whenever a draft post is changed is the same as setting up Ghost to Netlify deployment. The key part is adding all the possible events. In the linked guide, it shows the "Site Changed" event, which doesn't include all post updated events. Adding another webhook and selecting the event "Post updated" will catch those additional events. When adding the webhook to Netlify, the preview branch can be selected so the production site isn't reacting to draft post update events. Thanks to Kevin from the Ghost team for highlighting this detail.

Ghost settings for a webhook, with the event set to "Post updated"

Closing notes

I think the actual implementation looks easier than it was to explain. In theory, I could've just said "do this" and not explained it all. However, I think it's good to know what is happening under the hood.

The only drawback to this setup is the build times. My site is pretty small, so using an entire deployment to view a preview takes under 30 seconds. On a larger site, that could be a pain, as the draft preview would always be a little behind. I guess that would be a job for Eleventy Serverless.

Hope you found this useful. Let me know if you run into any issues!