Skip to content

Commit d723be6

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 a242839 commit d723be6

3 files changed

Lines changed: 129 additions & 99 deletions

File tree

src/app/clientEffects.tsx

Lines changed: 14 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

@@ -53,3 +54,16 @@ export function ScrollRestoration() {
5354
}, []);
5455
return null;
5556
}
57+
58+
export function ThemeInitFallback() {
59+
useEffect(() => {
60+
// The inline script in the root layout normally runs during HTML
61+
// parsing. If this route was client-rendered (React doesn't execute
62+
// inline scripts it inserts), run it now so the theme, platform class
63+
// and window.__setPreferredTheme exist.
64+
if (typeof window.__setPreferredTheme === 'undefined') {
65+
new Function(themeScript)();
66+
}
67+
}, []);
68+
return null;
69+
}

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
// Fonts are served from react.dev on every deployment (including the
15057
// translation forks), so they share one cache across subdomains.
15158
const FONT_ORIGIN = 'https://react.dev';
@@ -215,6 +122,7 @@ export default function RootLayout({children}: {children: React.ReactNode}) {
215122
)}
216123
</head>
217124
<body className="font-text font-medium antialiased text-lg bg-wash dark:bg-wash-dark text-secondary dark:text-secondary-dark leading-base">
125+
<ThemeInitFallback />
218126
<ScrollRestoration />
219127
<AnalyticsTracker />
220128
{children}

src/app/themeScript.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
// Runs before first paint from an inline <script> in the root layout. Also
9+
// evaluated by ThemeInitFallback when a route is rendered entirely on the
10+
// client (e.g. a not-found page under a dynamic segment), because React does
11+
// not execute inline scripts it creates during client rendering.
12+
export const themeScript = `
13+
(function () {
14+
try {
15+
let logShown = false;
16+
function setUwu(isUwu) {
17+
try {
18+
if (isUwu) {
19+
localStorage.setItem('uwu', true);
20+
document.documentElement.classList.add('uwu');
21+
if (!logShown) {
22+
console.log('uwu mode! turn off with ?uwu=0');
23+
console.log('logo credit to @sawaratsuki1004 via https://github.com/SAWARATSUKI/KawaiiLogos');
24+
logShown = true;
25+
}
26+
} else {
27+
localStorage.removeItem('uwu');
28+
document.documentElement.classList.remove('uwu');
29+
console.log('uwu mode off. turn on with ?uwu');
30+
}
31+
} catch (err) { }
32+
}
33+
window.__setUwu = setUwu;
34+
function checkQueryParam() {
35+
const params = new URLSearchParams(window.location.search);
36+
const value = params.get('uwu');
37+
switch(value) {
38+
case '':
39+
case 'true':
40+
case '1':
41+
return true;
42+
case 'false':
43+
case '0':
44+
return false;
45+
default:
46+
return null;
47+
}
48+
}
49+
function checkLocalStorage() {
50+
try {
51+
return localStorage.getItem('uwu') === 'true';
52+
} catch (err) {
53+
return false;
54+
}
55+
}
56+
const uwuQueryParam = checkQueryParam();
57+
if (uwuQueryParam != null) {
58+
setUwu(uwuQueryParam);
59+
} else if (checkLocalStorage()) {
60+
document.documentElement.classList.add('uwu');
61+
}
62+
} catch (err) { }
63+
})();
64+
65+
(function () {
66+
function setTheme(newTheme) {
67+
window.__theme = newTheme;
68+
if (newTheme === 'dark') {
69+
document.documentElement.classList.add('dark');
70+
} else if (newTheme === 'light') {
71+
document.documentElement.classList.remove('dark');
72+
}
73+
}
74+
75+
var preferredTheme;
76+
try {
77+
preferredTheme = localStorage.getItem('theme');
78+
} catch (err) { }
79+
80+
window.__setPreferredTheme = function(newTheme) {
81+
preferredTheme = newTheme;
82+
setTheme(newTheme);
83+
try {
84+
localStorage.setItem('theme', newTheme);
85+
} catch (err) { }
86+
};
87+
88+
var initialTheme = preferredTheme;
89+
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
90+
91+
if (!initialTheme) {
92+
initialTheme = darkQuery.matches ? 'dark' : 'light';
93+
}
94+
setTheme(initialTheme);
95+
96+
darkQuery.addEventListener('change', function (e) {
97+
if (!preferredTheme) {
98+
setTheme(e.matches ? 'dark' : 'light');
99+
}
100+
});
101+
102+
document.documentElement.classList.add(
103+
window.navigator.platform.includes('Mac')
104+
? "platform-mac"
105+
: "platform-win"
106+
);
107+
})();
108+
`;

0 commit comments

Comments
 (0)