3. Create a theming script for light mode / dark mode (without white flash) - so it needs to pull from the requests in order to render out the right solution. This is my code in the steps to reproduce:
The functionality is a light mode and dark mode toggle and it works exactly the way that it should where it will check on the server what the preference is set for the user and then pass that into the app via next.js 14 app router middleware.
This also gets rid of the FOUC (i think that's what it's called) where there is a white flash on the dark option before rendering/recognizing that it was the selected version. This white half-second flash doesn't occur, which is ideal. That's what I mean when I say the feature is perfect except for this nonce did not match error.
Here is my code and let's debug together:
middleware.ts
```ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64") || '';
console.log('nonce in middlweare', `${nonce}.........`)
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https: ${
process.env.NODE_ENV === "production" ? "" : `'unsafe-eval' http:`
};
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data: https://upload.wikimedia.org;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
block-all-mixed-content;
upgrade-insecure-requests;
`;
{/*
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https: http: 'unsafe-inline' ${
process.env.NODE_ENV === "production" ? "" : `'unsafe-eval'`
};
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data: https://upload.wikimedia.org;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
block-all-mixed-content;
upgrade-insecure-requests;
*/}
// Replace newline characters and spaces
const contentSecurityPolicyHeaderValue = cspHeader
.replace(/\s{2,}/g, " ")
.trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set(
"Content-Security-Policy",
contentSecurityPolicyHeaderValue,
);
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
// Add CSP
response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);
// Tell browser to only access via HTTPS
response.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
// Prevent browser from guessing tyupe of content if Content-Type header not set, prevent XSS exploits
response.headers.set('X-Content-Type-Options', 'nosniff');
// Control info sent when navigating to another
response.headers.set('Referrer-Policy', 'origin-when-cross-origin');
// Set nonce cookie
response.cookies.set('csp-nonce', nonce, { httpOnly: true, sameSite: 'strict' });
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
{
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
missing: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
],
};
app/page.tsx
import { headers } from "next/headers";
import Script from "next/script";
import Link from "next/link";
import SVGItem from "./components/SVGItem";
import TestClient from "./components/TestClient";
import TestImageComponent from "./components/TestImageComponent";
import Image from "./components/lib/Image";
import Head from "next/head";
import { Metadata } from "next";
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import { LinearProgress } from "@mui/material";
import dynamic from 'next/dynamic';
import LoadingThemeButton from '@/app/components/LoadingThemeButton';
const SetThemeButton = dynamic(() => import('@/app/components/SetThemeButton'), {
ssr: false,
loading: () => <LoadingThemeButton/>,
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app"
};
export default function Page() {
const nonce = headers().get("x-nonce") ?? undefined;
console.log('nonce in page', nonce)
return (
<main>
<h1>hello</h1>
{/* <LinearProgress color="success" /> */}
<SetThemeButton />
<h3>hello</h3>
</main>
);
}
layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
// MUI
import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import {lightTheme, darkTheme} from '@/app/styles/theme';
import Script from "next/script";
import ThemeScript from "@/app/styles/ThemeToggle";
import { headers } from "next/headers";
// Font
const inter = Inter({ subsets: ["latin"] });
// export const metadata: Metadata = {
// title: "Create Next App",
// description: "Generated by create next app",
// };
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
// const nonce = headers().get("x-nonce");
// console.log('nonce in layout', nonce)
return (
<html lang="en">
<head>
<ThemeScript />
</head>
<body className={inter.className}>
<AppRouterCacheProvider>
<ThemeProvider theme={darkTheme}>
{children}
</ThemeProvider>
</AppRouterCacheProvider>
</body>
</html>
);
}
app/styles/ThemeToggle.tsx
import { cookies, headers } from "next/headers";
import Script from "next/script";
type Theme = 'light' | 'dark';
type ThemeScriptProps = {
nonce: string; // nonce is a required string
};
declare global {
interface Window {
__theme: Theme;
__onThemeChange: (theme: Theme) => void;
__setPreferredTheme: (theme: Theme) => void;
}
}
function code() {
window.__onThemeChange = function () {};
function setTheme(newTheme: Theme) {
window.__theme = newTheme;
preferredTheme = newTheme;
document.documentElement.dataset.theme = newTheme;
window.__onThemeChange(newTheme);
}
var preferredTheme;
try {
preferredTheme = localStorage.getItem('theme') as Theme;
} catch (err) {}
window.__setPreferredTheme = function (newTheme: Theme) {
setTheme(newTheme);
try {
localStorage.setItem('theme', newTheme);
} catch (err) {}
};
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkQuery.addEventListener('change', function (e) {
window.__setPreferredTheme(e.matches ? 'dark' : 'light');
});
setTheme(preferredTheme || (darkQuery.matches ? 'dark' : 'light'));
}
export default function ThemeScript() {
const nonce = headers().get("x-nonce") ?? undefined;
console.log('nonce in theme', nonce)
// const nonce = cookies().get("csp-nonce") ?? undefined;
// console.log('nonce in theme', nonce?.value!)
return (
<script nonce={nonce} dangerouslySetInnerHTML={{ __html: `(${code})();` }} />
// return (
// <Script
// src="public/theme-switcher.js"
// nonce={nonce}
// strategy="beforeInteractive"
// />
)
}
Right now, if I comment out from the layout.tsx's <head> object, then the error goes away
This tells me that the nonce is not being passed correctly from my middleware.tsx file to layout.tsx into the even though I should be able to get this via the method of const nonce = headers().get("x-nonce") ?? undefined; as I am doing in my ThemeScript() function.
Link to the code that reproduces this issue
https://codesandbox.io/p/devbox/nextjs-approuter-middleware-content-security-policy-cs37j3
To Reproduce
I created a Codesandbox of the code and it is giving me a different error than what I see locally, but it's the same issue:
nonce
did not match. Server: "" Client: "ODMzNThhNzItYzgwYy00ZTk3LWJmYjItMDZiMDlhNGE1Y2Vh"suppressHydrationWarning
inlayout.tsx
as that isn't a good security move.π Codsandbox link: https://codesandbox.io/p/devbox/nextjs-approuter-middleware-content-security-policy-cs37j3
app/page.tsx
layout.tsx
app/styles/ThemeToggle.tsx
components/LoadingThemeButton.tsx
app/components/SetTheme.tsx
app/components/SetThemeButton.tsx
Current vs. Expected behavior
Current Behavior:
layout.tsx
's<head>
object, then the error goes awayconst nonce = headers().get("x-nonce") ?? undefined;
as I am doing in my ThemeScript() function.