Skip to content

Ghost Custom Themes — Structure, Upload and GScan Validation

DodaTech Updated 2026-06-28 10 min read

In this tutorial, you'll learn how to create custom themes for Ghost — understanding the complete theme structure, building a theme from scratch, uploading it to a Ghost site, and validating it with GScan to ensure compatibility and performance.

What You'll Learn

  • The complete Ghost theme file structure and requirements
  • Building a custom theme from scratch
  • The package.json manifest file in detail
  • Uploading a theme via the Ghost admin panel
  • Uploading a theme via the Ghost CLI
  • Using GScan for theme validation
  • Common GScan errors and how to fix them
  • Theme compatibility with Ghost versions
  • Theme development workflow and best practices

Why It Matters

Pre-built themes are convenient, but they cannot match your exact design vision. Custom themes give you complete control over your site's appearance. However, an incompatible theme can break your site, load slowly, or fail validation. Understanding the theme structure and validation Process ensures you create themes that are fast, secure, and compatible with future Ghost updates.

Real-World Use

A web designer is hired to create a custom Ghost theme for a client's photography portfolio. She builds the theme from scratch, tests it on a local Ghost installation, runs GScan to validate, fixes the reported issues, uploads it to the client's Ghost site via the admin panel, and activates it. The client gets a unique design without any third-party theme bloat.

Learning Path

flowchart LR
  A["Theme Assets"] --> B["Custom Themes
You are here"]:::current B --> C["Theme Customization"] C --> D["Membership System"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Complete Theme Structure

A valid Ghost theme has this structure:

my-theme/
├── assets/
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── index.js
│   └── images/
│       ├── logo.svg
│       └── screenshot.png
├── partials/
│   ├── header.hbs
│   └── footer.hbs
├── default.hbs
├── index.hbs
├── post.hbs
├── page.hbs
├── tag.hbs
├── author.hbs
├── error.hbs
└── package.json

Required Files

File Required Purpose
package.json Yes Theme manifest
default.hbs Yes Base template
index.hbs Yes Homepage/post listing
post.hbs Yes Single post view
page.hbs No (falls back to post.hbs) Single page view

Optional Files

File Purpose
tag.hbs Tag listing page
author.hbs Author listing page
error.hbs 404 and error pages
partials/*.hbs Reusable template components
custom-{slug}.hbs Custom page/post templates

Building a Custom Theme from Scratch

Step 1: Create the Directory Structure

mkdir -p my-theme/assets/css
mkdir -p my-theme/assets/js
mkdir -p my-theme/assets/images
mkdir -p my-theme/partials

Step 2: Create package.json

{
  "name": "my-custom-theme",
  "version": "1.0.0",
  "description": "A custom theme for my Ghost site",
  "author": {
    "name": "Your Name",
    "email": "you@example.com"
  },
  "config": {
    "posts_per_page": 10
  }
}

Step 3: Create default.hbs

<!DOCTYPE html>
<html lang="{{@site.locale}}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{meta_title}}</title>
  {{ghost_head}}
  <link rel="stylesheet" href="{{asset "css/style.css"}}">
</head>
<body>
  {{> header}}
  <main>
    {{{body}}}
  </main>
  {{> footer}}
  {{ghost_foot}}
  <script src="{{asset "js/index.js"}}"></script>
</body>
</html>

Step 4: Create index.hbs

{{!< default}}

<section class="post-feed">
  {{#foreach posts}}
    <article class="post-card">
      {{#if feature_image}}
        <img src="{{img_url feature_image size="s"}}" alt="{{title}}">
      {{/if}}
      <h2><a href="{{url}}">{{title}}</a></h2>
      <p>{{excerpt words="25"}}</p>
      <time datetime="{{date format="YYYY-MM-DD"}}">{{date}}</time>
    </article>
  {{/foreach}}
</section>

{{pagination}}

Step 5: Create post.hbs

{{!< default}}

<article class="post-full">
  {{#post}}
    {{#if feature_image}}
      <figure>
        <img src="{{img_url feature_image size="xl"}}" alt="{{feature_image_alt}}">
      </figure>
    {{/if}}
    <h1>{{title}}</h1>
    <div class="post-meta">
      <time datetime="{{date}}">{{date}}</time>
      {{#primary_author}}by <a href="{{url}}">{{name}}</a>{{/primary_author}}
    </div>
    <section class="post-content">
      {{content}}
    </section>
  {{/post}}
</article>

Step 6: Create Partials

header.hbs:

<header class="site-header">
  <a href="{{@site.url}}" class="site-logo">
    {{#if @site.logo}}
      <img src="{{@site.logo}}" alt="{{@site.title}}">
    {{else}}
      {{@site.title}}
    {{/if}}
  </a>
  <nav>{{navigation}}</nav>
</header>

footer.hbs:

<footer class="site-footer">
  <p>&copy; {{date format="YYYY"}} {{@site.title}}. All rights reserved.</p>
  <nav>{{navigation type="secondary"}}</nav>
</footer>

Step 7: Add CSS

Create a basic assets/css/style.css:

/* Reset and base */
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; line-height: 1.6; color: #333; max-width: 720px; margin: 0 auto; padding: 1rem; }

/* Header */
.site-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 0; border-bottom: 1px solid #eee; }
.site-logo { font-size: 1.5rem; font-weight: bold; color: #333; text-decoration: none; }

/* Post cards */
.post-card { margin: 2rem 0; padding-bottom: 2rem; border-bottom: 1px solid #eee; }
.post-card h2 { margin: 0 0 0.5rem; }
.post-card time { color: #666; font-size: 0.875rem; }

Step 8: Add Screenshot

Create a 1200x900 screenshot image at assets/images/screenshot.png showing your theme design.

Uploading a Theme

Method 1: Admin Panel Upload

  1. Go to Settings > Design > Change theme > Upload theme.
  2. Select your theme ZIP file.
  3. Ghost uploads and validates the theme.
  4. Click "Activate" to apply it.

Method 2: Ghost CLI Upload

# Zip the theme
cd my-theme
zip -r ../my-theme.zip .

# Upload via CLI
ghost theme upload ../my-theme.zip

Method 3: Manual File Copy

Copy the theme folder directly to content/themes/:

cp -r my-theme /var/www/ghost/content/themes/

Then activate it in the admin panel.

Theme Validation with GScan

GScan is Ghost's official theme validation tool. It checks your theme for:

  • Required files and structure
  • Template syntax errors
  • Deprecated helper usage
  • Performance best practices
  • Security issues
  • Ghost version compatibility

Running GScan

# Install GScan
npm install -g gscan

# Validate a theme
gscan /path/to/my-theme

# Validate with verbose output
gscan /path/to/my-theme -v

GScan Results

GScan categorizes issues by severity:

Level Meaning Action Required
Pass No issues None
Warning Minor issues should be fixed Recommended
Error Must be fixed before activation Required
Fatal Theme cannot be used Must fix

Common GScan Errors

Error Cause Fix
Missing default.hbs Base template not found Create default.hbs
Missing package.json Theme manifest missing Create package.json with name and version
Deprecated helper Using a removed helper Replace with current equivalent
Missing {{ghost_head}} SEO metadata not injected Add {{ghost_head}} to default.hbs <head>
Missing {{ghost_foot}} Scripts not injected Add {{ghost_foot}} before </body>
Asset not found Referenced asset does not exist Create the file or fix the path
Invalid JSON in package.json Syntax error in JSON Validate your JSON
Unsafe HTML Using inline event handlers Remove onclick, onload, etc.
Missing screenshot No theme preview image Add 1200x900 screenshot

Running GScan in CI/CD

gscan my-theme/ --fail-on warning

This exits with a non-zero code if any warnings or errors are found — useful for automated theme deployment pipelines.

Theme Compatibility

Ghost Version Compatibility

The package.json can specify the Ghost version range:

{
  "name": "my-theme",
  "version": "1.0.0",
  "engines": {
    "ghost": ">=5.0.0"
  }
}

When you upload a theme, Ghost checks compatibility. If the theme specifies a higher Ghost version than your installation, Ghost warns you.

Backward Compatibility

Ghost aims to maintain backward compatibility for themes across minor version updates. Major version updates (e.g., Ghost 4 to 5) may introduce breaking changes.

Theme Development Workflow

  1. Develop locally — Create and edit themes on a local Ghost installation.
  2. Test thoroughly — Verify all templates render correctly.
  3. Run GScan — Fix all errors and warnings before uploading.
  4. Create ZIP — Package the theme (only needed files).
  5. Upload to staging — Test on a staging Ghost site.
  6. Activate on production — After staging tests pass.

Excluding Unnecessary Files

When creating the theme ZIP, exclude development files:

zip -r ../my-theme.zip . -x "*.git*" -x "node_modules/*" -x "*.DS_Store" -x "src/*"

Only include files that the theme needs at runtime.

Common Mistakes

  1. Uploading a theme as a folder (not ZIP): The admin panel accepts only ZIP files. If you try to upload a folder, nothing happens. Always zip your theme before uploading.

  2. Including unnecessary files in the ZIP: Including .git folders, node_modules, or source maps increases the ZIP size and may trigger upload limits. Exclude development files.

  3. Missing the screenshot: A theme without a screenshot shows a blank preview in the admin panel. This looks unprofessional and makes it hard to identify themes. Always include a 1200x900 screenshot.

  4. Failing GScan validation and activating anyway: If GScan reports errors, Ghost may still allow activation, but the theme may break or cause issues. Always fix all errors before going live.

  5. Not testing on a staging site first: A theme that works locally may behave differently on production due to differences in Ghost version, database content, or server configuration. Always test on staging before production.

Practice Questions

  1. What files are required in a valid Ghost theme? Answer: package.json (theme manifest), default.hbs (base template), index.hbs (homepage listing), and post.hbs (single post view). page.hbs, tag.hbs, and author.hbs are optional.

  2. What is GScan and what does it check? Answer: GScan is Ghost's official theme validation tool. It checks for required files, template syntax errors, deprecated helpers, performance best practices, security issues, and Ghost version compatibility. Issues are categorized as Pass, Warning, Error, or Fatal.

  3. How do you upload a custom theme to a Ghost site? Answer: Three methods: (1) Admin panel — Settings > Design > Change theme > Upload theme. (2) Ghost CLI — ghost theme upload theme.zip. (3) Manual — copy the theme folder to content/themes/ and activate in the admin panel.

  4. Challenge: Create a custom Ghost theme from scratch, validate it with GScan, fix all errors, and upload it to a Ghost installation. Your theme should have: default.hbs, index.hbs, post.hbs, page.hbs, header partial, footer partial, package.json, a CSS file, and a screenshot. Achieve a "Pass" from GScan with zero warnings.

FAQ

Can I use a Ghost theme from the marketplace as a starting point?

Yes. Many developers start with the official Ghost theme 'Casper' and modify it. Download the source from GitHub, rename the theme, and customize it. You must change the name in package.json to avoid conflicts.

Why does GScan say 'Missing {{ghost_head}}' when I have it in my template?

Check that {{ghost_head}} is in default.hbs, not in a child template. It must be in the section of the base template. Also check for typos — it is {{ghost_head}} not {{ghosthead}}.

Can I upload a theme via the Ghost API?

Yes. The Admin API supports theme upload via POST /ghost/api/admin/themes/. You need an Admin API key and send the theme as a multipart/form-data file upload.

What happens if I activate a theme with GScan errors?

Ghost allows activation even with errors, but the theme may not render correctly. Some templates may be missing, helpers may not work, and performance may suffer. Always fix errors before activation.

How do I update a theme after it is installed?

Upload the new version as a ZIP file. Ghost prompts you to overwrite the existing theme. After uploading, the updated theme is active immediately. Check your site after updating.

Mini Project

Your task: Create a complete custom Ghost theme and deploy it.

  1. Design a simple blog theme with: a header with logo and navigation, a homepage with a grid of post cards, a single post view with feature image, a page view, and a footer with copyright.
  2. Build all required template files and partials.
  3. Create package.json with correct metadata.
  4. Add a CSS file with responsive styles.
  5. Create a 1200x900 screenshot.
  6. Run GScan and fix all issues until you get a Pass.
  7. ZIP the theme and upload it to a Ghost site.
  8. Activate the theme and verify all page types render correctly.

This exercise gives you a complete custom theme development workflow you can use for any project.

What's Next

Now that you can build custom themes, learn how to customize them further:

Continue to Lesson 19: Theme Customization — Navigation, custom routes, and dynamic routing with routes.yaml.

Related lessons:

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro