.skills/discourse-site-settings/SKILL.md
Use this skill before writing or reviewing ordinary Discourse site settings. For upcoming-change
feature flags, also use discourse-upcoming-changes. For database migrations that rename or
remove settings, use discourse-migration.
rg; avoid reading all of config/site_settings.yml.config/site_settings.yml; put plugin settings in
plugins/<plugin>/config/settings.yml.en.site_settings.<setting_name> in the matching
config/locales/server.en.yml.client: true only when frontend code must read the setting through siteSettings.area: for admin grouping when the setting belongs on a filtered settings page.SiteSetting.setting_name in Ruby and through @service siteSettings in JS.*_groups for group lists.*_categories for type: category_list.*_category for type: category._ids in public site setting names for category lists even though values are stored as ids.{{setting:setting_name}} token instead of writing
the raw setting name or hardcoding the translated label. This lets the admin UI render the
referenced setting consistently and avoids copy drifting when labels change.default: choose a safe default for existing sites. For upload settings, use the id of the
seeded upload from db/fixtures/010_uploads.rb.min, max, regex, and validator: enforce value constraints server-side; descriptions and
frontend affordances are not enough.mandatory_values: pipe-separated values that must always remain in the setting. Common for
group_list settings that must always include admins/moderators.disallowed_groups: pipe-separated group ids hidden from group selectors and stripped from API
updates. This only applies to group_list settings.requires_confirmation: use for risky changes that need an admin confirmation dialog. Valid
values are simple, simple_on_enable, and simple_on_disable; add matching client i18n under
admin.site_settings.requires_confirmation_messages.<setting_name> when the default copy is not
specific enough.themeable: true: only for client-side UI settings that themes may override. Prefer simple types
such as bool, integer, list, or enum; normal SiteSetting.setting = value writes are not
allowed for themeable settings.localizable: use for public text/content settings that content localization may translate. Use
a hash for metadata such as max_length: and cooked: true when localized markdown should be
cooked.area: use the filtered admin settings page key when the setting belongs with an existing admin
workflow.refresh: true: use when clients should reload after the setting changes.Start with the simplest type that matches the admin's mental model:
bool: on/off behavior.integer / float: numeric values; set min/max where possible.enum: one value from a fixed set; use enum: "ClassName" for reusable translated enums.list: multiple values from choices; use list_type: compact for selector-style UI and
list_type: simple for reorderable item lists.group / group_list: one or many groups; use mandatory_values and disallowed_groups where
relevant.category / category_list: one or many categories through the category picker.tag_list, emoji_list, tag_group_list, host_list, email, username, color, icon,
upload, uploaded_image_list, and file_size_restriction: use when the name describes the
data shape directly.json_schema / objects: structured settings. Prefer these only when a scalar/list setting is
not enough, and keep tests around schema validation and serialization.Use a plain type: category_list when the setting means exactly "these categories". Do not add a
scope enum just because the setting involves categories. For example, user-default settings such as
default_categories_watching should stay as a single category list; admins are selecting the
categories to apply, not defining an include/exclude query scope.
default_categories_watching:
type: category_list
default: ""
area: "user_defaults"
Use the two-setting category-scope pattern only when the feature filters a topic/post result set and admins may reasonably need all/public/include/exclude choices or subcategory control.
xyz_category_scope:
type: enum
default: "public"
enum: "CategoryScopeSiteSetting"
client: false
area: "feature-name"
xyz_categories:
type: category_list
default: ""
client: false
area: "feature-name"
depends_on:
- xyz_category_scope
depends_on_values:
xyz_category_scope:
- include
- include_strict
- exclude
- exclude_strict
depends_behavior: "hidden"
dependent_setting_display: "inline"
Semantics:
all: all regular topics.public: all non-read-restricted regular topics.include: use xyz_categories and their subcategories.include_strict: use only xyz_categories.exclude: start from all regular topics, then remove xyz_categories and their subcategories.exclude_strict: start from all regular topics, then remove only xyz_categories.The CategoryScopeSiteSetting enum provides translated admin labels for these stored values. Pair
it with depends_on_values, depends_behavior: "hidden", and dependent_setting_display: "inline"
so the category picker appears under the scope setting only when include/exclude modes need it.
Do not make exclude mean "public minus selected categories" unless the product requirement
explicitly says that. It is harder to explain and usually surprises admins.
Example from Discourse AI admin dashboard highlights:
ai_admin_dashboard_highlights_category_scope:
type: enum
default: "public"
enum: "CategoryScopeSiteSetting"
client: false
area: "ai-features/admin_dashboard"
ai_admin_dashboard_highlights_categories:
type: category_list
default: ""
client: false
area: "ai-features/admin_dashboard"
depends_on:
- ai_admin_dashboard_highlights_category_scope
depends_on_values:
ai_admin_dashboard_highlights_category_scope:
- include
- include_strict
- exclude
- exclude_strict
depends_behavior: "hidden"
dependent_setting_display: "inline"
Recommended description shape:
ai_admin_dashboard_highlights_category_scope: "Choose which categories AI highlights can use. Include and exclude options use {{setting:ai_admin_dashboard_highlights_categories}}. Personal messages are always excluded."
ai_admin_dashboard_highlights_categories: "Categories used when the AI highlights category scope is set to <strong>include</strong> or <strong>exclude</strong>."
Implementation notes:
Category.subcategory_ids(category_id) when non-strict modes include descendants.IN (:category_ids) / NOT IN (:category_ids) parameterized; parse category-list values
to integers before passing them into SQL.include_strict; let descriptions/admin labels explain them.depends_on without depends_on_values is for boolean parent settings.depends_on_values when the parent is an enum/string setting and the dependent only applies
to specific values.depends_behavior: "hidden" when the dependent setting should be hidden until applicable.dependent_setting_display: "inline" when a hidden dependent belongs visually under its
parent; save/reset controls are grouped with the parent setting.