Skip to content

Commit 269320a

Browse files
gaearonclaude
andcommitted
Initialize the theme on client-rendered routes
Not-found pages under the dynamic section routes are rendered entirely on the client (the PPR fallback shell is empty), and React doesn't execute inline scripts it inserts. So the theme/uwu/platform init never ran there: the page came up light regardless of preference, and the theme toggle threw because window.__setPreferredTheme didn't exist. Move the script to a shared module and evaluate it from a client effect when the inline version hasn't run. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 353e7f0 commit 269320a

3 files changed

Lines changed: 124 additions & 99 deletions

File tree

src/app/clientEffects.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import {useEffect, useRef} from 'react';
1111
import {usePathname} from 'next/navigation';
12+
import {themeScript} from './themeScript';
1213

1314
declare const gtag: (...args: any[]) => void;
1415

@@ -51,3 +52,13 @@ export function ScrollRestoration() {
5152
}, []);
5253
return null;
5354
}
55+
56+
export function ThemeInitFallback() {
57+
useEffect(() => {
58+
// Set by the inline <script> in the root layout when it ran.
59+
if (typeof window.__setPreferredTheme === 'undefined') {
60+
new Function(themeScript)();
61+
}
62+
}, []);
63+
return null;
64+
}

src/app/layout.tsx

Lines changed: 7 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
import type {Metadata, Viewport} from 'next';
99
import Script from 'next/script';
1010
import {siteConfig} from '../siteConfig';
11-
import {AnalyticsTracker, ScrollRestoration} from './clientEffects';
11+
import {
12+
AnalyticsTracker,
13+
ScrollRestoration,
14+
ThemeInitFallback,
15+
} from './clientEffects';
16+
import {themeScript} from './themeScript';
1217

1318
import '@docsearch/css';
1419
import '../styles/algolia.css';
@@ -48,104 +53,6 @@ export const metadata: Metadata = {
4853
},
4954
};
5055

51-
const themeScript = `
52-
(function () {
53-
try {
54-
let logShown = false;
55-
function setUwu(isUwu) {
56-
try {
57-
if (isUwu) {
58-
localStorage.setItem('uwu', true);
59-
document.documentElement.classList.add('uwu');
60-
if (!logShown) {
61-
console.log('uwu mode! turn off with ?uwu=0');
62-
console.log('logo credit to @sawaratsuki1004 via https://github.com/SAWARATSUKI/KawaiiLogos');
63-
logShown = true;
64-
}
65-
} else {
66-
localStorage.removeItem('uwu');
67-
document.documentElement.classList.remove('uwu');
68-
console.log('uwu mode off. turn on with ?uwu');
69-
}
70-
} catch (err) { }
71-
}
72-
window.__setUwu = setUwu;
73-
function checkQueryParam() {
74-
const params = new URLSearchParams(window.location.search);
75-
const value = params.get('uwu');
76-
switch(value) {
77-
case '':
78-
case 'true':
79-
case '1':
80-
return true;
81-
case 'false':
82-
case '0':
83-
return false;
84-
default:
85-
return null;
86-
}
87-
}
88-
function checkLocalStorage() {
89-
try {
90-
return localStorage.getItem('uwu') === 'true';
91-
} catch (err) {
92-
return false;
93-
}
94-
}
95-
const uwuQueryParam = checkQueryParam();
96-
if (uwuQueryParam != null) {
97-
setUwu(uwuQueryParam);
98-
} else if (checkLocalStorage()) {
99-
document.documentElement.classList.add('uwu');
100-
}
101-
} catch (err) { }
102-
})();
103-
104-
(function () {
105-
function setTheme(newTheme) {
106-
window.__theme = newTheme;
107-
if (newTheme === 'dark') {
108-
document.documentElement.classList.add('dark');
109-
} else if (newTheme === 'light') {
110-
document.documentElement.classList.remove('dark');
111-
}
112-
}
113-
114-
var preferredTheme;
115-
try {
116-
preferredTheme = localStorage.getItem('theme');
117-
} catch (err) { }
118-
119-
window.__setPreferredTheme = function(newTheme) {
120-
preferredTheme = newTheme;
121-
setTheme(newTheme);
122-
try {
123-
localStorage.setItem('theme', newTheme);
124-
} catch (err) { }
125-
};
126-
127-
var initialTheme = preferredTheme;
128-
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
129-
130-
if (!initialTheme) {
131-
initialTheme = darkQuery.matches ? 'dark' : 'light';
132-
}
133-
setTheme(initialTheme);
134-
135-
darkQuery.addEventListener('change', function (e) {
136-
if (!preferredTheme) {
137-
setTheme(e.matches ? 'dark' : 'light');
138-
}
139-
});
140-
141-
document.documentElement.classList.add(
142-
window.navigator.platform.includes('Mac')
143-
? "platform-mac"
144-
: "platform-win"
145-
);
146-
})();
147-
`;
148-
14956
// Translation forks load the fonts from react.dev too.
15057
const FONT_ORIGIN = 'https://react.dev';
15158
const FONT_PRELOADS = [
@@ -214,6 +121,7 @@ export default function RootLayout({children}: {children: React.ReactNode}) {
214121
)}
215122
</head>
216123
<body className="font-text font-medium antialiased text-lg bg-wash dark:bg-wash-dark text-secondary dark:text-secondary-dark leading-base">
124+
<ThemeInitFallback />
217125
<ScrollRestoration />
218126
<AnalyticsTracker />
219127
{children}

src/app/themeScript.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
// Inlined in the root layout's <head>. Also evaluated by ThemeInitFallback
9+
// on client-rendered routes, where React doesn't execute inline scripts.
10+
export const themeScript = `
11+
(function () {
12+
try {
13+
let logShown = false;
14+
function setUwu(isUwu) {
15+
try {
16+
if (isUwu) {
17+
localStorage.setItem('uwu', true);
18+
document.documentElement.classList.add('uwu');
19+
if (!logShown) {
20+
console.log('uwu mode! turn off with ?uwu=0');
21+
console.log('logo credit to @sawaratsuki1004 via https://github.com/SAWARATSUKI/KawaiiLogos');
22+
logShown = true;
23+
}
24+
} else {
25+
localStorage.removeItem('uwu');
26+
document.documentElement.classList.remove('uwu');
27+
console.log('uwu mode off. turn on with ?uwu');
28+
}
29+
} catch (err) { }
30+
}
31+
window.__setUwu = setUwu;
32+
function checkQueryParam() {
33+
const params = new URLSearchParams(window.location.search);
34+
const value = params.get('uwu');
35+
switch(value) {
36+
case '':
37+
case 'true':
38+
case '1':
39+
return true;
40+
case 'false':
41+
case '0':
42+
return false;
43+
default:
44+
return null;
45+
}
46+
}
47+
function checkLocalStorage() {
48+
try {
49+
return localStorage.getItem('uwu') === 'true';
50+
} catch (err) {
51+
return false;
52+
}
53+
}
54+
const uwuQueryParam = checkQueryParam();
55+
if (uwuQueryParam != null) {
56+
setUwu(uwuQueryParam);
57+
} else if (checkLocalStorage()) {
58+
document.documentElement.classList.add('uwu');
59+
}
60+
} catch (err) { }
61+
})();
62+
63+
(function () {
64+
function setTheme(newTheme) {
65+
window.__theme = newTheme;
66+
if (newTheme === 'dark') {
67+
document.documentElement.classList.add('dark');
68+
} else if (newTheme === 'light') {
69+
document.documentElement.classList.remove('dark');
70+
}
71+
}
72+
73+
var preferredTheme;
74+
try {
75+
preferredTheme = localStorage.getItem('theme');
76+
} catch (err) { }
77+
78+
window.__setPreferredTheme = function(newTheme) {
79+
preferredTheme = newTheme;
80+
setTheme(newTheme);
81+
try {
82+
localStorage.setItem('theme', newTheme);
83+
} catch (err) { }
84+
};
85+
86+
var initialTheme = preferredTheme;
87+
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
88+
89+
if (!initialTheme) {
90+
initialTheme = darkQuery.matches ? 'dark' : 'light';
91+
}
92+
setTheme(initialTheme);
93+
94+
darkQuery.addEventListener('change', function (e) {
95+
if (!preferredTheme) {
96+
setTheme(e.matches ? 'dark' : 'light');
97+
}
98+
});
99+
100+
document.documentElement.classList.add(
101+
window.navigator.platform.includes('Mac')
102+
? "platform-mac"
103+
: "platform-win"
104+
);
105+
})();
106+
`;

0 commit comments

Comments
 (0)