Ghost Theme Assets — CSS, JavaScript, package.json and Asset Pipeline
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:
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.csswould break.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.pngorassets/screenshot.jpg - 1200x900 pixels (4:3 ratio)
- Shows a realistic preview of the theme
Asset Versioning and Caching
Ghost handles asset caching automatically:
- When you upload a new version of a theme file, Ghost changes the
?v=query parameter. - Browsers see a new URL and download the updated file.
- 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
- Check the browser console for 404 errors.
- Verify the file exists in the correct path.
- Ensure the
{{asset}}helper path matches the actual file location. - Check file permissions (the Ghost Process must be able to read the file).
CSS Not Updating
- Hard refresh the browser (Ctrl+Shift+R or Cmd+Shift+R).
- Clear the browser cache.
- 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
- Check the browser console for error messages.
- Verify script load order — dependencies must load before dependent code.
- Check for syntax errors in your JavaScript files.
Common Mistakes
Hardcoding asset paths: Using
/assets/css/style.cssinstead of{{asset "css/style.css"}}breaks when Ghost is installed in a subdirectory. Always use the asset helper.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.
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.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.
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
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).What fields are required in a Ghost theme's package.json? Answer: The
namefield (theme identifier, lowercase with hyphens) andversionfield (semantic version). Adescriptionis also highly recommended for display in the admin panel.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.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
Mini Project
Your task: Build a complete asset pipeline for a Ghost theme.
- Create the assets/ directory structure with folders for css, js, fonts, and images.
- Create a package.json with name, version, description, and custom image sizes.
- Write a main CSS file with styles for header, content, footer, and responsive breakpoints.
- Write a JavaScript file that adds a mobile navigation toggle and smooth scrolling.
- Include all assets in default.hbs using the
{{asset}}helper. - Add a preloaded custom font.
- Add a theme screenshot (1200x900).
- 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:
- Theme Basics — Template hierarchy and layout
- Theme Customization — Navigation, routes, dynamic routing
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro