Here’s a copy/paste remote starter repo for a theme component…
It includes:
about.jsonsettings.ymllocales/en.ymlcommon/common.scss- a modern
api-initializersfile - optional screenshots folder
Per the docs, themes and theme components use the same general file structure, settings go in settings.yml, localized strings go in locales/*.yml, and screenshots can be defined in about.json with up to two files in a screenshots folder. Sources:
- Structure of themes and theme components
- Theme Developer Quick Reference Guide
- Add settings to your Discourse theme
- Add localizable strings to themes and theme components
- Adding metadata and screenshots to a Theme
Theme component starter repo
my-theme-component/
├─ about.json
├─ settings.yml
├─ common/
│ └─ common.scss
├─ javascripts/
│ └─ discourse/
│ └─ api-initializers/
│ └─ init-theme.gjs
├─ locales/
│ └─ en.yml
└─ screenshots/
├─ light.png
└─ dark.png
about.json
{
"name": "My Theme Component",
"component": true,
"authors": "Your Name",
"about_url": "https://meta.discourse.org/",
"license_url": "https://opensource.org/licenses/MIT",
"theme_version": "0.1.0",
"minimum_discourse_version": "3.0.0.beta1",
"screenshots": ["screenshots/light.png", "screenshots/dark.png"]
}
settings.yml
banner_enabled:
type: bool
default: true
banner_text:
type: string
default: "Welcome to our community"
banner_background:
type: string
default: "#0f766e"
banner_text_color:
type: string
default: "#ffffff"
banner_padding:
type: integer
default: 12
min: 0
max: 48
locales/en.yml
en:
theme_metadata:
description: "A starter theme component with settings, translations, and a simple welcome banner."
settings:
banner_enabled: "Enable the welcome banner"
banner_text: "Text shown in the welcome banner"
banner_background: "Banner background color"
banner_text_color: "Banner text color"
banner_padding: "Banner padding in pixels"
welcome_banner:
default_text: "Welcome to our community"
common/common.scss
.custom-welcome-banner {
text-align: center;
margin: 0 0 1rem 0;
border-radius: 0.5rem;
font-weight: 600;
background: $banner-background;
color: $banner-text-color;
padding: #{$banner-padding}px;
}
javascripts/discourse/api-initializers/init-theme.gjs
import { apiInitializer } from "discourse/lib/api";
import I18n from "discourse-i18n";
export default apiInitializer((api) => {
if (!settings.banner_enabled) {
return;
}
const configuredText =
settings.banner_text?.trim() ||
I18n.t(themePrefix("welcome_banner.default_text"));
api.renderInOutlet(
"discovery-list-container-top",
<template>
<div class="custom-welcome-banner">
{{configuredText}}
</div>
</template>
);
});
Equivalent full theme starter
If you want the same repo as a full theme, the structure can stay the same. The key change is:
about.json
{
"name": "My Theme",
"component": false,
"authors": "Your Name",
"about_url": "https://meta.discourse.org/",
"license_url": "https://opensource.org/licenses/MIT",
"theme_version": "0.1.0",
"minimum_discourse_version": "3.0.0.beta1",
"screenshots": ["screenshots/light.png", "screenshots/dark.png"]
}
That matches the docs: the skeleton is essentially the same, and the main technical distinction is whether component is true or false:
Notes
1. Settings in SCSS
The docs show that theme settings are exposed as SCSS variables using kebab-case equivalents, so:
banner_background→$banner-backgroundbanner_text_color→$banner-text-colorbanner_padding→$banner-padding
Source:
2. Theme translations
Theme strings belong in locales/en.yml, and in JS / .gjs you can use themePrefix(...) with I18n.t(...).
Source:
3. Screenshots
You can define up to two screenshots in about.json, and they should live in a screenshots folder.
Source:
4. If you use the Theme CLI
The modern tutorial recommends using the discourse_theme CLI to generate and sync a remote theme/theme component.
Source:
Super-minimal version
If you want the absolute smallest possible starter, this is enough:
my-theme-component/
├─ about.json
├─ common/
│ └─ common.scss
└─ javascripts/
└─ discourse/
└─ api-initializers/
└─ init-theme.gjs
with:
about.json
{
"name": "My Theme Component",
"component": true
}
But I’d recommend the fuller starter above because it already gives you:
- admin-editable settings
- localized descriptions
- a clean modern JS entry point
- screenshot support