The Ultimate Free Solo Blog Setup With Ghost And Gatsby — Smashing Magazine

Quick abstract ↬
When it involves instruments for publishing a weblog, it might look like there’s by no means an ideal resolution that mixes customization with straightforward admin. In this text, we are going to see step-by-step how one can get the perfect of each worlds by utilizing Ghost as a headless CMS for a Gatsby static web site. We will cowl all of the tough components in-depth and present you are able to do every thing without cost.

These days it appears there are an infinite variety of instruments and platforms for creating your personal weblog. However, a number of the choices on the market lean in direction of non-technical customers and summary away the entire choices for personalization and actually making one thing your personal.

If you might be somebody who is aware of their method round front-end development, it may be irritating to discover a resolution that provides you the management you need, whereas eradicating the admin from managing your weblog content material.

Enter the Headless Content Management System (CMS). With a Headless CMS, you may get the entire instruments to create and set up your content material, whereas sustaining 100% management of how it’s delivered to your readers. In different phrases, you get the entire backend construction of a CMS whereas not being restricted to its inflexible front-end themes and templates.

When it involves Headless CMS programs, I’m a giant fan of Ghost. Ghost is open-source and easy to make use of, with a number of nice APIs that make it versatile to make use of with static web site builders like Gatsby.

In this text, I’ll present you ways you should utilize Ghost and Gatsby collectively to get the final word private weblog setup that allows you to hold full management of your front-end supply, however leaves all of the boring content material administration to Ghost.

Oh, and it’s 100% free to arrange and run. That’s as a result of we can be working our Ghost occasion domestically after which deploying to Netlify, profiting from their beneficiant free tier.

Let’s dive in!

Setting Up Ghost And Gatsby

I’ve written a starter publish on this earlier than that covers the very fundamentals, so I received’t go too in-depth into them right here. Instead, I’ll concentrate on the extra superior points and gotchas that come up when working a headless weblog.

But briefly, right here’s what we have to do to get a primary set-up up and working that we will work from:

  • Install an area model of the Gatsby Starter Blog
  • Install Ghost domestically
  • Change the supply knowledge from Markdown to Ghost (swap out gatsby-source-file system for gatsby-source-ghost)
  • Modify the GraphQL queries in your gatsby-node, templates, and pages to match the gatsby-source-ghost schema

For extra particulars on any of those steps, you’ll be able to try my earlier article.

Or you’ll be able to simply begin from the code on this Github repository.

Dealing With Images

With the fundamentals out of the way in which, the primary subject we run into with a headless weblog that builds domestically is what to do with pictures.

Ghost by default serves pictures from its personal server. So if you go headless with a static web site, you’ll run right into a state of affairs the place your content material is constructed and served from an edge supplier like Netlify, however your pictures are nonetheless being served by your Ghost server.

This isn’t best from a efficiency perspective and it makes it not possible to construct and deploy your web site domestically (which implies you would need to pay month-to-month charges for a Digital Ocean droplet, AWS EC2 occasion, or another server to host your Ghost occasion).

But we will get round that if we will discover one other resolution to host our pictures &mdash, and fortunately, Ghost has storage converters that allow you to retailer pictures within the cloud.

For our functions, we’re going to use an AWS S3 converter, which permits us to host our pictures on AWS S3 together with Cloudfront to provide us an identical efficiency to the remainder of our content material.

There are two open-source choices obtainable: ghost-storage-adapter-s3 and ghost-s3-compat. I exploit ghost-storage-adapter-s3 since I discover the docs simpler to comply with and it was extra not too long ago up to date.

That being mentioned, if I adopted the docs precisely, I acquired some AWS errors, so right here’s the method that I adopted that labored for me:

  • Create a brand new S3 Bucket in AWS and choose Disable Static Hosting
  • Next, create a brand new Cloudfront Distribution and choose the S3 Bucket because the Origin
  • When configuring the Cloudfront Distribution, underneath S3 Bucket Access:

    • Select “Yes, use OAI (bucket can restrict access to only Cloudfront)”
    • Create a New OAI
    • And lastly, choose “Yes, update the bucket policy”

    Configuring the Cloudfront Distribution. (Large preview)

    This creates an AWS S3 Bucket that may solely be accessed through the Cloudfront Distribution that you’ve created.

Then, you simply have to create an IAM User for Ghost that may allow it to jot down new pictures to your new S3 Bucket. To do that, create a brand new Programmatic IAM User and fasten this coverage to it:

{
“Version”: “2012-10-17”,
“Statement”: [
{
“Sid”: “VisualEditor0”,
“Effect”: “Allow”,
“Action”: “s3:ListBucket”,
“Resource”: “arn:aws:s3:::YOUR-S3-BUCKET-NAME”
},
{
“Sid”: “VisualEditor1”,
“Effect”: “Allow”,
“Action”: [
“s3:PutObject”,
“s3:GetObject”,
“s3:PutObjectVersionAcl”,
“s3:DeleteObject”,
“s3:PutObjectAcl”
],
“Resource”: “arn:aws:s3:::YOUR-S3-BUCKET-NAME/*”
}
] }

With that, our AWS setup is full, we simply want to inform Ghost to learn and write our pictures there as an alternative of to its native server.

To do this, we have to go to the folder the place our Ghost occasion is put in and open the file: ghost.development.json orghost.manufacturing.json.(relying on what atmosphere you’re presently working)

Then we simply want so as to add the next:

{
“storage”: {
“active”: “s3”,
“s3”: {
“accessKeyId”: “[key]”,
“secretAccessKey”: “[secret]”,
“region”: “[region]”,
“bucket”: “[bucket]”,
“assetHost”: “https://[subdomain].example.com”, // cloudfront
“forcePathStyle”: true,
“acl”: “private”
}
}

The values for accessKeyId and secretAccessKey will be discovered out of your IAM setup, whereas the area and bucket discuss with the area and bucket title of your S3 bucket. Finally, the assetHost is the URL of your Cloudfront distribution.

Now, if you happen to restart your Ghost occasion, you will note that any new pictures you save are in your S3 bucket and Ghost is aware of to hyperlink to them there. (Note: Ghost received’t make updates retroactively, so remember to do that very first thing after a recent Ghost set up so that you don’t need to re-upload pictures later)

More after leap! Continue studying under ↓Feature Panel

With Images out of the way in which, the following tough factor we’d like to consider is inner hyperlinks. As you might be writing content material in Ghost and inserting hyperlinks in Posts and Pages, Ghost will mechanically add the location’s URL to all inner hyperlinks.

So for instance, if you happen to put a hyperlink in your weblog publish that goes to /my-post/, Ghost goes to create a hyperlink that goes to https://mysite.com/my-post/.

Normally, this isn’t a giant deal, however for Headless blogs this causes issues. This is as a result of your Ghost occasion can be hosted someplace separate out of your front-end and in our case it received’t even be reachable on-line since we can be constructing domestically.

This signifies that we might want to undergo every weblog publish and web page to right any inner hyperlinks. Thankfully, this isn’t as laborious because it sounds.

First, we are going to add this HTML parsing script in a brand new file known as exchangeLinks.js and put it in a brand new utils folder at src/utils:

const url = require(`url`);
const cheerio = require(‘cheerio’);

const exchangeLinks = async (htmlInput, siteUrlString) => {
const siteUrl = url.parse(siteUrlString);
const $ = cheerio.load(htmlInput);
const hyperlinks = $(‘a’);
hyperlinks.attr(‘href’, perform(i, href){
if (href) {
const hrefUrl = url.parse(href);
if (hrefUrl.protocol === siteUrl.protocol && hrefUrl.host === siteUrl.host) {
return hrefUrl.path
}

return href;
}

});
return $.html();
}

module.exports = exchangeLinks;

Then we are going to add the next to our gatsby-node.js file:

exports.onCreateNode = async ({ actions, node, getNodesByType }) => {
if (node.inner.proprietor !== `gatsby-source-ghost`) {
return
}
if (node.inner.kind === ‘GhostPage’ || node.inner.kind === ‘GhostPost’) {
const settings = getNodesByType(`GhostSettings`);
actions.createNodeField({
title: ‘html’,
worth: exchangeLinks(node.html, settings[0].url),
node
})
}
}

You will see that we’re including two new packages in exchangeLinks.js, so let’s begin by putting in these with NPM:

npm set up –save url cheerio

In our gatsby-node.js file, we’re hooking into Gatsby’s onCreateNode, and particularly into any nodes which can be created from knowledge that comes from gatsby-source-ghost (versus metadata that comes from our config file that we don’t care about for now).

Then we’re checking the node kind, to filter out any nodes that aren’t Ghost Pages or Posts (since these are the one ones that may have hyperlinks inside their content material).

Next, we’re getting the URL of the Ghost web site from the Ghost settings and passing that to our take awayLinks perform together with the HTML content material from the Page/Post.

In exchangeLinks, we’re utilizing cheerio to parse the HTML. Then we will then choose the entire hyperlinks on this HTML content material and map by way of their href attributes. We can then examine if the href attribute matches the URL of the Ghost Site — if it does, we are going to exchange the href attribute with simply the URL path, which is the interior hyperlink that we’re searching for (e.g. one thing like /my-post/).

Finally, we’re making this new HTML content material obtainable by way of GraphQL utilizing Gatsby’s createNodeField (Note: we should do it this manner since Gatsby doesn’t help you overwrite fields at this section within the construct).

Now our new HTML content material can be obtainable in our blog-post.js template and we will entry it by altering our GraphQL question to:

ghostPost(slug: { eq: $slug }) {
id
title
slug
excerpt
published_at_pretty: published_at(formatString: “DD MMMM, YYYY”)
html
meta_title
fields {
html
}
}

And with that, we simply have to tweak this part within the template:

To be:

This makes all of our inner hyperlinks reachable, however we nonetheless have yet one more drawback. All of those hyperlinks are anchor tags whereas with Gatsby we must be utilizing Gatsby Link for inner hyperlinks (to keep away from web page refreshes and to supply a extra seamless expertise).

Thankfully, there’s a Gatsby plugin that makes this very easy to resolve. It’s known as gatsby-plugin-catch-links and it appears to be like for any inner hyperlinks and mechanically replaces the anchor tags with Gatsby.

All we have to do is set up it utilizing NPM:

npm set up –save gatsby-plugin-catch-links

And add gatsby-plugin-catch-links into our plugins array in our gatsby-config file.

Adding Templates And Styles

Now the large stuff is technically working, however we’re lacking out on a number of the content material from our Ghost occasion.

The Gatsby Starter Blog solely has an Index web page and a template for Blog Posts, whereas Ghost by default has Posts, Pages, in addition to pages for Tags and Authors. So we have to create templates for every of those.

For this, we will leverage the Gatsby starter that was created by the Ghost staff.

As a place to begin for this venture, we will simply copy and paste lots of the information immediately into our venture. Here’s what we are going to take:

The meta information are including JSON structured knowledge markup to our templates. This is a superb profit that Ghost affords by default on their platform they usually’ve transposed it into Gatsby as a part of their starter template.

Then we took the Pagination and PostCard.js parts that we will drop proper into our venture. And with these parts, we will take the template information and drop them into our venture and they’ll work.

The fragments.js file makes our GraphQL queries rather a lot cleaner for every of our pages and templates — we now simply have a central supply for all of our GraphQL queries. And the siteConfig.js file has a couple of Ghost configuration choices which can be best to place in a separate file.

Now we are going to simply want to put in a couple of npm packages and replace our gatsby-node file to make use of our new templates.

The packages that we might want to set up are gatsby-awesome-pagination, @tryghost/helpers, and @tryghost/helpers-gatsby.

So we are going to do:

npm set up –save gatsby-awesome-pagination @tryghost/helpers @tryghost/helpers-gatsby

Then we have to make some updates to our gatsby-node file.

First, we are going to add the next new imports to the highest of our file:

const { paginate } = require(`gatsby-awesome-pagination`);
const { postsPerPage } = require(`./src/utils/siteConfig`);

Next, in our exports.createPages, we are going to replace our GraphQL question to:

{
allGhostPost(type: { order: ASC, fields: published_at }) {
edges {
node {
slug
}
}
}
allGhostTag(type: { order: ASC, fields: title }) {
edges {
node {
slug
url
postCount
}
}
}
allGhostCreator(type: { order: ASC, fields: title }) {
edges {
node {
slug
url
postCount
}
}
}
allGhostPage(type: { order: ASC, fields: published_at }) {
edges {
node {
slug
url
}
}
}
}

This will pull the entire GraphQL knowledge we’d like for Gatsby to construct pages primarily based on our new templates.

To do this, we are going to extract all of these queries and assign them to variables:

// Extract question outcomes
const tags = end result.knowledge.allGhostTag.edges
const authors = end result.knowledge.allGhostCreator.edges
const pages = end result.knowledge.allGhostPage.edges
const posts = end result.knowledge.allGhostPost.edges

Then we are going to load all of our templates:

// Load templates
const tagsTemplate = path.resolve(`./src/templates/tag.js`)
const authorTemplate = path.resolve(`./src/templates/writer.js`)
const pageTemplate = path.resolve(`./src/templates/web page.js`)
const postTemplate = path.resolve(`./src/templates/publish.js`)

Note right here that we’re changing our outdated blog-post.js template with publish.js, so we will go forward and delete blog-post.js from our templates folder.

Finally, we are going to add this code to construct pages from our templates and GraphQL knowledge:

// Create tag pages
tags.forEach(({ node }) => {
const completePosts = node.postCount !== null ? node.postCount : 0

// This half right here defines, that our tag pages will use
// a `/tag/:slug/` permalink.
const url = `/tag/${node.slug}`

const objects = Array.from({size: completePosts})

// Create pagination
paginate({
createPage,
objects: objects,
itemsPerPage: postsPerPage,
part: tagsTemplate,
pathPrefix: ({ pageNumber }) => (pageNumber === 0) ? url : `${url}/web page`,
context: {
slug: node.slug
}
})
})

// Create writer pages
authors.forEach(({ node }) => {
const completePosts = node.postCount !== null ? node.postCount : 0

// This half right here defines, that our writer pages will use
// a `/writer/:slug/` permalink.
const url = `/writer/${node.slug}`

const objects = Array.from({size: completePosts})

// Create pagination
paginate({
createPage,
objects: objects,
itemsPerPage: postsPerPage,
part: authorTemplate,
pathPrefix: ({ pageNumber }) => (pageNumber === 0) ? url : `${url}/web page`,
context: {
slug: node.slug
}
})
})

// Create pages
pages.forEach(({ node }) => {
// This half right here defines, that our pages will use
// a `/:slug/` permalink.
node.url = `/${node.slug}/`

createPage({
path: node.url,
part: pageTemplate,
context: {
// Data handed to context is on the market
// in web page queries as GraphQL variables.
slug: node.slug,
},
})
})

// Create publish pages
posts.forEach(({ node }) => {
// This half right here defines, that our posts will use
// a `/:slug/` permalink.
node.url = `/${node.slug}/`
createPage({
path: node.url,
part: postTemplate,
context: {
// Data handed to context is on the market
// in web page queries as GraphQL variables.
slug: node.slug,
},
})
})

Here, we’re looping in flip by way of our tags, authors, pages, and posts. For our pages and posts, we’re merely creating slugs after which creating a brand new web page utilizing that slug and telling Gatsby what template to make use of.

For the tags and writer pages, we’re additionally including pagination information utilizing gatsby-awesome-pagination that can be handed into the web page’s pageContext.

With that, all of our content material ought to now be efficiently constructed and displayed. But we may use a bit of labor on styling. Since we copied over our templates immediately from the Ghost Starter, we will use their types as properly.

Not all of those can be relevant, however to maintain issues easy and never get too slowed down in styling, I took the entire types from Ghost’s src/types/app.css ranging from the part Layout till the top. Then you’ll simply paste these into the top of your src/types.css file.

Observe the entire types beginning with kg — this refers to Koening which is the title of the Ghost editor. These types are essential for the Post and Page templates, as they’ve particular types that deal with the content material that’s created within the Ghost editor. These types make sure that the entire content material you might be writing in your editor is translated over and displayed in your weblog accurately.

Lastly, we’d like our web page.js and publish.js information to accommodate our inner hyperlink alternative from the earlier step, beginning with the queries:

Page.js

ghostPage(slug: { eq: $slug } ) {
…GhostPageFields
fields {
html
}
}

Post.js

ghostPost(slug: { eq: $slug } ) {
…GhostPostFields
fields {
html
}
}

And then the sections of our templates which can be utilizing the HTML content material. So in our publish.js we are going to change:

To:

And equally, in our web page.js file, we are going to change web page.html to web page.fields.html.

Dynamic Page Content

One of the disadvantages of Ghost when used as a conventional CMS, is that it’s not doable to edit particular person items of content material on a web page with out going into your precise theme information and laborious coding it.

Say you have got a bit in your web site that may be a Call-to-Action or buyer testimonials. If you wish to change the textual content in these containers, you’ll have to edit the precise HTML information.

One of the good components of going headless is that we will make dynamic content material on our web site that we will simply edit utilizing Ghost. We are going to do that by utilizing Pages that we are going to mark with ‘internal’ tags or tags that begin with a # image.

So for example, let’s go into our Ghost backend, create a brand new Page known as Message, kind one thing as content material, and most significantly, we are going to add the tag #message.

Now let’s return to our gatsby-node file. Currently, we’re constructing pages for all of our tags and pages, but when we modify our GraphQL question in createPages, we will exclude every thing inner:

allGhostTag(type: { order: ASC, fields: title }, **filter: {slug: {regex: “/^((?!hash-).)*$/”}}**) {
edges {
node {
slug
url
postCount
}
}
}
//…
allGhostPage(type: { order: ASC, fields: published_at }, **filter: {tags: {elemMatch: {slug: {regex: “/^((?!hash-).)*$/”}}}}**) {
edges {
node {
slug
url
html
}
}
}

We are including a filter on tag slugs with the regex expression /^((?!hash-).)*$/. This expression is saying to exclude any tag slugs that embrace hash-.

Now, we received’t be creating pages for our inner content material, however we will nonetheless entry it from our different GraphQL queries. So let’s add it to our index.js web page by including this to our question:

question GhostIndexQuestion($restrict: Int!, $skip: Int!) {
web site {
siteMetadata {
title
}
}
message: ghostPage
(tags: {elemMatch: {slug: {eq: “hash-message”}}}) {
fields {
html
}
}
allGhostPost(
type: { order: DESC, fields: [published_at] },
restrict: $restrict,
skip: $skip
) {
edges {
node {
…GhostPostFields
}
}
}
}

Here we’re creating a brand new question known as “message” that’s searching for our inner content material web page by filtering particularly on the tag #message. Then let’s use the content material from our #message web page by including this to our web page:

//…
const BlogIndex = ({ knowledge, location, pageContext }) => {
const web siteTitle = knowledge.web site.siteMetadata?.title || `Title`
const posts = knowledge.allGhostPost.edges
const message = knowledge.message;
//…
return (


)
}

Finishing Touches

Now we’ve acquired a very nice weblog setup, however we will add a couple of ultimate touches: pagination on our index web page, a sitemap, and RSS feed.

First, so as to add pagination, we might want to convert our index.js web page right into a template. All we have to do is lower and paste our index.js file from our src/pages folder over to our src/templates folder after which add this to the part the place we load our templates in gatsby-node.js:

// Load templates
const indexTemplate = path.resolve(`./src/templates/index.js`)

Then we have to inform Gatsby to create our index web page with our index.js template and inform it to create the pagination context.

Altogether we are going to add this code proper after the place we create our publish pages:

// Create Index web page with pagination
paginate({
createPage,
objects: posts,
itemsPerPage: postsPerPage,
part: indexTemplate,
pathPrefix: ({ pageNumber }) => {
if (pageNumber === 0) {
return `/`
} else {
return `/web page`
}
},
})

Now let’s open up our index.js template and import our Pagination part and add it proper beneath the place we map by way of our posts:

import Pagination from ‘../parts/pagination’
//…



//…

Then we simply want to alter the hyperlink to our weblog posts from:

to:

This prevents Gatsby Link from prefixing our hyperlinks on pagination pages — in different phrases, if we didn’t do that, a hyperlink on web page 2 would present as /web page/2/my-post/ as an alternative of simply /my-post/ like we would like.

With that accomplished, let’s arrange our RSS feed. This is a fairly easy step, as we will use a ready-made script from the Ghost staff’s Gatsby starter. Let’s copy their file generate-feed.js into our src/utils folder.

Then let’s use it in our gatsby-config.js by changing the present gatsby-plugin-feed part with:

{
resolve: `gatsby-plugin-feed`,
choices: {
question: `
{
allGhostSettings {
edges {
node {
title
description
}
}
}
}
`,
feeds: [
generateRSSFeed(config),
],
},
}

We might want to import our script together with our siteConfig.js file:

const config = require(`./src/utils/siteConfig`);
const generateRSSFeed = require(`./src/utils/generate-feed`);
//…

Finally, we have to make one necessary addition to our generate-feed.js file. Right after the GraphQL question and the output area, we have to add a title area:

#…
output: `/rss.xml`,
title: “Gatsby Starter Blog RSS Feed”,
#…

Without this title area, gatsby-plugin-feed will throw an error on the construct.

Then for our final of completion, let’s add our sitemap by putting in the package deal gatsby-plugin-advanced-sitemap:

npm set up –save gatsby-plugin-advanced-sitemap

And including it to our gatsby-config.js file:

{
resolve: `gatsby-plugin-advanced-sitemap`,
choices: {
question: `
{
allGhostPost {
edges {
node {
id
slug
updated_at
created_at
feature_image
}
}
}
allGhostPage {
edges {
node {
id
slug
updated_at
created_at
feature_image
}
}
}
allGhostTag {
edges {
node {
id
slug
feature_image
}
}
}
allGhostCreator {
edges {
node {
id
slug
profile_image
}
}
}
}`,
mapping: {
allGhostPost: {
sitemap: `posts`,
},
allGhostTag: {
sitemap: `tags`,
},
allGhostCreator: {
sitemap: `authors`,
},
allGhostPage: {
sitemap: `pages`,
},
},
exclude: [
`/dev-404-page`,
`/404`,
`/404.html`,
`/offline-plugin-app-shell-fallback`,
],
createLinkInHead: true,
addUncaughtPages: true,
}
}
}

The question, which additionally comes from the Ghost staff’s Gatsby starter, creates particular person sitemaps for our pages and posts in addition to our writer and tag pages.

Now, we simply need to make one small change to this question to exclude our inner content material. Same as we did within the prior step, we have to replace these queries to filter out tag slugs that include ‘hash-’:

allGhostPage(filter: {tags: {elemMatch: {slug: {regex: “/^((?!hash-).)*$/”}}}}) {
edges {
node {
id
slug
updated_at
created_at
feature_image
}
}
}
allGhostTag(filter: {slug: {regex: “/^((?!hash-).)*$/”}}) {
edges {
node {
id
slug
feature_image
}
}
}

Wrapping Up

With that, you now have a totally functioning Ghost weblog working on Gatsby you could customise from right here. You can create your entire content material by working Ghost in your localhost after which when you find yourself able to deploy, you merely run:

gatsby construct

And then you’ll be able to deploy to Netlify utilizing their command-line instrument:

netlify deploy -p

Since your content material solely lives in your native machine, it’s also a good suggestion to make occasional backups, which you are able to do utilizing Ghost’s export characteristic.

This exports your entire content material to a json file. Note, it doesn’t embrace your pictures, however these can be saved on the cloud anyway so that you don’t want to fret as a lot about backing these up.

I hope you loved this tutorial the place we lined:

  • Setting up Ghost and Gatsby;
  • Handling Ghost Images utilizing a storage converter;
  • Converting Ghost inner hyperlinks to Gatsby Link;
  • Adding templates and types for all Ghost content material varieties;
  • Using dynamic content material created in Ghost;
  • Setting up RSS feeds, sitemaps, and pagination.

If you have an interest in exploring additional what’s doable with a headless CMS, try my work at Epilocal, the place I’m utilizing an identical tech stack to construct instruments for native information and different unbiased, on-line publishers.

Note: You can discover the complete code for this venture on Github right here, and you may also see a working demo right here.

Further Reading on Smashing Magazine

Smashing Editorial
(vf, nl, il)

Leave a Reply

Your email address will not be published.