diff --git a/Build/Scripts/checkIntegritySetLabels.php b/Build/Scripts/checkIntegritySetLabels.php
index b001664560f72e61348c3209b9eb37315ae7b94b..2c7d23a8a2d66c607887568e427e8c586ac665bc 100755
--- a/Build/Scripts/checkIntegritySetLabels.php
+++ b/Build/Scripts/checkIntegritySetLabels.php
@@ -93,10 +93,14 @@ final readonly class CheckIntegritySetLabels
         ];
 
         $settingsDefinitions = Yaml::parseFile(dirname($labelFile) . '/settings.definitions.yaml');
-        foreach ($settingsDefinitions['settings'] as $key => $settingsDefinition) {
+        foreach (($settingsDefinitions['settings'] ?? []) as $key => $_) {
             $requiredLabels[] = 'settings.' . $key;
             $optionalLabels[] = 'settings.description.' . $key;
         }
+        foreach (($settingsDefinitions['categories'] ?? []) as $key => $_) {
+            $requiredLabels[] = 'categories.' . $key;
+            $optionalLabels[] = 'categories.description.' . $key;
+        }
 
         $setName = Yaml::parseFile(dirname($labelFile) . '/config.yaml')['name'];
 
diff --git a/Build/Sources/Sass/backend.scss b/Build/Sources/Sass/backend.scss
index 1fc8b62a7e1d382b456d92c0c724900d977bf335..5d3bf18ad7d4ad3e42fcb434ecea88a486484329 100644
--- a/Build/Sources/Sass/backend.scss
+++ b/Build/Sources/Sass/backend.scss
@@ -48,6 +48,7 @@
 @import "component/treelist";
 @import "component/indent";
 @import "component/pagination";
+@import "component/settings";
 @import "component/example";
 
 //
diff --git a/Build/Sources/Sass/component/_settings.scss b/Build/Sources/Sass/component/_settings.scss
new file mode 100644
index 0000000000000000000000000000000000000000..21b40957f8f363431df3b252e5b997710726043f
--- /dev/null
+++ b/Build/Sources/Sass/component/_settings.scss
@@ -0,0 +1,264 @@
+:root {
+    --settings-color: var(--typo3-component-color);
+    --settings-padding: calc(var(--typo3-spacing) * 2);
+    --settings-bg: var(--typo3-component-bg);
+    --settings-border-width: var(--typo3-component-border-width);
+    --settings-border-color: var(--typo3-component-border-color);
+    --settings-border-radius: var(--typo3-component-border-radius);
+    --settings-box-shadow: var(--typo3-component-box-shadow);
+    --settings-highlight: var(--typo3-component-primary-color);
+    --settings-indicator-bg: transparent;
+    --settings-item-color: var(--settings-color);
+    --settings-item-bg: var(--settings-bg);
+}
+
+.module[data-module-name="site_settings"] {
+    .module-body {
+        width: 100%;
+        max-width: 1320px;
+        margin: 0 auto;
+    }
+}
+
+.settings-container {
+    container-type: inline-size;
+}
+
+.settings {
+    display: flex;
+    align-items: flex-start;
+    flex-wrap: wrap;
+    color: var(--settings-color);
+    box-shadow: var(--settings-box-shadow);
+
+    &-navigation,
+    &-body {
+        padding: var(--settings-padding);
+    }
+
+    &-body {
+        background: var(--settings-bg);
+        border: var(--settings-border-width) solid var(--settings-border-color);
+        border-radius: var(--settings-border-radius);
+    }
+
+    &-navigation {
+        border-inline-start: var(--settings-border-width) solid var(--settings-border-color);
+        border-inline-end: var(--settings-border-width) solid var(--settings-border-color);
+        border-top: var(--settings-border-width) solid var(--settings-border-color);
+        border-bottom: 0;
+        flex: 1 1 auto;
+    }
+}
+
+/* clean-css ignore:start */
+@container (min-width: 780px) {
+    .settings {
+        flex-wrap: nowrap;
+        box-shadow: none;
+
+        &-body {
+            flex: 1 1 auto;
+            border-inline-start: var(--settings-border-width) solid var(--settings-border-color);
+            border-start-start-radius: 0;
+            box-shadow: var(--settings-box-shadow);
+        }
+
+        &-navigation {
+            box-shadow: var(--settings-box-shadow);
+            flex: 0 0 300px;
+            position: sticky;
+            top: calc(var(--module-body-padding-y) + var(--module-docheader-height));
+            border-inline-end: 0;
+            border-inline-start: var(--settings-border-width) solid var(--settings-border-color);
+            border-top: var(--settings-border-width) solid var(--settings-border-color);
+            border-bottom: var(--settings-border-width) solid var(--settings-border-color);
+        }
+    }
+}
+
+/* clean-css ignore:end */
+
+.settings-navigation {
+    ul {
+        list-style: none;
+        margin: 0;
+        padding: 0;
+
+        ul {
+            padding-inline-start: 1rem;
+        }
+    }
+}
+
+.settings-navigation-item {
+    color: var(--typo3-component-color);
+    position: relative;
+    display: flex;
+    border-radius: calc(var(--typo3-component-border-radius) - var(--typo3-component-border-width));
+    gap: .5em;
+    padding: var(--typo3-list-item-padding-y) var(--typo3-list-item-padding-x);
+    cursor: pointer;
+    text-decoration: none;
+
+    &.active,
+    &:hover,
+    &:focus {
+        z-index: 1;
+        outline-offset: -1px;
+    }
+
+    &:hover {
+        color: var(--typo3-component-hover-color);
+        background-color: var(--typo3-component-hover-bg);
+        outline: 1px solid var(--typo3-component-hover-border-color);
+    }
+
+    &.active,
+    &:focus {
+        color: var(--typo3-component-focus-color);
+        background-color: var(--typo3-component-focus-bg);
+        outline: 1px solid var(--typo3-component-focus-border-color);
+    }
+
+    &-icon {
+        user-select: none;
+        flex-shrink: 0;
+        flex-grow: 0;
+        width: var(--icon-size-small);
+    }
+
+    &-label {
+        user-select: none;
+        flex-grow: 1;
+    }
+}
+
+.settings-category {
+    max-width: 600px;
+    text-wrap: balance;
+
+    &-list + &-list {
+        margin-top: calc(var(--typo3-spacing) * 2);
+    }
+}
+
+.settings-item {
+    position: relative;
+    color: var(--settings-item-color);
+    background: var(--settings-item-bg);
+    border-radius: calc(var(--settings-border-radius) / 2);
+    padding-block: var(--typo3-component-padding-y);
+    padding-inline-start: calc(var(--typo3-component-padding-x) + 4px);
+    padding-inline-end: calc(var(--typo3-component-padding-x) + 3rem);
+    margin-inline-start: calc(-1 * var(--typo3-component-padding-x));
+    margin-inline-end: calc(-1 * var(--typo3-component-padding-x));
+
+    &:focus-within,
+    &:focus-within * {
+        --settings-item-bg: var(--typo3-component-focus-bg);
+        --settings-item-color: var(--typo3-component-focus-color);
+    }
+
+    &:focus-within {
+        outline-offset: -1px;
+        outline: 1px solid var(--typo3-component-focus-border-color);
+    }
+
+    &:hover,
+    &:focus,
+    &:focus-within {
+        .settings-item-actions {
+            opacity: 1;
+        }
+    }
+
+    &-indicator {
+        position: absolute;
+        background: var(--settings-indicator-bg);
+        inset-inline-start: 1px;
+        inset-block-start: 1px;
+        inset-block-end: 1px;
+        width: 4px;
+        border-radius: 0;
+    }
+
+    &[data-status="modified"],
+    &[data-status="modified"] * {
+        --settings-indicator-bg: #{$info};
+    }
+
+    &[data-status="error"],
+    &[data-status="error"] * {
+        --settings-indicator-bg: #{$danger};
+    }
+
+    &-actions {
+        opacity: 0;
+        position: absolute;
+        display: flex;
+        justify-content: center;
+        inset-inline-end: 0;
+        inset-block-start: 0;
+        inset-block-end: 0;
+        padding-block: var(--typo3-component-padding-y);
+        width: 3rem;
+        transition: opacity .3s ease-in-out;
+
+        & > .dropdown > button {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            background-color: transparent;
+            border: none;
+            color: inherit;
+            outline: none;
+            width: 32px;
+            height: 32px;
+            padding: 0;
+            margin-top: -4px;
+            border-radius: 50%;
+
+            &:hover {
+                background: color-mix(in srgb, var(--settings-item-bg), var(--settings-item-color) 10%);
+            }
+
+            &:focus {
+                background: color-mix(in srgb, var(--typo3-component-focus-bg), var(--typo3-component-focus-border-color) 20%);
+                color: var(--typo3-component-focus-color);
+            }
+
+            &:after {
+                display: none;
+            }
+        }
+    }
+
+    &-title {
+        margin-bottom: calc(var(--typo3-spacing) / 2);
+    }
+
+    &-label {
+        font-weight: bold;
+    }
+
+    &-description {
+        color: color-mix(in srgb, var(--settings-color), var(--settings-bg) 25%);
+    }
+
+    &-key {
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        font-family: var(--typo3-font-family-code);
+        color: var(--settings-highlight);
+    }
+
+    &-message {
+        margin-top: calc(var(--typo3-spacing) / 2);
+
+        &:empty {
+            display: none;
+        }
+    }
+}
diff --git a/Build/Sources/TypeScript/backend/copy-to-clipboard.ts b/Build/Sources/TypeScript/backend/copy-to-clipboard.ts
index 4e74d4e13bfadb85353066496b7c55a297f578dc..26a7d0eec92d28610c7571c28e05c1529c0dcf79 100644
--- a/Build/Sources/TypeScript/backend/copy-to-clipboard.ts
+++ b/Build/Sources/TypeScript/backend/copy-to-clipboard.ts
@@ -16,6 +16,37 @@ import { customElement, property } from 'lit/decorators';
 import Notification from '@typo3/backend/notification';
 import { lll } from '@typo3/core/lit-helper';
 
+export function copyToClipboard(text: string): void {
+  if (!text.length) {
+    console.warn('No text for copy to clipboard given.');
+    Notification.error(lll('copyToClipboard.error'));
+    return;
+  }
+  if (navigator.clipboard) {
+    navigator.clipboard.writeText(text).then((): void => {
+      Notification.success(lll('copyToClipboard.success'), '', 1);
+    }).catch((): void => {
+      Notification.error(lll('copyToClipboard.error'));
+    });
+  } else {
+    const textarea = document.createElement('textarea');
+    textarea.value = text;
+    document.body.appendChild(textarea);
+    textarea.focus();
+    textarea.select();
+    try {
+      if (document.execCommand('copy')) {
+        Notification.success(lll('copyToClipboard.success'), '', 1);
+      } else {
+        Notification.error(lll('copyToClipboard.error'));
+      }
+    } catch {
+      Notification.error(lll('copyToClipboard.error'));
+    }
+    document.body.removeChild(textarea);
+  }
+}
+
 /**
  * Module: @typo3/backend/copy-to-clipboard
  *
@@ -60,34 +91,12 @@ export class CopyToClipboard extends LitElement {
   }
 
   private copyToClipboard(): void {
-    if (typeof this.text !== 'string' || !this.text.length) {
+    if (typeof this.text !== 'string') {
       console.warn('No text for copy to clipboard given.');
       Notification.error(lll('copyToClipboard.error'));
       return;
     }
-    if (navigator.clipboard) {
-      navigator.clipboard.writeText(this.text).then((): void => {
-        Notification.success(lll('copyToClipboard.success'), '', 1);
-      }).catch((): void => {
-        Notification.error(lll('copyToClipboard.error'));
-      });
-    } else {
-      const textarea = document.createElement('textarea');
-      textarea.value = this.text;
-      document.body.appendChild(textarea);
-      textarea.focus();
-      textarea.select();
-      try {
-        if (document.execCommand('copy')) {
-          Notification.success(lll('copyToClipboard.success'), '', 1);
-        } else {
-          Notification.error(lll('copyToClipboard.error'));
-        }
-      } catch {
-        Notification.error(lll('copyToClipboard.error'));
-      }
-      document.body.removeChild(textarea);
-    }
+    copyToClipboard(this.text);
   }
 }
 
diff --git a/Build/Sources/TypeScript/backend/settings/editor.ts b/Build/Sources/TypeScript/backend/settings/editor.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0c4b81ea341172f72e98cb793212ff3dc57e7123
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/settings/editor.ts
@@ -0,0 +1,210 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import { html, LitElement, TemplateResult, nothing } from 'lit';
+import { customElement, property, state } from 'lit/decorators';
+import '@typo3/backend/element/spinner-element';
+import '@typo3/backend/element/icon-element';
+import Notification from '@typo3/backend/notification';
+import AjaxRequest from '@typo3/core/ajax/ajax-request';
+import { copyToClipboard } from '@typo3/backend/copy-to-clipboard';
+import { lll } from '@typo3/core/lit-helper';
+import '@typo3/backend/settings/editor/editable-setting';
+
+// preload known/common types
+import '@typo3/backend/settings/type/bool';
+import '@typo3/backend/settings/type/int';
+import '@typo3/backend/settings/type/number';
+import '@typo3/backend/settings/type/string';
+import '@typo3/backend/settings/type/stringlist';
+
+type ValueType = string|number|boolean|string[]|null;
+
+
+export interface Category {
+  key: string,
+  label: string,
+  description: string,
+  icon: string,
+  settings: EditableSetting[],
+  categories: Category[],
+}
+
+/** @see \TYPO3\CMS\Core\Settings\SettingDefinition */
+export interface SettingDefinition {
+  key: string,
+  type: string,
+  default: ValueType,
+  label: string,
+  description?: string|null,
+  enum: ValueType[],
+  categories: string[],
+  tags: string[],
+}
+
+/** @see \TYPO3\CMS\Backend\Dto\Settings\EditableSetting */
+export interface EditableSetting {
+  definition: SettingDefinition,
+  value: ValueType,
+  systemDefault: ValueType,
+  status: string,
+  warnings: string[],
+  typeImplementation: string,
+}
+
+@customElement('typo3-backend-settings-editor')
+export class SettingsEditorElement extends LitElement {
+
+  @property({ type: Array }) categories: Category[];
+  @property({ type: String, attribute: 'action-url' }) actionUrl: string;
+  @property({ type: String, attribute: 'dump-url' }) dumpUrl: string;
+  @property({ type: String, attribute: 'return-url' }) returnUrl: string;
+
+  @state() activeCategory: string = '';
+
+  visibleCategories: Record<string, boolean> = {};
+  observer: IntersectionObserver = null
+
+  protected createRenderRoot(): HTMLElement | ShadowRoot {
+    return this;
+  }
+
+  protected override firstUpdated(): void {
+    this.observer = new IntersectionObserver(
+      (entries) => {
+        entries.forEach(entry => {
+          const key = (entry.target as HTMLElement).dataset.key;
+          this.visibleCategories[key] = entry.isIntersecting;
+        })
+        const flatten = (list: Category[]): string[] => list.reduce((acc, c) => [...acc, c.key, ...flatten(c.categories)], []);
+        const active = flatten(this.categories).filter(key => this.visibleCategories[key])[0] || '';
+        if (active) {
+          this.activeCategory = active;
+        }
+      },
+      {
+        root: document.querySelector('.module'),
+        threshold: 0.1,
+        rootMargin: `-${getComputedStyle(document.querySelector('.module-docheader')).getPropertyValue('min-height')} 0px 0px 0px`
+      }
+    )
+  }
+
+  protected override updated(): void {
+    [...this.renderRoot.querySelectorAll('.settings-category')].map(entry => this.observer?.observe(entry));
+  }
+
+  protected renderCategoryTree(categories: Category[], level: number): TemplateResult {
+    return html`
+      <ul data-level=${level}>
+        ${categories.map(category => html`
+          <li>
+            <a href=${`#category-headline-${category.key}`}
+              @click=${() => this.activeCategory = category.key}
+              class="settings-navigation-item ${this.activeCategory === category.key ? 'active' : ''}">
+              <span class="settings-navigation-item-icon">
+                <typo3-backend-icon identifier=${category.icon ? category.icon : 'actions-dot'} size="small"></typo3-backend-icon>
+              </span>
+              <span class="settings-navigation-item-label">${category.label}</span>
+            </a>
+            ${category.categories.length === 0 ? nothing : html`
+              ${this.renderCategoryTree(category.categories, level + 1)}
+            `}
+          </li>
+        `)}
+      </ul>
+    `;
+  }
+
+  protected renderSettings(categories: Category[], level: number): TemplateResult[] {
+    return categories.map(category => html`
+      <div class="settings-category-list" data-key=${category.key}>
+        <div class="settings-category" data-key=${category.key}>
+          ${this.renderHeadline(Math.min(level + 1, 6), `category-headline-${category.key}`, html`${category.label}`)}
+          ${category.description ? html`<p>${category.description}</p>` : nothing}
+        </div>
+        ${category.settings.map((setting): TemplateResult => html`
+          <typo3-backend-editable-setting .setting=${setting} .dumpuri=${this.dumpUrl}></typo3-backend-editable-setting>
+        `)}
+      </div>
+      ${category.categories.length === 0 ? nothing : html`
+        ${this.renderSettings(category.categories, level + 1)}
+      `}
+    `);
+  }
+
+  protected renderHeadline(level: number, id: string, content: TemplateResult): TemplateResult {
+    switch (level) {
+      case 1:
+        return html`<h1 id=${id}>${content}</h1>`;
+      case 2:
+        return html`<h2 id=${id}>${content}</h2>`;
+      case 3:
+        return html`<h3 id=${id}>${content}</h3>`;
+      case 4:
+        return html`<h4 id=${id}>${content}</h4>`;
+      case 5:
+        return html`<h5 id=${id}>${content}</h5>`;
+      case 6:
+        return html`<h6 id=${id}>${content}</h6>`;
+      default:
+        throw new Error(`Invalid header level: ${level}`);
+    }
+  }
+
+  protected async onSubmit(e: SubmitEvent): Promise<void> {
+    const form = e.target as HTMLFormElement;
+
+    if ((e.submitter as HTMLButtonElement|null)?.value === 'export') {
+      e.preventDefault();
+      const formData = new FormData(form);
+      const response = await new AjaxRequest(this.dumpUrl).post(formData);
+
+      const result = await response.resolve();
+      if (typeof result.yaml === 'string') {
+        copyToClipboard(result.yaml);
+      } else {
+        console.warn('Value can not be copied to clipboard.', typeof result.yaml);
+        Notification.error(lll('copyToClipboard.error'));
+      }
+    }
+  }
+
+  protected render(): TemplateResult {
+    return html`
+      <form class="settings-container"
+            id="sitesettings_form"
+            name="sitesettings_form"
+            action=${this.actionUrl}
+            method="post"
+            @submit=${(e: SubmitEvent) => this.onSubmit(e)}
+      >
+        ${this.returnUrl ? html`<input type="hidden" name="returnUrl" value=${this.returnUrl} />` : nothing}
+        <div class="settings">
+          <div class="settings-navigation">
+            ${this.renderCategoryTree(this.categories ?? [], 1)}
+          </div>
+          <div class="settings-body">
+            ${this.renderSettings(this.categories ?? [], 1)}
+          </div>
+        </div>
+      </form>
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'typo3-backend-settings-editor': SettingsEditorElement;
+  }
+}
diff --git a/Build/Sources/TypeScript/backend/settings/editor/editable-setting.ts b/Build/Sources/TypeScript/backend/settings/editor/editable-setting.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b431246efca25e3fc81153fc70ac9cbf151dafe0
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/settings/editor/editable-setting.ts
@@ -0,0 +1,197 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import { html, LitElement, TemplateResult, nothing } from 'lit';
+import { customElement, property, state } from 'lit/decorators';
+import { until } from 'lit/directives/until.js';
+import '@typo3/backend/element/spinner-element';
+import '@typo3/backend/element/icon-element';
+import { copyToClipboard } from '@typo3/backend/copy-to-clipboard';
+import Notification from '@typo3/backend/notification';
+import { lll } from '@typo3/core/lit-helper';
+import AjaxRequest from '@typo3/core/ajax/ajax-request';
+import type { BaseElement } from '@typo3/backend/settings/type/base';
+
+type ValueType = string|number|boolean|string[]|null;
+
+/** @see \TYPO3\CMS\Core\Settings\SettingDefinition */
+interface SettingDefinition {
+  key: string,
+  type: string,
+  default: ValueType,
+  label: string,
+  description?: string|null,
+  enum: ValueType[],
+  categories: string[],
+  tags: string[],
+}
+
+/** @see \TYPO3\CMS\Backend\Dto\Settings\EditableSetting */
+interface EditableSetting {
+  definition: SettingDefinition,
+  value: ValueType,
+  systemDefault: ValueType,
+  status: string,
+  warnings: string[],
+  typeImplementation: string,
+}
+
+@customElement('typo3-backend-editable-setting')
+export class EditableSettingElement extends LitElement {
+
+  @property({ type: Object }) setting: EditableSetting;
+  @property({ type: String }) dumpuri: string;
+
+  @state()
+  hasChange: boolean = false;
+
+  typeElement: BaseElement<unknown> = null;
+
+  protected createRenderRoot(): HTMLElement | ShadowRoot {
+    return this;
+  }
+
+  protected render(): TemplateResult {
+    const { value, systemDefault, definition } = this.setting;
+    return html`
+      <div
+        class=${`settings-item settings-item-${definition.type} ${this.hasChange ? 'has-change' : ''}`}
+        tabindex="0"
+        data-status=${JSON.stringify(value) === JSON.stringify(systemDefault) ? 'none' : 'modified'}
+      >
+        <!-- data-status=modified|error|none-->
+        <div class="settings-item-indicator"></div>
+        <div class="settings-item-title">
+          <label for=${`setting-${definition.key}`} class="settings-item-label">${definition.label}</label>
+          <div class="settings-item-description">${definition.description}</div>
+          <div class="settings-item-key">${definition.key}</div>
+        </div>
+        <div class="settings-item-control">
+          ${until(this.renderField(), html`<typo3-backend-spinner></typo3-backend-spinner>`)}
+        </div>
+        <div class="settings-item-message"></div>
+        <div class="settings-item-actions">
+          ${this.renderActions()}
+        </div>
+      </div>
+    `;
+  }
+
+  protected async renderField(): Promise<HTMLElement> {
+    const { definition, value, typeImplementation } = this.setting;
+    let element = this.typeElement
+    if (!element) {
+      const implementation = await import(typeImplementation);
+      if (!('componentName' in implementation)) {
+        throw new Error(`module ${typeImplementation} is missing the "componentName" export`);
+      }
+      element = document.createElement(implementation.componentName);
+      this.typeElement = element;
+
+      element.addEventListener('typo3:setting:changed', (e: CustomEvent) => {
+        this.hasChange = JSON.stringify(this.setting.value) !== JSON.stringify(e.detail.value);
+      });
+    }
+
+    const attributes = {
+      key: definition.key,
+      formid: `setting-${definition.key}`,
+      name: `settings[${definition.key}]`,
+      value: Array.isArray(value) ? JSON.stringify(value) : String(value),
+      default: Array.isArray(definition.default) ? JSON.stringify(definition.default) : String(definition.default),
+    };
+    for (const [key, value] of Object.entries(attributes)) {
+      if (element.getAttribute(key) !== value) {
+        element.setAttribute(key, value);
+      }
+    }
+
+    return element;
+  }
+
+  protected renderActions(): TemplateResult {
+    const { definition } = this.setting;
+    return html`
+      <div class="dropdown">
+        <button class="dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+          <typo3-backend-icon identifier="actions-cog" size="small"></typo3-backend-icon>
+          <span class="visually-hidden">More actions</span>
+        </button>
+        <ul class="dropdown-menu">
+          <li>
+            <button class="dropdown-item dropdown-item-spaced"
+              type="button"
+              @click="${() => this.setToDefaultValue()}">
+              <typo3-backend-icon identifier="actions-undo" size="small"></typo3-backend-icon> ${lll('edit.resetSetting')}
+            </button>
+          </li>
+          <li><hr class="dropdown-divider"></li>
+          <li>
+            <typo3-copy-to-clipboard
+              text=${definition.key}
+              class="dropdown-item dropdown-item-spaced"
+            >
+              <typo3-backend-icon identifier="actions-clipboard" size="small"></typo3-backend-icon> ${lll('edit.copySettingsIdentifier')}
+            </typo3-copy-to-clipboard>
+          </li>
+          ${this.dumpuri ? html`
+            <li>
+              <button class="dropdown-item dropdown-item-spaced"
+                type="button"
+                @click="${() => this.copyAsYaml()}">
+                <typo3-backend-icon identifier="actions-clipboard-paste" size="small"></typo3-backend-icon> ${lll('edit.copyAsYaml')}
+
+              </a>
+            </li>
+          ` : nothing}
+        </ul>
+      </div>
+    `
+  }
+
+  protected setToDefaultValue(): void {
+    if (this.typeElement) {
+      this.typeElement.value = this.setting.systemDefault as unknown;
+    }
+  }
+
+  protected async copyAsYaml(): Promise<void> {
+    const formData = new FormData(this.typeElement.form);
+    const name = `settings[${this.setting.definition.key}]`
+    const value = formData.get(name);
+
+    const data = new FormData();
+    data.append('specificSetting', this.setting.definition.key);
+    data.append(name, value);
+
+    // @todo hookup with NProgress
+    const response = await new AjaxRequest(this.dumpuri).post(
+      data
+    );
+
+    const result = await response.resolve();
+
+    if (typeof result.yaml === 'string') {
+      copyToClipboard(result.yaml);
+    } else {
+      console.warn('Value can not be copied to clipboard.', typeof result.yaml);
+      Notification.error(lll('copyToClipboard.error'));
+    }
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'typo3-backend-editable-setting': EditableSettingElement;
+  }
+}
diff --git a/Build/Sources/TypeScript/backend/settings/type/base.ts b/Build/Sources/TypeScript/backend/settings/type/base.ts
new file mode 100644
index 0000000000000000000000000000000000000000..db6781ae726a83ac02eb7eba3267435b43600c6f
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/settings/type/base.ts
@@ -0,0 +1,194 @@
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/* eslint-disable @typescript-eslint/member-ordering */
+
+import { LitElement, PropertyDeclaration, ReactiveElement } from 'lit';
+import { defaultConverter } from '@lit/reactive-element';
+import { property } from 'lit/decorators';
+
+export const internals = Symbol('internals');
+const privateInternals = Symbol('privateInternals');
+export const getFormValue = Symbol('getFormValue');
+export const getFormState = Symbol('getFormState');
+
+/**
+ * Base element class for settings type to act as
+ * a form associated custom element.
+ *
+ * See https://web.dev/articles/more-capable-form-controls#defining_a_form-associated_custom_element
+ */
+export abstract class BaseElement<T = string> extends LitElement {
+
+  @property({ type: String }) key: string;
+  @property({ type: String }) formid: string;
+
+  static readonly formAssociated = true;
+
+  /* @property annotation needs to be provided by extending class */
+  value: T;
+
+  [privateInternals]?: ElementInternals;
+
+  protected createRenderRoot(): HTMLElement | ShadowRoot {
+    return this;
+  }
+
+  protected get [internals]() {
+    // Create internals in getter so that it can be used in methods called on
+    // construction in `ReactiveElement`, such as `requestUpdate()`.
+    if (!this[privateInternals]) {
+      this[privateInternals] = this.attachInternals();
+    }
+
+    return this[privateInternals];
+  }
+
+  public get form() {
+    return this[internals].form;
+  }
+
+  protected get labels() {
+    return this[internals].labels;
+  }
+
+  // Use @property for the `name` and `disabled` properties to add them to the
+  // `observedAttributes` array and trigger `attributeChangedCallback()`.
+  //
+  // We don't use Lit's default getter/setter (`noAccessor: true`) because
+  // the attributes need to be updated synchronously to work with synchronous
+  // form APIs, and Lit updates attributes async by default.
+  @property({ noAccessor: true })
+  public get name() {
+    return this.getAttribute('name') ?? '';
+  }
+  public set name(name: string) {
+    // Note: setting name to null or empty does not remove the attribute.
+    this.setAttribute('name', name);
+    // We don't need to call `requestUpdate()` since it's called synchronously
+    // in `attributeChangedCallback()`.
+  }
+
+  @property({ type: Boolean, noAccessor: true })
+  public get disabled() {
+    return this.hasAttribute('disabled');
+  }
+
+  public set disabled(disabled: boolean) {
+    this.toggleAttribute('disabled', disabled);
+    // We don't need to call `requestUpdate()` since it's called synchronously
+    // in `attributeChangedCallback()`.
+  }
+
+  public override attributeChangedCallback(
+    name: string,
+    old: string | null,
+    value: string | null,
+  ) {
+    // Manually `requestUpdate()` for `name` and `disabled` when their
+    // attribute or property changes.
+    // The properties update their attributes, so this callback is invoked
+    // immediately when the properties are set. We call `requestUpdate()` here
+    // instead of letting Lit set the properties from the attribute change.
+    // That would cause the properties to re-set the attribute and invoke this
+    // callback again in a loop. This leads to stale state when Lit tries to
+    // determine if a property changed or not.
+    if (name === 'name' || name === 'disabled') {
+      // Disabled's value is only false if the attribute is missing and null.
+      const oldValue = name === 'disabled' ? old !== null : old;
+      // Trigger a lit update when the attribute changes.
+      this.requestUpdate(name, oldValue);
+      return;
+    }
+
+    super.attributeChangedCallback(name, old, value);
+  }
+
+  public override requestUpdate(
+    name?: PropertyKey,
+    oldValue?: unknown,
+    options?: PropertyDeclaration,
+  ) {
+    super.requestUpdate(name, oldValue, options);
+    if (name === 'value') {
+      this.dispatchEvent(new CustomEvent('typo3:setting:changed', { detail: { value: this.value } }));
+      // Update the form value synchronously in `requestUpdate()` rather than
+      // `update()` or `updated()`, which are async. This is necessary to ensure
+      // that form data is updated in time for synchronous event listeners.
+      this[internals].setFormValue(this[getFormValue](), this[getFormState]());
+    }
+  }
+
+  public formDisabledCallback(disabled: boolean) {
+    this.disabled = disabled;
+  }
+
+  /**
+   * Callback triggered when <button type=reset> or form.reset() is triggered.
+   */
+  public formResetCallback() {
+    const oldValue = this.value;
+    const defaultValue = this.getAttribute('value');
+
+    // Workaround to trigger string to property conversion
+    this.attributeChangedCallback('value', this.valueToString(oldValue), null);
+    this.attributeChangedCallback('value', null, defaultValue);
+  }
+
+  /**
+   * Callback triggered when form is (re-)loaded by browser-back button.
+   */
+  public formStateRestoreCallback(state: FormValue): void {
+    if (typeof state === 'string') {
+      this.attributeChangedCallback('value', this.valueToString(this.value), null);
+      this.attributeChangedCallback('value', null, state);
+    } else {
+      throw new Error(`formStateRestoreCallback() needs to be implemented for <${this.localName}> for state type "${typeof state}"`);
+    }
+  }
+
+  protected [getFormState](): FormValue | null {
+    return this[getFormValue]();
+  }
+
+  protected [getFormValue](): string {
+    return this.valueToString(this.value);
+  }
+
+  protected valueToString(value: T): string {
+    const ctor = this.constructor as typeof ReactiveElement;
+    const options = ctor.getPropertyOptions('value');
+    const converter = typeof options.converter === 'object' && typeof options.converter?.toAttribute === 'function' ?
+      options.converter.toAttribute : defaultConverter.toAttribute;
+    return converter(value, options.type) as string;
+  }
+}
+
+export type FormValue = File | string | FormData;
+
+/**
+ * A value to be restored for a component's form value. If a component's form
+ * state is a `FormData` object, its entry list of name and values will be
+ * provided.
+ */
+export type FormRestoreState =
+  | File
+  | string
+  | Array<[string, FormDataEntryValue]>;
+
+/**
+ * The reason a form component is being restored for, either `'restore'` for
+ * browser restoration or `'autocomplete'` for restoring user values.
+ */
+export type FormRestoreReason = 'restore' | 'autocomplete';
diff --git a/Build/Sources/TypeScript/backend/settings/type/bool.ts b/Build/Sources/TypeScript/backend/settings/type/bool.ts
new file mode 100644
index 0000000000000000000000000000000000000000..782e1d5e92455eb7d6da8ed23dcaf3b8826c2bd1
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/settings/type/bool.ts
@@ -0,0 +1,55 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import { html, TemplateResult } from 'lit';
+import { customElement, property } from 'lit/decorators';
+import { BaseElement } from './base';
+
+export const componentName = 'typo3-backend-settings-type-bool';
+
+@customElement(componentName)
+export class BoolTypeElement extends BaseElement<boolean> {
+
+  @property({
+    type: Boolean,
+    converter: {
+      toAttribute: (value: boolean): string => {
+        return value ? '1' : '0';
+      },
+      fromAttribute: (value: string): boolean => {
+        return value === '1' || value === 'true';
+      }
+    }
+  }) value: boolean;
+
+  protected render(): TemplateResult {
+    return html`
+      <div class="form-check form-check-type-toggle">
+        <input
+          type="checkbox"
+          id=${this.formid}
+          class="form-check-input"
+          value="1"
+          .checked=${this.value}
+          @change=${(e: InputEvent) => this.value = (e.target as HTMLInputElement).checked ? true : false}
+        />
+      </div>
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'typo3-backend-settings-type-bool': BoolTypeElement;
+  }
+}
diff --git a/Build/Sources/TypeScript/backend/settings/type/color.ts b/Build/Sources/TypeScript/backend/settings/type/color.ts
new file mode 100644
index 0000000000000000000000000000000000000000..66a591a2fa6851bcedc345a5f1bff5323b395182
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/settings/type/color.ts
@@ -0,0 +1,63 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import { html, TemplateResult } from 'lit';
+import { customElement, property } from 'lit/decorators';
+import { BaseElement } from './base';
+import Alwan from 'alwan';
+
+export const componentName = 'typo3-backend-settings-type-color';
+
+@customElement(componentName)
+export class ColorTypeElement extends BaseElement<string> {
+
+  @property({ type: String }) value: string;
+
+  private alwan: Alwan|null = null;
+
+  protected firstUpdated(): void {
+    this.alwan = new Alwan(this.querySelector('input'), {
+      position: 'bottom-start',
+      format: 'hex',
+      opacity: false,
+      preset: false,
+      color: this.value,
+    });
+    this.alwan.on('color', (e): void => {
+      this.value = e.hex;
+    });
+  }
+
+  protected updateValue(value: string) {
+    this.value = value;
+    this.alwan?.setColor(value);
+  }
+
+  protected render(): TemplateResult {
+    return html`
+      <input
+        type="text"
+        id=${this.formid}
+        class="form-control"
+        .value=${this.value}
+        @change=${(e: InputEvent) => this.updateValue((e.target as HTMLInputElement).value)}
+      />
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'typo3-backend-settings-type-color': ColorTypeElement;
+  }
+}
diff --git a/Build/Sources/TypeScript/backend/settings/type/int.ts b/Build/Sources/TypeScript/backend/settings/type/int.ts
new file mode 100644
index 0000000000000000000000000000000000000000..08d807b7ea8762328fdadd243642feeec5df7c71
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/settings/type/int.ts
@@ -0,0 +1,42 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import { html, TemplateResult } from 'lit';
+import { customElement, property } from 'lit/decorators';
+import { BaseElement } from './base';
+
+export const componentName = 'typo3-backend-settings-type-int';
+
+@customElement(componentName)
+export class IntTypeElement extends BaseElement<number> {
+
+  @property({ type: Number }) value: number;
+
+  protected render(): TemplateResult {
+    return html`
+      <input
+        type="number"
+        id=${this.formid}
+        class="form-control"
+        .value=${this.value}
+        @change=${(e: InputEvent) => this.value = parseInt((e.target as HTMLInputElement).value, 10)}
+      />
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'typo3-backend-settings-type-int': IntTypeElement;
+  }
+}
diff --git a/Build/Sources/TypeScript/backend/settings/type/number.ts b/Build/Sources/TypeScript/backend/settings/type/number.ts
new file mode 100644
index 0000000000000000000000000000000000000000..efd06786b17e2f6d9415c5c1f37a07c45aacc602
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/settings/type/number.ts
@@ -0,0 +1,43 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import { html, TemplateResult } from 'lit';
+import { customElement, property } from 'lit/decorators';
+import { BaseElement } from './base';
+
+export const componentName = 'typo3-backend-settings-type-number';
+
+@customElement(componentName)
+export class NumberTypeElement extends BaseElement<number> {
+
+  @property({ type: Number }) value: number;
+
+  protected render(): TemplateResult {
+    return html`
+      <input
+        type="number"
+        id=${this.formid}
+        class="form-control"
+        step="0.01"
+        .value=${this.value}
+        @change=${(e: InputEvent) => this.value = parseFloat((e.target as HTMLInputElement).value)}
+      />
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'typo3-backend-settings-type-number': NumberTypeElement;
+  }
+}
diff --git a/Build/Sources/TypeScript/backend/settings/type/string.ts b/Build/Sources/TypeScript/backend/settings/type/string.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f9f500121ef3eacae67dce41947d95d8c69ff77a
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/settings/type/string.ts
@@ -0,0 +1,42 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import { html, TemplateResult } from 'lit';
+import { customElement, property } from 'lit/decorators';
+import { BaseElement } from './base';
+
+export const componentName = 'typo3-backend-settings-type-string';
+
+@customElement(componentName)
+export class StringTypeElement extends BaseElement<string> {
+
+  @property({ type: String }) value: string;
+
+  protected render(): TemplateResult {
+    return html`
+      <input
+        type="text"
+        id=${this.formid}
+        class="form-control"
+        .value=${this.value}
+        @change=${(e: InputEvent) => this.value = (e.target as HTMLInputElement).value}
+      />
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'typo3-backend-settings-type-string': StringTypeElement;
+  }
+}
diff --git a/Build/Sources/TypeScript/backend/settings/type/stringlist.ts b/Build/Sources/TypeScript/backend/settings/type/stringlist.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d0221ac98c32085ebdb1e226bdd1c7dce422748c
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/settings/type/stringlist.ts
@@ -0,0 +1,86 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import { html, TemplateResult } from 'lit';
+import { customElement, property } from 'lit/decorators';
+import { BaseElement } from './base';
+import { live } from 'lit/directives/live.js';
+
+export const componentName = 'typo3-backend-settings-type-stringlist';
+
+@customElement(componentName)
+export class StringlistTypeElement extends BaseElement<string[]> {
+
+  @property({ type: Array }) value: string[];
+
+  protected updateValue(value: string, index: number) {
+    const copy = [...this.value];
+    copy[index] = value;
+    this.value = copy;
+  }
+
+  protected addValue(index: number, value: string = '') {
+    this.value = this.value.toSpliced(index + 1, 0, value);
+  }
+
+  protected removeValue(index: number) {
+    this.value = this.value.toSpliced(index, 1);
+  }
+
+  protected renderItem(value: string, index: number): TemplateResult {
+    return html`
+      <tr>
+        <td width="99%">
+          <input
+            id=${`${this.formid}${index > 0 ? '-' + index : ''}`}
+            type="text"
+            class="form-control"
+            .value=${live(value)}
+            @change=${(e: InputEvent) => this.updateValue((e.target as HTMLInputElement).value, index)}
+          />
+        </td>
+        <td>
+          <div class="btn-group" role="group">
+            <button class="btn btn-default" type="button" @click=${() => this.addValue(index)}>
+              <typo3-backend-icon identifier="actions-plus" size="small"></typo3-backend-icon>
+            </button>
+            <button class="btn btn-default" type="button" @click=${() => this.removeValue(index)}>
+              <typo3-backend-icon identifier="actions-delete" size="small"></typo3-backend-icon>
+            </button>
+          </div>
+        </td>
+      </tr>
+    `;
+  }
+
+  protected render(): TemplateResult {
+    const value = this.value || [];
+    return html`
+      <div class="form-control-wrap">
+        <div class="table-fit">
+          <table class="table table-hover">
+            <tbody>
+              ${value.map((v, i) => this.renderItem(v, i))}
+            </tbody>
+          </table>
+        </div>
+      </div>
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'typo3-backend-settings-type-stringlist': StringlistTypeElement;
+  }
+}
diff --git a/Build/tests/playwright/accessibility/modules.spec.ts b/Build/tests/playwright/accessibility/modules.spec.ts
index f8631df126aa91959d15f0b804026a9e8222bde8..0f10c141e5e65e99161ff9de2861c932c99ad2a4 100644
--- a/Build/tests/playwright/accessibility/modules.spec.ts
+++ b/Build/tests/playwright/accessibility/modules.spec.ts
@@ -20,6 +20,10 @@ test.describe('modules', () => {
       'label': 'the info module',
       'route': 'module/web/info',
     },
+    'mod_site_settings': {
+      'label': 'the site settings module',
+      'route': 'module/site/settings',
+    },
     'mod_reports': {
       'label': 'the reports module',
       'route': 'module/system/reports',
diff --git a/typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php b/typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php
index b1397c528f04feb1a3db9f84d674a406105df9b0..c8c26e451f7f9b4925bd5f7c77347fbb4de5b5c5 100644
--- a/typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php
+++ b/typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php
@@ -150,7 +150,9 @@ class SiteConfigurationController
             throw new \RuntimeException('Existing config for site ' . $siteIdentifier . ' not found', 1521561226);
         }
 
-        $returnUrl = $this->uriBuilder->buildUriFromRoute('site_configuration');
+        $returnUrl = GeneralUtility::sanitizeLocalUrl(
+            (string)($request->getQueryParams()['returnUrl'] ?? '')
+        ) ?: $this->uriBuilder->buildUriFromRoute('site_configuration');
 
         $formDataCompilerInput = [
             'request' => $request,
@@ -183,7 +185,7 @@ class SiteConfigurationController
         ]);
 
         $this->pageRenderer->getJavaScriptRenderer()->includeTaggedImports('backend.form');
-        $this->configureEditViewDocHeader($view);
+        $this->configureEditViewDocHeader($view, $siteIdentifier);
         $view->setTitle(
             $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf:mlang_tabs_tab'),
             $siteIdentifier ?? ''
@@ -212,11 +214,16 @@ class SiteConfigurationController
 
         $siteTca = GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca();
 
-        $overviewRoute = $this->uriBuilder->buildUriFromRoute('site_configuration');
+        $queryParams = $request->getQueryParams();
         $parsedBody = $request->getParsedBody();
+
+        $returnUrl = GeneralUtility::sanitizeLocalUrl(
+            (string)($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '')
+        ) ?: $this->uriBuilder->buildUriFromRoute('site_configuration');
+
         if (isset($parsedBody['closeDoc']) && (int)$parsedBody['closeDoc'] === 1) {
             // Closing means no save, just redirect to overview
-            return new RedirectResponse($overviewRoute);
+            return new RedirectResponse($returnUrl);
         }
         $isSave = $parsedBody['_savedok'] ?? $parsedBody['doSave'] ?? false;
         $isSaveClose = $parsedBody['_saveandclosedok'] ?? false;
@@ -433,7 +440,7 @@ class SiteConfigurationController
 
         $saveRoute = $this->uriBuilder->buildUriFromRoute('site_configuration.edit', ['site' => $siteIdentifier]);
         if ($isSaveClose) {
-            return new RedirectResponse($overviewRoute);
+            return new RedirectResponse($returnUrl);
         }
         return new RedirectResponse($saveRoute);
     }
@@ -678,7 +685,7 @@ class SiteConfigurationController
     /**
      * Create document header buttons of "edit" action
      */
-    protected function configureEditViewDocHeader(ModuleTemplate $view): void
+    protected function configureEditViewDocHeader(ModuleTemplate $view, ?string $siteIdentifier): void
     {
         $buttonBar = $view->getDocHeaderComponent()->getButtonBar();
         $lang = $this->getLanguageService();
@@ -697,6 +704,19 @@ class SiteConfigurationController
             ->setIcon($this->iconFactory->getIcon('actions-document-save', IconSize::SMALL));
         $buttonBar->addButton($closeButton);
         $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
+        if ($siteIdentifier) {
+            $exportButton = $buttonBar->makeLinkButton()
+                ->setTitle($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:edit.editSiteSettings'))
+                ->setIcon($this->iconFactory->getIcon('actions-cog', IconSize::SMALL))
+                ->setShowLabelText(true)
+                ->setHref((string)$this->uriBuilder->buildUriFromRoute('site_settings.edit', [
+                    'site' => $siteIdentifier,
+                    'returnUrl' => $this->uriBuilder->buildUriFromRoute('site_configuration.edit', [
+                        'site' => $siteIdentifier,
+                    ]),
+                ]));
+            $buttonBar->addButton($exportButton, ButtonBar::BUTTON_POSITION_RIGHT, 2);
+        }
     }
 
     /**
diff --git a/typo3/sysext/backend/Classes/Controller/SiteSettingsController.php b/typo3/sysext/backend/Classes/Controller/SiteSettingsController.php
new file mode 100644
index 0000000000000000000000000000000000000000..778c1165f0035261f712fde02be3074ad82f7073
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Controller/SiteSettingsController.php
@@ -0,0 +1,326 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Backend\Controller;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Symfony\Component\Yaml\Yaml;
+use TYPO3\CMS\Backend\Attribute\AsController;
+use TYPO3\CMS\Backend\Dto\Settings\EditableSetting;
+use TYPO3\CMS\Backend\Routing\UriBuilder;
+use TYPO3\CMS\Backend\Template\Components\ButtonBar;
+use TYPO3\CMS\Backend\Template\ModuleTemplate;
+use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Http\JsonResponse;
+use TYPO3\CMS\Core\Http\RedirectResponse;
+use TYPO3\CMS\Core\Imaging\IconFactory;
+use TYPO3\CMS\Core\Imaging\IconSize;
+use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Core\Messaging\FlashMessageService;
+use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\Settings\Category;
+use TYPO3\CMS\Core\Settings\SettingDefinition;
+use TYPO3\CMS\Core\Settings\SettingsTypeRegistry;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\Set\CategoryRegistry;
+use TYPO3\CMS\Core\Site\SiteFinder;
+use TYPO3\CMS\Core\Site\SiteSettingsService;
+use TYPO3\CMS\Core\SysLog\Action\Setting as SettingAction;
+use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
+use TYPO3\CMS\Core\SysLog\Type;
+use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Backend controller: The "Site settings" module
+ *
+ * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
+ */
+#[AsController]
+readonly class SiteSettingsController
+{
+    public function __construct(
+        protected ModuleTemplateFactory $moduleTemplateFactory,
+        protected SiteFinder $siteFinder,
+        protected SiteSettingsService $siteSettingsService,
+        protected SettingsTypeRegistry $settingsTypeRegistry,
+        protected CategoryRegistry $categoryRegistry,
+        protected UriBuilder $uriBuilder,
+        protected PageRenderer $pageRenderer,
+        protected FlashMessageService $flashMessageService,
+        protected IconFactory $iconFactory,
+    ) {}
+
+    public function overviewAction(ServerRequestInterface $request): ResponseInterface
+    {
+        $view = $this->moduleTemplateFactory->create($request);
+        $view->assign('sites', array_map(
+            fn(Site $site): array => [
+                'site' => $site,
+                'siteTitle' => $this->getSiteTitle($site),
+                'hasSettingsDefinitions' => $this->siteSettingsService->hasSettingsDefinitions($site),
+                'localSettings' => $this->siteSettingsService->getLocalSettings($site),
+            ],
+            array_filter(
+                $this->siteFinder->getAllSites(),
+                static fn(Site $site): bool => $site->getSets() !== []
+            )
+        ));
+
+        return $view->renderResponse('SiteSettings/Overview');
+    }
+
+    public function editAction(ServerRequestInterface $request): ResponseInterface
+    {
+        $identifier = $request->getQueryParams()['site'] ?? null;
+        if ($identifier === null) {
+            throw new \RuntimeException('Site identifier to edit must be set', 1713394528);
+        }
+
+        $returnUrl = GeneralUtility::sanitizeLocalUrl(
+            (string)($request->getQueryParams()['returnUrl'] ?? '')
+        ) ?: null;
+        $overviewUrl = (string)$this->uriBuilder->buildUriFromRoute('site_settings');
+
+        $site = $this->siteFinder->getSiteByIdentifier($identifier);
+        $view = $this->moduleTemplateFactory->create($request);
+
+        $settings = $this->siteSettingsService->getUncachedSettings($site);
+        $setSettings = $this->siteSettingsService->getSetSettings($site);
+
+        $categoryEnhancer = function (Category $category) use (&$categoryEnhancer, $settings, $setSettings): Category {
+            return new Category(...[
+                ...get_object_vars($category),
+                'label' => $this->getLanguageService()->sL($category->label),
+                'description' => $category->description !== null ? $this->getLanguageService()->sL($category->description) : $category->description,
+                'categories' => array_map($categoryEnhancer, $category->categories),
+                'settings' => array_map(
+                    fn(SettingDefinition $definition): EditableSetting => new EditableSetting(
+                        definition: $this->resolveSettingLabels($definition),
+                        value: $settings->get($definition->key),
+                        systemDefault: $setSettings->get($definition->key),
+                        typeImplementation: $this->settingsTypeRegistry->get($definition->type)->getJavaScriptModule(),
+                    ),
+                    $category->settings
+                ),
+            ]);
+        };
+
+        $categories = array_map(
+            $categoryEnhancer,
+            $this->categoryRegistry->getCategories(...$site->getSets())
+        );
+        $hasSettings = count($categories) > 0;
+
+        $this->addDocHeaderCloseAndSaveButtons($view, $site, $returnUrl ?? $overviewUrl, $hasSettings);
+        if ($hasSettings) {
+            $this->addDocHeaderExportButton($view, $site);
+        }
+
+        $this->addDocHeaderSiteConfigurationButton($view, $site);
+        $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_copytoclipboard.xlf');
+        $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf');
+
+        $view->assign('siteIdentifier', $site->getIdentifier());
+        $view->assign('siteTitle', $this->getSiteTitle($site));
+        $view->assign('rootPageId', $site->getRootPageId());
+
+        $view->assign('actionUrl', (string)$this->uriBuilder->buildUriFromRoute('site_settings.save', array_filter([
+            'site' => $site->getIdentifier(),
+            'returnUrl' => $returnUrl,
+        ], static fn(?string $v): bool => $v !== null)));
+        $view->assign('returnUrl', $returnUrl);
+        $view->assign('dumpUrl', (string)$this->uriBuilder->buildUriFromRoute('site_settings.dump', ['site' => $site->getIdentifier()]));
+        $view->assign('categories', $categories);
+
+        return $view->renderResponse('SiteSettings/Edit');
+    }
+
+    private function resolveSettingLabels(SettingDefinition $definition): SettingDefinition
+    {
+        $languageService = $this->getLanguageService();
+        return new SettingDefinition(...[
+            ...get_object_vars($definition),
+            'label' => $languageService->sL($definition->label),
+            'description' => $definition->description !== null ? $languageService->sL($definition->description) : null,
+        ]);
+    }
+
+    public function saveAction(ServerRequestInterface $request): ResponseInterface
+    {
+        $identifier = $request->getQueryParams()['site'] ?? null;
+        if ($identifier === null) {
+            throw new \RuntimeException('Site identifier to edit must be set', 1713394529);
+        }
+
+        $site = $this->siteFinder->getSiteByIdentifier($identifier);
+
+        $view = $this->moduleTemplateFactory->create($request);
+
+        $parsedBody = $request->getParsedBody();
+
+        $returnUrl = GeneralUtility::sanitizeLocalUrl(
+            (string)($parsedBody['returnUrl'])
+        ) ?: null;
+        $overviewUrl = $this->uriBuilder->buildUriFromRoute('site_settings');
+        $CMD = $parsedBody['CMD'] ?? '';
+        $isSave = $CMD === 'save' || $CMD === 'saveclose';
+        $isSaveClose = $parsedBody['CMD'] === 'saveclose';
+        if (!$isSave) {
+            return new RedirectResponse($returnUrl ?? $overviewUrl);
+        }
+
+        $changes = $this->siteSettingsService->computeSettingsDiff($site, $parsedBody['settings'] ?? []);
+        $this->siteSettingsService->writeSettings($site, $changes['settings']);
+
+        if ($changes['changes'] !== [] || $changes['deletions'] !== []) {
+            $this->getBackendUser()->writelog(
+                Type::SITE,
+                SettingAction::CHANGE,
+                SystemLogErrorClassification::MESSAGE,
+                0,
+                'Site settings changed for \'%s\': %s',
+                [$site->getIdentifier(), json_encode($changes)],
+                'site'
+            );
+
+            $languageService = $this->getLanguageService();
+            $message = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:save.message.updated');
+            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, '', ContextualFeedbackSeverity::OK, true);
+            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
+            $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
+            $defaultFlashMessageQueue->enqueue($flashMessage);
+        }
+
+        if ($isSaveClose) {
+            return new RedirectResponse($returnUrl ?? $overviewUrl);
+        }
+        $editRoute = $this->uriBuilder->buildUriFromRoute('site_settings.edit', array_filter([
+            'site' => $site->getIdentifier(),
+            'returnUrl' => $returnUrl,
+        ], static fn(?string $v): bool => $v !== null));
+        return new RedirectResponse($editRoute);
+    }
+
+    public function dumpAction(ServerRequestInterface $request): ResponseInterface
+    {
+        $identifier = $request->getQueryParams()['site'] ?? null;
+        if ($identifier === null) {
+            throw new \RuntimeException('Site identifier to edit must be set', 1724772561);
+        }
+
+        $site = $this->siteFinder->getSiteByIdentifier($identifier);
+        $parsedBody = $request->getParsedBody();
+        $specificSetting = (string)($parsedBody['specificSetting'] ?? '');
+
+        $minify = $specificSetting !== '' ? false : true;
+        $changes = $this->siteSettingsService->computeSettingsDiff($site, $parsedBody['settings'] ?? [], $minify);
+        $settings = $changes['settings'];
+        if ($specificSetting !== '') {
+            $value = ArrayUtility::getValueByPath($settings, $specificSetting, '.');
+            $settings = ArrayUtility::setValueByPath([], $specificSetting, $value, '.');
+        }
+
+        $yamlContents = Yaml::dump($settings, 99, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP);
+
+        return new JsonResponse([
+            'yaml' => $yamlContents,
+        ]);
+    }
+
+    protected function addDocHeaderCloseAndSaveButtons(ModuleTemplate $moduleTemplate, Site $site, string $closeUrl, bool $saveEnabled): void
+    {
+        $languageService = $this->getLanguageService();
+        $buttonBar = $moduleTemplate->getDocHeaderComponent()->getButtonBar();
+        $closeButton = $buttonBar->makeLinkButton()
+            ->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:close'))
+            ->setIcon($this->iconFactory->getIcon('actions-close', IconSize::SMALL))
+            ->setShowLabelText(true)
+            ->setHref($closeUrl);
+        $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
+        $saveButton = $buttonBar->makeInputButton()
+            ->setName('CMD')
+            ->setValue('save')
+            ->setForm('sitesettings_form')
+            ->setIcon($this->iconFactory->getIcon('actions-document-save', IconSize::SMALL))
+            ->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:save'))
+            ->setShowLabelText(true)
+            ->setDisabled(!$saveEnabled);
+        $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
+    }
+
+    protected function addDocHeaderExportButton(ModuleTemplate $moduleTemplate, Site $site): void
+    {
+        $languageService = $this->getLanguageService();
+        $buttonBar = $moduleTemplate->getDocHeaderComponent()->getButtonBar();
+        $exportButton = $buttonBar->makeInputButton()
+            ->setTitle($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:edit.yamlExport'))
+            ->setIcon($this->iconFactory->getIcon('actions-database-export', IconSize::SMALL))
+            ->setShowLabelText(true)
+            ->setName('CMD')
+            ->setValue('export')
+            ->setForm('sitesettings_form');
+        $buttonBar->addButton($exportButton, ButtonBar::BUTTON_POSITION_RIGHT, 2);
+    }
+
+    protected function addDocHeaderSiteConfigurationButton(ModuleTemplate $moduleTemplate, Site $site): void
+    {
+        $languageService = $this->getLanguageService();
+        $buttonBar = $moduleTemplate->getDocHeaderComponent()->getButtonBar();
+        $exportButton = $buttonBar->makeLinkButton()
+            ->setTitle($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:edit.editSiteConfiguration'))
+            ->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL))
+            ->setShowLabelText(true)
+            ->setHref((string)$this->uriBuilder->buildUriFromRoute('site_configuration.edit', [
+                'site' => $site->getIdentifier(),
+                'returnUrl' => $this->uriBuilder->buildUriFromRoute('site_settings.edit', [
+                    'site' => $site->getIdentifier(),
+                ]),
+            ]));
+        $buttonBar->addButton($exportButton, ButtonBar::BUTTON_POSITION_RIGHT, 3);
+    }
+
+    protected function getSiteTitle(Site $site): string
+    {
+        $websiteTitle = $site->getConfiguration()['websiteTitle'] ?? '';
+        if ($websiteTitle !== '') {
+            return $websiteTitle;
+        }
+        $rootPage = BackendUtility::getRecord('pages', $site->getRootPageId());
+        $title = $rootPage['title'] ?? '';
+        if ($title !== '') {
+            return $title;
+        }
+
+        return '(unkown)';
+    }
+
+    protected function getLanguageService(): LanguageService
+    {
+        return $GLOBALS['LANG'];
+    }
+
+    protected function getBackendUser(): BackendUserAuthentication
+    {
+        return $GLOBALS['BE_USER'];
+    }
+}
diff --git a/typo3/sysext/backend/Classes/Dto/Settings/EditableSetting.php b/typo3/sysext/backend/Classes/Dto/Settings/EditableSetting.php
new file mode 100644
index 0000000000000000000000000000000000000000..1059956500bad071a49cb85f7c497d342cdd35b7
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Dto/Settings/EditableSetting.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Backend\Dto\Settings;
+
+use TYPO3\CMS\Core\Settings\SettingDefinition;
+
+/**
+ * @internal
+ */
+final readonly class EditableSetting implements \JsonSerializable
+{
+    public function __construct(
+        public SettingDefinition $definition,
+        public string|int|float|bool|array|null $value,
+        public string|int|float|bool|array|null $systemDefault,
+        public string $typeImplementation,
+    ) {}
+
+    public function jsonSerialize(): array
+    {
+        return get_object_vars($this);
+    }
+}
diff --git a/typo3/sysext/backend/Configuration/Backend/Modules.php b/typo3/sysext/backend/Configuration/Backend/Modules.php
index a28cf40c883309f538a1a0e16395a0a47938dd17..57b174d903ae34b9830cd2a3c09e2cab20b1f68e 100644
--- a/typo3/sysext/backend/Configuration/Backend/Modules.php
+++ b/typo3/sysext/backend/Configuration/Backend/Modules.php
@@ -7,6 +7,7 @@ use TYPO3\CMS\Backend\Controller\PageTsConfig\PageTsConfigIncludesController;
 use TYPO3\CMS\Backend\Controller\PageTsConfig\PageTsConfigRecordsOverviewController;
 use TYPO3\CMS\Backend\Controller\RecordListController;
 use TYPO3\CMS\Backend\Controller\SiteConfigurationController;
+use TYPO3\CMS\Backend\Controller\SiteSettingsController;
 use TYPO3\CMS\Backend\Security\ContentSecurityPolicy\CspModuleController;
 
 /**
@@ -73,6 +74,31 @@ return [
             ],
         ],
     ],
+    'site_settings' => [
+        'parent' => 'site',
+        'position' => ['after' => 'site_configuration'],
+        // @todo implement access=user
+        'access' => 'admin',
+        'path' => '/module/site/settings',
+        'iconIdentifier' => 'module-site-settings',
+        'labels' => 'LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings_module.xlf',
+        'routes' => [
+            '_default' => [
+                'target' => SiteSettingsController::class . '::overviewAction',
+            ],
+            'edit' => [
+                'target' => SiteSettingsController::class . '::editAction',
+            ],
+            'save' => [
+                'target' => SiteSettingsController::class . '::saveAction',
+                'methods' => ['POST'],
+            ],
+            'dump' => [
+                'target' => SiteSettingsController::class . '::dumpAction',
+                'methods' => ['POST'],
+            ],
+        ],
+    ],
     'about' => [
         'parent' => 'help',
         'position' => ['before' => '*'],
diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration.xlf
index e95c73ad861c42845ec2f786dba7f6926f409e1d..a8e36e11cecee91cafeba3d245af1e62d61d8ee3 100644
--- a/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration.xlf
+++ b/typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration.xlf
@@ -27,6 +27,12 @@
 			<trans-unit id="overview.editSiteConfiguration" resname="overview.editSiteConfiguration">
 				<source>Edit site configuration</source>
 			</trans-unit>
+			<trans-unit id="overview.editSiteSettings" resname="overview.editSiteSettings">
+				<source>Edit site settings</source>
+			</trans-unit>
+			<trans-unit id="overview.editSiteSettingsUnavailable" resname="overview.editSiteSettingsUnavailable">
+				<source>This site provides no configurable settings</source>
+			</trans-unit>
 			<trans-unit id="overview.deleteSiteConfiguration" resname="overview.deleteSiteConfiguration">
 				<source>Delete site configuration</source>
 			</trans-unit>
@@ -51,6 +57,9 @@
 			<trans-unit id="overview.duplicatedRootPage.message" resname="overview.duplicatedRootPage.message">
 				<source>The page with ID "%1s" is used in the following site configurations:</source>
 			</trans-unit>
+			<trans-unit id="edit.editSiteSettings" resname="edit.editSiteSettings">
+				<source>Edit site settings</source>
+			</trans-unit>
 			<trans-unit id="validation.identifierRenamed.title" resname="validation.identifierRenamed.title">
 				<source>Renamed identifier</source>
 			</trans-unit>
diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang_sitesettings.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang_sitesettings.xlf
new file mode 100644
index 0000000000000000000000000000000000000000..9263bf44ae510b52cd1d85cf36a45e3c43b56f17
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Private/Language/locallang_sitesettings.xlf
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+	<file source-language="en" datatype="plaintext" original="EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf" date="2024-09-10T12:13:00Z" product-name="backend">
+		<header/>
+		<body>
+			<trans-unit id="overview.title" resname="overview.title">
+				<source>Site Settings Overview</source>
+			</trans-unit>
+			<trans-unit id="overview.setSummary" resname="overview.setSummary">
+				<source>Sets (%d)</source>
+			</trans-unit>
+			<trans-unit id="overview.customSettingsSummary" resname="overview.customSettingsSummary">
+				<source>Custom Settings (%d)</source>
+			</trans-unit>
+			<trans-unit id="overview.editSettings" resname="overview.editSettings">
+				<source>Edit Settings</source>
+			</trans-unit>
+			<trans-unit id="overview.message.notEditable" resname="overview.message.notEditable">
+				<source>This sets of this sites do not provide any configurable settings.</source>
+			</trans-unit>
+			<trans-unit id="edit.title" resname="edit.noSettings.title">
+				<source>Site Settings for "{siteTitle}"</source>
+			</trans-unit>
+			<trans-unit id="edit.noSettings.title" resname="edit.noSettings.title">
+				<source>No Settings available</source>
+			</trans-unit>
+			<trans-unit id="edit.noSettings.message" resname="edit.noSettings.message">
+				<source>The site '%s' does provide configurable settings.</source>
+			</trans-unit>
+			<trans-unit id="edit.yamlExport" resname="edit.yamlExport">
+				<source>YAML export</source>
+			</trans-unit>
+			<trans-unit id="edit.editSiteConfiguration" resname="edit.editSiteConfiguration">
+				<source>Edit site configuration</source>
+			</trans-unit>
+			<trans-unit id="edit.resetSetting" resname="edit.resetSetting">
+				<source>Reset setting</source>
+			</trans-unit>
+			<trans-unit id="edit.copySettingsIdentifier" resname="edit.copySettingsIdentifier">
+				<source>Copy identifier</source>
+			</trans-unit>
+			<trans-unit id="edit.copyAsYaml" resname="edit.copyAsYaml">
+				<source>Copy as YAML</source>
+			</trans-unit>
+			<trans-unit id="save.message.updated" resname="save.message.updated">
+				<source>Settings updated.</source>
+			</trans-unit>
+			<trans-unit id="categories.other" resname="categories.other">
+				<source>Other</source>
+			</trans-unit>
+		</body>
+	</file>
+</xliff>
diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang_sitesettings_module.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang_sitesettings_module.xlf
new file mode 100644
index 0000000000000000000000000000000000000000..e87a0f3dedb6bcb0c929aa98ef31178024fad63b
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Private/Language/locallang_sitesettings_module.xlf
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+	<file source-language="en" datatype="plaintext" original="EXT:backend/Resources/Private/Language/locallang_sitesettings_module.xlf" date="2024-02-28T22:22:22Z" product-name="backend">
+		<header/>
+		<body>
+			<trans-unit id="mlang_tabs_tab" resname="mlang_tabs_tab">
+				<source>Settings</source>
+			</trans-unit>
+			<trans-unit id="mlang_labels_tablabel" resname="mlang_labels_tablabel">
+				<source>Site settings</source>
+			</trans-unit>
+			<trans-unit id="mlang_labels_tabdescr" resname="mlang_labels_tabdescr">
+				<source>This module allows you to configure site settings.</source>
+			</trans-unit>
+		</body>
+	</file>
+</xliff>
diff --git a/typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Overview.html b/typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Overview.html
index 17b1308879d7d54eb0bc72c4c3e000258ceec302..ef7368d5c8647a239276469909444c64e1896542 100644
--- a/typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Overview.html
+++ b/typo3/sysext/backend/Resources/Private/Templates/SiteConfiguration/Overview.html
@@ -148,6 +148,31 @@
                                             <f:be.link route="site_configuration.edit" parameters="{site: page.siteIdentifier}" title="{f:translate(key:'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:overview.editSiteConfiguration')}" class="btn btn-default">
                                                 <core:icon identifier="actions-open" />
                                             </f:be.link>
+
+                                            <f:if condition="{page.siteConfiguration.sets->f:count()} > 0">
+                                                <f:then>
+                                                    <f:variable name="returnUrl">{f:be.uri(route: 'site_configuration')}</f:variable>
+                                                    <f:be.link
+                                                        route="site_settings.edit"
+                                                        parameters="{site: page.siteIdentifier, returnUrl: returnUrl}"
+                                                        title="{f:translate(key:'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:overview.editSiteSettings')}"
+                                                        class="btn btn-default"
+                                                    >
+                                                        <core:icon identifier="actions-cog" />
+                                                    </f:be.link>
+                                                </f:then>
+                                                <f:else>
+                                                    <button
+                                                        disabled
+                                                        type="button"
+                                                        class="btn btn-default"
+                                                        title="{f:translate(key:'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:overview.editSiteSettingsUnavailable')}"
+                                                    >
+                                                        <core:icon identifier="actions-cog" />
+                                                    </button>
+                                                </f:else>
+                                            </f:if>
+
                                             <button
                                                 type="submit"
                                                 class="btn btn-default t3js-modal-trigger"
diff --git a/typo3/sysext/backend/Resources/Private/Templates/SiteSettings/Edit.html b/typo3/sysext/backend/Resources/Private/Templates/SiteSettings/Edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..ed8dce6871e3545a4ac603529e0325ce6f97b081
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Private/Templates/SiteSettings/Edit.html
@@ -0,0 +1,35 @@
+<f:layout name="Module" />
+
+<f:section name="Content">
+
+    <f:be.pageRenderer includeJavaScriptModules="{0: '@typo3/backend/settings/editor.js'}"/>
+
+    <h1>
+        Site Settings for "{siteTitle}"
+        <br>
+        <small class="text-muted">{siteIdentifier} <code>[pid: {rootPageId}]</code></small>
+    </h1>
+
+    <f:if condition="{categories->f:count()} == 0">
+        <f:then>
+            <f:be.infobox
+                state="{f:constant(name: 'TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper::STATE_INFO')}"
+                title="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:edit.noSettings.title')}"
+            >
+                <f:translate
+                    key="LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:edit.noSettings.message"
+                    arguments="{0: siteIdentifier}"
+                />
+        </f:be.infobox>
+        </f:then>
+        <f:else>
+            <typo3-backend-settings-editor
+                action-url="{actionUrl}"
+                return-url="{returnUrl}"
+                dump-url="{dumpUrl}"
+                categories="{categories->f:format.json()}"
+            ></typo3-backend-settings-editor>
+
+        </f:else>
+    </f:if>
+</f:section>
diff --git a/typo3/sysext/backend/Resources/Private/Templates/SiteSettings/Overview.html b/typo3/sysext/backend/Resources/Private/Templates/SiteSettings/Overview.html
new file mode 100644
index 0000000000000000000000000000000000000000..1c2f3abcba87ea9b6456ac8183975670a71095c0
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Private/Templates/SiteSettings/Overview.html
@@ -0,0 +1,54 @@
+<f:layout name="Module" />
+
+<f:section name="Content">
+
+    <h1><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:overview.title"/></h1>
+
+    <div class="card-container">
+        <f:for each="{sites}" as="c">
+            <div class="card card-size-small">
+                <div class="card-header">
+                    <div class="card-header-body">
+                        <h2 class="card-title">{c.siteTitle}</h2>
+                        <span class="card-subtitle">{c.site.identifier} [pid: {c.site.rootPageId}]</span>
+                    </div>
+                </div>
+                <div class="card-body">
+                    <details open name="details-{c.site.identifier}">
+                        <summary><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:overview.setSummary" arguments="{0: '{c.site.sets->f:count()}'}"/></summary>
+                        <p>
+                            <f:for each="{c.site.sets}" as="set" iteration="i">
+                                <code>{set}</code><f:if condition="{i.isLast}"><f:else><br></f:else></f:if>
+                            </f:for>
+                        </p>
+                    </details>
+
+                    <f:if condition="{c.localSettings.allFlat->f:count()} > 0">
+                        <details open name="details-{c.site.identifier}">
+                            <summary><f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:overview.customSettingsSummary" arguments="{0: '{c.localSettings.map->f:count()}'}"/></summary>
+                            <p>
+                                <f:for each="{c.localSettings.map}" as="setting" key="key" iteration="i">
+                                    <code>{key}: <strong>{setting->f:format.json()}</strong></code><br>
+                                </f:for>
+                            </p>
+                        </details>
+                    </f:if>
+                </div>
+                <f:if condition="{c.hasSettingsDefinitions}">
+                    <f:then>
+                        <div class="card-footer">
+                            <f:be.link route="site_settings.edit" parameters="{site: c.site.identifier}" class="btn btn-default">
+                                <f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:overview.editSettings"/></h1>
+                            </f:be.link>
+                        </div>
+                    </f:then>
+                    <f:else>
+                        <div class="card-footer text-body-secondary">
+                            <f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:overview.message.notEditable"/></h1>
+                        </div>
+                    </f:else>
+                </f:if>
+            </div>
+        </f:for>
+    </div>
+</f:section>
diff --git a/typo3/sysext/backend/Resources/Public/Css/backend.css b/typo3/sysext/backend/Resources/Public/Css/backend.css
index 6d410ff2ac14b6fe2501c3299bc2d0e5a72f2924..1aa6f7e3bc1d627b1cadc685911404a384ebd132 100644
--- a/typo3/sysext/backend/Resources/Public/Css/backend.css
+++ b/typo3/sysext/backend/Resources/Public/Css/backend.css
@@ -3655,6 +3655,64 @@ typo3-backend-live-search-result-item-action>* .livesearch-result-item-title .sm
 .indent{--indent-base:16px;--indent-level:0;margin-inline-start:calc(var(--indent-base) * var(--indent-level))}
 .indent-inline-block{display:inline-block}
 .pagination{flex-wrap:wrap;row-gap:4px}
+:root{--settings-color:var(--typo3-component-color);--settings-padding:calc(var(--typo3-spacing) * 2);--settings-bg:var(--typo3-component-bg);--settings-border-width:var(--typo3-component-border-width);--settings-border-color:var(--typo3-component-border-color);--settings-border-radius:var(--typo3-component-border-radius);--settings-box-shadow:var(--typo3-component-box-shadow);--settings-highlight:var(--typo3-component-primary-color);--settings-indicator-bg:transparent;--settings-item-color:var(--settings-color);--settings-item-bg:var(--settings-bg)}
+.module[data-module-name=site_settings] .module-body{width:100%;max-width:1320px;margin:0 auto}
+.settings-container{container-type:inline-size}
+.settings{display:flex;align-items:flex-start;flex-wrap:wrap;color:var(--settings-color);box-shadow:var(--settings-box-shadow)}
+.settings-body,.settings-navigation{padding:var(--settings-padding)}
+.settings-body{background:var(--settings-bg);border:var(--settings-border-width) solid var(--settings-border-color);border-radius:var(--settings-border-radius)}
+.settings-navigation{border-inline-start:var(--settings-border-width) solid var(--settings-border-color);border-inline-end:var(--settings-border-width) solid var(--settings-border-color);border-top:var(--settings-border-width) solid var(--settings-border-color);border-bottom:0;flex:1 1 auto}
+
+@container (min-width: 780px) {
+  .settings {
+    flex-wrap: nowrap;
+    box-shadow: none;
+  }
+  .settings-body {
+    flex: 1 1 auto;
+    border-inline-start: var(--settings-border-width) solid var(--settings-border-color);
+    border-start-start-radius: 0;
+    box-shadow: var(--settings-box-shadow);
+  }
+  .settings-navigation {
+    box-shadow: var(--settings-box-shadow);
+    flex: 0 0 300px;
+    position: sticky;
+    top: calc(var(--module-body-padding-y) + var(--module-docheader-height));
+    border-inline-end: 0;
+    border-inline-start: var(--settings-border-width) solid var(--settings-border-color);
+    border-top: var(--settings-border-width) solid var(--settings-border-color);
+    border-bottom: var(--settings-border-width) solid var(--settings-border-color);
+  }
+}
+.settings-navigation ul{list-style:none;margin:0;padding:0}
+.settings-navigation ul ul{padding-inline-start:1rem}
+.settings-navigation-item{color:var(--typo3-component-color);position:relative;display:flex;border-radius:calc(var(--typo3-component-border-radius) - var(--typo3-component-border-width));gap:.5em;padding:var(--typo3-list-item-padding-y) var(--typo3-list-item-padding-x);cursor:pointer;text-decoration:none}
+.settings-navigation-item.active,.settings-navigation-item:focus,.settings-navigation-item:hover{z-index:1;outline-offset:-1px}
+.settings-navigation-item:hover{color:var(--typo3-component-hover-color);background-color:var(--typo3-component-hover-bg);outline:1px solid var(--typo3-component-hover-border-color)}
+.settings-navigation-item.active,.settings-navigation-item:focus{color:var(--typo3-component-focus-color);background-color:var(--typo3-component-focus-bg);outline:1px solid var(--typo3-component-focus-border-color)}
+.settings-navigation-item-icon{-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;flex-grow:0;width:var(--icon-size-small)}
+.settings-navigation-item-label{-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-grow:1}
+.settings-category{max-width:600px;text-wrap:balance}
+.settings-category-list+.settings-category-list{margin-top:calc(var(--typo3-spacing) * 2)}
+.settings-item{position:relative;color:var(--settings-item-color);background:var(--settings-item-bg);border-radius:calc(var(--settings-border-radius)/ 2);padding-block:var(--typo3-component-padding-y);padding-inline-start:calc(var(--typo3-component-padding-x) + 4px);padding-inline-end:calc(var(--typo3-component-padding-x) + 3rem);margin-inline-start:calc(-1 * var(--typo3-component-padding-x));margin-inline-end:calc(-1 * var(--typo3-component-padding-x))}
+.settings-item:focus-within,.settings-item:focus-within *{--settings-item-bg:var(--typo3-component-focus-bg);--settings-item-color:var(--typo3-component-focus-color)}
+.settings-item:focus-within{outline-offset:-1px;outline:1px solid var(--typo3-component-focus-border-color)}
+.settings-item:focus .settings-item-actions,.settings-item:focus-within .settings-item-actions,.settings-item:hover .settings-item-actions{opacity:1}
+.settings-item-indicator{position:absolute;background:var(--settings-indicator-bg);inset-inline-start:1px;inset-block-start:1px;inset-block-end:1px;width:4px;border-radius:0}
+.settings-item[data-status=modified],.settings-item[data-status=modified] *{--settings-indicator-bg:#abdced}
+.settings-item[data-status=error],.settings-item[data-status=error] *{--settings-indicator-bg:#d13a2e}
+.settings-item-actions{opacity:0;position:absolute;display:flex;justify-content:center;inset-inline-end:0;inset-block-start:0;inset-block-end:0;padding-block:var(--typo3-component-padding-y);width:3rem;transition:opacity .3s ease-in-out}
+.settings-item-actions>.dropdown>button{display:flex;justify-content:center;align-items:center;background-color:transparent;border:none;color:inherit;outline:0;width:32px;height:32px;padding:0;margin-top:-4px;border-radius:50%}
+.settings-item-actions>.dropdown>button:hover{background:color-mix(in srgb,var(--settings-item-bg),var(--settings-item-color) 10%)}
+.settings-item-actions>.dropdown>button:focus{background:color-mix(in srgb,var(--typo3-component-focus-bg),var(--typo3-component-focus-border-color) 20%);color:var(--typo3-component-focus-color)}
+.settings-item-actions>.dropdown>button:after{display:none}
+.settings-item-title{margin-bottom:calc(var(--typo3-spacing)/ 2)}
+.settings-item-label{font-weight:700}
+.settings-item-description{color:color-mix(in srgb,var(--settings-color),var(--settings-bg) 25%)}
+.settings-item-key{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:var(--typo3-font-family-code);color:var(--settings-highlight)}
+.settings-item-message{margin-top:calc(var(--typo3-spacing)/ 2)}
+.settings-item-message:empty{display:none}
 .example{color:var(--typo3-component-color);background-color:var(--typo3-surface-base);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);padding:var(--typo3-spacing);margin-bottom:var(--typo3-spacing)}
 .example+:not(.example){margin-top:var(--typo3-component-spacing)}
 .example>:last-child{margin-bottom:0}
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/copy-to-clipboard.js b/typo3/sysext/backend/Resources/Public/JavaScript/copy-to-clipboard.js
index 68ec8919e6d1068052433caad040cd7dfffaa890..2a0592b52960d4db3986fdba15a809b84f393918 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/copy-to-clipboard.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/copy-to-clipboard.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-var __decorate=function(t,o,e,r){var i,c=arguments.length,l=c<3?o:null===r?r=Object.getOwnPropertyDescriptor(o,e):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)l=Reflect.decorate(t,o,e,r);else for(var p=t.length-1;p>=0;p--)(i=t[p])&&(l=(c<3?i(l):c>3?i(o,e,l):i(o,e))||l);return c>3&&l&&Object.defineProperty(o,e,l),l};import{html,css,LitElement}from"lit";import{customElement,property}from"lit/decorators.js";import Notification from"@typo3/backend/notification.js";import{lll}from"@typo3/core/lit-helper.js";let CopyToClipboard=class extends LitElement{constructor(){super(),this.addEventListener("click",(t=>{t.preventDefault(),this.copyToClipboard()})),this.addEventListener("keydown",(t=>{"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),this.copyToClipboard())}))}connectedCallback(){this.hasAttribute("role")||this.setAttribute("role","button"),this.hasAttribute("tabindex")||this.setAttribute("tabindex","0")}render(){return html`<slot></slot>`}copyToClipboard(){if("string"!=typeof this.text||!this.text.length)return console.warn("No text for copy to clipboard given."),void Notification.error(lll("copyToClipboard.error"));if(navigator.clipboard)navigator.clipboard.writeText(this.text).then((()=>{Notification.success(lll("copyToClipboard.success"),"",1)})).catch((()=>{Notification.error(lll("copyToClipboard.error"))}));else{const t=document.createElement("textarea");t.value=this.text,document.body.appendChild(t),t.focus(),t.select();try{document.execCommand("copy")?Notification.success(lll("copyToClipboard.success"),"",1):Notification.error(lll("copyToClipboard.error"))}catch{Notification.error(lll("copyToClipboard.error"))}document.body.removeChild(t)}}};CopyToClipboard.styles=[css`:host { cursor: pointer; appearance: button; }`],__decorate([property({type:String})],CopyToClipboard.prototype,"text",void 0),CopyToClipboard=__decorate([customElement("typo3-copy-to-clipboard")],CopyToClipboard);export{CopyToClipboard};
\ No newline at end of file
+var __decorate=function(o,t,e,r){var i,c=arguments.length,l=c<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,e):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)l=Reflect.decorate(o,t,e,r);else for(var p=o.length-1;p>=0;p--)(i=o[p])&&(l=(c<3?i(l):c>3?i(t,e,l):i(t,e))||l);return c>3&&l&&Object.defineProperty(t,e,l),l};import{html,css,LitElement}from"lit";import{customElement,property}from"lit/decorators.js";import Notification from"@typo3/backend/notification.js";import{lll}from"@typo3/core/lit-helper.js";export function copyToClipboard(o){if(!o.length)return console.warn("No text for copy to clipboard given."),void Notification.error(lll("copyToClipboard.error"));if(navigator.clipboard)navigator.clipboard.writeText(o).then((()=>{Notification.success(lll("copyToClipboard.success"),"",1)})).catch((()=>{Notification.error(lll("copyToClipboard.error"))}));else{const t=document.createElement("textarea");t.value=o,document.body.appendChild(t),t.focus(),t.select();try{document.execCommand("copy")?Notification.success(lll("copyToClipboard.success"),"",1):Notification.error(lll("copyToClipboard.error"))}catch{Notification.error(lll("copyToClipboard.error"))}document.body.removeChild(t)}}let CopyToClipboard=class extends LitElement{constructor(){super(),this.addEventListener("click",(o=>{o.preventDefault(),this.copyToClipboard()})),this.addEventListener("keydown",(o=>{"Enter"!==o.key&&" "!==o.key||(o.preventDefault(),this.copyToClipboard())}))}connectedCallback(){this.hasAttribute("role")||this.setAttribute("role","button"),this.hasAttribute("tabindex")||this.setAttribute("tabindex","0")}render(){return html`<slot></slot>`}copyToClipboard(){if("string"!=typeof this.text)return console.warn("No text for copy to clipboard given."),void Notification.error(lll("copyToClipboard.error"));copyToClipboard(this.text)}};CopyToClipboard.styles=[css`:host { cursor: pointer; appearance: button; }`],__decorate([property({type:String})],CopyToClipboard.prototype,"text",void 0),CopyToClipboard=__decorate([customElement("typo3-copy-to-clipboard")],CopyToClipboard);export{CopyToClipboard};
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/settings/editor.js b/typo3/sysext/backend/Resources/Public/JavaScript/settings/editor.js
new file mode 100644
index 0000000000000000000000000000000000000000..61a41bd6c761f6e3a2624ea9756259d133dcbcd1
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/settings/editor.js
@@ -0,0 +1,62 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+var __decorate=function(t,e,r,i){var o,n=arguments.length,s=n<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)s=Reflect.decorate(t,e,r,i);else for(var a=t.length-1;a>=0;a--)(o=t[a])&&(s=(n<3?o(s):n>3?o(e,r,s):o(e,r))||s);return n>3&&s&&Object.defineProperty(e,r,s),s};import{html,LitElement,nothing}from"lit";import{customElement,property,state}from"lit/decorators.js";import"@typo3/backend/element/spinner-element.js";import"@typo3/backend/element/icon-element.js";import Notification from"@typo3/backend/notification.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import{copyToClipboard}from"@typo3/backend/copy-to-clipboard.js";import{lll}from"@typo3/core/lit-helper.js";import"@typo3/backend/settings/editor/editable-setting.js";import"@typo3/backend/settings/type/bool.js";import"@typo3/backend/settings/type/int.js";import"@typo3/backend/settings/type/number.js";import"@typo3/backend/settings/type/string.js";import"@typo3/backend/settings/type/stringlist.js";let SettingsEditorElement=class extends LitElement{constructor(){super(...arguments),this.activeCategory="",this.visibleCategories={},this.observer=null}createRenderRoot(){return this}firstUpdated(){this.observer=new IntersectionObserver((t=>{t.forEach((t=>{const e=t.target.dataset.key;this.visibleCategories[e]=t.isIntersecting}));const e=t=>t.reduce(((t,r)=>[...t,r.key,...e(r.categories)]),[]),r=e(this.categories).filter((t=>this.visibleCategories[t]))[0]||"";r&&(this.activeCategory=r)}),{root:document.querySelector(".module"),threshold:.1,rootMargin:`-${getComputedStyle(document.querySelector(".module-docheader")).getPropertyValue("min-height")} 0px 0px 0px`})}updated(){[...this.renderRoot.querySelectorAll(".settings-category")].map((t=>this.observer?.observe(t)))}renderCategoryTree(t,e){return html`
+      <ul data-level=${e}>
+        ${t.map((t=>html`
+          <li>
+            <a href=${`#category-headline-${t.key}`}
+              @click=${()=>this.activeCategory=t.key}
+              class="settings-navigation-item ${this.activeCategory===t.key?"active":""}">
+              <span class="settings-navigation-item-icon">
+                <typo3-backend-icon identifier=${t.icon?t.icon:"actions-dot"} size="small"></typo3-backend-icon>
+              </span>
+              <span class="settings-navigation-item-label">${t.label}</span>
+            </a>
+            ${0===t.categories.length?nothing:html`
+              ${this.renderCategoryTree(t.categories,e+1)}
+            `}
+          </li>
+        `))}
+      </ul>
+    `}renderSettings(t,e){return t.map((t=>html`
+      <div class="settings-category-list" data-key=${t.key}>
+        <div class="settings-category" data-key=${t.key}>
+          ${this.renderHeadline(Math.min(e+1,6),`category-headline-${t.key}`,html`${t.label}`)}
+          ${t.description?html`<p>${t.description}</p>`:nothing}
+        </div>
+        ${t.settings.map((t=>html`
+          <typo3-backend-editable-setting .setting=${t} .dumpuri=${this.dumpUrl}></typo3-backend-editable-setting>
+        `))}
+      </div>
+      ${0===t.categories.length?nothing:html`
+        ${this.renderSettings(t.categories,e+1)}
+      `}
+    `))}renderHeadline(t,e,r){switch(t){case 1:return html`<h1 id=${e}>${r}</h1>`;case 2:return html`<h2 id=${e}>${r}</h2>`;case 3:return html`<h3 id=${e}>${r}</h3>`;case 4:return html`<h4 id=${e}>${r}</h4>`;case 5:return html`<h5 id=${e}>${r}</h5>`;case 6:return html`<h6 id=${e}>${r}</h6>`;default:throw new Error(`Invalid header level: ${t}`)}}async onSubmit(t){const e=t.target;if("export"===t.submitter?.value){t.preventDefault();const r=new FormData(e),i=await new AjaxRequest(this.dumpUrl).post(r),o=await i.resolve();"string"==typeof o.yaml?copyToClipboard(o.yaml):(console.warn("Value can not be copied to clipboard.",typeof o.yaml),Notification.error(lll("copyToClipboard.error")))}}render(){return html`
+      <form class="settings-container"
+            id="sitesettings_form"
+            name="sitesettings_form"
+            action=${this.actionUrl}
+            method="post"
+            @submit=${t=>this.onSubmit(t)}
+      >
+        ${this.returnUrl?html`<input type="hidden" name="returnUrl" value=${this.returnUrl} />`:nothing}
+        <div class="settings">
+          <div class="settings-navigation">
+            ${this.renderCategoryTree(this.categories??[],1)}
+          </div>
+          <div class="settings-body">
+            ${this.renderSettings(this.categories??[],1)}
+          </div>
+        </div>
+      </form>
+    `}};__decorate([property({type:Array})],SettingsEditorElement.prototype,"categories",void 0),__decorate([property({type:String,attribute:"action-url"})],SettingsEditorElement.prototype,"actionUrl",void 0),__decorate([property({type:String,attribute:"dump-url"})],SettingsEditorElement.prototype,"dumpUrl",void 0),__decorate([property({type:String,attribute:"return-url"})],SettingsEditorElement.prototype,"returnUrl",void 0),__decorate([state()],SettingsEditorElement.prototype,"activeCategory",void 0),SettingsEditorElement=__decorate([customElement("typo3-backend-settings-editor")],SettingsEditorElement);export{SettingsEditorElement};
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/settings/editor/editable-setting.js b/typo3/sysext/backend/Resources/Public/JavaScript/settings/editor/editable-setting.js
new file mode 100644
index 0000000000000000000000000000000000000000..08813c02d256b442daf41394d85e57653544ed98
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/settings/editor/editable-setting.js
@@ -0,0 +1,69 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+var __decorate=function(t,e,i,n){var o,s=arguments.length,a=s<3?e:null===n?n=Object.getOwnPropertyDescriptor(e,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(t,e,i,n);else for(var l=t.length-1;l>=0;l--)(o=t[l])&&(a=(s<3?o(a):s>3?o(e,i,a):o(e,i))||a);return s>3&&a&&Object.defineProperty(e,i,a),a};import{html,LitElement,nothing}from"lit";import{customElement,property,state}from"lit/decorators.js";import{until}from"lit/directives/until.js";import"@typo3/backend/element/spinner-element.js";import"@typo3/backend/element/icon-element.js";import{copyToClipboard}from"@typo3/backend/copy-to-clipboard.js";import Notification from"@typo3/backend/notification.js";import{lll}from"@typo3/core/lit-helper.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";let EditableSettingElement=class extends LitElement{constructor(){super(...arguments),this.hasChange=!1,this.typeElement=null}createRenderRoot(){return this}render(){const{value:t,systemDefault:e,definition:i}=this.setting;return html`
+      <div
+        class=${`settings-item settings-item-${i.type} ${this.hasChange?"has-change":""}`}
+        tabindex="0"
+        data-status=${JSON.stringify(t)===JSON.stringify(e)?"none":"modified"}
+      >
+        <!-- data-status=modified|error|none-->
+        <div class="settings-item-indicator"></div>
+        <div class="settings-item-title">
+          <label for=${`setting-${i.key}`} class="settings-item-label">${i.label}</label>
+          <div class="settings-item-description">${i.description}</div>
+          <div class="settings-item-key">${i.key}</div>
+        </div>
+        <div class="settings-item-control">
+          ${until(this.renderField(),html`<typo3-backend-spinner></typo3-backend-spinner>`)}
+        </div>
+        <div class="settings-item-message"></div>
+        <div class="settings-item-actions">
+          ${this.renderActions()}
+        </div>
+      </div>
+    `}async renderField(){const{definition:t,value:e,typeImplementation:i}=this.setting;let n=this.typeElement;if(!n){const t=await import(i);if(!("componentName"in t))throw new Error(`module ${i} is missing the "componentName" export`);n=document.createElement(t.componentName),this.typeElement=n,n.addEventListener("typo3:setting:changed",(t=>{this.hasChange=JSON.stringify(this.setting.value)!==JSON.stringify(t.detail.value)}))}const o={key:t.key,formid:`setting-${t.key}`,name:`settings[${t.key}]`,value:Array.isArray(e)?JSON.stringify(e):String(e),default:Array.isArray(t.default)?JSON.stringify(t.default):String(t.default)};for(const[t,e]of Object.entries(o))n.getAttribute(t)!==e&&n.setAttribute(t,e);return n}renderActions(){const{definition:t}=this.setting;return html`
+      <div class="dropdown">
+        <button class="dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+          <typo3-backend-icon identifier="actions-cog" size="small"></typo3-backend-icon>
+          <span class="visually-hidden">More actions</span>
+        </button>
+        <ul class="dropdown-menu">
+          <li>
+            <button class="dropdown-item dropdown-item-spaced"
+              type="button"
+              @click="${()=>this.setToDefaultValue()}">
+              <typo3-backend-icon identifier="actions-undo" size="small"></typo3-backend-icon> ${lll("edit.resetSetting")}
+            </button>
+          </li>
+          <li><hr class="dropdown-divider"></li>
+          <li>
+            <typo3-copy-to-clipboard
+              text=${t.key}
+              class="dropdown-item dropdown-item-spaced"
+            >
+              <typo3-backend-icon identifier="actions-clipboard" size="small"></typo3-backend-icon> ${lll("edit.copySettingsIdentifier")}
+            </typo3-copy-to-clipboard>
+          </li>
+          ${this.dumpuri?html`
+            <li>
+              <button class="dropdown-item dropdown-item-spaced"
+                type="button"
+                @click="${()=>this.copyAsYaml()}">
+                <typo3-backend-icon identifier="actions-clipboard-paste" size="small"></typo3-backend-icon> ${lll("edit.copyAsYaml")}
+
+              </a>
+            </li>
+          `:nothing}
+        </ul>
+      </div>
+    `}setToDefaultValue(){this.typeElement&&(this.typeElement.value=this.setting.systemDefault)}async copyAsYaml(){const t=new FormData(this.typeElement.form),e=`settings[${this.setting.definition.key}]`,i=t.get(e),n=new FormData;n.append("specificSetting",this.setting.definition.key),n.append(e,i);const o=await new AjaxRequest(this.dumpuri).post(n),s=await o.resolve();"string"==typeof s.yaml?copyToClipboard(s.yaml):(console.warn("Value can not be copied to clipboard.",typeof s.yaml),Notification.error(lll("copyToClipboard.error")))}};__decorate([property({type:Object})],EditableSettingElement.prototype,"setting",void 0),__decorate([property({type:String})],EditableSettingElement.prototype,"dumpuri",void 0),__decorate([state()],EditableSettingElement.prototype,"hasChange",void 0),EditableSettingElement=__decorate([customElement("typo3-backend-editable-setting")],EditableSettingElement);export{EditableSettingElement};
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/base.js b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/base.js
new file mode 100644
index 0000000000000000000000000000000000000000..b0594e2ad7ce34360def341daf55866c391416b7
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/base.js
@@ -0,0 +1,13 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+var __decorate=function(t,e,r,a){var l,o=arguments.length,n=o<3?e:null===a?a=Object.getOwnPropertyDescriptor(e,r):a;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,r,a);else for(var s=t.length-1;s>=0;s--)(l=t[s])&&(n=(o<3?l(n):o>3?l(e,r,n):l(e,r))||n);return o>3&&n&&Object.defineProperty(e,r,n),n};import{LitElement}from"lit";import{defaultConverter}from"@lit/reactive-element";import{property}from"lit/decorators.js";export const internals=Symbol("internals");const privateInternals=Symbol("privateInternals");export const getFormValue=Symbol("getFormValue");export const getFormState=Symbol("getFormState");export class BaseElement extends LitElement{createRenderRoot(){return this}get[internals](){return this[privateInternals]||(this[privateInternals]=this.attachInternals()),this[privateInternals]}get form(){return this[internals].form}get labels(){return this[internals].labels}get name(){return this.getAttribute("name")??""}set name(t){this.setAttribute("name",t)}get disabled(){return this.hasAttribute("disabled")}set disabled(t){this.toggleAttribute("disabled",t)}attributeChangedCallback(t,e,r){if("name"!==t&&"disabled"!==t)super.attributeChangedCallback(t,e,r);else{const r="disabled"===t?null!==e:e;this.requestUpdate(t,r)}}requestUpdate(t,e,r){super.requestUpdate(t,e,r),"value"===t&&(this.dispatchEvent(new CustomEvent("typo3:setting:changed",{detail:{value:this.value}})),this[internals].setFormValue(this[getFormValue](),this[getFormState]()))}formDisabledCallback(t){this.disabled=t}formResetCallback(){const t=this.value,e=this.getAttribute("value");this.attributeChangedCallback("value",this.valueToString(t),null),this.attributeChangedCallback("value",null,e)}formStateRestoreCallback(t){if("string"!=typeof t)throw new Error(`formStateRestoreCallback() needs to be implemented for <${this.localName}> for state type "${typeof t}"`);this.attributeChangedCallback("value",this.valueToString(this.value),null),this.attributeChangedCallback("value",null,t)}[getFormState](){return this[getFormValue]()}[getFormValue](){return this.valueToString(this.value)}valueToString(t){const e=this.constructor.getPropertyOptions("value");return("object"==typeof e.converter&&"function"==typeof e.converter?.toAttribute?e.converter.toAttribute:defaultConverter.toAttribute)(t,e.type)}}BaseElement.formAssociated=!0,__decorate([property({type:String})],BaseElement.prototype,"key",void 0),__decorate([property({type:String})],BaseElement.prototype,"formid",void 0),__decorate([property({noAccessor:!0})],BaseElement.prototype,"name",null),__decorate([property({type:Boolean,noAccessor:!0})],BaseElement.prototype,"disabled",null);
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/bool.js b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/bool.js
new file mode 100644
index 0000000000000000000000000000000000000000..847539a455f74ae4a31d596a1e95ff5b09113fb8
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/bool.js
@@ -0,0 +1,24 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+var __decorate=function(e,t,o,r){var c,l=arguments.length,n=l<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(e,t,o,r);else for(var p=e.length-1;p>=0;p--)(c=e[p])&&(n=(l<3?c(n):l>3?c(t,o,n):c(t,o))||n);return l>3&&n&&Object.defineProperty(t,o,n),n};import{html}from"lit";import{customElement,property}from"lit/decorators.js";import{BaseElement}from"@typo3/backend/settings/type/base.js";export const componentName="typo3-backend-settings-type-bool";let BoolTypeElement=class extends BaseElement{render(){return html`
+      <div class="form-check form-check-type-toggle">
+        <input
+          type="checkbox"
+          id=${this.formid}
+          class="form-check-input"
+          value="1"
+          .checked=${this.value}
+          @change=${e=>this.value=!!e.target.checked}
+        />
+      </div>
+    `}};__decorate([property({type:Boolean,converter:{toAttribute:e=>e?"1":"0",fromAttribute:e=>"1"===e||"true"===e}})],BoolTypeElement.prototype,"value",void 0),BoolTypeElement=__decorate([customElement(componentName)],BoolTypeElement);export{BoolTypeElement};
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/color.js b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/color.js
new file mode 100644
index 0000000000000000000000000000000000000000..4bff7e97bc021ca48237c580a020fad19494b306
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/color.js
@@ -0,0 +1,21 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+var __decorate=function(e,t,o,r){var l,a=arguments.length,n=a<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(e,t,o,r);else for(var p=e.length-1;p>=0;p--)(l=e[p])&&(n=(a<3?l(n):a>3?l(t,o,n):l(t,o))||n);return a>3&&n&&Object.defineProperty(t,o,n),n};import{html}from"lit";import{customElement,property}from"lit/decorators.js";import{BaseElement}from"@typo3/backend/settings/type/base.js";import Alwan from"alwan";export const componentName="typo3-backend-settings-type-color";let ColorTypeElement=class extends BaseElement{constructor(){super(...arguments),this.alwan=null}firstUpdated(){this.alwan=new Alwan(this.querySelector("input"),{position:"bottom-start",format:"hex",opacity:!1,preset:!1,color:this.value}),this.alwan.on("color",(e=>{this.value=e.hex}))}updateValue(e){this.value=e,this.alwan?.setColor(e)}render(){return html`
+      <input
+        type="text"
+        id=${this.formid}
+        class="form-control"
+        .value=${this.value}
+        @change=${e=>this.updateValue(e.target.value)}
+      />
+    `}};__decorate([property({type:String})],ColorTypeElement.prototype,"value",void 0),ColorTypeElement=__decorate([customElement(componentName)],ColorTypeElement);export{ColorTypeElement};
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/int.js b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/int.js
new file mode 100644
index 0000000000000000000000000000000000000000..c0096f2e119087b132a022a78e3102954a304327
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/int.js
@@ -0,0 +1,21 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+var __decorate=function(e,t,r,o){var n,l=arguments.length,p=l<3?t:null===o?o=Object.getOwnPropertyDescriptor(t,r):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)p=Reflect.decorate(e,t,r,o);else for(var m=e.length-1;m>=0;m--)(n=e[m])&&(p=(l<3?n(p):l>3?n(t,r,p):n(t,r))||p);return l>3&&p&&Object.defineProperty(t,r,p),p};import{html}from"lit";import{customElement,property}from"lit/decorators.js";import{BaseElement}from"@typo3/backend/settings/type/base.js";export const componentName="typo3-backend-settings-type-int";let IntTypeElement=class extends BaseElement{render(){return html`
+      <input
+        type="number"
+        id=${this.formid}
+        class="form-control"
+        .value=${this.value}
+        @change=${e=>this.value=parseInt(e.target.value,10)}
+      />
+    `}};__decorate([property({type:Number})],IntTypeElement.prototype,"value",void 0),IntTypeElement=__decorate([customElement(componentName)],IntTypeElement);export{IntTypeElement};
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/number.js b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/number.js
new file mode 100644
index 0000000000000000000000000000000000000000..4631762a071dc3e20841279a3ebbd8b4272eb629
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/number.js
@@ -0,0 +1,22 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+var __decorate=function(e,t,r,o){var n,m=arguments.length,l=m<3?t:null===o?o=Object.getOwnPropertyDescriptor(t,r):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)l=Reflect.decorate(e,t,r,o);else for(var p=e.length-1;p>=0;p--)(n=e[p])&&(l=(m<3?n(l):m>3?n(t,r,l):n(t,r))||l);return m>3&&l&&Object.defineProperty(t,r,l),l};import{html}from"lit";import{customElement,property}from"lit/decorators.js";import{BaseElement}from"@typo3/backend/settings/type/base.js";export const componentName="typo3-backend-settings-type-number";let NumberTypeElement=class extends BaseElement{render(){return html`
+      <input
+        type="number"
+        id=${this.formid}
+        class="form-control"
+        step="0.01"
+        .value=${this.value}
+        @change=${e=>this.value=parseFloat(e.target.value)}
+      />
+    `}};__decorate([property({type:Number})],NumberTypeElement.prototype,"value",void 0),NumberTypeElement=__decorate([customElement(componentName)],NumberTypeElement);export{NumberTypeElement};
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/string.js b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/string.js
new file mode 100644
index 0000000000000000000000000000000000000000..22196ed140c1bac1c756dd806ae2fbaf12f03229
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/string.js
@@ -0,0 +1,21 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+var __decorate=function(e,t,r,o){var n,l=arguments.length,p=l<3?t:null===o?o=Object.getOwnPropertyDescriptor(t,r):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)p=Reflect.decorate(e,t,r,o);else for(var i=e.length-1;i>=0;i--)(n=e[i])&&(p=(l<3?n(p):l>3?n(t,r,p):n(t,r))||p);return l>3&&p&&Object.defineProperty(t,r,p),p};import{html}from"lit";import{customElement,property}from"lit/decorators.js";import{BaseElement}from"@typo3/backend/settings/type/base.js";export const componentName="typo3-backend-settings-type-string";let StringTypeElement=class extends BaseElement{render(){return html`
+      <input
+        type="text"
+        id=${this.formid}
+        class="form-control"
+        .value=${this.value}
+        @change=${e=>this.value=e.target.value}
+      />
+    `}};__decorate([property({type:String})],StringTypeElement.prototype,"value",void 0),StringTypeElement=__decorate([customElement(componentName)],StringTypeElement);export{StringTypeElement};
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/stringlist.js b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/stringlist.js
new file mode 100644
index 0000000000000000000000000000000000000000..2cb792e936b3808346715a7f288c5fa437530170
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/settings/type/stringlist.js
@@ -0,0 +1,45 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+var __decorate=function(t,e,l,o){var i,r=arguments.length,n=r<3?e:null===o?o=Object.getOwnPropertyDescriptor(e,l):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,l,o);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(r<3?i(n):r>3?i(e,l,n):i(e,l))||n);return r>3&&n&&Object.defineProperty(e,l,n),n};import{html}from"lit";import{customElement,property}from"lit/decorators.js";import{BaseElement}from"@typo3/backend/settings/type/base.js";import{live}from"lit/directives/live.js";export const componentName="typo3-backend-settings-type-stringlist";let StringlistTypeElement=class extends BaseElement{updateValue(t,e){const l=[...this.value];l[e]=t,this.value=l}addValue(t,e=""){this.value=this.value.toSpliced(t+1,0,e)}removeValue(t){this.value=this.value.toSpliced(t,1)}renderItem(t,e){return html`
+      <tr>
+        <td width="99%">
+          <input
+            id=${`${this.formid}${e>0?"-"+e:""}`}
+            type="text"
+            class="form-control"
+            .value=${live(t)}
+            @change=${t=>this.updateValue(t.target.value,e)}
+          />
+        </td>
+        <td>
+          <div class="btn-group" role="group">
+            <button class="btn btn-default" type="button" @click=${()=>this.addValue(e)}>
+              <typo3-backend-icon identifier="actions-plus" size="small"></typo3-backend-icon>
+            </button>
+            <button class="btn btn-default" type="button" @click=${()=>this.removeValue(e)}>
+              <typo3-backend-icon identifier="actions-delete" size="small"></typo3-backend-icon>
+            </button>
+          </div>
+        </td>
+      </tr>
+    `}render(){const t=this.value||[];return html`
+      <div class="form-control-wrap">
+        <div class="table-fit">
+          <table class="table table-hover">
+            <tbody>
+              ${t.map(((t,e)=>this.renderItem(t,e)))}
+            </tbody>
+          </table>
+        </div>
+      </div>
+    `}};__decorate([property({type:Array})],StringlistTypeElement.prototype,"value",void 0),StringlistTypeElement=__decorate([customElement(componentName)],StringlistTypeElement);export{StringlistTypeElement};
\ No newline at end of file
diff --git a/typo3/sysext/core/Classes/Configuration/SiteWriter.php b/typo3/sysext/core/Classes/Configuration/SiteWriter.php
index a25919f451a034d437d9c80b9c2482a001aef8ef..c1ffb446711af8f4d83c7bfe68a9e71cc11ae31b 100644
--- a/typo3/sysext/core/Classes/Configuration/SiteWriter.php
+++ b/typo3/sysext/core/Classes/Configuration/SiteWriter.php
@@ -96,7 +96,15 @@ class SiteWriter
     public function writeSettings(string $siteIdentifier, array $settings): void
     {
         $fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->settingsFileName;
-        $yamlFileContents = Yaml::dump($settings, 99, 2);
+        if ($settings === []) {
+            if (!is_file($fileName)) {
+                return;
+            }
+            if (is_writable($fileName) && @unlink($fileName)) {
+                return;
+            }
+        }
+        $yamlFileContents = Yaml::dump($settings, 99, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP);
         if (!GeneralUtility::writeFile($fileName, $yamlFileContents)) {
             throw new SiteConfigurationWriteException('Unable to write site settings in sites/' . $siteIdentifier . '/' . $this->configFileName, 1590487411);
         }
diff --git a/typo3/sysext/core/Classes/Settings/Category.php b/typo3/sysext/core/Classes/Settings/Category.php
new file mode 100644
index 0000000000000000000000000000000000000000..df74b7a3b2eff6956e00e28e72c1be4a3723929f
--- /dev/null
+++ b/typo3/sysext/core/Classes/Settings/Category.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Settings;
+
+/**
+ * @internal
+ */
+class Category
+{
+    public function __construct(
+        public string $key,
+        public string $label,
+        public ?string $description = null,
+        public ?string $icon = null,
+        public array $settings = [],
+        public array $categories = [],
+    ) {}
+
+    public function toArray(): array
+    {
+        return array_filter(get_object_vars($this), fn(mixed $value) => $value !== null && $value !== []);
+    }
+
+    public static function __set_state(array $state): self
+    {
+        return new self(...$state);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Settings/CategoryAccumulator.php b/typo3/sysext/core/Classes/Settings/CategoryAccumulator.php
new file mode 100644
index 0000000000000000000000000000000000000000..72d31756d2c1792aeefee908eeb3b63aceb17da1
--- /dev/null
+++ b/typo3/sysext/core/Classes/Settings/CategoryAccumulator.php
@@ -0,0 +1,93 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Settings;
+
+class CategoryAccumulator
+{
+    /**
+     * Retrieve list of ordered sets, matched by
+     * $setNames, including their dependencies (recursive)
+     *
+     * @param CategoryDefinition[] $categoryDefinitions
+     * @param SettingDefinition[] $settingsDefinitions
+     * @return list<Category>
+     */
+    public function getCategories(iterable $categoryDefinitions, iterable $settingsDefinitions): array
+    {
+        $categories = [];
+        foreach ($categoryDefinitions as $category) {
+            $data = $category->toArray();
+            $parent = $data['parent'] ?? null;
+            unset($data['parent']);
+            $categories[$category->key] = [
+                'children' => [],
+                'parent' => $parent,
+                'data' => $data,
+            ];
+        }
+        foreach ($categoryDefinitions as $category) {
+            if ($category->parent === null) {
+                continue;
+            }
+            if (!isset($categories[$category->parent])) {
+                throw new \RuntimeException('Missing parent category: ' . $category->parent, 1716291554);
+            }
+            $categories[$category->parent]['children'][] = $category->key;
+        }
+
+        $categorizedSettings = [];
+        foreach ($settingsDefinitions as $definition) {
+            $category = $definition->category ?? null;
+            $categorizedSettings[isset($categories[$category]) ? $category : 'other'][] = $definition;
+        }
+
+        if (isset($categorizedSettings['other'])) {
+            $categories['other'] = [
+                'children' => [],
+                'parent' => null,
+                'data' => [
+                    'key' => 'other',
+                    'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_sitesettings.xlf:categories.other',
+                    'description' => '',
+                ],
+            ];
+        }
+
+        $instances = [];
+        foreach ($categories as $key => $category) {
+            if ($category['parent'] === null) {
+                $instances[] = $this->createInstance($categories, $key, $categorizedSettings);
+            }
+        }
+
+        return $instances;
+    }
+
+    private function createInstance(array $categories, string $key, array $categorizedSettings): Category
+    {
+        try {
+            return new Category(...[
+                ...$categories[$key]['data'],
+                'settings' => $categorizedSettings[$key] ?? [],
+                'categories' => array_map(fn($key) => $this->createInstance($categories, $key, $categorizedSettings), $categories[$key]['children']),
+            ]);
+        } catch (\Error $e) {
+            throw new \Exception('Invalid category definition: ' . json_encode($categories[$key]['data']), 1720528084, $e);
+        }
+    }
+}
diff --git a/typo3/sysext/core/Classes/Settings/CategoryDefinition.php b/typo3/sysext/core/Classes/Settings/CategoryDefinition.php
new file mode 100644
index 0000000000000000000000000000000000000000..4e176271154732b357acb58554925d1aa2c0a254
--- /dev/null
+++ b/typo3/sysext/core/Classes/Settings/CategoryDefinition.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Settings;
+
+/**
+ * @internal
+ */
+readonly class CategoryDefinition
+{
+    public function __construct(
+        public string $key,
+        public string $label,
+        public ?string $description = null,
+        public ?string $icon = null,
+        public ?string $parent = null,
+    ) {}
+
+    public function toArray(): array
+    {
+        return array_filter(get_object_vars($this), fn(mixed $value) => $value !== null && $value !== []);
+    }
+
+    public static function __set_state(array $state): self
+    {
+        return new self(...$state);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Settings/SettingDefinition.php b/typo3/sysext/core/Classes/Settings/SettingDefinition.php
index 99238f17ebf1736acdd92fc70a873cdf14ebc9f5..f74cb89756852b52ed4772297997645f20b5fb3d 100644
--- a/typo3/sysext/core/Classes/Settings/SettingDefinition.php
+++ b/typo3/sysext/core/Classes/Settings/SettingDefinition.php
@@ -29,7 +29,7 @@ readonly class SettingDefinition
         public string $label,
         public ?string $description = null,
         public array $enum = [],
-        public array $categories = [],
+        public ?string $category = null,
         public array $tags = [],
     ) {}
 
diff --git a/typo3/sysext/core/Classes/Settings/SettingsTypeInterface.php b/typo3/sysext/core/Classes/Settings/SettingsTypeInterface.php
index 43529a174689023ff5e59313ae3ef0d18f6307da..18e8ea11ccbd5dbc7bbac2a284186605e4944b2c 100644
--- a/typo3/sysext/core/Classes/Settings/SettingsTypeInterface.php
+++ b/typo3/sysext/core/Classes/Settings/SettingsTypeInterface.php
@@ -28,4 +28,6 @@ interface SettingsTypeInterface
     public function validate(mixed $value, SettingDefinition $definition): bool;
 
     public function transformValue(mixed $value, SettingDefinition $definition): mixed;
+
+    public function getJavaScriptModule(): string;
 }
diff --git a/typo3/sysext/core/Classes/Settings/Type/BoolType.php b/typo3/sysext/core/Classes/Settings/Type/BoolType.php
index 3306cc3ee992abc2d6a4c746f2c48bb30677be15..64f25acd0e39dde63e86399d3879091a58edd06b 100644
--- a/typo3/sysext/core/Classes/Settings/Type/BoolType.php
+++ b/typo3/sysext/core/Classes/Settings/Type/BoolType.php
@@ -74,4 +74,9 @@ readonly class BoolType implements SettingsTypeInterface
         }
         return false;
     }
+
+    public function getJavaScriptModule(): string
+    {
+        return '@typo3/backend/settings/type/bool.js';
+    }
 }
diff --git a/typo3/sysext/core/Classes/Settings/Type/ColorType.php b/typo3/sysext/core/Classes/Settings/Type/ColorType.php
index 034dc17d57b83adcf5b61e8f8133febd0bdeae38..06d229234da30838d365878311dbf3200c3641bf 100644
--- a/typo3/sysext/core/Classes/Settings/Type/ColorType.php
+++ b/typo3/sysext/core/Classes/Settings/Type/ColorType.php
@@ -141,4 +141,9 @@ readonly class ColorType implements SettingsTypeInterface
 
         return '#' . $values;
     }
+
+    public function getJavaScriptModule(): string
+    {
+        return '@typo3/backend/settings/type/color.js';
+    }
 }
diff --git a/typo3/sysext/core/Classes/Settings/Type/IntType.php b/typo3/sysext/core/Classes/Settings/Type/IntType.php
index ecda4f255d12f02357d9da750b40cd3ec351d1df..8601ff0c6db2cec47d37ab400df72cf2227bbb6c 100644
--- a/typo3/sysext/core/Classes/Settings/Type/IntType.php
+++ b/typo3/sysext/core/Classes/Settings/Type/IntType.php
@@ -52,4 +52,9 @@ readonly class IntType implements SettingsTypeInterface
 
         return (int)$value;
     }
+
+    public function getJavaScriptModule(): string
+    {
+        return '@typo3/backend/settings/type/int.js';
+    }
 }
diff --git a/typo3/sysext/core/Classes/Settings/Type/NumberType.php b/typo3/sysext/core/Classes/Settings/Type/NumberType.php
index 1b76dbc46e45d710af33878c0afc59558f98264e..f3528741cbe07bb9f0498c083f4ac5b60d33a41c 100644
--- a/typo3/sysext/core/Classes/Settings/Type/NumberType.php
+++ b/typo3/sysext/core/Classes/Settings/Type/NumberType.php
@@ -64,4 +64,9 @@ readonly class NumberType implements SettingsTypeInterface
         }
         return $value;
     }
+
+    public function getJavaScriptModule(): string
+    {
+        return '@typo3/backend/settings/type/number.js';
+    }
 }
diff --git a/typo3/sysext/core/Classes/Settings/Type/StringListType.php b/typo3/sysext/core/Classes/Settings/Type/StringListType.php
index fb96faa66ca220ff749aef67009129741faf9da4..511818c9aee15d3aaba633fefebf3b529b7a4806 100644
--- a/typo3/sysext/core/Classes/Settings/Type/StringListType.php
+++ b/typo3/sysext/core/Classes/Settings/Type/StringListType.php
@@ -40,6 +40,15 @@ readonly class StringListType implements SettingsTypeInterface
     public function transformValue(mixed $value, SettingDefinition $definition): array
     {
         $stringType = new StringType($this->logger);
+        if (is_string($value)) {
+            // A json-encoded stringlist only needs 2-levels
+            $depth = 2;
+            try {
+                $value = json_decode($value, false, 2, JSON_THROW_ON_ERROR);
+            } catch (\JsonException) {
+                // invalid json, ignore and handle below
+            }
+        }
         if (!is_array($value) || !$this->doValidate($stringType, $value, $definition)) {
             $this->logger->warning('Setting validation field, reverting to default: {key}', ['key' => $definition->key]);
             return $definition->default;
@@ -60,4 +69,9 @@ readonly class StringListType implements SettingsTypeInterface
         }
         return true;
     }
+
+    public function getJavaScriptModule(): string
+    {
+        return '@typo3/backend/settings/type/stringlist.js';
+    }
 }
diff --git a/typo3/sysext/core/Classes/Settings/Type/StringType.php b/typo3/sysext/core/Classes/Settings/Type/StringType.php
index 4ee3f7bd5207c8fe9ce53e0232168bd0f1e17fb0..dca3c1c6c996d8a231363947e35fb796f342fadd 100644
--- a/typo3/sysext/core/Classes/Settings/Type/StringType.php
+++ b/typo3/sysext/core/Classes/Settings/Type/StringType.php
@@ -51,4 +51,9 @@ readonly class StringType implements SettingsTypeInterface
         }
         return (string)$value;
     }
+
+    public function getJavaScriptModule(): string
+    {
+        return '@typo3/backend/settings/type/string.js';
+    }
 }
diff --git a/typo3/sysext/core/Classes/Site/Entity/Site.php b/typo3/sysext/core/Classes/Site/Entity/Site.php
index e237fbaeb270c137d18ec82590f6f980876f1cd7..8f41763774da31586aaf877bba3bf284cc2738a9 100644
--- a/typo3/sysext/core/Classes/Site/Entity/Site.php
+++ b/typo3/sysext/core/Classes/Site/Entity/Site.php
@@ -67,6 +67,12 @@ class Site implements SiteInterface
      */
     protected $configuration;
 
+    /**
+     * Raw attributes for this site
+     * @var array
+     */
+    protected $rawConfiguration;
+
     /**
      * @var array<LanguageRef, SiteLanguage>
      */
@@ -102,6 +108,7 @@ class Site implements SiteInterface
         $this->settings = $settings;
         $this->typoscript = $typoscript;
         $this->tsConfig = $tsConfig;
+        $this->rawConfiguration = $configuration;
         // Merge settings back in configuration for backwards-compatibility
         $configuration['settings'] = $this->settings->getAll();
         $this->configuration = $configuration;
@@ -329,6 +336,11 @@ class Site implements SiteInterface
         return $this->configuration;
     }
 
+    public function getRawConfiguration(): array
+    {
+        return $this->rawConfiguration;
+    }
+
     public function getSettings(): SiteSettings
     {
         return $this->settings;
diff --git a/typo3/sysext/core/Classes/Site/Set/CategoryRegistry.php b/typo3/sysext/core/Classes/Site/Set/CategoryRegistry.php
new file mode 100644
index 0000000000000000000000000000000000000000..5f8e373cecf39ac4d9e6a3abc5676f851d7ff06e
--- /dev/null
+++ b/typo3/sysext/core/Classes/Site/Set/CategoryRegistry.php
@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Site\Set;
+
+use TYPO3\CMS\Core\Settings\Category;
+use TYPO3\CMS\Core\Settings\CategoryAccumulator;
+
+class CategoryRegistry
+{
+    public function __construct(
+        protected SetRegistry $setRegistry,
+    ) {}
+
+    /**
+     * Retrieve list of instantiated categories for the list of
+     * provided $setNames, including their dependencies (recursive)
+     *
+     * @return list<Category>
+     */
+    public function getCategories(string ...$setNames): array
+    {
+        $sets = $this->setRegistry->getSets(...$setNames);
+        $categories = [];
+
+        $categoryDefinitions = [];
+        foreach ($sets as $set) {
+            foreach ($set->categoryDefinitions as $definition) {
+                $categoryDefinitions[] = $definition;
+            }
+        }
+        $settingsDefinitions = [];
+        foreach ($sets as $set) {
+            foreach ($set->settingsDefinitions as $definition) {
+                $settingsDefinitions[] = $definition;
+            }
+        }
+
+        $cateryAccumulator = new CategoryAccumulator();
+        return $cateryAccumulator->getCategories(
+            $categoryDefinitions,
+            $settingsDefinitions,
+        );
+    }
+}
diff --git a/typo3/sysext/core/Classes/Site/Set/SetDefinition.php b/typo3/sysext/core/Classes/Site/Set/SetDefinition.php
index f62864e557a1410cce847d279ea661d70bda3c4a..7e80e03993a43198c562dd020cb6bbc28bfc6048 100644
--- a/typo3/sysext/core/Classes/Site/Set/SetDefinition.php
+++ b/typo3/sysext/core/Classes/Site/Set/SetDefinition.php
@@ -17,6 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Site\Set;
 
+use TYPO3\CMS\Core\Settings\CategoryDefinition;
 use TYPO3\CMS\Core\Settings\SettingDefinition;
 
 readonly class SetDefinition
@@ -24,6 +25,7 @@ readonly class SetDefinition
     /**
      * @param list<string> $dependencies
      * @param SettingDefinition[] $settingsDefinitions
+     * @param CategoryDefinition[] $categoryDefinitions
      */
     public function __construct(
         public string $name,
@@ -31,6 +33,7 @@ readonly class SetDefinition
         public array $dependencies = [],
         public array $optionalDependencies = [],
         public array $settingsDefinitions = [],
+        public array $categoryDefinitions = [],
         public ?string $typoscript = null,
         public ?string $pagets = null,
         public array $settings = [],
diff --git a/typo3/sysext/core/Classes/Site/Set/YamlSetDefinitionProvider.php b/typo3/sysext/core/Classes/Site/Set/YamlSetDefinitionProvider.php
index 1d7e48b416f36c28e66c74a94ec4db27f505e7ab..0a20d7aca78a2f621ad6a30d227e895f40e3b87c 100644
--- a/typo3/sysext/core/Classes/Site/Set/YamlSetDefinitionProvider.php
+++ b/typo3/sysext/core/Classes/Site/Set/YamlSetDefinitionProvider.php
@@ -20,6 +20,7 @@ namespace TYPO3\CMS\Core\Site\Set;
 use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
 use Symfony\Component\Yaml\Exception\ParseException;
 use Symfony\Component\Yaml\Yaml;
+use TYPO3\CMS\Core\Settings\CategoryDefinition;
 use TYPO3\CMS\Core\Settings\SettingDefinition;
 
 /**
@@ -66,7 +67,8 @@ class YamlSetDefinitionProvider
             if (!is_array($settingsDefinitions['settings'] ?? null)) {
                 throw new \RuntimeException('Missing "settings" key in settings definitions. Filename: ' . $settingsDefinitionsFile, 1711024378);
             }
-            $set['settingsDefinitions'] = $settingsDefinitions['settings'];
+            $set['settingsDefinitions'] = $settingsDefinitions['settings'] ?? [];
+            $set['categoryDefinitions'] = $settingsDefinitions['categories'] ?? [];
         }
 
         $settingsFile = $path . '/settings.yaml';
@@ -115,9 +117,25 @@ class YamlSetDefinitionProvider
                 }
                 $settingsDefinitions[] = $definition;
             }
+
+            $categoryDefinitions = [];
+            foreach (($set['categoryDefinitions'] ?? []) as $category => $options) {
+                if ($labels) {
+                    $options['label'] ??= 'LLL:' . $labels . ':categories.' . $category;
+                    $options['description'] ??= 'LLL:' . $labels . ':categories.description.' . $category;
+                }
+                try {
+                    $definition = new CategoryDefinition(...[...['key' => $category], ...$options]);
+                } catch (\Error $e) {
+                    throw new \Exception('Invalid category-category definition: ' . json_encode($options), 1702623313, $e);
+                }
+                $categoryDefinitions[] = $definition;
+            }
+
             $setData = [
                 ...$set,
                 'settingsDefinitions' => $settingsDefinitions,
+                'categoryDefinitions' => $categoryDefinitions,
             ];
             $setData['typoscript'] ??= $basePath;
             $setData['pagets'] ??= $basePath . '/page.tsconfig';
diff --git a/typo3/sysext/core/Classes/Site/SiteSettingsFactory.php b/typo3/sysext/core/Classes/Site/SiteSettingsFactory.php
index 5413f36bbabff88205450a9966fdab047c2bc3a2..2507ffab844c5d3813dc2ae45a119ed9c32c6b47 100644
--- a/typo3/sysext/core/Classes/Site/SiteSettingsFactory.php
+++ b/typo3/sysext/core/Classes/Site/SiteSettingsFactory.php
@@ -135,6 +135,31 @@ readonly class SiteSettingsFactory
         );
     }
 
+    public function createSettingsForKeys(array $settingKeys, string $siteIdentifier, array $inlineSettings = []): SiteSettings
+    {
+        $fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->settingsFileName;
+        if (file_exists($fileName)) {
+            $settingsTree = $this->yamlFileLoader->load(GeneralUtility::fixWindowsFilePath($fileName));
+        } else {
+            $settingsTree = $inlineSettings;
+        }
+
+        /** @var array<string, string|int|float|bool|array|null> $settingsMap */
+        $settingsMap = [];
+        foreach ($settingKeys as $key) {
+            if (!ArrayUtility::isValidPath($settingsTree, $key, '.')) {
+                continue;
+            }
+            $settingsMap[$key] = ArrayUtility::getValueByPath($settingsTree, $key, '.');
+        }
+        $flatSettings = $settingsTree === [] ? [] : ArrayUtility::flattenPlain($settingsTree);
+        return new SiteSettings(
+            settings: $settingsMap,
+            settingsTree: $settingsTree,
+            flatSettings: $flatSettings,
+        );
+    }
+
     protected function validateSettings(array $settings, array $definitions): array
     {
         foreach ($definitions as $definition) {
diff --git a/typo3/sysext/core/Classes/Site/SiteSettingsService.php b/typo3/sysext/core/Classes/Site/SiteSettingsService.php
new file mode 100644
index 0000000000000000000000000000000000000000..8117e463cdddaebe95a0ce3d40e2f89a76975585
--- /dev/null
+++ b/typo3/sysext/core/Classes/Site/SiteSettingsService.php
@@ -0,0 +1,180 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Site;
+
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Component\Yaml\Yaml;
+use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
+use TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException;
+use TYPO3\CMS\Core\Configuration\SiteWriter;
+use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Core\Messaging\FlashMessageService;
+use TYPO3\CMS\Core\Settings\SettingDefinition;
+use TYPO3\CMS\Core\Settings\SettingsTypeRegistry;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\Entity\SiteSettings;
+use TYPO3\CMS\Core\Site\Set\SetRegistry;
+use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * @internal
+ */
+readonly class SiteSettingsService
+{
+    public function __construct(
+        protected SiteWriter $siteWriter,
+        #[Autowire(service: 'cache.core')]
+        protected PhpFrontend $codeCache,
+        protected SetRegistry $setRegistry,
+        protected SiteSettingsFactory $siteSettingsFactory,
+        protected SettingsTypeRegistry $settingsTypeRegistry,
+        protected FlashMessageService $flashMessageService,
+    ) {}
+
+    public function hasSettingsDefinitions(Site $site): bool
+    {
+        return count($this->getDefinitions($site)) > 0;
+    }
+
+    public function getUncachedSettings(Site $site): SiteSettings
+    {
+        // create a fresh Settings instance instead of using
+        // $site->getSettings() which may have been loaded from cache
+        return $settings = $this->siteSettingsFactory->createSettings(
+            $site->getSets(),
+            $site->getIdentifier(),
+            $site->getRawConfiguration()['settings'] ?? [],
+        );
+    }
+
+    public function getSetSettings(Site $site): SiteSettings
+    {
+        return $this->siteSettingsFactory->createSettings($site->getSets());
+    }
+
+    public function getLocalSettings(Site $site): SiteSettings
+    {
+        $definitions = $this->getDefinitions($site);
+        return $this->siteSettingsFactory->createSettingsForKeys(
+            array_map(static fn(SettingDefinition $d) => $d->key, $definitions),
+            $site->getIdentifier(),
+            $site->getRawConfiguration()['settings'] ?? []
+        );
+    }
+
+    public function computeSettingsDiff(Site $site, array $rawSettings, bool $minify = true): array
+    {
+        $settings = [];
+        $localSettings = [];
+
+        $definitions = $this->getDefinitions($site);
+        foreach ($rawSettings as $key => $value) {
+            $definition = $definitions[$key] ?? null;
+            if ($definition === null) {
+                throw new \RuntimeException('Unexpected setting ' . $key . ' is not defined', 1724067004);
+            }
+            $type = $this->settingsTypeRegistry->get($definition->type);
+            $settings[$key] = $type->transformValue($value, $definition);
+        }
+
+        // Settings from sets – setting values without config/sites/*/settings.yaml applied
+        $setSettings = $this->siteSettingsFactory->createSettings($site->getSets());
+        // Settings from config/sites/*/settings.yaml only (our persistence target)
+        $localSettings = $this->siteSettingsFactory->createSettingsForKeys(
+            array_map(static fn(SettingDefinition $d) => $d->key, $definitions),
+            $site->getIdentifier(),
+            $site->getRawConfiguration()['settings'] ?? []
+        );
+
+        // Read existing settings, as we *must* not remove any settings that may be present because of
+        //  * "undefined" settings that were supported since TYPO3 v12
+        //  * (temporary) inactive sets
+        $settingsTree = $localSettings->getAll();
+
+        // Merge incoming settings into current settingsTree
+        $changes = [];
+        $deletions = [];
+        foreach ($settings as $key => $value) {
+            if ($minify && $value === $setSettings->get($key)) {
+                if (ArrayUtility::isValidPath($settingsTree, $key, '.')) {
+                    $settingsTree = $this->removeByPathWithAncestors($settingsTree, $key, '.');
+                    $deletions[] = $key;
+                }
+                continue;
+            }
+            $settingsTree = ArrayUtility::setValueByPath($settingsTree, $key, $value, '.');
+            $changes[] = $key;
+        }
+        return [
+            'settings' => $settingsTree,
+            'changes' => $changes,
+            'deletions' => $deletions,
+        ];
+    }
+
+    public function writeSettings(Site $site, array $settings): void
+    {
+        try {
+            $this->siteWriter->writeSettings($site->getIdentifier(), $settings);
+        } catch (SiteConfigurationWriteException $e) {
+            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $e->getMessage(), '', ContextualFeedbackSeverity::ERROR, true);
+            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
+            $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
+            $defaultFlashMessageQueue->enqueue($flashMessage);
+        }
+        // SiteWriter currently does not invalidate the code cache, see #103804
+        $this->codeCache->flush();
+    }
+
+    private function getDefinitions(Site $site): array
+    {
+        $sets = $this->setRegistry->getSets(...$site->getSets());
+        $definitions = [];
+        foreach ($sets as $set) {
+            foreach ($set->settingsDefinitions as $settingDefinition) {
+                $definitions[$settingDefinition->key] = $settingDefinition;
+            }
+        }
+        return $definitions;
+    }
+
+    private function removeByPathWithAncestors(array $array, string $path, string $delimiter): array
+    {
+        if ($path === '') {
+            return $array;
+        }
+        if (!ArrayUtility::isValidPath($array, $path, $delimiter)) {
+            return $array;
+        }
+
+        $array = ArrayUtility::removeByPath($array, $path, $delimiter);
+        $parts = explode($delimiter, $path);
+        array_pop($parts);
+        $parentPath = implode($delimiter, $parts);
+
+        if ($parentPath !== '' && ArrayUtility::isValidPath($array, $parentPath, $delimiter)) {
+            $parent = ArrayUtility::getValueByPath($array, $parentPath, $delimiter);
+            if ($parent === []) {
+                return $this->removeByPathWithAncestors($array, $parentPath, $delimiter);
+            }
+        }
+        return $array;
+    }
+}
diff --git a/typo3/sysext/core/Documentation/Changelog/13.3/Feature-104794-IntroduceSiteSettingsEditor.rst b/typo3/sysext/core/Documentation/Changelog/13.3/Feature-104794-IntroduceSiteSettingsEditor.rst
new file mode 100644
index 0000000000000000000000000000000000000000..ff55fa8f52eeeff06a35aa57e939c5a624425014
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/13.3/Feature-104794-IntroduceSiteSettingsEditor.rst
@@ -0,0 +1,81 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-104794-1725980585:
+
+=================================================
+Feature: #104794 - Introduce Site Settings Editor
+=================================================
+
+See :issue:`104794`
+
+Description
+===========
+
+A new Site Settings editor has been introduced that allows to configure per-site
+settings in file:`config/sites/*/settings.yaml`.
+
+The new backend module :guilabel:`Site Management > Settings`
+provides an overview of sites which offer configurable settings and makes
+them editable based on
+:doc:`Site Set provided Settings Definitions <../13.1/Feature-103437-IntroduceSiteSets>`.
+
+The editor shows a list of settings categories and respective settings. It will
+persist all settings into file:`config/sites/*/settings.yaml`. The module will
+only persist settings that deviate from the site-scoped default value. That
+means it will only change the minimal difference to the settings set defined
+by the active sets for the respective site.
+
+The backend module is currently available for administrators only, but will
+likely be extended to be made available for editors in future.
+
+Anonymous (undefined) site settings – as supported since TYPO3 v10 –
+will not be made editable, but will be preserved as-is when persisting changes
+through the settings editor.
+
+
+Categorization
+--------------
+
+Sets can define categories and settings definitions can reference the category
+by ID in order to assign a setting to a specific category.
+These definitions are placed in :file:`settings.definitions.yaml`
+next to the site set file :file:`config.yaml`.
+
+..  code-block:: yaml
+    :caption: EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml
+
+    categories:
+      myCategory:
+        label: 'My Category'
+
+    settings:
+      my.example.setting:
+        label: 'My example setting'
+        category: myCategory
+        type: string
+        default: ''
+
+      my.seoRelevantSetting:
+        label: 'My SEO relevant setting'
+        # show in EXT:seo provided category "seo"
+        category: seo
+        type: int
+        default: 5
+
+The settings ordering is defined through the loading order of extensions and by
+the order of categories. Uncategorized settings will be grouped into a virtual
+"Other" category and shown at the end of the list of available settings.
+
+
+Impact
+======
+
+Site-scoped settings will most likely be the place to configure site-wide
+configuration, which was previously only possible to modify via Constant Editor,
+modifying TypoScript constants.
+
+It is recommended to use site-sets and their UI configuration in favor of
+Typoscript Constants in the future.
+
+
+.. index:: Backend, Frontend, YAML, ext:backend
diff --git a/typo3/sysext/felogin/Configuration/Sets/Felogin/labels.xlf b/typo3/sysext/felogin/Configuration/Sets/Felogin/labels.xlf
index 6209e971b1d39cef34218dac7699106ef1987568..fd462d9ab14a155c560b2b6372cd73e35874bfee 100644
--- a/typo3/sysext/felogin/Configuration/Sets/Felogin/labels.xlf
+++ b/typo3/sysext/felogin/Configuration/Sets/Felogin/labels.xlf
@@ -6,6 +6,11 @@
 			<trans-unit id="label" resname="label">
 				<source>Frontend Login</source>
 			</trans-unit>
+
+			<trans-unit id="categories.felogin" resname="categories.felogin">
+				<source>Frontend Login</source>
+			</trans-unit>
+
 			<trans-unit id="settings.felogin.pid" resname="settings.felogin.pid">
 				<source>User Storage Page</source>
 			</trans-unit>
diff --git a/typo3/sysext/felogin/Configuration/Sets/Felogin/settings.definitions.yaml b/typo3/sysext/felogin/Configuration/Sets/Felogin/settings.definitions.yaml
index 4f0f94cf1254b06b2ec5bbbfe089267f702aeec8..1c5987de4f75fb51e5e6c51cfe5f8c104cf7028d 100644
--- a/typo3/sysext/felogin/Configuration/Sets/Felogin/settings.definitions.yaml
+++ b/typo3/sysext/felogin/Configuration/Sets/Felogin/settings.definitions.yaml
@@ -1,7 +1,11 @@
+categories:
+  felogin: ~
+
 settings:
   felogin.pid:
     default: '0'
     type: string
+    category: felogin
   felogin.recursive:
     default: '0'
     type: string
@@ -12,72 +16,96 @@ settings:
       '3': '3'
       '4': '4'
       '255': '255'
+    category: felogin
   felogin.showForgotPassword:
     default: false
     type: bool
+    category: felogin
   felogin.showPermaLogin:
     default: false
     type: bool
+    category: felogin
   felogin.showLogoutFormAfterLogin:
     default: false
     type: bool
+    category: felogin
   felogin.emailFrom:
     default: ''
     type: string
+    category: felogin
   felogin.emailFromName:
     default: ''
     type: string
+    category: felogin
   felogin.replyToEmail:
     default: ''
     type: string
+    category: felogin
   felogin.dateFormat:
     default: 'Y-m-d H:i'
     type: string
+    category: felogin
   felogin.email.layoutRootPath:
     default: ''
     type: string
+    category: felogin
   felogin.email.templateRootPath:
     default: 'EXT:felogin/Resources/Private/Email/Templates/'
     type: string
+    category: felogin
   felogin.email.partialRootPath:
     default: ''
     type: string
+    category: felogin
   felogin.email.templateName:
     default: PasswordRecovery
     type: string
+    category: felogin
   felogin.redirectMode:
     default: ''
     type: string
+    category: felogin
   felogin.redirectFirstMethod:
     default: false
     type: bool
+    category: felogin
   felogin.redirectPageLogin:
     default: 0
     type: int
+    category: felogin
   felogin.redirectPageLoginError:
     default: 0
     type: int
+    category: felogin
   felogin.redirectPageLogout:
     default: 0
     type: int
+    category: felogin
   felogin.redirectDisable:
     default: false
     type: bool
+    category: felogin
   felogin.forgotLinkHashValidTime:
     default: 12
     type: int
+    category: felogin
   felogin.domains:
     default: ''
     type: string
+    category: felogin
   felogin.exposeNonexistentUserInForgotPasswordDialog:
     default: false
     type: bool
+    category: felogin
   felogin.view.templateRootPath:
     default: ''
     type: string
+    category: felogin
   felogin.view.partialRootPath:
     default: ''
     type: string
+    category: felogin
   felogin.view.layoutRootPath:
     default: ''
     type: string
+    category: felogin
diff --git a/typo3/sysext/fluid_styled_content/Configuration/Sets/FluidStyledContent/labels.xlf b/typo3/sysext/fluid_styled_content/Configuration/Sets/FluidStyledContent/labels.xlf
index e418034c8c34e3853d85ef1c9777cf9e02226583..eec39bda99b5366fbf12c140c1ed1e11b1fca694 100644
--- a/typo3/sysext/fluid_styled_content/Configuration/Sets/FluidStyledContent/labels.xlf
+++ b/typo3/sysext/fluid_styled_content/Configuration/Sets/FluidStyledContent/labels.xlf
@@ -6,6 +6,17 @@
 			<trans-unit id="label" resname="label">
 				<source>Fluid Styled Content</source>
 			</trans-unit>
+
+			<trans-unit id="categories.fsc" resname="categories.fsc">
+				<source>Fluid Styled Content</source>
+			</trans-unit>
+			<trans-unit id="categories.fsc.templates" resname="categories.fsc.templates">
+				<source>Templates</source>
+			</trans-unit>
+			<trans-unit id="categories.fsc.content" resname="categories.fsc.content">
+				<source>Content Elements</source>
+			</trans-unit>
+
 			<trans-unit id="settings.styles.content.defaultHeaderType" resname="settings.styles.content.defaultHeaderType">
 				<source>Default Header type</source>
 			</trans-unit>
diff --git a/typo3/sysext/fluid_styled_content/Configuration/Sets/FluidStyledContent/settings.definitions.yaml b/typo3/sysext/fluid_styled_content/Configuration/Sets/FluidStyledContent/settings.definitions.yaml
index da97c227c184c2689d3d3cb0b49fc9a454c42c47..b9b96085724242f37a1c44621e2c3a1a5c695a57 100644
--- a/typo3/sysext/fluid_styled_content/Configuration/Sets/FluidStyledContent/settings.definitions.yaml
+++ b/typo3/sysext/fluid_styled_content/Configuration/Sets/FluidStyledContent/settings.definitions.yaml
@@ -1,13 +1,23 @@
+categories:
+  fsc: ~
+  fsc.templates:
+    parent: fsc
+  fsc.content:
+    parent: fsc
+
 settings:
   styles.content.defaultHeaderType:
     default: 2
     type: int
+    category: fsc.content
   styles.content.shortcut.tables:
     default: tt_content
     type: string
+    category: fsc.content
   styles.content.allowTags:
     default: 'a, abbr, acronym, address, article, aside, b, bdo, big, blockquote, br, caption, center, cite, code, col, colgroup, dd, del, dfn, dl, div, dt, em, figure, font, footer, header, h1, h2, h3, h4, h5, h6, hr, i, img, ins, kbd, label, li, link, mark, meta, nav, ol, p, pre, q, s, samp, sdfield, section, small, span, strike, strong, style, sub, sup, table, thead, tbody, tfoot, td, th, tr, title, tt, u, ul, var'
     type: string
+    category: fsc.content
   styles.content.image.lazyLoading:
     default: lazy
     type: string
@@ -15,6 +25,7 @@ settings:
       lazy: 'Lazy'
       eager: 'Eager'
       auto: 'Auto'
+    category: fsc.content
   styles.content.image.imageDecoding:
     default: ''
     type: string
@@ -22,60 +33,80 @@ settings:
       sync: 'Sync'
       async: 'Asynchronous'
       auto: 'Auto'
+    category: fsc.content
   styles.content.textmedia.maxW:
     default: 600
     type: int
+    category: fsc.content
   styles.content.textmedia.maxWInText:
     default: 300
     type: int
+    category: fsc.content
   styles.content.textmedia.columnSpacing:
     default: 10
     type: int
+    category: fsc.content
   styles.content.textmedia.rowSpacing:
     default: 10
     type: int
+    category: fsc.content
   styles.content.textmedia.textMargin:
     default: 10
     type: int
+    category: fsc.content
   styles.content.textmedia.borderColor:
     default: '#000000'
     type: color
+    category: fsc.content
   styles.content.textmedia.borderWidth:
     default: 2
     type: int
+    category: fsc.content
   styles.content.textmedia.borderPadding:
     default: 0
     type: int
+    category: fsc.content
   styles.content.textmedia.linkWrap.width:
     default: 800m
     type: string
+    category: fsc.content
   styles.content.textmedia.linkWrap.height:
     default: 600m
     type: string
+    category: fsc.content
   styles.content.textmedia.linkWrap.newWindow:
     default: false
     type: bool
+    category: fsc.content
   styles.content.textmedia.linkWrap.lightboxEnabled:
     default: false
     type: bool
+    category: fsc.content
   styles.content.textmedia.linkWrap.lightboxCssClass:
     default: lightbox
     type: string
+    category: fsc.content
   styles.content.textmedia.linkWrap.lightboxRelAttribute:
     default: 'lightbox[{field:uid}]'
     type: string
+    category: fsc.content
   styles.content.links.extTarget:
     default: _blank
     type: string
+    category: fsc.content
   styles.content.links.keep:
     default: path
     type: string
+    category: fsc.content
   styles.templates.templateRootPath:
     default: ''
     type: string
+    category: fsc.templates
   styles.templates.partialRootPath:
     default: ''
     type: string
+    category: fsc.templates
   styles.templates.layoutRootPath:
     default: ''
     type: string
+    category: fsc.templates
diff --git a/typo3/sysext/indexed_search/Configuration/Sets/IndexedSearch/labels.xlf b/typo3/sysext/indexed_search/Configuration/Sets/IndexedSearch/labels.xlf
index 3531deaf42d91e3ecf61b01ba430cfb947e1c1e6..5690f89050c84308d91eb6600c1fbfc84df44a69 100644
--- a/typo3/sysext/indexed_search/Configuration/Sets/IndexedSearch/labels.xlf
+++ b/typo3/sysext/indexed_search/Configuration/Sets/IndexedSearch/labels.xlf
@@ -7,6 +7,13 @@
 				<source>Indexed Search</source>
 			</trans-unit>
 
+			<trans-unit id="categories.indexedsearch" resname="categories.indexedsearch">
+				<source>Indexed Search</source>
+			</trans-unit>
+			<trans-unit id="categories.indexedsearch.templates" resname="categories.indexedsearch.templates">
+				<source>Templates</source>
+			</trans-unit>
+
 			<trans-unit id="settings.indexedsearch.view.templateRootPath" resname="settings.indexedsearch.view.templateRootPath">
 				<source>Path to template root (FE)</source>
 			</trans-unit>
diff --git a/typo3/sysext/indexed_search/Configuration/Sets/IndexedSearch/settings.definitions.yaml b/typo3/sysext/indexed_search/Configuration/Sets/IndexedSearch/settings.definitions.yaml
index 26e579f799bc506869d5efb3b4056971cd6ce003..621cb185125cad3c760f3b42208fb2693839f652 100644
--- a/typo3/sysext/indexed_search/Configuration/Sets/IndexedSearch/settings.definitions.yaml
+++ b/typo3/sysext/indexed_search/Configuration/Sets/IndexedSearch/settings.definitions.yaml
@@ -1,16 +1,26 @@
+categories:
+  indexedsearch: ~
+  indexedsearch.templates:
+    parent: indexedsearch
+
 settings:
   indexedsearch.view.templateRootPath:
     default: 'EXT:indexed_search/Resources/Private/Templates/'
     type: string
+    category: indexedsearch.templates
   indexedsearch.view.partialRootPath:
     default: 'EXT:indexed_search/Resources/Private/Partials/'
     type: string
+    category: indexedsearch.templates
   indexedsearch.view.layoutRootPath:
     default: 'EXT:indexed_search/Resources/Private/Layouts/'
     type: string
+    category: indexedsearch.templates
   indexedsearch.targetPid:
     default: 0
     type: int
+    category: indexedsearch
   indexedsearch.rootPidList:
     default: ''
     type: string
+    category: indexedsearch
diff --git a/typo3/sysext/redirects/Configuration/Backend/Modules.php b/typo3/sysext/redirects/Configuration/Backend/Modules.php
index a54ebe9cc6969e7ac660101f06b31e19aeebf262..548d1df1e73efd2f7b7a4c0cee234e5eb10cb046 100644
--- a/typo3/sysext/redirects/Configuration/Backend/Modules.php
+++ b/typo3/sysext/redirects/Configuration/Backend/Modules.php
@@ -8,7 +8,7 @@ use TYPO3\CMS\Redirects\Controller\ManagementController;
 return [
     'site_redirects' => [
         'parent' => 'site',
-        'position' => ['after' => 'site_configuration'],
+        'position' => ['after' => 'site_settings'],
         'access' => 'user',
         'path' => '/module/site/redirects',
         'iconIdentifier' => 'module-redirects',
diff --git a/typo3/sysext/redirects/Configuration/Sets/redirects/config.yaml b/typo3/sysext/redirects/Configuration/Sets/redirects/config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0bdb82bff694bc4d844941b2ea9c9bce1e571e06
--- /dev/null
+++ b/typo3/sysext/redirects/Configuration/Sets/redirects/config.yaml
@@ -0,0 +1 @@
+name: typo3/redirects
diff --git a/typo3/sysext/redirects/Configuration/Sets/redirects/labels.xlf b/typo3/sysext/redirects/Configuration/Sets/redirects/labels.xlf
new file mode 100644
index 0000000000000000000000000000000000000000..667f44fb767165e3d2031169aa3f2b62115d1248
--- /dev/null
+++ b/typo3/sysext/redirects/Configuration/Sets/redirects/labels.xlf
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+	<file source-language="en" datatype="plaintext" original="EXT:redirects/Configuration/Sets/redirects/labels.xlf" date="2024-09-10T12:30:00Z"
+		  product-name="redirects">
+		<header/>
+		<body>
+			<trans-unit id="label" resname="label">
+				<source>Redirects</source>
+			</trans-unit>
+
+			<trans-unit id="categories.redirects" resname="categories.redirects">
+				<source>Redirects</source>
+			</trans-unit>
+
+			<trans-unit id="settings.redirects.autoUpdateSlugs" resname="settings.redirects.autoUpdateSlugs">
+				<source>Automatically update slugs of all sub pages</source>
+			</trans-unit>
+
+			<trans-unit id="settings.redirects.autoCreateRedirects" resname="settings.redirects.autoCreateRedirects">
+				<source>Automatically create redirects for pages with a new slug (works only in LIVE workspace)</source>
+			</trans-unit>
+			<trans-unit id="settings.description.redirects.autoCreateRedirects" resname="settings.description.redirects.autoCreateRedirects">
+				<source>This feature works only in LIVE workspace.</source>
+			</trans-unit>
+
+			<trans-unit id="settings.redirects.redirectTTL" resname="settings.redirects.redirectTTL">
+				<source>Time To Live in days for redirect records to be created - `0` disables TTL, no expiration</source>
+			</trans-unit>
+
+			<trans-unit id="settings.redirects.httpStatusCode" resname="settings.redirects.httpStatusCode">
+				<source>HTTP status code for automatically created redirects</source>
+			</trans-unit>
+
+			<trans-unit id="settings.description.redirects.httpStatusCode" resname="settings.description.redirects.httpStatusCode">
+				<source>See https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#Temporary_redirections for more information.</source>
+			</trans-unit>
+		</body>
+	</file>
+</xliff>
diff --git a/typo3/sysext/redirects/Configuration/Sets/redirects/settings.definitions.yaml b/typo3/sysext/redirects/Configuration/Sets/redirects/settings.definitions.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..be227986169b8c6fce97f5980bbb7fae01d4fb3f
--- /dev/null
+++ b/typo3/sysext/redirects/Configuration/Sets/redirects/settings.definitions.yaml
@@ -0,0 +1,20 @@
+categories:
+  redirects: ~
+
+settings:
+  redirects.autoUpdateSlugs:
+    type: bool
+    default: true
+    category: redirects
+  redirects.autoCreateRedirects:
+    type: bool
+    default: true
+    category: redirects
+  redirects.redirectTTL:
+    type: int
+    default: 0
+    category: redirects
+  redirects.httpStatusCode:
+    type: int
+    default: 307
+    category: redirects
diff --git a/typo3/sysext/seo/Configuration/Sets/Sitemap/labels.xlf b/typo3/sysext/seo/Configuration/Sets/Sitemap/labels.xlf
index 9696019fb34b6a14b006fa6195f23678ce5cb2f6..8648dc1af831e9fd3ef3f9552040a9cacc8d5ca3 100644
--- a/typo3/sysext/seo/Configuration/Sets/Sitemap/labels.xlf
+++ b/typo3/sysext/seo/Configuration/Sets/Sitemap/labels.xlf
@@ -8,6 +8,13 @@
 				<source>SEO Sitemap</source>
 			</trans-unit>
 
+			<trans-unit id="categories.seo" resname="categories.seo">
+				<source>SEO Sitemap</source>
+			</trans-unit>
+			<trans-unit id="categories.seo.templates" resname="categories.seo.templates">
+				<source>Template Paths</source>
+			</trans-unit>
+
 			<trans-unit id="settings.seo.sitemap.view.templateRootPath" resname="settings.seo.sitemap.view.templateRootPath">
 				<source>Path to template root (FE)</source>
 			</trans-unit>
diff --git a/typo3/sysext/seo/Configuration/Sets/Sitemap/settings.definitions.yaml b/typo3/sysext/seo/Configuration/Sets/Sitemap/settings.definitions.yaml
index 8d55e56eb8adc5283e23a2803cc8a91f3ac0880d..52e69f005bcfaf9367b8c32db22151796e6ebe70 100644
--- a/typo3/sysext/seo/Configuration/Sets/Sitemap/settings.definitions.yaml
+++ b/typo3/sysext/seo/Configuration/Sets/Sitemap/settings.definitions.yaml
@@ -1,19 +1,30 @@
+categories:
+  seo: ~
+  seo.templates:
+    parent: seo
+
 settings:
   seo.sitemap.view.templateRootPath:
     default: 'EXT:seo/Resources/Private/Templates/'
     type: string
+    category: seo.templates
   seo.sitemap.view.partialRootPath:
     default: 'EXT:seo/Resources/Private/Partials/'
     type: string
+    category: seo.templates
   seo.sitemap.view.layoutRootPath:
     default: 'EXT:seo/Resources/Private/Layouts/'
     type: string
+    category: seo.templates
   seo.sitemap.pages.excludedDoktypes:
     default: '3, 4, 6, 7, 199, 254'
     type: string
+    category: seo
   seo.sitemap.pages.excludePagesRecursive:
     default: ''
     type: string
+    category: seo
   seo.sitemap.pages.additionalWhere:
     default: "{#no_index} = 0 AND {#canonical_link} = ''"
     type: string
+    category: seo