Nitro Theming
Translated from German using DeepL.
Date: June 2023
Reading time: 7 minutes
I recently implemented a light mode on https://education.merkleinc.ch/ (opens in a new tab).
This blog post describes what theming is and how you can integrate it into Nitro.
What is theming?
A theme (also known as a skinn) is a collection of graphic elements such as colors, typography, icons and images. It is therefore not the functionality, but the appearance of a web application.
Websites often offer two themes (light and dark). However, the possibilities are unlimited.
The user can then usually switch between the variants themselves and thus decide on the desired UI.
The two most common types of themes are compared below.
Category | Lightmode | Darkmode |
---|---|---|
Popularity | 🟥 Less popular | 🟩 Preferred by approx. 85% (however, black is sometimes associated with negative emotions). |
User Experience | 🟨 - | 🟩 Varies, but is sometimes considered better in dark mode. |
Focus | 🟩 For people with normal vision, the dark text on a light background is clearer and easier to understand. | 🟨 People with a visual impairment perform better on dark pages. |
Physical | 🟥 Leads to: eye fatigue, headaches (poor sleep) | 🟩 Less: Eye fatigue, headaches (poor sleep) |
Battery | 🟥 - | 🟩 Consumes less electricity |
Conclusion | Light websites with dark text are generally quicker to read and more focused. | The dark design is extremely popular nowadays and is offered by many websites. |
Both modes have their advantages and disadvantages. The best option is usually to offer the user the option of switching between the themes themselves.
Implementation in Nitro
The following describes how to implement theming in Nitro (opens in a new tab).
Activation
New project
During the Nitro installation, you will be asked whether you want to use theming functionalities. If you answer with y (yes)
, you can already start with the implementation.
Existing project
The theming feature can also be added to existing projects by adapting the following configuration:
{
"generator-nitro": {
"name": "theming-project",
"templateEngine": "hbs",
"jsCompiler": "js",
"themes": true, <--- This value must be set to true
"clientTemplates": false,
"exampleCode": false,
"exporter": false
}
}
An update must be carried out for this adjustment to be adopted.
npm run nitro:update
The existing project is then extended with the necessary functionalities.
Themes Configuration
The basic configuration is located under config/default/themes.js
.
This contains an array with theme objects. A theme consists of an id
, a name
and an isDefault
Boolean.
Here is an example:
'use strict';
const config = {
themes: [
{
id: 'light',
name: 'Light Theme',
isDefault: true,
},
{
id: 'dark',
name: 'Dark Theme',
},
],
};
module.exports = config.themes;
However, the object can be extended with additional properties as required. These can then be used in the views.
Here is an example:
<p>{{theme.name}}</p>
Webpack
Webpack is there to bundle resources. The theming feature must be activated in the Webpack configuration.
const options = {
...
features: {
banner: true,
bundleAnalyzer: false,
theme: theme,
dynamicAlias: {
search: '/theme/light',
replace: `/theme/${theme}`,
},
},
};
Dynamic Alias
Thanks to the dynamicAlias
you can always import the code of the default theme. The path is then changed in the other theme when Webpack is compiled. Here is an example:
The following files are used to define the colors:
colors/css/
theme/
dark.scss
light.scss
variables/
colors.scss
In the Colors file, the main and secondary colors are defined with variables, which are set depending on a further import. In this example, they are read from the /theme/dark.scss
file.
@import './theme/dark';
$project-main: $main; // #000
$project-secondary: $secondary; // #fff
If the user switches to the light theme, Webpack adapts this import path. The file then uses the definitions from /theme/light.scss
. As a result, two different colors are used on the entire page.
Entry Point
It is important that you also have corresponding entry points. In this example, these would be: ui.light.js
& ui.dark.js
This allows each theme to load its own code (components/scripts, ...). If you do not want this, you can simply import the existing ui.js
into the files.
NPM Scripts
Scripts have been added to package.json
by activating the themes.
With npm start
the application is started with the default theme.
"start": "npm run start:dark",
The other themes can be started via separate start scripts.
"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",
At startup, the theme is then set via the THEME
environment variable. This is read in the webpack configuration (options.js
).
Only one theme can be started at a time in development mode. This does not allow a quick switch between the two. In production mode, however, a parallel start is possible.
"prod": "npm-run-all prod:*",
Gulp
Gulp is used to copy assets and resize images.
It is also possible to use different images and SVGs for different themes. The Gulp task (/config/default/gulp.js
) can be configured for this purpose.
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',
},
],
},
};
There is then one folder per theme under /public/assets/
, which contains the corresponding graphics.
In the views, there is little need to worry about the path. If you use the asset helper, the corresponding directory is always searched for.
<img
class='m-card__image'
src='{{asset name='/img/picture.jpg'}}'
alt='img of day or night'
/>
<!-- searches in public/assets/<theme>/img/picture.jpg -->
This gives you an image tag that displays a different image depending on the theme:
Light | Dark |
---|---|
![]() | ![]() |
Theme Routes
As already described, this clean switch between the two themes is only possible in productive mode.
The functionality is made possible by a route. An endpoint is offered for each theme in themes.js
. If this is addressed, the application switches to the corresponding 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');
}
});
}
The entire file can be viewed here: https://github.com/merkle-open/generator-nitro/blob/develop/packages/project-nitro/project/routes/_themes.js (opens in a new tab)
In addition to the desired theme, you can also specify a ref
parameter. In my case, I used this to tell the route the current file name. This way, the user stays on the same page when switching and does not always switch to 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
If the project needs to be exported statically, it is important to customize the exporter. This configuration differs depending on the project and may be time-consuming to create.
In my case, I implemented a light and dark mode. You can see what is being done here in the comments.
'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;
The result is an export in which each view appears twice. Once with the default theme and once as a light theme.
SEO
In the Exporter chapter, you can see that there are two HTML files for each page. This is a problem. Because Google penalizes duplicate content.
To avoid losing SEO value, you can show on one of the duplicate pages that this is only a duplicate and point to the originals.
<link rel="canonical" href="https://education.namics.com/kontakt" />
This is defined in line 78 of the exporter configuration (chapter Exporter).
You can find out more about canonicals here: https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls?hl=de (opens in a new tab)
Conclusion
I see the following advantages to Nitro Theming:
- Quick to implement
- You can easily add components, scripts and assets that depend on the theme
- There are many configuration options when exporting
I do not find the following points ideal:
- Switching between two themes can only be tested in production mode. The application must always be restarted if changes are made.
- The finished website then runs under 2 (or more) URLs
- There are many views (each HTML file appears several times)
- You have to think about the canonicals
- The user's choice cannot be saved well
- An animated transition between themes is not possible
- It is time consuming to customize the exporter configuration to your needs.
I found the implementation of the theming functionality exciting and was able to learn new things in some areas.