Skip to content

Ghost Theme Assets — CSS, JavaScript, package.json and Asset Pipeline

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you'll learn how to manage assets in Ghost themes — organizing CSS and JavaScript files, creating the package.json manifest, using the asset helper for correct URLs, and understanding Ghost's asset pipeline.

What You'll Learn

  • The assets folder structure for Ghost themes
  • The package.json theme manifest and required fields
  • Using the {{asset}} helper for correct file URLs
  • Including CSS and JavaScript in your templates
  • Asset Minification and Caching in Ghost
  • Organizing CSS: main styles, components, vendor files
  • Organizing JavaScript: scripts, libraries, modules
  • Working with image assets in themes
  • Debugging asset loading issues

Why It Matters

Assets — CSS, JavaScript, images, fonts — make your theme look and function correctly. The asset helper generates the correct URLs whether your site is at the root domain or a subdirectory. The package.json file tells Ghost your theme's name, version, and configuration. A misconfigured package.json can prevent your theme from installing. Broken asset paths result in unstyled pages or broken JavaScript functionality.

Real-World Use

A theme developer creates a magazine theme with a main stylesheet, a print stylesheet, a JavaScript carousel script, and custom fonts. She uses the {{asset}} helper for all asset paths so the theme works whether the site is at blog.example.com (subdomain) or example.com/blog/ (subdirectory). She adds version hashing to force cache refresh when the theme updates.

Learning Path

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

The Assets Folder Structure

All theme assets go in the assets/ directory.

my-theme/
├── assets/
│   ├── css/
│   │   ├── style.css
│   │   ├── print.css
│   │   └── vendor/
│   │       └── normalize.css
│   ├── js/
│   │   ├── index.js
│   │   └── vendor/
│   │       └── jquery.min.js
│   ├── fonts/
│   │   └── custom-font.woff2
│   └── images/
│       ├── logo.svg
│       └── default-avatar.png
├── default.hbs
├── index.hbs
├── post.hbs
└── package.json

Ghost serves these assets automatically. You do not need to configure any routing for them.

The {{asset}} Helper

The {{asset}} helper generates the correct URL for any file in the assets/ directory.

<!-- CSS -->
<link rel="stylesheet" href="{{asset "css/style.css"}}">

<!-- JavaScript -->
<script src="{{asset "js/index.js"}}"></script>

<!-- Images -->
<img src="{{asset "images/logo.svg"}}" alt="Logo">

<!-- Fonts -->
<link rel="preload" href="{{asset "fonts/custom-font.woff2"}}" as="font" crossorigin>

Why Use the Asset Helper?

The asset helper handles two important cases:

  1. Subdirectory installations: If your Ghost site is at example.com/blog/, the asset helper prepends /blog/ to asset paths. Using hardcoded paths like /assets/css/style.css would break.

  2. Cache busting: In production, Ghost appends a content hash to asset URLs. When you update the theme, the URL changes, forcing browsers to download the new file instead of using a cached version.

<!-- Development output -->
<link rel="stylesheet" href="/assets/css/style.css">

<!-- Production output (with hash) -->
<link rel="stylesheet" href="/assets/css/style.css?v=abc123def456">

Minified Assets

Ghost automatically serves a minified version of CSS and JS files in production. You do not need to run a build tool — Ghost handles compression at the server level. However, you should still write clean, well-organized source files.

The package.json File

package.json is the theme manifest file. It provides Ghost with metadata about your theme.

{
  "name": "my-magazine-theme",
  "version": "1.0.0",
  "description": "A modern magazine theme for Ghost",
  "author": {
    "name": "Your Name",
    "email": "you@example.com",
    "url": "https://example.com"
  },
  "config": {
    "posts_per_page": 10,
    "image_sizes": {
      "xxs": {
        "width": 100
      },
      "xs": {
        "width": 300
      }
    }
  }
}

Required Fields

Field Description
name Theme identifier (lowercase, hyphens only)
version Semantic version (e.g., 1.0.0)
description Short theme description

Optional Fields

Field Description
author Theme author information
config.posts_per_page Number of posts per page (default: 5)
config.image_sizes Custom image sizes for the {{img_url}} helper
config.customSettings Custom theme settings (requires Labs feature)

Custom Image Sizes

Define custom image sizes in package.json to generate additional image variants:

{
  "config": {
    "image_sizes": {
      "featured_hero": {
        "width": 1600,
        "height": 800
      },
      "card_thumbnail": {
        "width": 400,
        "height": 300
      }
    }
  }
}

Then use them in templates:

<img src="{{img_url feature_image size="featured_hero"}}">

Including CSS

There are several approaches to organizing CSS in Ghost themes.

Single Stylesheet

Simplest approach — one CSS file for everything:

<link rel="stylesheet" href="{{asset "css/style.css"}}">

Multiple Stylesheets

Separate into logical files:

<link rel="stylesheet" href="{{asset "css/normalize.css"}}">
<link rel="stylesheet" href="{{asset "css/style.css"}}">
<link rel="stylesheet" href="{{asset "css/print.css"}}" media="print">

Critical CSS

For performance, inline critical CSS in the <head> and load the full stylesheet asynchronously:

<style>
  /* Critical above-the-fold styles */
  body { font-family: system-ui, sans-serif; }
  .header { padding: 1rem; }
  /* ... */
</style>
<link rel="stylesheet" href="{{asset "css/style.css"}}" media="print" onload="this.media='all'">

Including JavaScript

Script Placement

Best practice: load JavaScript at the bottom of the page, just before </body>:

{{ghost_foot}}
<script src="{{asset "js/index.js"}}"></script>

Multiple Scripts

<script src="{{asset "js/vendor/jquery.min.js"}}"></script>
<script src="{{asset "js/vendor/slick.min.js"}}"></script>
<script src="{{asset "js/index.js"}}"></script>

Async and Defer

<!-- async: load while HTML is parsing, execute when ready -->
<script async src="{{asset "js/analytics.js"}}"></script>

<!-- defer: load while HTML is parsing, execute after HTML is parsed -->
<script defer src="{{asset "js/index.js"}}"></script>

Font Assets

Include custom fonts in your theme's assets folder:

<!-- Preload fonts for performance -->
<link rel="preload" href="{{asset "fonts/inter-regular.woff2"}}" as="font" crossorigin>
<link rel="preload" href="{{asset "fonts/inter-bold.woff2"}}" as="font" crossorigin>

<!-- Font-face declaration in CSS -->
<style>
  @font-face {
    font-family: 'Inter';
    src: url('{{asset "fonts/inter-regular.woff2"}}') format('woff2');
    font-weight: 400;
    font-display: swap;
  }
</style>

Theme Images

Images used by the theme itself (not user-uploaded content) go in assets/images/.

<!-- Default avatar for authors without a photo -->
<img src="{{asset "images/default-avatar.png"}}" alt="Default avatar">

<!-- Logo fallback -->
<img src="{{asset "images/logo.svg"}}" alt="{{@site.title}}">

<!-- Theme screenshot (shown in admin) -->
<!-- Must be at: assets/screenshot.png or assets/screenshot.jpg -->

Theme Screenshot

Ghost shows a preview of your theme in the admin panel. The screenshot must be:

  • Located at assets/screenshot.png or assets/screenshot.jpg
  • 1200x900 pixels (4:3 ratio)
  • Shows a realistic preview of the theme

Asset Versioning and Caching

Ghost handles asset caching automatically:

  1. When you upload a new version of a theme file, Ghost changes the ?v= query parameter.
  2. Browsers see a new URL and download the updated file.
  3. Old cached versions are not used.

This means you do not need to manually add version parameters to your asset URLs. The {{asset}} helper handles it.

Debugging Asset Issues

Asset Not Loading

  1. Check the browser console for 404 errors.
  2. Verify the file exists in the correct path.
  3. Ensure the {{asset}} helper path matches the actual file location.
  4. Check file permissions (the Ghost Process must be able to read the file).

CSS Not Updating

  1. Hard refresh the browser (Ctrl+Shift+R or Cmd+Shift+R).
  2. Clear the browser cache.
  3. Check the generated URL for the ?v= hash — if it matches the old URL, the theme may not have been re-uploaded correctly.

JavaScript Errors

  1. Check the browser console for error messages.
  2. Verify script load order — dependencies must load before dependent code.
  3. Check for syntax errors in your JavaScript files.

Common Mistakes

  1. Hardcoding asset paths: Using /assets/css/style.css instead of {{asset "css/style.css"}} breaks when Ghost is installed in a subdirectory. Always use the asset helper.

  2. Missing package.json: If you upload a theme without package.json, Ghost cannot identify it and the upload fails. Every theme needs a package.json with at minimum name and version.

  3. Putting styles in the wrong folder: Only files in the assets/ directory are served by Ghost. CSS files placed in the theme root are not accessible to the browser.

  4. Including unnecessary vendor libraries: jQuery is not required for Ghost themes. Modern Ghost themes use vanilla JavaScript. Every KB of JavaScript affects page load speed.

  5. Not preloading fonts: Custom fonts are render-blocking resources. Without preloading, the browser discovers fonts late in the page load, causing layout shift and slow text rendering.

Practice Questions

  1. What is the purpose of the {{asset}} helper in Ghost themes? Answer: The asset helper generates the correct URL for files in the assets/ directory. It handles subdirectory installations (prepending the correct base path) and cache busting (appending content hashes in production).

  2. What fields are required in a Ghost theme's package.json? Answer: The name field (theme identifier, lowercase with hyphens) and version field (semantic version). A description is also highly recommended for display in the admin panel.

  3. How does Ghost handle CSS caching for theme assets? Answer: Ghost appends a content hash (?v=hash) to asset URLs in production. When the theme file changes, the hash changes, forcing browsers to download the new version. This is handled automatically by the {{asset}} helper.

  4. Challenge: Create a complete assets structure for a Ghost theme. Include: a main CSS file with responsive styles, a print CSS file, a JavaScript file with a mobile menu toggle, a custom font (WOFF2), a default avatar image, a logo SVG, and a 1200x900 screenshot. Wire everything together with correct {{asset}} calls in default.hbs.

FAQ

Can I use Sass, Less, or PostCSS in Ghost themes?

Ghost does not compile Sass or Less. You must pre-compile your CSS and upload the final .css file. Use your local development environment to compile and then copy the output to your theme's assets/css/ folder.

What is the maximum file size for theme assets?

Ghost does not enforce a strict limit, but keep individual asset files under 500 KB for good performance. Large assets (especially uncompressed images) slow down your site.

Can I link to external CDN assets (Bootstrap, Font Awesome)?

Yes. You can link to external CDNs in your theme templates. However, this creates a dependency on the external service. For reliability, consider self-hosting or using a subresource integrity (SRI) hash.

How do I add a favicon to my Ghost theme?

Place a favicon file (favicon.ico or favicon.png) in your assets/ folder and link it in default.hbs: <link rel='icon' href='{{asset 'favicon.ico'}}' type='image/x-icon' />.

Can I use ES6+ JavaScript features in my theme?

Yes, but browser support varies. Use a transpiler like Babel in your development workflow to convert ES6+ to widely supported ES5. Alternatively, use feature detection and progressive enhancement.

Mini Project

Your task: Build a complete asset pipeline for a Ghost theme.

  1. Create the assets/ directory structure with folders for css, js, fonts, and images.
  2. Create a package.json with name, version, description, and custom image sizes.
  3. Write a main CSS file with styles for header, content, footer, and responsive breakpoints.
  4. Write a JavaScript file that adds a mobile navigation toggle and smooth scrolling.
  5. Include all assets in default.hbs using the {{asset}} helper.
  6. Add a preloaded custom font.
  7. Add a theme screenshot (1200x900).
  8. Test the theme locally and verify all assets load correctly.

This exercise gives you a complete, production-ready theme asset structure.

What's Next

Now that you understand assets, learn how to build custom themes:

Continue to Lesson 18: Custom Themes — Theme structure, theme upload, and GScan validation.

Related lessons:

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro