I've got a nuxt2 app in ssr. I've implemented custom-theming plugins (code provided below).
The theme gets saved in cookies and when the clients makes a request, the server knows what theme, because of the sent cookies. The theme gets applied in a plugin by assigning a certain class to the body:
this.ctx.app.head.bodyAttrs = this.ctx.app.head.bodyAttrs || {};
this.ctx.app.head.bodyAttrs.class = this.value + '-mode';
When ssr-rendered content hits the client, everything is fine, but with the first navigating to another internal-link, the attributes of the body get deleted (not only the assigned classes, also the data-n-head).
The code is somewhat close to what color-mode uses, because I need to replace color-mode.
Thank you!
The serverside-plugin:
import BaseTheming, { getModeFromCookies } from "./BaseTheming"
export default (ctx, inject) => {
inject(
'colorMode', new ServerSideTheming(ctx)
);
}
class ServerSideTheming extends BaseTheming {
ctx;
constructor(ctx) {
super(getModeFromCookies(ctx.req.headers?.cookie ?? ''), ctx.route.path);
this.ctx = ctx;
this.saveSettings();
}
get preference() {
return this._activeTheme;
}
set preference(value) {
this._activeTheme = value;
this.saveSettings();
}
saveSettings() {
this.ctx.app.head.bodyAttrs = this.ctx.app.head.bodyAttrs || {};
this.ctx.app.head.bodyAttrs.class = this.value + '-mode';
}
}
client-side plugin:
import Vue from 'vue';
import { defaultThemes, excludedRoutes } from './BaseTheming';
export default (ctx, inject) => {
const colorMode = new Vue({
data() {
return {
themes: defaultThemes,
activeTheme: defaultThemes[0],
excludedRoutes,
};
},
computed: {
isRouteExcluded() {
return this.excludedRoutes.some((route) => ctx.route.path.includes(route));
},
preference: {
get() {
return this.activeTheme;
},
set(value) {
const classList = document.body.classList;
this.themes.forEach((theme) => {
if (theme === value) {
classList.add(theme + '-mode');
} else {
classList.remove(theme + '-mode');
}
});
this.activeTheme = value;
this.saveSettings();
}
},
value() {
return this.activeTheme;
}
},
created() {
if (this.isRouteExcluded) {
this.activeTheme = defaultThemes[0];
} else {
this.parseBodyClassList();
}
},
methods: {
saveSettings() {
if (!this.isRouteExcluded) {
try {
const now = new Date(Date.now());
now.setDate(now.getDate() + 10000);
const variable = `nuxt-color-mode=${this.value};`;
document.cookie = `${variable}expires=${now.toUTCString()};path=/`;
} catch (ex) {
console.error('localstorage blocked', ex);
}
}
},
parseBodyClassList() {
// can be removed by June 2023
const themeFromPrefColorMode = localStorage.getItem('nuxt-color-mode');
if (themeFromPrefColorMode) {
this.preference = themeFromPrefColorMode;
localStorage.removeItem('nuxt-color-mode');
} else {
document.body.classList.forEach((className) => {
const theme = className.split('-')[0];
if (defaultThemes.includes(theme)) {
this.activeTheme = theme;
}
});
}
}
}
});
// watching the body for changes, because something deletes the assigned classes
const observer = new MutationObserver((mutationList) => {
for (const mutation of mutationList) {
if (mutation.attributeName === 'class' && document.body.classList.length === 0) {
colorMode.preference = colorMode.value;
}
}
});
observer.observe(document.body, { attributes: true, attributeOldValue: true });
inject('colorMode', colorMode);
};
the base-theming class for completeness:
export const defaultThemes = ['light', 'dark'];
export const excludedRoutes = ['embed', 'angebot/topic'];
export default class BaseTheming {
themes = defaultThemes;
_activeTheme = this.themes[0];
excludedRoutes = ['embed', 'angebot/topic'];
route = '';
get isRouteExcluded() {
return this.excludedRoutes.some((route) => this.route.includes(route));
}
constructor(preset, route) {
this.route = route;
if (this.isRouteExcluded) {
this._activeTheme = defaultThemes[0];
} else if (this.themes.includes(preset)) {
this._activeTheme = preset;
}
}
get value() {
return this._activeTheme;
}
saveSettings() {
throw new Error('saveSettings not implemented');
}
getSettings() {
throw new Error('getSettings not implemented');
}
}
export function getModeFromCookies(cookie) {
const value = `; ${cookie}`;
const parts = value.split(`; nuxt-color-mode=`);
if (parts.length === 2) {
const theme = parts.pop().split(';').shift();
if (defaultThemes.includes(theme)) {
return theme;
}
}
return defaultThemes[0];
}