How to Use Ghost as a Headless CMS in an Elixir/Phoenix Project

In this post, we'll see how to use the Ghost publishing platform as a headless CMS in any Elixir project, including those with Phoenix and LiveView.

How to Use Ghost as a Headless CMS in an Elixir/Phoenix Project
Photo by Wilhelm Gunkel / Unsplash

I recently wanted to add some new blog & page content to a running shoe price comparison site I built with Elixir/Phoenix and LiveView. I also happen to maintain a couple of different sites using the Ghost publishing platform. So naturally, I asked the question: can I use Ghost’s content creation tools to manage the content for my Elixir/Phoenix project?

Of course! Though I did have to write a small Elixir client for the Ghost content API, it was simple enough to access the content and take advantage of all the powerful Ghost CMS tools in my Phoenix app.

The Ghost CMS Platform logo & Phoenix Framework logo

In this post, we’ll learn how to use the Ghost publishing platform as a simple CMS for presenting content in any Elixir project, including those built with the Phoenix Framework and LiveView.

Why Use a Headless CMS

First of all, what is a headless CMS?

A headless CMS is a content management system that allows content creators to create, edit and manage content on a different site than where it is ultimately displayed. It also provides API access so that the content can be displayed by a variety of frontends and interfaces. With a headless CMS, developers can offload managing content creation tools, and at the same time have more control over how the content is presented.

In practical terms, a headless CMS gives a developer pre-built tools for creating and managing content that can be displayed on a site they’re building, without building out a full blogging platform themselves.

Though some developers will be tempted to build their own content management, there are tons of features including SEO tools, media management, permissions & powerful content editors which make a headless CMS a better choice in most cases.

What is Ghost?

Ghost is a popular open-source blogging platform that is known for its simplicity and power. It is lightweight and built on a modern technology stack, making it a great choice for developers who want a straightforward and easy-to-use CMS. Though most people use Ghost as the frontend for their content as well, the platform also features a robust content API, which allows it to function as a headless CMS.

On the content creation side, Ghost offers a range of other features, including a simple and easy-to-use editor, media management, user management and built-in SEO tools. It even includes tools for mailing lists, audience management, payments and more.

Overall, Ghost is a great option for developers looking to use a headless CMS in their project, and its feature set and API make it a strong contender for managing blog-like content in Elixir/Phoenix and LiveView applications.

While there are a number of other headless CMS options that can work with Elixir, here are some of the things I really like about Ghost:

  • lightweight & responsive
  • clean & modern content management interface
  • options for both fully-managed hosting or self-hosted content
  • completely free for self-hosted & reasonably pricing for managed
  • fully open-source

On the last two points: many headless CMS providers are closed-source and don’t allow the option to self-host. While they may offer free or affordable tiers for small sites, they come with a real risk of vendor lock-in or spiraling costs as a site grows. I recommend the Ghost’s managed hosting to help support the development of the platform, but also appreciate the peace of mind that the self-hosted, open-source option provides.

How to Use Ghost in Your Elixir/Phoenix Project

Let’s consider a simple use case where we have blog content in a Ghost CMS, and we’d like to display it in our Phoenix/LiveView app. We’ll integrate the client, then build a simple LiveView to display the blog index, and then individual posts.

GitHub - jonklein/ghost_content: An Elixir client for the Ghost Content API
An Elixir client for the Ghost Content API. Contribute to jonklein/ghost_content development by creating an account on GitHub.

First, you’ll need to add the ghost_content package to your project.

# mix.exs

def deps do
  [    
    {:ghost_content, "~> 0.1.0"}
  ]
end
mix.exs changes to add ghost_content package

Next, you’ll need to configure the package using your Ghost API host, and API key. For help on getting the proper values for your Ghost blog, consult the Ghost Content API documentation.

# config.exs

config :my_app, :ghost_content,
  host: "https://my.ghost.host", 
  api_key: "my-ghost-api-key"
config.exs changes to configure ghost_content package

Once you’ve added & configured ghost_content in your project, we’ll add the LiveViews to fetch and display the blog content. In the examples below, we’ll show the bare minimum to get up and running, but it should be simple enough to expand on the functionality once you get the blog index and posts displaying in your app.

First, let’s add simple routes to our new blog functionality. We’ll show a list of all posts at /blog, and show individual posts at /blog/:slug , where :slug is the identifier for the post we’ve configured in the Ghost CMS.

# router.ex

live("/blog/", MyApp.BlogLive.Index, :index)
live("/blog/:slug", MyApp.BlogLive.Show, :show)
Routing for a simple blog with ghost_content

With our routing in place, we’ll create the index page showing our list of blog posts. We simply make a call to the Ghost content API to fetch the posts in our mount() call, then render the contents, linking out to the individual posts:

defmodule MyApp.BlogLive.Index do 
  use MyAppWeb, :live_view
  
  def mount(_params, _session, socket) do
    {:ok, %{posts: posts}} =
      GhostContent.config(:my_app)
      |> GhostContent.get_posts()
    
    {:ok, assign(socket, :posts, posts)}
  end  

  def render(assigns) do
    ~H"""
      <h1>Recent Blog Posts</h1>
      <ul>
      <%= for post <- @posts do %>
        <li><a href={~p"/blog/#{post.slug}"}><%= post.title %></a></li>
      <% end %>
      </ul>
    """  
  end
end
Sample LiveView blog index page with ghost_content

Finally, we’ll add a simple LiveView to show individual posts. In this case, we’ll use the slug from the route to lookup the post content:

defmodule MyApp.BlogLive.Show do
  use MyAppWeb, :live_view
  def mount(%{"slug" => slug}, _session, socket) do
    {:ok, %{posts: [post]}} =
      GhostContent.config(:my_app)
      |> GhostContent.get_post_by_slug(slug)
    
    {:ok, assign(socket, :post, post)}
  end
 
  def render(assigns) do
    ~H"""
      <%= @post.title %>  
      <div class="ghost-blog-content">
        <%= raw(@post.html) %>
      </div>
    """
  end
end
Sample LiveView blog display page with ghost_content

Note that to display the content we use raw(@post.html). This is because the content API is giving us actual HTML content and we want to render it as-is (rather than with HTML content escaped). Using raw() can be dangerous with untrusted content, but in this case, because we have authored the content ourselves, it is trusted.

Regarding styling, because we’re displaying raw HTML content that was authored in Ghost, the content we render in our app is unstyled. We won’t be able to take common styling approaches like adding class names to the individual HTML elements in our content. Instead, we simply wrap the entire blog content with a div with a special class name (ghost-blog-content), then apply styles to elements within that div in our app.css:

# app.css 

.ghost-blog-content h1 { font-size: 2em; }
.ghost-blog-content h2 { font-size: 1.5em; }

# more .ghost-blog-content styles ...

And voila! With these simple LiveViews in place, we have the foundations of a Ghost blog in our Phoenix app. There are a number of details we’ve ignored like pagination, displaying tag & author data, navigation, etc., but these are simple enough to add once we have the basics in place.

One final note on SEO: to take full advantage of the SEO functionality of Ghost, be sure to grab the proper values from the Ghost post structure (title, feature_image and description) and expose them in the proper meta tags in your HTML content.


Jonathan is software developer & engineering leader who loves Elixir. When he’s not writing code, he can be found running or scouring the internet for great deals on running shoes.