How Custom Theme Settings Work and Are Cached in Ghost
Ghost caches custom theme settings in a two-tier system—using a lightweight public value cache for frontend rendering and an internal definition cache for the admin UI—synchronizing the database via CustomThemeSettingsService.activateTheme whenever a theme is activated.
Ghost allows theme developers to define custom theme settings directly in a theme's package.json, enabling dynamic configuration without modifying templates. When a theme is activated, the platform parses these definitions using the gscan validator and initializes a sophisticated caching layer to eliminate database queries during page rendering. According to the TryGhost/Ghost source code, this architecture ensures high-performance theme customization through database synchronization, NQL-based visibility rules, and dual-cache population.
Declaring Custom Theme Settings in package.json
Theme settings are declared in the theme's configuration file under the customSettings key. Each setting requires a type (e.g., select, boolean, color), default value, and optional constraints.
{
"name": "my-theme",
"customSettings": {
"header_typography": {
"type": "select",
"options": ["Serif", "Sans Serif"],
"default": "Serif",
"visibility": "member_count > 0"
},
"show_featured": {
"type": "boolean",
"default": true
}
}
}
The visibility property accepts an NQL (NexT Generation Query Language) expression that determines whether the setting's value is exposed publicly.
Theme Activation and Database Synchronization
When a theme is activated, CustomThemeSettingsService.activateTheme(name, theme) ([custom-theme-settings-service.js](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js#L46-L84)) orchestrates the initialization process:
- Repository Sync –
_syncRepositoryWithTheme(lines [75-131]) loads existing records via the BREAD service (custom-theme-settings-bread-service.js), removes obsolete settings, adds new definitions, and resets invalidselectvalues. - Cache Population – The service populates two distinct caches: a public value cache for the theme engine and an internal cache for the admin UI.
The BREAD service (custom-theme-settings-bread-service.js) acts as a thin abstraction over the Bookshelf model, providing CRUD operations for the settings table while the main service handles business logic.
The Two-Tier Caching Strategy
Ghost maintains separate caches to optimize for different access patterns:
Public Value Cache – CustomThemeSettingsCache ([custom-theme-settings-cache.js](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/shared/custom-theme-settings-cache/custom-theme-settings-cache.js)) stores only visible key/value pairs in a simple in-memory map:
populate(settings) {
this.clear();
settings.forEach(s => this._content[s.key] = s.value);
}
This cache is consulted by the theme engine on every page render, avoiding database lookups entirely during request processing.
Internal Full-Definition Cache – _populateInternalCacheForTheme stores complete setting objects (including metadata, allowed options, and validation rules) for the admin UI to list, edit, and validate settings.
Visibility Rules and Conditional Settings
Each setting may declare a visibility rule evaluated against the full settings context. The _computeCachedSettings method (lines [85-98] in custom-theme-settings-service.js) processes these NQL expressions:
// If member_count is 0, header_typography evaluates to null
// nql('member_count > 0').queryJSON({member_count: 0}) → false
// Value replaced with HIDDEN_SETTING_VALUE (null)
When a rule evaluates to false, the setting's value is replaced with null (defined as HIDDEN_SETTING_VALUE) before entering the public cache, ensuring sensitive configuration never leaks to the frontend.
Reading Settings at Render Time
The theme engine accesses cached values through CustomThemeSettingsCache.getAll() during the rendering pipeline:
// In the theme rendering pipeline
const themeSettings = cache.getAll();
// Returns: {header_typography: 'Serif', show_featured: true}
if (themeSettings.header_typography === 'Serif') {
// render Serif header
}
This approach guarantees sub-millisecond access to theme configuration without database overhead.
Updating Settings via Admin API
The admin UI interacts with settings through listSettings() and updateSettings() methods exposed by the higher-level settings-service.js:
// HTTP PATCH /admin/settings/custom-theme
await service.updateSettings([
{key: 'header_typography', value: 'Sans Serif'},
{key: 'show_featured', value: false}
]);
The updateSettings method validates each value against its type definition (checking allowed options for selects, regex patterns for colors, etc.) before persisting through the BREAD service. After successful updates, both caches are automatically refreshed to reflect changes immediately.
Preview Mode and Development Workflow
During theme development, Ghost supports live preview via the x-ghost-preview header containing a custom= JSON payload. The theme-engine middleware parses this data, merges it with cached values, and passes the result to Handlebars via hbs.updateLocalTemplateOptions.
The middleware strictly filters unknown keys, discarding any custom settings not defined in the theme's package.json before rendering.
Key Source Files
Summary
- Declaration: Custom theme settings are defined in
package.jsonand validated bygscanbefore activation. - Synchronization:
CustomThemeSettingsService.activateThemesyncs the database with theme definitions via_syncRepositoryWithTheme, removing obsolete settings and adding new ones. - Dual Caching: A public value cache eliminates database queries during rendering, while an internal definition cache supports admin UI operations.
- Visibility Control: NQL expressions in
package.jsondetermine whether values appear in the public cache or are hidden (replaced withnull). - Runtime Access: The theme engine reads directly from
CustomThemeSettingsCachefor high-performance configuration access. - Live Preview: The
x-ghost-previewheader allows temporary override of cached values during theme development.
Frequently Asked Questions
How do I add a custom theme setting to my Ghost theme?
Add a customSettings object to your theme's package.json file. Each setting requires a type (boolean, select, color, etc.) and default value. Select types must include an options array defining allowed values. After uploading and activating the theme, the setting appears automatically in Ghost Admin under Settings > Design.
Why are my custom theme settings returning null in the theme?
Ghost replaces setting values with null (the HIDDEN_SETTING_VALUE) when a visibility NQL rule evaluates to false. Check your package.json for visibility expressions like member_count > 0—if the condition isn't met, the value is hidden from the public cache used by the theme engine. Remove the visibility property or adjust the NQL expression to make the setting always available.
How does Ghost handle theme setting updates without restarting?
When you modify settings via the Admin API (updateSettings), Ghost validates the input against the theme's definition, persists changes through the BREAD service, and immediately repopulates both the public value cache and internal definition cache. The new values take effect on the next page request without requiring a server restart or theme reactivation.
Where are custom theme settings stored in the database?
Ghost stores custom theme settings in a dedicated database table accessed through custom-theme-settings-bread-service.js, which wraps the Bookshelf ORM model. During theme activation, CustomThemeSettingsService syncs this table with the theme's current package.json definition, ensuring the database schema matches the theme's declared settings.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →