Ghost Custom Themes — Structure, Upload and GScan Validation
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>© {{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
- Go to
Settings > Design > Change theme > Upload theme. - Select your theme ZIP file.
- Ghost uploads and validates the theme.
- 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
Recommended Workflow
- Develop locally — Create and edit themes on a local Ghost installation.
- Test thoroughly — Verify all templates render correctly.
- Run GScan — Fix all errors and warnings before uploading.
- Create ZIP — Package the theme (only needed files).
- Upload to staging — Test on a staging Ghost site.
- 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
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.
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.
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.
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.
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
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.
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.
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.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
Mini Project
Your task: Create a complete custom Ghost theme and deploy it.
- 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.
- Build all required template files and partials.
- Create package.json with correct metadata.
- Add a CSS file with responsive styles.
- Create a 1200x900 screenshot.
- Run GScan and fix all issues until you get a Pass.
- ZIP the theme and upload it to a Ghost site.
- 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:
- Handlebars Templates — Advanced templating in Ghost
- Theme Assets — CSS, JS, and asset management
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro