Grav individuell anpassen

Grav Plugins Themes Child Theme

Bei mir fing es mit kleineren Formatierungen an, kleine Unzufriedenheiten wie etwas aussah oder wieviel Platz es einnahm bzw. verschwendete und ich stieß früh auf eine custom.css ohne Inhalt im Grav Quark Theme, die für solche Anpassungen gedacht war. Mach das nicht - Plugins und Themes werden bei sogenannten Updates nicht aktualisiert, sie werden gelöscht und die neue Version installiert. Deine Änderungen sind dann weg!

Es gibt in Grav mehrere Wege das zu vermeiden. Vermutlich hast du in einer Installationsanweisung schon einmal einen Hinweis gesehen die Konfigurationsdatei in ein Verzeichnis unterhalb von user/config/ zu kopieren und dann nur diese Kopie anzupassen. Das ist einer der Wege. Aber wie ist das bei größeren Anpassungswünschen? Eine Twig Vorlage verändern oder gar eigene, ganz neue Vorlagen hinzufügen? Das mag zuerst kompliziert klingen, aber ließ einfach weiter und bilde dir erst am Ende des Beitrags eine Meinung.

Ein Child-Theme erstellen

Keine Sorge, du musst kein eigenständiges Theme entwickeln. Du hast dich ja bereits für ein Theme entschieden und das sollst du auch weiter nutzen. Ein Kind deines Themes ist mit wenigen Handgriffen erstellt. Es müssen dabei nur ein paar Regeln beachtet werden, damit Grav es auch als eigenständiges Theme erkennt so das du es später aktivieren kannst. Ich nehme als Beispiel das Quark 2 Theme, da ich es selbst verwende und so sicherstellen kann die Grav Regeln richtig wiederzugeben.

Initialisierung

Dein Theme braucht einen Namen den du zwar frei vergeben kannst, ich würde aber dazu raten den Namen des Eltern-Themes mit aufzunehmen um das später auch zuordnen zu können. Wenn du das Admin2 Plugin nutzt, kannst du einfach unter Themes dein Plugin auswählen und dir den Slug von dessen Übersichtsseite notieren, ohne Admin2 Plugin musst du im Dateisystem unterhalb von user/themes/ in das Verzeichnis deines genutzten Themes wechseln (in meinem Beispiel wäre das user/themes/quark2/) und dort aus der Dateiblueprints.yaml'' den Slug auslesen (welcher im Beispiel auch quark2 ist). Ich habe mich dafür entschieden aus einem Teil des Hostname (wichtig bei Multisite-Setups), dem Eltern-Theme und einem angehängten "child" den neuen Namen zusammenzusetzen: jcs-quark2-child

Die Gedankenstriche dienen zwar nur der Lesbarkeit, aber das hilft ja auch im Dateisystem. Also leg damit ein neues Verzeichnis unterhalb von user/themes/ an:

cd user/themes
mkdir jcs-quark2-child

In das neue Verzeichnis kopierst du dann 3 Dateien aus dem Theme das du bereits verwendest, zwei davon tragen den Slug des Themes im Namen und müssen das auch in deinem neuen Child-Theme wieder tun:

cp user/themes/quark2/blueprints.yaml user/themes/jcs-quark2-child/blueprints.yaml
cp user/themes/quark2/quark2.yaml user/themes/jcs-quark2-child/jcs-quark2-child.yaml
cp user/themes/quark2/quark2.php user/themes/jcs-quark2-child/jcs-quark2-child.php
# wenn du es später schick im Admin2 Plugin angezeigt bekommen möchtest, auch noch die Images
cp user/themes/quark2/screenshotjpg user/themes/jcs-quark2-child/screenshotjpg
cp user/themes/quark2/thumbnail.jpg user/themes/jcs-quark2-child/thumbnail.jpg

Bis hier her hast du einen Klon des vorhandenen Themes angelegt, den du jetzt als eigenständiges Kind definieren musst. Dazu bearbeitest du als erstes die user/themes/jcs-quark2-child/jcs-quark2-child.yaml. Der Name ist frei zu vergeben und darunter findest du dein Theme später im Admin2 Plugin unter "Themes" wieder. Der Slug muss dem Verzeichnisnamen entsprechen. Das Eltern-Theme (hier quark2) trägst du mit dessen Slug unter dependencies zusätzlich zu bereits bestehenden Abhängigkeiten ein. Alles was unterhalb von form: steht lässt du unangetastet stehen.

name: JCS Quark 2 Child
slug: jcs-quark2-child
type: theme
version: 1.0.0
description: Individuelles Child-Theme auf Basis von Quark 2
icon: meteor
author:
  name: Christian Schmidt
  email: dont@mail.me
  url: https://www.jcs-net.de
homepage: https://www.jcs-net.de
keywords: theme, quark2, child
bugs: https://www.jcs-net.de
license: MIT
dependencies:
  - { name: grav, version: '>=1.7.0' }
  - quark2
compatibility:
  grav: ['1.7', '1.8', '2.0']

Als nächstes bearbeitest du deine eigentliche Theme Klasse (hier user/themes/jcs-quark2-child/jcs-quark2-child.php). In den ersten Zeilen findest du die Klassen-Definition: class Quark2 extends Theme in der class Quark2 den Namen deines Eltern-Themes definiert (Quark2) - das ist Case Sensitiv, aufschreiben! Danach löscht du den Inhalt der Datei vollständig und ersetzt ihn durch die Definition deiner eigenen Theme Klasse.

namespace Grav\Theme;

use RocketTheme\Toolbox\Event\Event;

class JcsQuark2Child extends Quark2
{
    public static function getSubscribedEvents()
    {
        return array_merge(
            Quark2::getSubscribedEvents(),
            [
                'onTwigSiteVariables' => ['onTwigSiteVariables', 0],
            ]
        );
    }

    public function onTwigSiteVariables()
    {
        $this->grav['assets']->addCss('theme://css/custom.css');
    }
}

Zu ändern hast du hier wieder nur die Klassendefinition class JcsQuark2Child extends Quark2 und Quark2::getSubscribedEvents(),. JcsQuark2Child ist der Name deiner Theme-Klasse: übernimm einfach den Verzeichnisnamen, setze jeden Wortanfang auf einen Großbuchstaben um und entferne die Bindestriche. extends Quark2 macht die Magie: Quark2 ist der Name, den du dir im vorherigen Schritt notiert hattest und das besagt, dass dein Theme alle Eigenschaften und Methoden des Elternthemes Quark2 erben soll, wobei getSubscribedEvents() dafür sorgt, dass dein Theme auch von Ereignissen durch Grav informiert wird - wir hängen uns da einfach an die gleiche Methode innerhalb des Quark2 Themes an.

Zu guter Letzt noch die Voreinstellungen für dein Theme in der user/themes/jcs-quark2-child/jcs-quark2-child.yaml. Setze zunächst enabled: false - deine Website verfügt bereits über ein aktives Theme! Die anderen Werte kannst du bereits nach deinen Vorlieben anpassen. Jetzt muss die Familie nur noch zusammengeführt werden. Füge über besagtem enabled: false folgende Zeilen ein:

streams:
  schemes:
    theme:
      type: ReadOnlyStream
      prefixes:
        '':
          - 'user://themes/jcs-quark2-child'
          - 'user://themes/quark2'

# Von quark2.yaml übernommene Default-Werte (werden vom Parent-Theme
# NICHT automatisch vererbt.
# Können über Admin (Blueprint-Formularfelder oben) weiter angepasst werden.
enabled: false

Danach noch alle Caches einmal löschen - entweder über die Admin2 Oberfläche oder aus der Shell:

bin/grav clearcache

Aktivierung

Bist du bis hierher ohne Schreibfehler durch die Anleitung gekommen, sollte jetzt dein neues Child-Theme im Admin2 Interface auftauchen und du kannst es in der Übersicht aller installierten Themes aktivieren. Dein zuvor aktives Theme wird dabei automatisch deaktiviert, auf deiner Website ändert sich aber sichtbar nichts, da dein Child-Theme ja von eben diesem erbt. An diesem Punkt hast du dir eine Pause verdient.

grav-child-theme.png

Das Kind für Anpassungen nutzen

Bisher haben wir lediglich die Voraussetzungen für eigene Anpassungen geschaffen, aber wofür der Aufwand? Nun, vor der Erstellung des Child-Themes war das Problem, dass eigene Anpassungen ein Update der geänderten Komponente nicht überstehen konnten. Jetzt kann das Eltern-Theme ohne Angst aktualisiert werden. Das Child Theme erbt die Änderungen die as dem Update hervorgehen und überschreibt nur was du im Child-Theme angepasst hast, bzw. fügt Eigenschaften hinzu. Fangen wir doch als Beispiel mit den CSS Eigenschaften an.

Ein eigener Stil

Zu Begin des Beitrags habe ich die custom.css bereits erwähnt. Eine solche können wir jetzt gefahrlos nutzen. Erstelle in deinem Theme ein Verzeichnis css/ und darin eine Datei custom.css. Da Quark2 diese bereits bei sich einbindet, überschreibt deine custom.css diese und wird so automatisch eingebunden.

mkdir user/themes/jcs-quark2-child/css
touch user/themes/jcs-quark2-child/css/custom.css

Als Beispiel nehme ich meine erste Änderung am Quark2 Theme: Bei der Umstellung von Quark auf Quark2 fiel mir auf, dass Quark2 vor viele Überschriften eine Art Kicker setzt und den wollte ich wieder loswerden. Zwar habe ich nur bei H2 Überschriften solche Kicker vorgefunden, mir aber gedacht ich sollte mich gleich vor zukünftigen Auswüchsen dieser Art schützen (kommentiere jede Änderung so ausführlich, dass du später noch feststellen kannst warum du sie eingefügt hast):

/* Quark2 zeichnet standardmäßig einen kurzen "Kicker"-Strich vor h2 (und
   ggf. weiteren Ebenen) via ::before. */
h1::before,
h2::before,
h3::before,
h4::before,
h5::before,
h6::before {
  display: none !important;
}

Funktionale Änderungen

Du kannst im Prinzip jede Datei aus dem Eltern-Theme in deines kopieren und dort anpassen und ganz neue eigene Vorlagen erstellen. Besonders mächtig wird das Child-Theme aber durch die geteilte Verzeichnisstruktur in Grav. Nicht nur Themes, auch Plugins teilen sich diese Struktur und so kannst du beispielweise im Verzeichnis templates/ sowohl Vorlagen aus dem Eltern-Theme als auch aus den installierten Plugins überschreiben. Ich nehme wieder ein bei mir eingerichtetes Beispiel: das Archives Plugin erstellt in der Sidebar eine verlinkte Liste mit Monaten (Monat Jahr) in denen Beiträge im Blog veröffentlicht wurden, die dann durch anklicken eine Auflistung mit diesen Beiträgen zur Ansicht aufruft. Eine recht praktische Funktionalität, die aber sehr verschwenderisch mit dem vorhandenen Platz umgeht und daher auch nur eine begrenzte Zahl an Monaten zur Anzeige bringt - konfigurierbar über die Admin Konfiguration. Zwar bringt das Plugin eine Vorlage mit, die nur die Jahre auflistet, aber das war mir zu wenig. Wie wäre es daraus eine Baumstruktur zu erstellen deren Knoten die Jahre und deren Blätter die Monate abbilden?

Die Vorlage des Plugins findet sich in user/plugins/archives/templates/partials/archives.html.twig und muss im Child-Theme abgebildet werden um sie zu überschreiben:

mkdir user/themes/jcs-quark2-child/templates
mkdir user/themes/jcs-quark2-child/templates/partials
touch user/themes/jcs-quark2-child/templates/partials/archives.html.twig

Dann wird diese Vorlage mit Leben gefüllt:

{#
  Überschreibung der Datei „templates/partials/archives.html.twig“ des Plugins „grav-plugin-archives“.

  Grav wertet Twig-Vorlagen im aktiven Theme aus, bevor es auf
  die vom Plugin bereitgestellten Vorlagen zurückgreift. Daher reicht es aus, diese Datei unter demselben relativen Pfad
  im Child-Theme abzulegen, um die flache Liste des Plugins durch eine
  Baumstruktur „Jahr > Monat“ zu ersetzen, ohne das Plugin selbst zu verändern:

    user/themes/jcs-quark2-child/templates/partials/archives.html.twig

  Struktur:
    - Oberste Ebene = ein ein- und ausklappbares <details> pro Jahr (native HTML-Ausklappfunktion,
      kein JS erforderlich, per Tastatur bedienbar).
    - Das aktuellste Jahr (das erste, auf das man stößt) ist zunächst geöffnet, alle anderen
      sind zunächst ausgeblendet. Dies setzt „plugins.archives.order.dir: desc“ voraus (die
      Standard-Einstellung des Plugins), sodass das neueste Jahr als erste Gruppe gerendert wird.
    - Monate sind unter ihrem Jahr in <li> verschachtelt, wobei die ursprünglichen Plugin-
      Klassen (archives / archive_date / label) beibehalten werden, sodass die integrierte archives.css
      sie weiterhin stylt – nur der neue Wrapper auf Jahrsebene benötigt ein wenig
      zusätzliches CSS.
    - Monatsnamen durchlaufen den |td-Filter (Translate Date-Plugin, ICU-Muster)
      anstelle des rohen |date-Filters, entsprechend der Art und Weise, wie der Rest der
      Website Datumsangaben lokalisiert (Blog-Liste, einzelne Beiträge). |td folgt der
      aktuellen Sprache von Grav, sodass auf /de automatisch deutsche Monatsnamen und
      auf /en automatisch englische angezeigt werden sollten.
#}

{% set year_counts = {} %}
{% for month, items in archives_data %}
    {% set y = month|date('Y') %}
    {% set year_counts = year_counts|merge({ (y): (year_counts[y] ?? 0) + items|length }) %}
{% endfor %}

<div class="archives-tree">
{% set active_year = null %}
{% set first_group = true %}
{% for month, items in archives_data %}
    {% set year = month|date('Y') %}

    {% if year != active_year %}
        {% if active_year is not null %}
                </ul>
            </details>
        {% endif %}
        {% set active_year = year %}

        <details class="archive-year"{% if first_group %} open{% endif %}>
            <summary class="archive-year-toggle">
                <a href="{{ archives_url ?? base_url }}/{{ config.plugins.archives.taxonomy_names.year }}{{ config.system.param_sep }}{{ month|date(config.plugins.archives.taxonomy_values.year)|e('url') }}">
                    {{ year }}
                </a>
                {% if archives_show_count %}
                <span class="label">{{ year_counts[year] }}</span>
                {% endif %}
            </summary>
            <ul class="archives archive-months">
        {% set first_group = false %}
    {% endif %}
            <li>
                <a href="{{ archives_url ?? base_url }}/{{ config.plugins.archives.taxonomy_names.month }}{{ config.system.param_sep }}{{ month|date(config.plugins.archives.taxonomy_values.month)|lower|e('url') }}">
                    {% if archives_show_count %}
                    <span class="label">{{ items|length }}</span>
                    {% endif %}
                    <span class="archive_date">{{ month|td(null, 'MMMM') }}</span>
                </a>
            </li>
{% endfor %}
{% if active_year is not null %}
        </ul>
    </details>
{% endif %}
</div>

Und zu guter Letzt noch das benötigte CSS in die user/themes/jcs-quark2-child/css/custom.css eingefügt. An dieser Stelle noch ein Hinweis der dich vor größeren Fehlersuchen bewahren sollte: Änderungen an dieser Datei solltest du immer ganz am Ende einfügen. Hast du bereits früher einmal die gleichen Klassen bearbeitet, überschreibt das zuverlässig diese früheren Anpassungen. Es bietet sich aber an die Datei hin und wieder auf doppelte Klassen zu untersuchen und diese zusammenzuführen.

/* Die plugin-eigene „archives.css“ übernimmt weiterhin die Gestaltung von .archive_date und .label;
   diese Datei gestaltet lediglich den neuen Wrapper auf Jahrsebene und verringert den Zeilenabstand. */

.archives-tree .archive-year {
    margin-bottom: 0.25rem;
}

.archives-tree .archive-year-toggle {
    display: flex;
    align-items: center;
    cursor: pointer;
    list-style: none;
    font-weight: 600;
}

/* Das standardmäßige, vom Betriebssystem gestaltete Aufklappdreieck wird
   ausgeblendet, damit beide Browser einheitlich aussehen; stattdessen 
   zeichnen wir unser eigenes über ::before. */
.archives-tree .archive-year-toggle::-webkit-details-marker {
    display: none;
}

/* Behalte für das übergeordnete Element die Standardposition „flex-start“ bei und schiebe, falls du
   „archives_show_count“ aktivierst, nur das Badge mit „margin-left: auto“ nach rechts
   (siehe .archives-tree .archive-year-toggle .label weiter unten). */
.archives-tree .archive-year-toggle::before {
    content: "\25B8"; /* ▸ */
    display: inline-block;
    flex: 0 0 auto;
    margin-right: 0.5em;
    transition: transform 0.15s ease;
}

.archives-tree .archive-year[open] > .archive-year-toggle::before {
    transform: rotate(90deg);
}

.archives-tree .archive-year-toggle > a {
    flex: 0 1 auto;
}

.archives-tree .archive-year-toggle .label {
    margin-left: auto;
}

.archives-tree .archive-months {
    margin: 0 0 0.25rem 1.1rem;
    padding: 0;
    list-style: none;
}

/* Die Basisstile des Themes bzw. Plugins scheinen jeder Archivzeile großzügige
   Ränder/Abstände zuzuweisen (dies ist derselbe „verschwendete Platz“ wie bei der ursprünglichen 
   flachen Liste, nur jetzt besser sichtbar, da die Einträge oben und unten enger
   beieinander gruppiert sind). Erzwinge hier eine kompakte Zeilenhöhe; !important, da wir
   die genauen konkurrierenden Selektoren bzw. deren Spezifität in dieser Sitzung nicht kennen –
   entferne es, sobald du sichergestellt hast, dass es sonst nirgendwo benötigt wird. */
.archives-tree .archive-months li {
    margin: 0 !important;
    padding: 0 !important;
    line-height: 1.7 !important;
}

.archives-tree .archive-months li a {
    display: flex !important;
    align-items: center;
    gap: 0.4em;
    padding: 0.1rem 0 !important;
    margin: 0 !important;
}

Ich habe bei dieser Anpassung einige Dinge vorausgesetzt, wie z.B. die Sortierreihenfolge. Als Besitzer einer Website kann man das (anders als ein Theme/Plugin Entwickler) machen, sollte sich das aber in die Kommentare schreiben. Ich habe schon viele Stunden bei der Fehlersuche verbracht für Dinge die ich Monate zuvor selbst eingerichtet dann aber wieder vergessen hatte.

Fazit

Wenn du die Schritte dieses Beitrages zumindest aus dem Kapitel "Ein Child-Theme erstellen" auf deiner Website durchgeführt hast, hast du jetzt alles an der Hand jede gewünschte Änderung Updatesicher durchzuführen. Wenn du dich an funktionale Änderungen wie bei meinem Archives Plugin Beispiel nicht heran traust, packe dein Child Theme in ein Archiv und nutze ein LLM. Ein durchdachter Prompt mit einem Link auf deine Website und das angehängte Archiv versetzen ChatGPT, Claude, Mistral und deren Kollegen in die Lage das für dich umzusetzen. Auch beim Zusammenfassen einer zu sehr gewachsenen custom.css können die echte Retter sein.

Fragen zum Thema beantworte ich gerne im Fediverse.