Imagine: your media blog on Ghost lags at 5000 posts, pages load in 4 seconds, and Google sees LCP 4.2s. Typical for themes where nested {{#get}} requests without caching and unoptimized images. We solve this by rewriting the theme for Ghost 5 with custom settings and proper caching.
Unlike WordPress, Handlebars in Ghost doesn't execute arbitrary code — only calls built-in helpers. This is a plus for security but a minus for performance if you don't know the details. Our team with years of experience in Ghost development (30+ projects) creates themes that pass gscan without warnings and score 90+ in Lighthouse. Here's how it works.
Problems We Solve
N+1 queries via {{#get}}. A common mistake is fetching data for each post with a separate request. The solution is to use eager loading via the include parameter in the main query or extract data into partials with memoization.
Lack of responsive images. Without img_url with size and srcset, the theme loads full originals — 3000px on mobile. This dramatically increases LCP and CLS. The correct approach is to define image_sizes in package.json and build srcset for each image.
Neglecting custom settings. Many developers hardcode styles and functionality that the editor could change via Admin. We use @custom in the config — this gives the editor control without redeployment.
How Handlebars Affects Ghost Performance
Handlebars allows creating safe templates without the RCE risk typical of PHP engines. Ghost version 5.0+ stably supports all helpers for building complex interfaces, from pagination to Members. Compared to Nunjucks, Handlebars is the native language that gives maximum control with minimal room for error. It's 2x faster than PHP template engines in WordPress under equal load, due to no code execution overhead.
How We Do It: Media Blog Optimization Case
Client — a media with 5000+ posts, theme on old Bootstrap, LCP 4.2s, INP 300ms. Goals: preserve design, improve Core Web Vitals, add Members for premium content.
Stack: Ghost 5.5, Handlebars, Node.js 18, Nginx, Cloudflare. We completely rewrote the template structure:
- Replaced nested
{{#get}}calls with a single query usinginclude=tags,authors. - Configured
image_sizeswith four variants and addedsizesattribute. - Implemented lazy loading for below‑fold images.
- Used
@customfor theme colour scheme and header type selection.
Result: LCP — 1.8s, CLS — 0.05, INP — 100ms. The site passed gscan without issues.
Process of Work
- Analytics: audit the current theme, measure metrics, identify bottlenecks.
- Design: create partial structure layout, custom settings schema, prototype Members flows.
- Implementation: write all hbs templates, styling,
package.jsonconfiguration, integrate with Ghost API. - Testing: gscan validation, device checks, Lighthouse profiling.
- Deployment: upload via Admin or CI/CD, configure caching on Nginx and Cloudflare, monitor metrics.
Approximate Timelines
| Theme Type | Time |
|---|---|
| Minimal working theme | 2–3 days |
| Full theme (index, post, tag, author) | 5–8 days |
| With Members and custom settings | 8–12 days |
| With Newsletter templates | +1–2 days |
Pricing is individual. We estimate the project within one day.
Typical Mistakes in Ghost Themes
| Mistake | Solution |
|---|---|
Missing error.hbs |
Create the mandatory file |
Using {{#each}} without post_class |
Always apply {{post_class}} |
Ignoring card_assets |
Include in package.json |
| Hardcoded markup instead of responsive | Use img_url with srcset |
Forgotten @custom settings |
Export all parameters to package.json |
What is Included in the Work
- Theme source code with comments
- Documentation for custom settings
- Members and Newsletter setup
- gscan validation with zero warnings
- 30 days of free support after delivery
How to Implement Members Restrictions in a Custom Theme?
Add an access check block in post.hbs. Ghost provides the {{#if access}} context for paid content and {{#unless @member}} for guests. Example:
{{#post}} {{#if access}} <div class="premium-content">{{content}}</div> {{else}} <div class="paywall"> <h3>This article is for subscribers</h3> {{#unless @member}} <a href="#/portal/signup">Subscribe</a> {{/unless}} </div> {{/if}} {{/post}} To display plans, use {{#get "tiers"}} and output name and price.
Template Examples
Core Helpers on index.hbs
{{! index.hbs — posts list}} {{#foreach posts}} <article class="post-card {{post_class}}"> {{#if feature_image}} <figure> <img srcset="{{img_url feature_image size="s"}} 300w, {{img_url feature_image size="m"}} 600w, {{img_url feature_image size="l"}} 1000w" sizes="(max-width: 768px) 100vw, 50vw" src="{{img_url feature_image size="m"}}" alt="{{title}} — custom Ghost theme"> </figure> {{/if}} <div class="post-card-content"> <header> {{#primary_tag}} <a href="{{url}}" class="post-tag">{{name}}</a> {{/primary_tag}} <h2><a href="{{url}}">{{title}}</a></h2> </header> {{#if excerpt}} <p>{{excerpt words="30"}}</p> {{/if}} <footer> {{#primary_author}} <img src="{{img_url profile_image size="xs"}}" alt="{{name}}"> <a href="{{url}}">{{name}}</a> {{/primary_author}} <time datetime="{{date format="YYYY-MM-DD"}}"> {{date format="DD MMM YYYY"}} </time> {{#if @custom.show_reading_time}} <span>{{reading_time}}</span> {{/if}} </footer> </div> </article> {{/foreach}} {{pagination}} Dynamic Data via {{#get}}
{{! Recent posts by tag}} {{#get "posts" limit="5" filter="tag:javascript" order="published_at desc"}} {{#foreach posts}} <a href="{{url}}">{{title}}</a> {{/foreach}} {{/get}} Mandatory Theme Files
my-theme/ ├── package.json ├── index.hbs ├── post.hbs ├── page.hbs ├── error.hbs ├── tag.hbs (optional) ├── author.hbs (optional) ├── partials/ │ ├── header.hbs │ ├── footer.hbs │ └── post-card.hbs └── assets/ ├── css/screen.css └── js/main.js { "name": "my-theme", "description": "Custom Ghost theme", "version": "1.0.0", "engines": { "ghost": ">=5.0.0", "ghost-api": "v5" }, "license": "MIT", "config": { "posts_per_page": 12, "image_sizes": { "xs": { "width": 300 }, "s": { "width": 600 }, "m": { "width": 1200 }, "l": { "width": 2000 } }, "card_assets": true, "custom": { "header_style": { "type": "select", "options": ["Center", "Left", "Right"], "default": "Center" }, "show_reading_time": { "type": "boolean", "default": true } } } } Contact us for a project estimate. Order custom Ghost theme development — we guarantee quality code that passes gscan with no errors. Get a consultation for your project — we'll assess complexity and timelines within one day.







