# Creating consistent admin interfaces

**URL:** https://meta.discourse.org/t/creating-consistent-admin-interfaces/326780
**Category:** Contributing
**Tags:** guide, interface
**Created:** [September 17, 2024, 5:50am UTC](https://meta.discourse.org/t/creating-consistent-admin-interfaces/326780 "2024-09-17T05:50:12Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![Discourse](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/discourse/32/148734_2.png) [@Discourse](https://meta.discourse.org/u/Discourse)
#### Post date: [September 17, 2024, 5:50am UTC](https://meta.discourse.org/t/creating-consistent-admin-interfaces/326780/1 "2024-09-17T05:50:12Z")

</div>

These guidelines aim to create a cohesive admin interface, focusing on usability, accessibility, and a structured layout. See the table of contents for what is included and to navigate to each section easily.

_Note: The terminology used here is defined in the [admin interface glossary](https://meta.discourse.org/t/understanding-the-admin-interface/315108)._

# 0. Preface - Config page structure and sidebar links

When adding new config pages in the admin interface, each page will need a link for the sidebar, and each page will need both a title and header description. This is so we are consistent everywhere, and future enhancements to admin search can display the entire layout of the admin interface.

 ![image](https://global.discourse-cdn.com/meta/original/4X/c/b/d/cbd83e14dc38764408af8807e259c302bc7e7052.png)

Generally the structure of the admin interface looks like this:

- Admin interface
  - Config page (shown in sidebar)
    - Settings tab
    - Optional other third-level tabs
      - Edit/New third-level page for resources

Eventually, a “section overview” will be inserted inbetween the root interface and config pages.

#### Sidebar links

All admin pages should be added to the `ADMIN_NAV_MAP` in [discourse/frontend/discourse/app/lib/sidebar/admin-nav-map.js at main · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/main/frontend/discourse/app/lib/sidebar/admin-nav-map.js) . Each item at the very least should have these keys:

- `name` - A unique identifier for the link, should be `snake_case`
- `route` OR `href` - The `route` is an Ember route identifier, such as `adminUsers`. For admins, these are defined in [the admin route map](https://github.com/discourse/discourse/blob/main/frontend/discourse/admin/routes/admin-route-map.js) . A `href` can be used instead, but `route` is preferred.
- `label` OR `text` - Label is an I18n key, which should generally be `admin.config.page_name.title` (see translations section below). If `text` is used, it will be already translated text.

These optional keys can also be provided:

- `description` - It is **recommended** you provide this too. It is an I18n key, it should generally be `admin.config.page_name.header_description`.
- `icon` - Also **recommended** , this is shown next to the link in the sidebar.
- `routeModels` - Array of URL data for the case of route params. For example `adminCustomizeThemes` has a `:type` route param, so you can pass `routeModels: ["components"]`. The array items are used in the same order as the route params appear.
- `moderator`: Set this to `true` if moderators should see this page in the sidebar.
- `keywords`: An I18n key, with a `|` separated list of keywords for the sidebar link, used for extra “search juice” when filtering/searching pages.
- `links`: A list of 3rd-level routes that are beneath the page on the sidebar. These are not shown in the sidebar itself. This will be used for future admin search features.
- `settings_area` and `settings_category`: If the page shows only a list of filtered site settings, then one of these should be filled in. If the site setting has an `area` defined, which is used in `AdminAreaSettings`, then `settings_area` should be used. If an entire category of settings is shown on the page, and also used on `AdminAreaSettings`, then `settings_category` should be used.
- `multi_tabbed`: If the page has a setting tab _and_ other tabs, then this should be set to true. It helps generate links for the admin search system.

#### Translations

The title and header description for every config page should be under:

- admin
  - config
    - page\_name
      - title: “Page title”
      - header\_description: “This page is for xyz”

You can see examples of this here:

> <https://github.com/discourse/discourse/blob/main/config/locales/client.en.yml>

# 1. Breadcrumbs

Breadcrumbs serve as a navigational tool, aiding users in understanding their current location, content structure, and hierarchy within the admin interface.

> Admin \> Breadcrumb \> Trail  
> **Page title**

## 🎨 Design

**Structure**

1. **Admin** : fixed prefix that appears at the beginning of every breadcrumb trail, linking to `/admin`
2. **Link** : opens the page in the same window
3. **Separator** : an `angle-right` icon separates each link

**Usage**

When to use:

- Present on every admin page
- Sit above the content (title, description, tabs)
- Show the currently selected page

When not to use:

- When visiting a new or edit route

**Content**

- Each item includes a link to its related page
- Shows the currently selected page

**Accessibility**

- A `nav` element with `aria-label="Breadcrumb"` wraps an ordered list to provide a navigation landmark
- Apply `aria-current="page"` on the last link to indicate it’s the current page
- For more details, see [WAI-ARIA Authoring Practices Breadcrumb Example](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/examples/breadcrumb/)

## 🛠 Implementation

The `DBreadcrumbsContainer` component must be placed somewhere on the page:

```gjs
<DBreadcrumbsContainer />

```

Then, every `DBreadcrumbsItem` element added to a any component on a route or a child route will be rendered into this container. Each `DBreadcrumbsItem` has a `@label` and `@path` that must be provided:

```gjs
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
  @path="/admin/plugins"
  @label={{i18n "admin.plugins.title"}}
/>
<DBreadcrumbsItem
  @path="/admin/plugins/{{@plugin.name}}"
  @label={{@plugin.nameTitleized}}
/>

```

How this looks with a visual example, using the Discourse AI plugin:

 ![An example of breadcrumbs](https://global.discourse-cdn.com/meta/original/4X/1/0/2/102fc33b254820693c81eff6d1f1b5f047c5df22.jpeg)

# 2. Page header and title

The top section of an admin page, containing the page title, along with optional actions and description.

 ![A page title example](https://global.discourse-cdn.com/meta/original/4X/8/2/6/8262f16848adb479020260989b85b8802a2dac5c.png)

## 🎨 Design

**Structure**

- **Page title:** Title of the page

- **Page description:** Intro or description of what the content covers _(optional)_

- **Primary action:** Page title primary action _(optional)_

- **Secondary action:** Page title secondary action button settings _(optional)_

**Usage and content**

- **Page title:** Utilize heading level 1 to explain the main subject of the page in sentence case. Usually the I18n translation should be under `admin.config.your_page.title`.

- **Page description:** Supports basic markdown nodes such as `_italic_`, ` **bold** `, and `[link name](url)`

- **Primary action:** Utilize `btn-primary`. Don’t include an icon. Usually the I18n translation should be under `admin.config.your_page.header_description`.

- **Secondary action:** Utilize `btn-default` button settings, visible only if a primary action exists. Don’t include an icon.

## 🛠 Implementation

The `DPageHeader` component is used here. This accepts arguments for `@titleLabel`, `@descriptionLabel`, `@learnMoreUrl`, and `@shouldDisplay`. This uses named `yields` in Ember to provide 5 named blocks for the content:

1. `breadcrumbs` - Any additional `DBreadcrumbsItem` components for the page should be placed here.
2. `actions` - Used to define the buttons to the right of the title. This yields an object called `actions` which can be used to render `Default`, `Primary`, `Danger`, and `Wrapped` buttons.
3. `title` - An alternative to `@titleLabel`, allowing custom markup inside the heading.
4. `drawer` - An optional collapsible drawer section, shown when `@showDrawer` is true.
5. `tabs` - Used to define the tabs for the page using `NavItem` components. `@hideTabs` can be used to remove this part of the header if it’s not needed.

A complete example is below:

```gjs
<DPageHeader
  @titleLabel={{i18n "admin.config.backups.title"}}
  @descriptionLabel={{i18n "admin.config.backups.header_description"}}
  @learnMoreUrl="https://meta.discourse.org/t/create-download-and-restore-a-backup-of-your-discourse-database/122710"
>
  <:breadcrumbs>
    <DBreadcrumbsItem
      @path="/admin/backups"
      @label={{i18n "admin.backups.title"}}
    />
  </:breadcrumbs>
  <:actions as |actions|>
    <actions.Primary
      @action={{routeAction "showStartBackupModal"}}
      @title="admin.backups.operations.backup.title"
      @label="admin.backups.operations.backup.label"
      class="admin-backups__start"
    />
  </:actions>
  <:tabs>
    <NavItem
      @route="admin.backups.settings"
      @label="settings"
      class="admin-backups-tabs__settings"
    />
    <NavItem
      @route="admin.backups.index"
      @label="admin.backups.menu.backup_files"
      class="admin-backups-tabs__files"
    />
    <NavItem
      @route="admin.backups.logs"
      @label="admin.backups.menu.logs"
      class="admin-backups-tabs__logs"
    />
    <PluginOutlet @name="downloader" @connectorTagName="div" />
  </:tabs>
</DPageHeader>

```

Page titles for the browser tab are handled in Ember routes using the `titleToken` functionality. Every time this is used in a route it adds the token to the end of the browser tab title. Be aware that you **must** use the `DiscourseRoute` class to extend your route, not the normal `Route` from ember for this to work:

```javascript
titleToken() {
  return i18n("admin.config.backups.title");
}

```

 ![Browser tab title example](https://global.discourse-cdn.com/meta/original/4X/d/b/a/dba1e7560a5278c07c0ec7a192fd6d40c7068709.png)

> 👉 The page header is automatically hidden for `/new` and `/edit` paths to support [Third-level routes](https://meta.discourse.org/t/creating-consistent-admin-interfaces/326780#p-1605522-h-5e-third-level-route-29). It can be overridden by using `@shouldDisplay` argument.

# 3. Tabs

An optional navigation that provides access to deeper levels of settings or features. We also refer to this as “third-level” pages or navigation.

 ![Example of tabs](https://global.discourse-cdn.com/meta/original/4X/4/b/9/4b97501534a53434d90d8eb7210c94c5cfedbb7d.png)

## 🎨 Design

We’re using tabs to switch between different but related views within the same context.

**Usage**

- Not used for primary navigation
- Only active one at a time

## 🛠 Implementation

See the [Page Header](#p-1605522-h-2-page-header-and-title-4) details, the tabs are defined in the `DPageHeader` component.

▫ ▫ ▫ ▫ ▫ ▫ ▫ ▫ ▫ ▫ ▫ ▫ ▫ ▫ ▫ ▫

# 4. Overview/section landing page

Allows users to view the contents of a section, especially when the sidebar is collapsed or on mobile.

 ![An example of an over/section landing page](https://global.discourse-cdn.com/meta/original/4X/4/2/e/42e3a1acceab8b680478e78a94ad3f2b8ecfeeff.png)

## 🎨 Design

**Structure**  
Use three equal column layout using a grid system. On small screens, these columns will stack vertically.

**Design and usage**

- Can be accessed through breadcrumbs (Admin \> Community \> Overview)
- Each section should have one, except for plugins (which show installed) and reports (only one page)
- The item has a:
  - name - same as the section link
  - description - a short description of what the page is about
  - icon - same icon used for the sidebar

## 🛠 Implementation

_Code snippets or link to a topic/GitHub_

# 5. Page content

The main area of an admin page where settings, configurations, and other content are displayed and interacted with.

 ![The basic structure of page](https://global.discourse-cdn.com/meta/original/4X/7/b/9/7b98f8c74abd71d88ba5dc8baad4ae2b8375400c.png)

## 🎨 Design

**Structure**  
Use a 2/3 + 1/3 layout using a grid system. The primary section takes up two-thirds and the secondary section takes up one-third of the space. On small screens, these columns will stack vertically.

- **Config area:** A specific section within the page content dedicated to settings and configurations.
- **Help/reference/inset:** An area within the page content providing guides, documentation, or additional contextual information. _(optional)_

**Design and usage**

- Group similar settings and actions together in cards
- Structure primary/secondary layouts so the primary (2/3) section is used for main settings, and the secondary (1/3) section is for additional information or helpful context
- If the secondary section is not available, keep the primary section width the same

**Content**

- Follow the guidelines for [config area](#p-1605522-h-5b-config-area-18) when adding content
- Follow the guidelines for [help inset](#p-1605522-h-5c-help-inset-23) content

## 🛠 Implementation

_Code snippets or GitHub links_

# 5.a. Subhead

A subhead is a secondary heading used to break up the content under a section, usually below the tabs.

 ![A subheading example](https://global.discourse-cdn.com/meta/original/4X/0/8/f/08fff2b2c8162bf4692e3aaaced420c86537ffde.png)

**Structure**

- **Subheading:** Subheading of what the content covers _(optional)_
- **Primary action:** Subheading primary action _(optional)_
- **Secondary action:** Subheading secondary action button settings _(optional)_

**Usage and content**

- **Subheading:** Utilize heading level 2 to explain the main subject of the related content. Only include if:

- **Primary action:** Utilize `btn-primary`. Don’t include an icon.

- **Secondary action:** Utilize `btn-default` button settings, visible only if a primary action exists. Don’t include an icon.

## 🛠 Implementation

This is similar to `DPageHeader`, there is an `DPageSubheader` component. The main difference is that there is only a single named yield for `actions`.

1. `actions` - Used to define the buttons to the right of the title. This yields an object called `actions` which can be used to render `Default`, `Primary`, `Danger`, and `Wrapped` buttons.

```gjs
<DPageSubheader @titleLabel="admin.config.backups.subheader.title">
  <:actions>
    <actions.Primary
      @action={{routeAction "showStartBackupModal"}}
      @title="admin.backups.operations.backup.title"
      @label="admin.backups.operations.backup.label"
      class="admin-backups__start"
    />
  </:actions>
</DPageSubheader>

```

* * *

# 5.b. Config area

The config area is made up of cards or sections. Cards are great for grouping related info and tasks, helping users scan and prioritize content more easily.

## 🎨 Design

### Card

Cards are set up with a 2px border-radius and use a background of `--secondary`. It also have a 1px solid border with `--primary-low` and 20px padding around the content.

_Default variation_

 ![Default card layout](https://global.discourse-cdn.com/meta/original/4X/4/4/e/44e0a7875c201d742299412f9fb62e344d6fa992.png)

_Accordion variation_

 ![Accordian variation of a card](https://global.discourse-cdn.com/meta/original/4X/f/8/e/f8e64ef057e417c43cbfdb8233f102f5be2c226a.png)

**Design and usage**

- Group related information
- Display information so admins and mods see the most important stuff first
- Use headings that clearly explain what the card is for
- Break complicated ones into multiple sections, if necessary

_default variation_

- Stick to one primary call to action per card
- Place primary call to action at the bottom of the card for next steps

_accordion variation_

- Use the top right corner of the card for optional actions like “View all”

**Content**

- All forms should use the FormKit ember components in core described [in the docs](https://docs-demo.dtaylor.uk/docs/category/formkit)

- Card headers should be sentence-cased

## 🛠 Implementation

We have an `AdminConfigAreaCard` component that should be used for all of these cards. For now this only has `@translatedHeading` and `@heading` arguments, in future we can add actions and make them collapsible and so on:

```gjs
<AdminConfigAreaCard
  @heading="admin.config_areas.about.general_settings"
  class="admin-config-area-about__general-settings-section"
>
  <AdminConfigAreasAboutGeneralSettings
    @generalSettings={{this.generalSettings}}
    @setGlobalSavingStatus={{this.setSavingStatus}}
    @globalSavingStatus={{this.saving}}
  />
</AdminConfigAreaCard>

```

### Embedded site settings

_This section is a work in progress._

# 5.c. Help inset

This section provides additional guidance, documentation, or context within the page content.

 ![](https://global.discourse-cdn.com/meta/original/4X/0/7/3/073e9bedc73621b4cab4744508fd205b97992869.png) ![](https://global.discourse-cdn.com/meta/original/4X/6/6/b/66b8b28ea2a470033b2948d7aa1459566fffc745.png)

_v1_

## 🎨 Design

**Design and usage**

- Show related documentation or guides about the page’s content to provide useful info
- Include an icon in the heading to make it easily recognizable
- Place this section in the secondary (1/3) layout area

**Content**

- Headers should be sentence-cased

## 🛠 Implementation

_Code snippets or link to a topic/GitHub_

# 5.d. Table

Tables display information in a grid of cells, columns, and rows, making it easy for admins to quickly scan items and take action.

 ![image](https://global.discourse-cdn.com/meta/original/4X/5/a/b/5aba62659ce89958e5a3306a6470999635cef2a9.png)

## 🎨 Design

**Usage**

- Use tables to display structured content where each entry shares the same attributes.
- Allow admins to review, enable/disable, edit, and delete data sets.
- Suitable for data sets that will continue to grow over time.

**Design**

- Use horizontal lines between rows to visually separate content, including the last row. Avoid using borders or frames around the table to prevent it from looking like a net.
- Do not apply vertical lines between columns. Tables without vertical lines are generally easier to scan and read.

**Additional actions**

- **Row actions:** Include additional actions in the far-right column of each table row.
  - If there is two or more interactive elements, the primary action (e.g., “Edit”) should be a text button, and all other row actions including “Delete” should be grouped in a `[...]` dropdown. Icons in the dropdown menus are encouraged to break things up visually.
  - If there is only a “Delete” action and no primary action, use an inline “Delete” text button styled as `btn-default`.
  - You should wrap the main column’s text (generally `d-table__cell --overview`) with a link taking the admin directly to the Show/Edit page corresponding to the row for quick access.

- **Delete confirmation** : All “Delete” buttons should show a confirmation before carrying out the action.

**Content**

- **Header:** The table header is the top row that identifies the columns below. It provides clarity, especially if the data is non-descriptive or ambiguous. Headers should be short, descriptive, and relevant, using title case. Avoid headers that are too long for the content in the rows below.
- **Columns:** Order columns by priority or in a way that tells a coherent story with the data. Size columns according to their content, with narrow columns for small content and wider columns for paragraphs.
- **Rows:** Rows should support text, buttons, links, and icons to enhance the data presentation.
- **No data:** Empty lists should use the `AdminConfigAreaEmptyList` component with a CTA button and label to guide the user towards creating new records

 ![image](https://global.discourse-cdn.com/meta/original/4X/d/2/6/d2609092e09d8acc894f423415721d854020debc.png)

## 🛠 Implementation

There is a small collection of CSS classes that must be used with tables to make them work well on mobile and desktop.

`<table>` elements should have the `d-table` class applied.

`<thead>` elements should have the `d-table__header` class applied.

`<tr>` elements should have the `d-table__row` class applied.

`<td>` elements containing a lot of descriptive text (usually the leftmost column) should use the `d-table __cell --overview` classes. All other cells should use `d-table__ cell --detail`.

`<td>` elements with the `d-table __cell --overview` classes can wrap the inner row content in a link taking the admin directly to the Edit/Show page for the row. This link should follow this structure and have the `d-table__ overview-link` CSS class applied. Ideally the `LinkTo` component should be used but `<a>` is fine too as long as `getURL` is used with it.

The `d-table__overview-name` class should be applied to the name part here, but not the description.

```hbs
<td class="d-table__cell --overview">
  <LinkTo
    class="d-table__overview-link"
    @route="adminPlugins.show.explorer.details"
    @model={{query.id}}
  >
    <strong class="query-name d-table__overview-name">{{query.name}}</strong>
    {{#if query.is_default}}
      <span class="query-badge">{{i18n
          "explorer.default_query"
        }}</span>
    {{/if}}
    <div class="query-desc">{{query.description}}</div>
  </LinkTo>
</td>

```

```hbs
<td class="d-table__cell --overview">
  <a class="d-table __overview-name admin-flag-item__ name d-table__overview-link" href={{this.editUrl}}>
    {{@flag.name}}
  </a>
</td>

```

`<td>` elements which wrap the buttons on each row should have the `d-table-cell --controls` CSS classes applied. This ensures the buttons are aligned. Each button should have the `btn-small` class applied too.

For mobile, each `<td>` element _except_ the `d-table-cell --overview` should also include a `<div>` with the class `d-table__mobile-label`, that contains an I18n label that is the same as the one in the `<th>` for that column:

```hbs
<td class="d-table__cell --detail">
  <div class="d-table__mobile-label">
    {{i18n "chat.incoming_webhooks.emoji"}}
  </div>
  {{replaceEmoji webhook.emoji}}
</td>

```

This displays the table row as an easier to read card-based format on mobile:

 ![image](https://global.discourse-cdn.com/meta/original/4X/1/7/0/1709f6fc02fd98428f95f25abd0807387154fe4a.png)

For `[...]` dropdown menus, `DMenu` should be used with `DropdownMenu`, here is an example:

```gjs
<DMenu
  @identifier="backup-item-menu"
  @title={{i18n "more_options"}}
  @icon="ellipsis-vertical"
  class="btn-small"
>
  <:content>
    <DropdownMenu as |dropdown|>
      <dropdown.item>
        <DButton ...[button args here] />
      </dropdown.item>
      <dropdown.item>
        <DButton ...[button args here] />
      </dropdown.item>
    </DropdownMenu>
  </:content>
</DMenu>

```

Toggles in the table row are handled using the `DToggleSwitch` component:

```hbs
<DToggleSwitch
  @state={{this.enabled}}
  class="admin-flag-item__toggle {{@flag.name_key}}"
  {{on "click" (fn this.toggleFlagEnabled @flag)}}
/>

```

Putting it all together here is a minimal example of an admin table:

```hbs
 <table class="d-table">
    <thead class="d-table__header">
      <tr>
        <th>Name</th>
        <th>Description</th>
        <th></th>
      </tr>
    </thead>
    <tbody>
      <tr class="d-table__row">
        <td class="d-table__cell --overview">
          <LinkTo @route="admin.exampleRoute" class="d-table__overview-link">
            <span class="d-table__overview-name">Example Item</span>
            <span class="d-table__overview-about">A short description</span>
          </LinkTo>
        </td>
        <td class="d-table__cell --detail">
          <span class="d-table__mobile-label">Description</span>
          Some detail content here
        </td>
        <td class="d-table__cell --controls">
          <div class="d-table__cell-actions">
            <button class="btn btn-default btn-small">Edit</button>
          </div>
        </td>
      </tr>
    </tbody>
  </table>

```

# 5.e Third-level route

A third-level route is one that can be reached only from a config area. These usually come in the form of edit/new routes like this one for flags:

 ![](https://global.discourse-cdn.com/meta/original/4X/9/4/4/9449be80fb8ab7d65cd6bcb7fde24d3c4e445944.png)

This is where forms using FormKit will be placed in most cases.

Use the standard RESTful routes for these:

| Action | Path |
| --- | --- |
| New | `<resource>/new` |
| Edit | `<resource>/:id/edit` |

and ensure that the routes are also routed in the back-end. (Reloading the new- or edit page should not result in an error.)

## 🎨 Design

**Usage**

- Prefer having these third-level routes over having inline forms on the main route or within a table. Standalone edit and new routes are best, as they can easily be linked to.
- Do not show the top part of the page UI (breadcrumbs, page header and subheader)
- Instead, show a single “Back to X” link that allows the admin to get to the main config area
- The content of the page should be wrapped in at least one `AdminConfigAreaCard`
- Any sub-titles on the page should be done with config area cards

## 🛠 Implementation

There is a simple `BackButton` component that can be used on the top of the page to go back:

```gjs
<BackButton
  @route="adminConfig.flags"
  @label="admin.config_areas.flags.back"
/>

```

# 6. Filtered setting config pages

Many of our admin interface config pages are simple lists of filtered site settings. This allows admins to find related groups of settings without having to be overwhelmed by the full “All site settings” list, until we create more specialized config pages like `/admin/config/about/`.

## 🛠 Implementation

There are a few things you need to add one of these routes. First, you can either show an entire `category` of site settings which are the top level keys in `site_settings.yml` (e.g. `branding:`), or you can use a setting `area`.

Site settings can live in multiple `areas`, and you can display one or more on the same page.

1. Add a route to the admin route map below `adminConfig`, for example:

```javascript
this.route("trustLevels", { path: "/trust-levels" }, function () {
  this.route("settings", {
    path: "/",
  });
});

```

1. Add a new route .js file, the file will match a path like `frontend/discourse/admin/routes/admin-config/localization.js` depending on the name of your new route. This should inherit from `AdminConfigWithSettingsRoute` and include a `titleToken()`.

```javascript
import { i18n } from "discourse-i18n";
import AdminConfigWithSettingsRoute from "../admin-config-with-settings-route";

export default class AdminConfigLocalizationRoute extends AdminConfigWithSettingsRoute {
  titleToken() {
    return i18n("admin.config.localization.title");
  }
}

```

1. Add a controller, this is mainly to enable setting search and filtering. It must inherit from `AdminAreaSettingsBaseController`:

```javascript
import AdminAreaSettingsBaseController from "discourse/admin/controllers/admin-area-settings-base";

export default class AdminConfigLocalizationSettingsController extends AdminAreaSettingsBaseController {}

```

1. Finally, add a route template file in `.gjs` format, at a path like `frontend/discourse/admin/templates/admin-config/localization/settings.gjs`. This should contain the normal `DPageHeader` and breadcrumbs, but to display the settings you need `AdminAreaSettings`.

```gjs
<div class="admin-config-page__main-area">
  <AdminAreaSettings
    @showBreadcrumb={{false}}
    @area="localization"
    @path="/admin/config/localization"
    @filter={{@controller.filter}}
    @adminSettingsFilterChangedCallback={{@controller.adminSettingsFilterChangedCallback}}
  />
</div>

```

The important things to change here are the `@path`, and `@area` (or alternatively use `@categories`). As mentioned earlier, fill this in with either the site setting area you want to display, or the categories.

# 7. General guidance

- URL slugs must use hyphens (`-`) to indicate spaces in words, rather than underscores (`_`).

- All text in admin interfaces should follow the text formatting guidelines outlined here:

> [@Formatting text in Discourse documentation and UIs](https://meta.discourse.org/t/formatting-text-in-discourse-documentation-and-uis/324637):
>
> information_source Discourse UIs and documentation are currently being reviewed and edited to align with these guidelines. We are working toward full alignment as quickly as possible. When writing for Discourse, take note of the following guidelines for formatting text. These guidelines apply to text in all areas, notably documentation and the Discourse UI. Capitalisation When writing documentation or UI text, default to using sentence case (i.e. the first letter of the phrase is capitali…

# 8. Plugins

Some plugins need an in-depth configuration UI for their plugin (for example AI, Automation, Gamification) rather than only having a collection of site settings. For example, here is Discourse AI:

 ![Discourse AI plugin configuration UI](https://global.discourse-cdn.com/meta/original/4X/2/a/0/2a0be0179a5e4ce18ff39e1d299d485d1c413bbd.png)

Some examples of plugins using this are:

- Discourse AI [GitHub - discourse/discourse-ai: Discourse AI now lives in the discourse/discourse repo · GitHub](https://github.com/discourse/discourse-ai)
- Discourse Gamification [GitHub - discourse/discourse-gamification · GitHub](https://github.com/discourse/discourse-gamification)
- Discourse Chat (core)

## 🎨 Design

**Usage**

- General admin UI guidelines should be followed when making independent plugin UIs.

## 🛠 Implementation

#### Ember Routing

- All route templates will be under  
`admin/assets/javascripts/discourse/templates/admin-plugins/show/`
- All route js files will be under `admin/assets/javascripts/discourse/routes/` and  
prefixed with `admin-plugins-show-`
- The admin route map should be in a file like `admin-PLUGIN-NAME-plugin-route-map.js`
- The route map should have a structure like this. The important part is that  
we use `admin.adminPlugins.show` as the `resource`.

```javascript
export default {
  resource: "admin.adminPlugins.show",

  path: "/plugins",

  map() {
    this.route("discourse-ai-personas", { path: "ai-personas" }, function () {
      this.route("new");
      this.route("show", { path: "/:id" });
    });
  },
};

```

- The current example of how this all works is seen in the Discourse AI plugin, if you go to `/admin/plugins/discourse-ai/ai-personas`
- If you only have a “top level” route, e.g. one that doesn’t define sub-routes, then the template path will be something like `admin/assets/javascripts/discourse/templates/admin-plugins/show/your-route-name.gjs`. If there are sub routes then you get into the territory of needing `index.gjs`, `show.gjs`, and `new.gjs` templates and so on.

#### Navigation

Plugins can either show their navigation in an inner sidebar, or on the top tabbed navigation bar. The latter is highly recommended, and in future the inner sidebar support may be dropped.

- Any links that will be shown in either the top bar or the inner sidebar for  
the plugin show page should be defined in an initializer (e.g.  
`assets/javascripts/initializers/admin-plugin-configuration-nav.js`) using  
`api.addAdminPluginConfigurationNav` . Links need a `label`, `route`, and `description` (which is used for admin search)
- This initializer should only run if the user is admin.
- The site settings link for the plugin is generated automatically, no need to include it here.
- An example can be seen here [discourse-ai/assets/javascripts/initializers/admin-plugin-configuration-nav.js at ab4544d8977ec0e9d6aa42b4551df8317aa9b365 · discourse/discourse-ai · GitHub](https://github.com/discourse/discourse-ai/blob/ab4544d8977ec0e9d6aa42b4551df8317aa9b365/assets/javascripts/initializers/admin-plugin-configuration-nav.js#L13-L20) .

#### Server-Side

- `add_admin_route` is still used to show the custom admin routes in the admin sidebar and from the /plugins index with the tabs along the top. Basically, this defines the root page of your plugin UI.
  - `use_new_show_route: true` should be passed as an additional argument here so the new plugin show page is used.

#### UI Conventions

- Each index route for the plugin should show a `DPageSubheader` component to describe the intent of that route and to add any related action buttons.
- Action buttons that need to be rendered into the main plugin page header must use the `admin-plugin-config-page-actions` outlet with a dedicated component. The best place to do this is in the same initializer where `addAdminPluginConfigurationNav` is used.
  - `plugin` and `actions` are passed as `outletArgs`. `plugin` is the model representation of the current plugin so the name of the plugin and other things can be accessed, `actions` are the yielded action button components from `DPageHeader`.

```javascript
api.renderInOutlet(
  "admin-plugin-config-page-actions",
  ChatAdminPluginActions
);

```

**Related topics:**

- [Understanding the admin interface](https://meta.discourse.org/t/understanding-the-admin-interface/315108)
- [Formatting text in Discourse documentation and UIs](https://meta.discourse.org/t/formatting-text-in-discourse-documentation-and-uis/324637)

> Last edited by @martin 2026-02-26T01:41:47Z
> 
> Last checked by @hugh 2024-09-17T05:50:18Z
> 
> > **Check document**
> >
> > Perform check on document:

---

<div class="post-metadata">

### Author: ![Moin](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/moin/32/554653_2.png) [@Moin](https://meta.discourse.org/u/Moin)
#### Post date: [September 17, 2024, 2:09pm UTC](https://meta.discourse.org/t/creating-consistent-admin-interfaces/326780/6 "2024-09-17T14:09:53Z")

</div>

> [@Discourse](#):
>
> See the [Page Header](#p-1605522-h-2-page-header-4) details

And

> [@Discourse](#):
>
> Follow the guidelines for [help inset](#p-1605522-h-5c-help-inset-24) content

still don’t work. I think the second one is -23 instead of -24

---

<div class="post-metadata">

### Author: ![tobiaseigen](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/tobiaseigen/32/539204_2.png) [@tobiaseigen](https://meta.discourse.org/u/tobiaseigen)
#### Post date: [September 17, 2024, 3:54pm UTC](https://meta.discourse.org/t/creating-consistent-admin-interfaces/326780/7 "2024-09-17T15:54:56Z")

</div>

So glad to finally see this up on meta. Months of work went into this, and we are going to be using it to standardize the UI and navigation of every page in the admin interface.

Should we maybe just remove that table of contents and rely on discotoc instead? I think that would be less fragile, though I do like seeing the table of contents at the top of the post.

---

<div class="post-metadata">

### Author: ![hugh](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/hugh/32/336717_2.png) [@hugh](https://meta.discourse.org/u/hugh)
#### Post date: [September 18, 2024, 6:11am UTC](https://meta.discourse.org/t/creating-consistent-admin-interfaces/326780/8 "2024-09-18T06:11:05Z")

</div>

Thanks @Moin - all fixed!

> [@tobiaseigen](#):
>
> Should we maybe just remove that table of contents and rely on discotoc instead? I think that would be less fragile

I’ve made this change, otherwise it’s simply a duplicated table of contents.

---

<div class="post-metadata">

### Author: ![tobiaseigen](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/tobiaseigen/32/539204_2.png) [@tobiaseigen](https://meta.discourse.org/u/tobiaseigen)
#### Post date: [November 19, 2024, 5:06pm UTC](https://meta.discourse.org/t/creating-consistent-admin-interfaces/326780/14 "2024-11-19T17:06:55Z")

</div>

A post was split to a new topic: [Display username in browser tab when on user admin](https://meta.discourse.org/t/display-username-in-browser-tab-when-on-user-admin/337049)
