The Ghost Themes Developer Hub

Welcome to the Ghost Themes developer hub. You'll find comprehensive guides and documentation to help you start working with Ghost Themes as quickly as possible, as well as support if you get stuck. Let's jump right in!

Get Started    

Migrating themes to 1.0.0

A guide to getting your theme ready for Ghost 1.0.0

Welcome to the brave new world of Ghost 1.0.0. We've made a number of changes that themes need to take into account, in order to work with the new versions of Ghost.

This guide aims to make the process of migrating themes as fast & painless as possible 💨

Most themes will only need to make a handful of small adjustments - so don't get put off by the length of the guide. Updating your theme should only take a few minutes. Upload your theme to GScan to get an instant report on which changes affect you.

If you've got a multiple themes, or a particularly complex theme to upgrade, GScan can be installed as a command line tool to run against local folders whilst developing.

Themes are now validated

Your theme will now be validated when uploaded or activated on Ghost 1.0. If the theme has fatal errors, the user will not be able to upload or activate it.

For a quick reference list of all the things that have changed in Ghost 1.0, check out the theme changelog.

Prepare your `package.json` file

As a very first step, ensure you've got a valid package.json file. Ghost 1.0.0 has a bunch of new requirements around this.

If you don't have one yet, create a new file in the root directory of your theme and name it package.json. If you're not sure how this should look like, Ghost's default Casper theme is a very good orientation point here.

Once you've created the package.json file, we're going to add (at least) three properties. This example shows the minimum recommended properties for your package.json file:

{
    "name": "your-theme-name",
    "description": "A brief explanation of your theme",
    "version": "0.5.0",
    "engines": {
        "ghost": ">=1.0.0"
    },
    "license": "MIT",
    "author": {
        "email": "[email protected]"
    },
    "config": {
        "posts_per_page": 10
    }
}

name (required)

All themes need a name property to tell us, how the theme is called. The name must be lowercase and hyphenated. See here for the full documentation.

version (required)

In order to manage updates, your theme is required to provide a semver-compliant version number. See here for the full documentation.

author.email (required)

The email is required so that themes which are distributed have a method of contacting the author so users can get support and more importantly so that Ghost can reach out about breaking changes and security updates. See here for the full documentation.

engines.ghost (recommended)

The engines.ghost property indicates what version of Ghost your theme is compatible with. This should be a valid semver version or version range that Ghost can use to understand if your theme can be installed. For a theme that is build with Ghost 1.0 you should use "engines": {"ghost": "^1.0.0"}. See here for the full documentation.

license (recommended)

A valid license string, as per npm package.json. We recommend MIT. See here

config.posts_per_page (recommended)

Since Ghost 1.0, the setting for posts_per_page does no longer live in Ghost admin. As this should be a theme individual setting, that's where we moved it to. Now, each theme can define this setting in the package.json file. If nothing is defined, Ghost will fall back to 5 posts per page. See here for the full documentation.

Check your theme after you made changes

After you made some changes to your theme, zip the new version and upload it to https://gscan.ghost.org to check the current work in progress.

Fix any fatal errors

Some validation rules in GScan are fatal errors. Those need to be fixed first, because they'll prevent your theme from being uploaded or activated.

Theme upload and activation restrictions

The usage of the following deprecated helpers and object attributes will cause that the theme can't be uploaded or activated:

  • {{pageUrl}}
  • {{image}}
  • {{author.image}}
  • {{author.cover}}
  • {{post.image}}
  • {{post.author.image}}
  • {{post.author.cover}}
  • {{post.tags.[#].image}}
  • {{@blog.cover}}
  • {{tag.image}}

If your theme contains a Disqus embed, this will also trigger a fatal error.

  • Switch from using "post-{{id}}" as an identifier to "post-{{comment_id}}"

Theme activation and upload is also not possible if the following cases apply:

  • Usage of invalid Handlebars
  • Missing index.hbs template
  • Missing post.hbs template
  • Usage of Symlinks

Replace {{pageUrl}} with {{page_url}}

If your theme still uses the deprecated helper {{pageUrl}}, you need to replace it with {{page_url}}. The functionality of the helper stays the same, just the word pageUrl needs to be replaced with page_url.

Replace {{image}} with {{img_url}}

The {{image}} helper was replaced with the {{img_url}} helper, but its usage depends strongly on the case.

Not every usage of {{image}} needs to be replaced with {{img_url}}, as {{img_url}} outputs (like the name suggests) the URL of the desired image.

Let's take the following example:

{{#post}}
<a class="post-meta" href="{{@blog.url}}">&larr; Back to the front page</a>
<h1 class="post-title">{{{title}}}</h1>
<section class="post-content">
    {{#if image}}<img src="{{image}}" alt="Post image" />{{/if}}
    {{content}}
</section>
{{/post}}

In this example, we are in a post context and need to check for the existence of the post feature image. The first reference to image happens in the {{#if}} block helper. The image attribute of post context was renamed to feature_image, so that's what we need to change it to in this conditional.

The second reference on the other hand needs the proper URL of the referred feature_image, which means, we'd need to use the new {{img_url}} helper in this case.

The updated code should now look like this:

{{#post}}
<a class="post-meta" href="{{@blog.url}}">&larr; Back to the front page</a>
<h1 class="post-title">{{{title}}}</h1>
<section class="post-content">
    {{#if feature_image}}<img src="{{img_url feature_image}}" alt="Post image" />{{/if}}
    {{content}}
</section>
{{/post}}

When to use {{img_url}}

When you need the URL of the image, it is safe to use {{img_url}}. Depending on the context, you'll need to refer to the correct attribute:

Check the data attributes for tag, post and author images, to find out which attributes you need to reference for each context.

Replace old image data attributes

author context

The image attribute in author context was replaced with profile_image and the cover attribute was replaced with cover_image.

Let's start with the easiest ones:

  • {{author.image}}
  • {{author.cover}}

No matter if used in an {{#if}} block helper, in a post context like e. g {{post.author.image}} or on its own, it's safe to just replace the occurrences of the words author.image with author.profile_image and author.cover with author.cover_image.

Many themes use the attribute of cover itself, when in author context. So whenever you see a code snippet with a similar usage to this:

{{#author}}
    {{#if cover}}{{/if}}
{{/author}}

Just go ahead and replace the word cover with cover_image as well.

post context

The image attribute in post context was replaced with feature_image.

It is safe to replace every occurrence of post.image in your code with post.feature_image.

Any usage of the attribute image within the post context, needs to be replace with feature_image:

{{#post}}
    {{#if feature_image}}
        {{feature_image}}
    {{/if}}
{{/post}}

tag context

The image attribute in tag context was replaced with feature_image.

It is safe to replace every occurrence of tag.image in your code with tag.feature_image.

No matter if used in an {{#if}} block helper, or in a post context like e. g {{post.tag.image}}, {{post.tags.[#].image}}, {{tags.[#].image}} or on its own, it's safe to just replace the words tag.image with tag.feature_image.

Any usage of the attribute image within the tag context, needs to be replace with feature_image:

{{#tag}}
    {{#if feature_image}}
        {{feature_image}}
    {{/if}}
{{/tag}}

Replace the {{@blog.cover}} helper with {{@blog.cover_image}}

The cover attribute for the global @blog data was replaced with cover_image.

Every usage of {{@blog.cover}} can safely be replaced with {{@blog.cover_image}}.

Fix Disqus

If you have Disqus embed code in your theme, you'll need to make a quick amend to the identifier to use {{comment_id}} instead of {{id}}.

Modern embed code must change from
this.page.identifier = 'ghost-{{id}}'; to this.page.identifier = 'ghost-{{comment_id}}';

Older embed code must change from
var disqus_identifier = 'ghost-{{id}}'; to var disqus_identifier = 'ghost-{{comment_id}}';

Check instances of {{id}}

If you're using {{id}} elsewhere in your theme, for example to create css classes, you may wish to also change to use {{comment_id}}. The difference is that {{id}} will output new ObjectID style IDs for all posts, whereas {{comment_id}} will output LTS incremental IDs for imported posts, and ObjectIDs for new posts.

Other fatal errors

If any of these validation rules are not passed, your theme will not be uploaded or activated:

Non fatal errors

GScan will list some more validation rules in the "Must fix" section which are also errors, but will still allow your theme to be uploaded and/or activated. These errors should still be fixed, as they could cause your theme to not work properly or might not work anymore in a future version of Ghost.

Clean up your <head>

In Ghost 1.0.0, {{ghost_head}} has taken over handling both meta description and favicons. Any custom code in themes should be removed.

Favicon code is no longer needed.

Ghost now handles favicons via an upload, and code is output automatically for you in {{ghost_head}}.

In your default.hbs template check out the <head> section and remove any code that outputs a favicon e.g. <link rel="shortcut icon" href="{{asset "favicon.ico"}}">.

The usage of {{meta_description}} in HTML head is no longer required

Ghost now outputs this for you automatically in {{ghost_head}}.

In your default.hbs template check out the <head> section and simply delete the line where the {{meta_description}} helper is used.

This line usually looks similar to this: <meta name="description" content="{{meta_description}}" />

Replace {{@blog.posts_per_page}} with {{@config.posts_per_page}}

Now that posts_per_page is a individual theme setting (it's defined in your package.json file, and if not defined will fall back to a default value of 5) and not a global blog setting anymore, the way to access it has also changed.

Where ever you find the usage of {{@blog.posts_per_page}}, it's safe to just replace it with {{@config.posts_per_page}}.

Replace {{content words="0"}} with the {{img_url}} helper

The usage of {{content words="0"}} has always been a hack and was actually never supported by Ghost. If you want to access the post's feature_image, please use the {{img_url}} helper instead.

Deprecated CSS classes

With Ghost 1.0 we renamed some CSS classes in the {{body_class}} and {{post_class}} helpers.

If your theme uses one of the following classes in the stylesheet, please replace them:

  • Replace .archive-template with the .paged CSS class
  • Replace .page with the .page-template CSS class
  • Replace .page-template-slug (e. g. .page-template-welcome) with the .page-slug CSS class

Check out the rendered classes per context here!

{{get}} helper changes to {{else}}

The {{get}} helper and other public API features are now enabled by default.

Also in 1.0, the {{else}} block is only triggered for the {{get}} helper if there is an error.
If you were using the {{get}} helper with an {{else}} clause to do something different when there are no results, you will need to move the {{else}} to be part of the {{foreach}} or resource block helper that is used inside of the {{get}} helper.

E.g. instead of

{{#get "posts"}}
  {{#foreach posts}}
    {{! If there are posts}}
  {{/foreach}}
{{else}}
  {{! If there are NO posts}}
{{/get}}

You will need to do:

{{#get "posts"}}
  {{#foreach posts}}
       {{! If there are posts}}
  {{else}}
  {{! If there are NO posts}}
  {{/foreach}}
{{/get}}

https://themes.ghost.org/docs/get#section-using-else-


Phew, that's it! 😅

Now check your theme on GScan again and it should hopefully look like this:

Get help from the community

If you're having trouble or need some answers to your questions, swing by in our slack community. We have a dedicated #themes channel, where you can find easily help from others.

Migrating themes to 1.0.0

A guide to getting your theme ready for Ghost 1.0.0