Nitro Theming
Datum: Juni 2023
Lesedauer: 7 Minuten
Vor kurzem habe ich auf https://education.merkleinc.ch/ (opens in a new tab) einen Lightmode implementiert.
In diesem Blogpost wird beschrieben, was Theming ist und wie man es in Nitro einbauen kann.
Was ist Theming?
Ein Theme (oder auch Skinns genannt) ist eine Zusammenstellung von grafischen Elementen wie beispielsweise Farben, Typografien, Icons und Bilder. Es ist also nicht die Funktionalität, sondern der Anstrich einer Webanwendung.
Häufig bieten Webseiten zwei Themes an (hell und dunkel). Die Möglichkeiten sind aber unbegrenzt.
Der User kann dann meistens selbst zwischen den Varianten wechseln und sich so für das gewünschte UI entscheiden.
Nachfolgend werden die zwei häufigsten Arten von Themes gegenübergestellt.
Kategorie | Lightmode | Darkmode |
---|---|---|
Beliebtheit | 🟥 Weniger beliebt | 🟩 Bevorzugt von ca. 85% (Schwarz wird aber manchmal mit negativen Emotionen verknüpft.) |
User Experience | 🟨 - | 🟩 Variiert, wird aber im Darkmode manchmal als besser angesehen. |
Fokus | 🟩 Für Personen mit normaler Sicht ist der dunkle Text auf hellem Hintergrund klarer und schneller zu verstehen. | 🟨 Personen mit einer Sehstörung performen besser auf dunklen Seiten. |
Physisch | 🟥 Führt zu: Ermüdung der Augen, Kopfschmerzen (Schlechter Schlaf) | 🟩 Weniger: Ermüdung der Augen, Kopfschmerzen (Schlechter Schlaf) |
Batterie | 🟥 - | 🟩 Verbraucht weniger Strom |
Fazit | Auf hellen Webseiten mit dunklem Text kann in der Regel schneller und fokussierter gelesen werden. | Das dunkle Design ist heutzutage äusserst beliebt und wird von vielen Webseiten angeboten. |
Beide Modus haben ihre Vor- und Nachteile. Die beste Möglichkeit ist es meistens, dem User die Möglichkeit zu bieten, selbst zwischen den Themes wechseln zu können.
Implementierung in Nitro
Nachfolgend wird beschreiben, wie man das Theming in Nitro (opens in a new tab) einbauen kann.
Aktivierung
Neues Projekt
Bei der Nitro-Installation wird nachgefragt, ob man Theming-Funktionalitäten nutzen will. Beantwortet man das mit y (yes)
, so kann man bereits mit der Implementierung beginnen.
Bestehendes Projekt
Das Theming-Feature kann aber auch, durch die Anpassung der folgenden Konfiguration, bestehenden Projekten hinzugefügt werden:
{
"generator-nitro": {
"name": "theming-project",
"templateEngine": "hbs",
"jsCompiler": "js",
"themes": true, <--- Diesen Wert muss man auf true setzen.
"clientTemplates": false,
"exampleCode": false,
"exporter": false
}
}
Damit diese Anpassung übernommen wird, muss man ein Update durchführen.
npm run nitro:update
So wird das bestehende Projekt dann mit den nötigen Funktionalitäten erweitert.
Themes Configuration
Die grundlegende Konfiguration liegt unter config/default/themes.js
.
Darin befindet sich ein Array mit Theme-Objekten. Ein Theme besteht jeweils aus einer id
, einem name
und einem isDefault
Boolean.
Hier ein Beispiel:
'use strict';
const config = {
themes: [
{
id: 'light',
name: 'Light Theme',
isDefault: true,
},
{
id: 'dark',
name: 'Dark Theme',
},
],
};
module.exports = config.themes;
Das Objekt kann aber beliebig mit weiteren Properties erweitert werden. Diese können dann in den Views genutzt werden.
Hier ein Beispiel:
<p>{{theme.name}}</p>
Webpack
Webpack ist dazu da, Ressourcen zu bündeln. In der Webpack-Konfiguration muss das Theming-Feature aktiviert werden.
const options = {
...
features: {
banner: true,
bundleAnalyzer: false,
theme: theme,
dynamicAlias: {
search: '/theme/light',
replace: `/theme/${theme}`,
},
},
};
Dynamic Alias
Dank dem dynamicAlias
kann man immer den Code des Default-Themes importieren. Im anderen Theme wird dann der Pfad beim Kompilieren von Webpack geändert. Hier ein Beispiel:
Für die Definition der Farben werden die folgenden Dateien genutzt:
colors/css/
theme/
dark.scss
light.scss
variables/
colors.scss
Im Colors File werden die Haupt- und Sekundärfarben mit Variablen definiert, welche abhängig eines weiteren Imports gesetzt werden. In diesem Beispiel werden sie aus der /theme/dark.scss
Datei ausgelesen.
@import './theme/dark';
$project-main: $main; // #000
$project-secondary: $secondary; // #fff
Wechselt der User zum Light-Theme, so passt Webpack diesen Import-Pfad an. Die Datei nutzt dann also die Definitionen aus /theme/light.scss
. Dadurch werden auf der ganzen Seite zwei andere Farben genutzt.
Entry Point
Wichtig ist, dass man auch entsprechende Entry Points hat. In diesem Beispiel wären dies: ui.light.js
& ui.dark.js
Dadurch kann jedes Theme seinen eigenen Code (Komponenten/Scripts, ...) laden. Will man das nicht, so kann man in den Dateien einfach das bereits vorhandene ui.js
importieren.
NPM Scripts
Dem package.json
wurden, durch die Aktivierung der Themes, Scripts hinzugefügt.
Mit npm start
wird die Applikation mit dem Default-Theme gestartet.
"start": "npm run start:dark",
Die weiteren Themes können über separate Start-Scripts gestartet werden.
"start:dark": "cross-env THEME=dark PORT=8083 PROXY=8084 npm run dev",
"start:light": "cross-env THEME=light PORT=8081 PROXY=8082 npm run dev",
Beim Starten wird dann jeweils das Theme über die THEME
-Environment-Variable gesetzt. Diese wird in der Webpack-Konfiguration (options.js
) ausgelesen.
Im Entwicklungsmodus kann jeweils nur ein Theme gestartet werden. Dies erlaubt keinen schnellen Wechsel zwischen den beiden. Im Produktiven ist aber ein paralleler Start möglich.
"prod": "npm-run-all prod:*",
Gulp
Gulp wird eingesetzt, um Assets zu kopieren und Bilder zu verkleinern.
Es ist möglich, für verschiedene Themes auch unterschiedliche Bilder und SVGs zu nutzen. Dazu kann der Gulp Task (/config/default/gulp.js
) konfiguriert werden.
const config = {
gulp: {
...
minifyImages: [
// copies and minifies all source images to dest folder
{
src: 'src/shared/assets/img/dark/**/*',
dest: 'public/assets/dark/img',
},
{
src: 'src/shared/assets/img/light/**/*',
dest: 'public/assets/light/img',
},
],
svgSprites: [
// generates icon sprite with the name of the last folder in src
{
src: 'src/patterns/atoms/icon/img/icons/*.svg',
dest: 'public/assets/dark/svg/',
},
{
src: 'src/patterns/atoms/icon/img/icons/*.svg',
dest: 'public/assets/light/svg',
},
],
},
};
Danach gibt es unter /public/assets/
einen Ordner pro Theme, der die entsprechenden Grafiken enthält.
In den Views muss man sich wenig Gedanken über den Pfad machen. Verwendet man den Asset-Helper, so wird immer im entsprechenden Verzeichnis gesucht.
<img
class='m-card__image'
src='{{asset name='/img/picture.jpg'}}'
alt='img of day or night'
/>
<!-- Sucht in public/assets/<theme>/img/picture.jpg -->
So hat man einen Image Tag, der je nach Theme ein anders Bild anzeigt:
Light | Dark |
---|---|
![]() | ![]() |
Theme Routes
Wie bereits beschrieben, ist dieser saubere Wechsel zwischen den beiden Themes nur im produktiven Modus möglich.
Ermöglicht wird die Funktonalität von einer Route. In themes.js
wird für jedes Theme ein Endpoint angeboten. Wird dieser angesprochen, so wechselt die Applikation auf das entsprechende Theme.
for (const theme of validThemes) {
app.route(`/theme/${theme.id}`).get((req, res, next) => {
req.session.theme = theme.id;
if (req.query && req.query.ref) {
res.redirect(req.query.ref);
} else {
res.redirect('/index');
}
});
}
Die ganze Datei ist hier ersichtlich: https://github.com/merkle-open/generator-nitro/blob/develop/packages/project-nitro/project/routes/_themes.js (opens in a new tab)
Neben dem gewünschten Theme, kann man auch noch einen ref
Parameter mitgeben. In meinem Fall nutzte ich das, um der Route den aktuellen Dateinamen mitzuteilen. So bleibt der User beim Wechseln auf derselben Seite und wechselt nicht immer auf Index.
<a
aria-label='switch'
class='o-header__switch col-lg-1 justify-content-end'
href='theme/{{theme.opposite}}?ref=/{{_nitro.filename}}'
>
<span class='o-header__switch-icon'>
{{pattern name='icon' icon=theme.switchIcon}}
</span>
</a>
Exporter
Muss das Projekt statisch exportiert werden, so ist die Anpassung des Exporters entscheidend. Diese Konfiguration unterscheidet sich je nach Projekt und ist unter Umständen aufwendig zu erstellen.
In meinem Fall implementierte ich einen Light- und Darkmode. An den Kommentaren kann nachvollzogen werden, was hier gemacht wird.
'use strict';
/**
* Nitro Exporter Config
*/
const regexFilename = '[A-Za-z0-9-]+';
const config = {
exporter: [
{
dest: 'export',
i18n: ['default', 'light'],
publics: ['public/*', 'public/assets/**/*', 'public/content/**/*'],
renames: [
{
src: 'export/assets/**',
base: 'export/assets',
dest: 'export/',
},
],
replacements: [
{
glob: [
'export/*.html',
'export/*/css/*.css',
'export/*/js/*.js',
],
replace: [
{
from: '/assets',
to: '',
},
],
},
{
glob: ['export/*.html'],
replace: [
{
// turn page title into filename
from: `href="theme\/light\\?ref=/(${regexFilename})"`,
to: 'href="$1-light.html"',
},
{
// add html to file ending
from: 'href="([a-z0-9-]+)"',
to: 'href="$1.html"',
},
],
},
{
glob: ['export/*-light.html'],
replace: [
{
// navigation path renames
from: `href="(${regexFilename}).html"`,
to: 'href="$1-light.html"',
},
{
// fix switch button link
from: `(${regexFilename})-light-light.html`,
to: '$1.html',
},
{
// css, js, icons
from: 'dark/',
to: 'light/',
},
{
// change icon
from: '#moon',
to: '#sun',
},
{
// theme switcher text
from: 'Dunkel',
to: 'Hell',
},
{
// point to the original content
from: `<!-- filename: (${regexFilename}) -->`,
to: '<link rel="canonical" href="https://education.namics.com/$1" />',
},
],
},
{
glob: ['export/*.html'],
replace: [
{
// remove comment
from: `<!-- filename: ${regexFilename} -->`,
to: '',
},
],
},
],
views: [
'404',
'ausbildung',
'index',
'kontakt',
'projekte',
'schnuppertage',
'ueber-uns',
],
//additionalRoutes: ['api/lottie/shipment.json', 'api/lottie/bouncing.json'],
minifyHtml: true,
zip: false,
},
],
};
module.exports = config.exporter;
Das Resultat ist ein Export, in welchem jede View zweimal vorkommt. Einmal mit dem Default-Theme und einmal als Light-Theme.
SEO
Im Kapitel Exporter ist erkennbar, dass für jede Seite zwei HTML-Dateien existieren. Dies ist ein Problem. Denn Google bestraft doppelten Inhalt.
Um beim SEO-Wert keine Einbussen zu machen, kann man auf einer der doppelten Seite zeigen, dass dies nur ein Duplikat ist und auf die Originale zeigen.
<link rel="canonical" href="https://education.namics.com/kontakt" />
In der Zeile 78 ist der Exporter Konfiguration (Kapitel Exporter) ist dies definiert.
Mehr über Canonicals erfährt man hier: https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls?hl=de (opens in a new tab)
Fazit
Folgende Vorteile sehe ich am Nitro Theming:
- Schnell implementiert
- Man kann leicht vom Theme abhängige Komponenten, Scripts und Assets einbauen
- Beim Export gibt es viele Konfigurationsmöglichkeiten
Die folgenden Punkte finde ich nicht ideal:
- Der Wechsel zwischen zwei Themes ist nur im produktiven Modus testbar. Bei Änderungen muss die Applikation immer neu gestartet werden.
- Die fertige Webseite läuft dann unter 2 (oder mehr) URLs
- Es gibt viele Views (jede HTML-Datei kommt mehrmals vor)
- Man muss an die Canonicals denken
- Die Wahl des Nutzers kann nicht gut gespeichert werden
- Ein animierter Übergang zwischen den Themes ist nicht möglich
- Es ist zeitaufwendig, die Exporter Konfiguration an die eigenen Bedürfnisse anzupassen.
Ich fand die Implementierung der Theming-Funktionalität spannend und konnte dadurch in einigen Bereichen Neues lernen.