Skip to main content

✨ Bootstrap 5

MODX 3: Bootstrap 5 mit Bootswatch-Themes integrieren

Diese Anleitung nutzt Bootstrap 5.3.8 mit Themes von Bootswatch als Beispiel und zeigt, wie du Bootstrap lokal in einem Ordner bs5 ablegst, die Bootswatch-Themes vorbereitest, überflüssige Dateien entfernst und externe Google-Font-Imports aus den Bootswatch-bootstrap.min.css bereinigst. Seit Bootstrap 5.3 wird außerdem ein integrierter Darkmodus über data-bs-theme unterstützt, daher funktioniert das Vorgehen auch bei anderen Themes, die auf einer ähnlichen Struktur basieren.


Bootstrap 5 lokal in MODX nutzen (ohne externe Quellen)

Bootstrap soll vollständig lokal in MODX eingebunden werden, damit keine externen Ressourcen wie CDNs oder Google-Fonts geladen werden. Dieser Abschnitt beschreibt, wie du Bootstrap 5 herunterlädst und in den Ordner assets/bs5 kopierst.


📥 Bootstrap ZIP herunterladen

  1. Öffne die offizielle Bootstrap Release-Seite:
    👉 https://github.com/twbs/bootstrap/releases/latest
  2. Lade das ZIP herunter (z.B. bootstrap-5.x.x-dist.zip).
  3. Entpacke das ZIP auf deinem Linux Manjaro Desktop.

📁 Benötigte Dateien aus dem Bootstrap-Paket

Im entpackten Verzeichnis findest du:

bootstrap-5.x.x/dist/css/bootstrap.min.css
bootstrap-5.x.x/dist/js/bootstrap.bundle.min.js

Diese beiden Dateien reichen vollständig aus:

  • bootstrap.min.css → alle Bootstrap 5 Styles lokal
  • bootstrap.bundle.min.js → Bootstrap JS + Popper lokal

Popper ist bereits im Bundle enthalten.


📂 Bootstrap lokal in dein MODX-Projekt kopieren

Erstelle in MODX folgenden Ordner (Empfehlung):

assets/bs5/

Kopiere die beiden Dateien hinein:

assets/bs5/bootstrap.min.css
assets/bs5/bootstrap.bundle.min.js

Damit ist Bootstrap vollständig lokal eingebunden.


Vorbereitung in MODX

Führe in MODX folgende Schritte durch:

  1. Lege eine neue Kategorie an, z.B.:

    • BS5
  2. Erstelle ein neues Template, z.B.:

    • tmpBS5
  3. Setze dieses Template in den Systemeinstellungen als
    default_template

  4. Lege folgende Chunks an (für pdoMenu):

    • menuBSOuter
    • menuBSLink
    • menuBSParent
    • tmpBS_meinStyle

    Diese werden später für die Navigation benötigt.

  5. Lege einen eigenen Chunk für den Footer an (das macht die Struktur übersichtlicher):

    • bsFooter
  6. Optional kannst du eine Template Variable (TV) anlegen, um den Seitentitel ein- oder ausblenden zu können:

    • tvPagetitle
      • Optionsschaltflächen (Radio-Buttons)
      • Radio-Button-Optionen: ja||nein
        • Standardoption: ja
        • Leere Eingabe erlauben: Ja
        • Spalten: 1
      • Anschließend die TV dem Template zuweisen (Reiter Template-Zugriff)

TVs lassen sich sehr gut um viele Funktionen erweitern.


MODX Template tmpBS5 mit pdoMenu

Im folgenden Beispiel wird das Template tmpBS5 so aufgebaut, dass:

  • Bootstrap 5 wird vollständig lokal eingebunden
  • Ein Bootswatch-Theme (z.B. minty) wird lokal zugeschaltet; die Theme-Zeile ist bereits aktiv
  • Die Navigation wird mit pdoMenu erzeugt

Template-Inhalt (tmpBS5)

Optional vorbereitet für Darkmodus

<!doctype html>
<html lang="de" data-bs-theme="light">
<head>
    <meta charset="utf-8">
    <title>[[*pagetitle]] - [[++site_name]]</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <base href="[[++site_url]]">
    <link rel="icon" type="image/png" href="assets/modx/content/images/icon_48x48.png">

    <!-- Bootstrap Grund-Styles (lokal) -->
    <link rel="stylesheet" href="[[++assets_url]]bs5/bootstrap.min.css">

    <!-- Bootswatch-Theme (lokal, z.B. spacelab) -->
    <link rel="stylesheet" href="[[++assets_url]]bs5/spacelab/bootstrap.min.css">

    <!-- Bootstrap Icons (optional, für .bi-* Icons) -->
    <link rel="stylesheet" href="[[++assets_url]]bs5/bootstrap-icons/bootstrap-icons.min.css">

    <!-- Eigene Styles (optional) -->
    <style>[[$tmpBS_meinStyle]]</style>
    <style>[[$tmpBS_DarkFixes]]</style>
</head>

<body>

<header class="mb-4">
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container-fluid">

            <a class="navbar-brand" href="[[~1? &scheme=`abs`]]">[[++site_name]]</a>

            <button class="navbar-toggler"
                    type="button"
                    data-bs-toggle="collapse"
                    data-bs-target="#mainMenu"
                    aria-controls="mainMenu"
                    aria-expanded="false"
                    aria-label="Navigation umschalten">
                <span class="navbar-toggler-icon"></span>
            </button>

            <div class="collapse navbar-collapse" id="mainMenu">

                [[!pdoMenu?
                    &parents=`0`
                    &level=`2`
                    &scheme=`abs`
                    &tpl=`menuBSLink`
                    &tplParentRow=`menuBSParent`
                    &tplOuter=`menuBSOuter`
                    &tplInner=`menuBSInner`
                    &hereClass=`active`
                    &firstClass=`first`
                    &lastClass=`last`
                ]]

                <!-- Optional vorbereitet für Darkmodus -->
                <button class="btn btn-sm btn-outline-light ms-lg-3 mt-3 mt-lg-0"
                        type="button"
                        id="themeToggle"
                        title="Farbschema umschalten: Auto → Hell → Dunkel"
                        aria-label="Farbschema umschalten">
                    <i class="bi bi-moon-stars" id="themeIcon"></i>
                </button>

            </div>

        </div>
    </nav>
</header>

<main class="container mb-5">

    <!-- Optional mit einer TV den Seitentitel ein- oder ausblenden -->
    [[*tvPagetitle:is=`ja`:then=`
    <h1 class="mb-3">[[*longtitle:default=`[[*pagetitle]]`]]</h1>
    `:else=``]]

    [[!*id:isnot=`1`:then=`
    <nav aria-label="breadcrumb" class="mb-3">
        <div class="d-flex flex-wrap gap-2">
            <a href="[[~1? &scheme=`abs`]]" class="btn btn-sm btn-outline-secondary">
                <i class="bi bi-house"></i> Start
            </a>
            [[!pdoCrumbs?
                &scheme=`abs`
                &showHome=`0`
                &showAtHome=`0`
                &tplWrapper=`@INLINE [[+output]]`
                &tpl=`@INLINE <a href="[[+link]]" class="btn btn-sm btn-outline-secondary">[[+menutitle]]</a>`
                &tplCurrent=`@INLINE <span class="btn btn-sm btn-secondary disabled">[[+menutitle]]</span>`
            ]]
        </div>
    </nav>
    `:else=``]]

    [[*content]]
    <div style="clear: both"></div>

</main>

<footer class="bg-light border-top py-3">
    <div class="container">
        <p class="small text-muted mb-0">
            &copy; [[!copyrightYear? &start=`2004`]] [[++site_name]]
        </p>
    </div>
</footer>

<!-- JS Darkmodus -->
[[$modxBS_JS_ToggleSave]]

<!-- Bootstrap JS Bundle (lokal, inkl. Popper) -->
<script src="[[++assets_url]]bs5/bootstrap.bundle.min.js"></script>

</body>
</html>

JS-Inhalt (modxBS_JS_ToggleSave)

<script>
(function () {
    var storageKey = 'pms-color-mode';
    var toggleBtn = document.getElementById('themeToggle');
    var icon = document.getElementById('themeIcon');

    var MODES = ['auto', 'light', 'dark'];

    function getStoredMode() {
        try {
            var m = localStorage.getItem(storageKey);
            return MODES.indexOf(m) !== -1 ? m : null;
        } catch (e) {
            return null;
        }
    }

    function storeMode(mode) {
        try {
            localStorage.setItem(storageKey, mode);
        } catch (e) {
            // z.B. Private Mode
        }
    }

    function getSystemMode() {
        if (window.matchMedia &&
            window.matchMedia('(prefers-color-scheme: dark)').matches) {
            return 'dark';
        }
        return 'light';
    }

    function applyTheme(mode) {
        var html = document.documentElement;
        var effective = mode === 'auto' ? getSystemMode() : mode;

        html.setAttribute('data-bs-theme', effective);

        if (!icon) return;

        // Icon je nach aktuellem Modus/Zustand ändern:
        icon.classList.remove('bi-moon-stars', 'bi-sun', 'bi-circle-half');
        if (mode === 'auto') {
            // Systemmodus aktiv
            icon.classList.add('bi-circle-half');
        } else if (effective === 'dark') {
            // aktuell dunkel (manuell gewählt)
            icon.classList.add('bi-moon-stars');
        } else {
            // aktuell hell (manuell gewählt)
            icon.classList.add('bi-sun');
        }

        // Tooltip an aktuellen Zustand anpassen
        if (toggleBtn) {
            var title;
            if (mode === 'auto') {
                title = 'Aktuelles Farbschema: System (automatisch)';
            } else if (effective === 'dark') {
                title = 'Aktuelles Farbschema: dunkel';
            } else {
                title = 'Aktuelles Farbschema: hell';
            }
            toggleBtn.setAttribute('title', title);
            toggleBtn.setAttribute('aria-label', title);
        }
    }

    function initTheme() {
        var mode = getStoredMode();
        if (!mode) {
            mode = 'auto'; // Standard: Systemmodus
            storeMode(mode);
        }
        applyTheme(mode);

        // Auf Systemwechsel reagieren, wenn auto gewählt ist
        if (window.matchMedia) {
            var mq = window.matchMedia('(prefers-color-scheme: dark)');
            if (mq.addEventListener) {
                mq.addEventListener('change', function () {
                    if (getStoredMode() === 'auto') {
                        applyTheme('auto');
                    }
                });
            } else if (mq.addListener) { // ältere Browser
                mq.addListener(function () {
                    if (getStoredMode() === 'auto') {
                        applyTheme('auto');
                    }
                });
            }
        }
    }

    initTheme();

    if (toggleBtn) {
        toggleBtn.addEventListener('click', function () {
            var current = getStoredMode() || 'auto';
            var idx = MODES.indexOf(current);
            var next = MODES[(idx + 1) % MODES.length]; // auto -> light -> dark -> auto ...
            storeMode(next);
            applyTheme(next);
        });
    }
})();
</script>

🧩 Chunks für pdoMenu

Chunk: menuBSInner

[[+wrapper]]

<li class="nav-item">
    <a class="nav-link [[+class]]" href="[[+link]]">[[+menutitle]]</a>
</li>

Chunk: menuBSOuter

<ul class="navbar-nav me-auto mb-2 mb-lg-0">
    [[+wrapper]]
</ul>

Chunk: menuBSParent

<li class="nav-item dropdown [[+class]]">
    <a class="nav-link dropdown-toggle" href="[[+link]]" data-bs-toggle="dropdown" aria-expanded="false">
        [[+menutitle]]
    </a>
    <ul class="dropdown-menu">
        [[+wrapper]]
    </ul>
</li>

Chunk: bsFooter

Beispiel-Footerzeile:

<footer class="bg-light border-top py-3">
    <div class="container">

        <p class="small text-muted mb-0">
            &copy; [[!copyrightYear? &start=`2014`]] [[++site_name]]
            |
            <i class="bi bi-clock-history"></i>
            Lastupdate: [[!SiteLastUpdated:strtotime:date=`%Y-%m-%d`? &context=`web`]]
        </p>

        <p class="small text-muted mb-0">
            <i class="bi bi-shield-lock"></i>
            <a href="[[~42]]" target="_self">Impressum</a>
            |
            <i class="bi bi-exclamation-circle"></i>
            <a href="[[~12]]" target="_self">Datenschutz</a>
            |
            <i class="bi bi-envelope"></i>
            <a href="[[~55]]" target="_self">Kontakt</a>
        </p>

    </div>
</footer>

(Die Bootstrap-Icons intallieren wir später)

Chunk: tmpBS_meinStyle

/* Blogkarten: wenn nur eine Spalte vorhanden ist (kein Bild),
   soll die Textspalte die volle Breite nutzen */
.modx-blog-card .row.g-0 > .col-md-9:only-child {
    flex: 0 0 100%;
    max-width: 100%;
}

/* Dropdown-Hintergrund */
.navbar .dropdown-menu {
    background-color: #ffffff;
    border: 1px solid #ddd;
    border-radius: 6px;
    padding: 0.25rem 0.5rem;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

/* Einzelne Einträge etwas luftiger */
.navbar .dropdown-menu .nav-link {
    color: #212529;
    padding: 0.4rem 0.75rem;
    border-radius: 4px;
    white-space: nowrap;
}

/* Hover */
.navbar .dropdown-menu .nav-link:hover,
.navbar .dropdown-menu .nav-link:focus {
    background-color: #e9ecef;
    color: #212529;
}

/* Spacelab-Theme: Stil für blockquote ohne Klassen */
blockquote {
  margin: 0 0 1.5rem;
  padding: 0.75rem 1.25rem;
  border-left: 4px solid #b3bbc5;  /* dezente silber-blau-graue Linie */
  color: #495057;                  /* Textfarbe passend zum Theme */
  background-color: #f8f9fa;       /* heller Hintergrund zum Absetzen */
  font-style: italic;
  line-height: 1.4;
}

/* Bilder im RTE immer sauber einbinden */
main img {
    max-width: 100%;
    height: auto;
    display: block;               /* verhindert hässliche Umbrüche */
    margin: 0.5rem 0 1.25rem;     /* schöner Abstand im Text */
    border-radius: 0.25rem;       /* leichte Rundung passend zu Spacelab */
    box-shadow: 0 4px 12px rgba(0,0,0,0.15); /* Schatten */
}

/* Bilder mit Text links/rechts fließen lassen – TinyMCE erzeugt <img style="float:left"> */
main img[style*="float: left"],
main img[style*="float:left"] {
    float: left;
    margin: 0.25rem 1rem 1rem 0;
    max-width: 50%;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15); /* Schatten */
}

main img[style*="float: right"],
main img[style*="float:right"] {
    float: right;
    margin: 0.25rem 0 1rem 1rem;
    max-width: 50%;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15); /* Schatten */
}

/* Listen im Content schöner neben Float-Bildern darstellen */
main ul:not(.list-group) {
    list-style-position: inside;
    padding-left: 0;
    margin-left: 1.1rem;
}

/* Nach Float sauber resetten */
main::after {
    content: "";
    display: block;
    clear: both;
}

.download.card.file-list-card {
    max-width: 520px;
    margin: 2rem 0;
    box-shadow: 0 4px 12px rgba(0,0,0,0.10);
}
.file-list-card .card-header {
    background-color: #f8f9fa;
    font-weight: 600;
    font-size: 0.95rem;
}
.file-list-card .list-group-item {
    padding: 0.35rem 0.75rem;
}
.file-list-card .file-link {
    display: inline-flex;
    align-items: center;
}
.file-list-card .file-link:hover {
    text-decoration: underline;
}
.file-list-card .badge {
    font-size: 0.75rem;
    font-weight: 400;
}


/* bs5GalleryModal – Bild, Infobar, Buttons */
.bs5-gallery-modal .modal-body {
    position: relative;
    padding: 0.5rem 2.5rem 3.5rem;  /* links/rechts Platz für Pfeile, unten für Zoom */
    overflow: auto;
}
/* Bild: nicht zu hoch + Luft nach unten */
.bs5-gallery-modal img {
    max-height: 60vh;               /* genug Platz für Caption + Buttons */
    margin-bottom: 1rem;
    transform-origin: center center;
    transition: transform 0.2s ease;
    position: relative;
    z-index: 1;
}
/* Infobar (Name · Größe · Datum) gut lesbar */
.bs5-gallery-modal .gallery-infobar {
    color: #f8f9fa;
    opacity: 0.9;
    margin-bottom: 0.5rem;
}
/* Alle Buttons im Modal über das Bild legen */
.bs5-gallery-modal .modal-body .btn {
    z-index: 2;
}
/* Pfeil-Buttons als runde „Bubbles“ */
.bs5-gallery-modal .btn-outline-light {
    border-radius: 50%;
    width: 2.2rem;
    height: 2.2rem;
    padding: 0;
    line-height: 2.2rem;
}
/* Galerie-Modal darf fast die ganze Breite nutzen */
.bs5-gallery-modal .modal-dialog {
    max-width: 90vw;
}
/* Ab Desktop ruhig höher gehen */
@media (min-width: 992px) {
    .bs5-gallery-modal img {
        max-height: 75vh;
    }
}


/* FULLCalendar */
.fc .fc-event-title {
    white-space: normal !important;
    overflow-wrap: anywhere;
    line-height: 1.2;
}
@media (max-width: 600px) {
  /* Buttons in der FullCalendar-Toolbar verkleinern */
  .fc .fc-toolbar-chunk .btn {
    padding: 2px 6px;
    font-size: 0.75rem;
    line-height: 1.1;
  }
  /* Optional: Abstand zwischen Buttons leicht reduzieren */
  .fc .fc-toolbar-chunk .btn-group .btn {
    margin-right: 2px;
  }
}
/* Standard: Events erstmal "neutral" lassen */
.fc a.fc-event {
  cursor: default;
  text-decoration: none;
}
/* Nur Events mit href als Link behandeln */
.fc a.fc-event[href] {
  cursor: pointer;
  transition: filter 0.15s ease, opacity 0.15s ease;
}
/* Hover-Effekt NUR bei Events mit href */
.fc a.fc-event[href]:hover {
  filter: brightness(1.15);
  opacity: 0.9;
}
/* Titel nur bei klickbaren Events unterstreichen */
.fc a.fc-event[href] .fc-event-title {
  text-decoration: underline;
}


/* Wetter-Events im Kalender */
.fc-event-weather {
  background: transparent !important;
  border: 0 !important;
  box-shadow: none !important;
}
.fc-weather-event {
  display: flex;
  flex-direction: column;
  align-items: flex-start;   /* links ausrichten */
  justify-content: flex-start;
  padding: 0.2rem 0.2rem;
}
.fc-weather-icon {
  max-width: 48px;          /* evtl. anpassen */
  max-height: 48px;
  display: block;
  margin-bottom: 2px;
}
.fc-weather-temp {
  font-size: 0.8rem;
  line-height: 1.1;
  white-space: nowrap;
}

Chunk: tmpBS_DarkFixes

Darkmodus

/* ==========================================
   Bootswatch / Bootstrap 5.3 Darkmode Fixes
   Universell für alle Themes
   ========================================== */

/* Dropdown-Menüs */
[data-bs-theme="dark"] .navbar .dropdown-menu {
    background-color: var(--bs-body-bg);
    border-color: var(--bs-border-color);
    box-shadow: 0 4px 12px rgba(0,0,0,0.6);
}

[data-bs-theme="dark"] .navbar .dropdown-menu .nav-link {
    color: var(--bs-body-color);
}

[data-bs-theme="dark"] .navbar .dropdown-menu .nav-link:hover,
[data-bs-theme="dark"] .navbar .dropdown-menu .nav-link:focus {
    background-color: var(--bs-secondary-bg);
    color: var(--bs-body-color);
}

/* Card-Header (z.B. Downloads, Module usw.) */
[data-bs-theme="dark"] .card-header {
    background-color: var(--bs-secondary-bg) !important;
    color: var(--bs-body-color) !important;
    border-bottom-color: var(--bs-border-color);
}

/* List-Groups (z.B. Download-Listen) */
[data-bs-theme="dark"] .list-group-item {
    background-color: var(--bs-body-bg);
    color: var(--bs-body-color);
    border-color: var(--bs-border-color);
}

/* Footer */
[data-bs-theme="dark"] footer,
[data-bs-theme="dark"] .pms-footer {
    background-color: var(--bs-body-bg) !important;
    color: var(--bs-secondary-color) !important;
    border-top: 1px solid var(--bs-border-color);
}

[data-bs-theme="dark"] footer a {
    color: var(--bs-link-color);
}

[data-bs-theme="dark"] footer a:hover {
    color: var(--bs-link-hover-color);
}

/* Blockquotes */
[data-bs-theme="dark"] blockquote {
    background-color: rgba(255,255,255,0.08);
    color: var(--bs-body-color);
    border-left-color: var(--bs-border-color);
}

/* Tabellen */
[data-bs-theme="dark"] table {
    color: var(--bs-body-color);
}

[data-bs-theme="dark"] table thead {
    background-color: var(--bs-secondary-bg);
}

[data-bs-theme="dark"] table td,
[data-bs-theme="dark"] table th {
    border-color: var(--bs-border-color);
}

/* Breadcrumb-Buttons */
[data-bs-theme="dark"] .btn-outline-secondary {
    color: var(--bs-body-color);
    border-color: var(--bs-border-color);
}

[data-bs-theme="dark"] .btn-outline-secondary:hover {
    background-color: var(--bs-secondary-bg);
    color: var(--bs-body-color);
}

/* Bilder leicht abdunkeln (optional)
[data-bs-theme="dark"] img {
    box-shadow: 0 4px 16px rgba(0,0,0,0.7);
}
*/

/* FullCalendar Wetter-Temperaturen */
[data-bs-theme="dark"] .fc-weather-temp {
    color: #f8f9fa;
    text-shadow: 0 0 2px rgba(0,0,0,0.8);
}

Snippet: copyrightYear

<?php
# [[!copyrightYear]]
# [[!copyrightYear? &start=`2020`]]

$firstYear = !empty($start) ? $start : strftime("%Y");
$currentYear = strftime("%Y");

if ($firstYear == $currentYear) {
  $output = $currentYear;
} else {
  $output = $firstYear.' - '.$currentYear;
}

return $output;

In einem zweiten Browserfenster die Frontpage prüfen. Eventuell ist ein Refresh (F5) notwendig.


🧩 Reihenfolge ist wichtig

Richtig:

  1. Bootstrap CSS
  2. Bootswatch Theme CSS
  3. Eigene Styles CSS
  4. Bootstrap JS (bundle)

Nur so überschreibt Bootswatch korrekt die Bootstrap-Styles.


✅ Ergebnis

  • Bootstrap läuft 100% lokal
  • Keine externen Skripte oder Fonts
  • DSGVO-konform
  • tmpBS5 ist vorbereitet und als Standard gesetzt
  • Navigation wird über pdoMenu + Chunks korrekt im Bootstrap-Stil ausgegeben
  • Saubere, erweiterbare Basis für ein MODX 3 Projekt