Discourse Theme Component Starter

Here’s a copy/paste remote starter repo for a theme component…
It includes:

  • about.json
  • settings.yml
  • locales/en.yml
  • common/common.scss
  • a modern api-initializers file
  • 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:


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-background
  • banner_text_color → $banner-text-color
  • banner_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