Skip to content

Commit dda4a08

Browse files
committed
fix: switch Mermaid to client-side rendering for Vercel compatibility (#542)
* fix: switch Mermaid to client-side rendering for Vercel compatibility * fix: ensure i18next resources are loaded before rendering client components to prevent translation keys from showing in Vercel builds --------- Co-authored-by: zzq0826 <[email protected]>
1 parent dfe72a6 commit dda4a08

File tree

6 files changed

+106
-11
lines changed

6 files changed

+106
-11
lines changed

astro.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export default defineConfig({
7171
remarkPlugins: [remarkMath, remarkGfm],
7272
rehypePlugins: [
7373
rehypeSlug,
74-
[rehypeMermaid, { strategy: "img-png" }],
74+
[rehypeMermaid, { strategy: "pre-mermaid" }],
7575
[
7676
rehypeAutolinkHeadings,
7777
{

src/components/Footer/Subscribe/Subscribe.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { useState, useEffect } from "preact/hooks"
33
import MailchimpSubscribe from "react-mailchimp-subscribe"
44
import SubscribeSvg from "~/assets/svgs/footer/subscribe.svg?react"
55
import { clsx } from "~/lib"
6-
import i18next, { changeLanguage, t } from "i18next"
6+
import { t } from "i18next"
7+
import { useI18nReady } from "~/hooks/useI18nReady"
78

89
import EmailInput from "./EmailInput.tsx"
910
import styles from "./Subscribe.module.css"
@@ -19,8 +20,7 @@ export default function Subscribe(props) {
1920
const [email, setEmail] = useState("")
2021
const [customMessage, setCustomMessage] = useState("")
2122
const [emailValid, setEmailValid] = useState(false)
22-
23-
i18next.changeLanguage(props.lang)
23+
const isReady = useI18nReady(props.lang)
2424

2525
useEffect(() => {
2626
setCustomMessage("")
@@ -42,6 +42,10 @@ export default function Subscribe(props) {
4242
setEmail(e.target.value)
4343
}
4444

45+
if (!isReady) {
46+
return null
47+
}
48+
4549
return (
4650
<div className={clsx(styles.container, "dark:bg-dark-highlight")}>
4751
<div className={styles.subscribeBox}>
@@ -50,22 +54,28 @@ export default function Subscribe(props) {
5054
</span>
5155

5256
<div className={styles.copyBox}>
53-
<div className={styles.subscribeTitle}>{ t("landing.NewsletterCTA.title") }</div>
54-
<div className={styles.subscribeText}>
55-
{ t("landing.NewsletterCTA.text") }
56-
</div>
57+
<div className={styles.subscribeTitle}>{t("landing.NewsletterCTA.title")}</div>
58+
<div className={styles.subscribeText}>{t("landing.NewsletterCTA.text")}</div>
5759
</div>
5860
<MailchimpSubscribe
5961
url={url}
60-
render={({ subscribe, status, message }: any) => (
62+
render={({
63+
subscribe,
64+
status,
65+
message,
66+
}: {
67+
subscribe: (data: { EMAIL: string }) => void
68+
status: string
69+
message: string
70+
}) => (
6171
<div className={styles.emailBox}>
6272
<EmailInput
6373
className={styles.emailInput}
6474
value={email}
6575
onChange={handleChangeEmail}
6676
onClick={() => handleSubmit(subscribe)}
6777
onEnter={() => handleSubmit(subscribe)}
68-
placeholder= { t("landing.NewsletterCTA.placeholder") }
78+
placeholder={t("landing.NewsletterCTA.placeholder")}
6979
end={status === "success"}
7080
/>
7181
{customMessage && <div className={styles.errorMessage}>{customMessage}</div>}

src/components/HeadCommon.astro

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ import "../styles/design-system/global-styles.css"
2020
<!-- Scrollable a11y code helper -->
2121
<script src="/make-scrollable-code-focusable.js" is:inline></script>
2222

23+
<!-- Mermaid.js for client-side rendering -->
24+
<script type="module">
25+
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs"
26+
mermaid.initialize({ startOnLoad: true, theme: "default" })
27+
</script>
28+
2329
<!-- Google Tag Manager -->
2430
<!-- <script type="text/javascript">
2531
;(function (w, d, s, l, i) {

src/components/RightSidebar/TableOfContents/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { useStore } from "@nanostores/preact"
33
import type { FunctionalComponent } from "preact"
44
import { useState, useEffect, useRef } from "preact/hooks"
55
import { shouldUpdateToc } from "./tocStore"
6-
import i18next, { t } from "i18next"
6+
import { t } from "i18next"
7+
import { useI18nReady } from "~/hooks/useI18nReady"
78

89
export interface Heading {
910
depth: number
@@ -21,6 +22,7 @@ const TableOfContents: FunctionalComponent<{
2122
const [currentID, setCurrentID] = useState("overview")
2223
const onThisPageID = "on-this-page-heading"
2324
const $shouldUpdateToc = useStore(shouldUpdateToc)
25+
const isReady = useI18nReady()
2426

2527
useEffect(() => {
2628
if (!tableOfContents.current) return
@@ -79,6 +81,10 @@ const TableOfContents: FunctionalComponent<{
7981
setHeadings(headingList)
8082
}
8183

84+
if (!isReady) {
85+
return null
86+
}
87+
8288
return (
8389
<>
8490
<h2 className="heading">{t("rightSidebar.onThisPage")}</h2>

src/hooks/useI18nReady.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useState, useEffect } from "preact/hooks"
2+
import i18next, { changeLanguage } from "i18next"
3+
4+
/**
5+
* Custom hook to ensure i18next is initialized and translation resources are loaded
6+
* before rendering components that use translations.
7+
*
8+
* This fixes the issue where Vercel build shows translation keys instead of translated text
9+
* due to SSR/CSR timing differences.
10+
*
11+
* @param lang - Optional language code to change to. If provided, will change language and wait for resources.
12+
* @returns boolean indicating if i18next is ready (initialized and resources loaded)
13+
*/
14+
export function useI18nReady(lang?: string): boolean {
15+
const [isReady, setIsReady] = useState(false)
16+
17+
useEffect(() => {
18+
const ensureResourcesLoaded = async () => {
19+
try {
20+
// Wait for i18next to be initialized
21+
if (!i18next.isInitialized) {
22+
await new Promise<void>((resolve) => {
23+
const handler = () => {
24+
i18next.off("initialized", handler)
25+
resolve()
26+
}
27+
i18next.on("initialized", handler)
28+
})
29+
}
30+
31+
// Change language if needed (this will wait for resources to load)
32+
if (lang && i18next.language !== lang) {
33+
await changeLanguage(lang)
34+
}
35+
36+
// Double-check that resources are actually loaded
37+
const targetLang = lang || i18next.language || "en"
38+
if (i18next.hasResourceBundle(targetLang, "translation")) {
39+
setIsReady(true)
40+
} else {
41+
// Wait for resources to be loaded
42+
await new Promise<void>((resolve) => {
43+
const checkResources = () => {
44+
if (i18next.hasResourceBundle(targetLang, "translation")) {
45+
i18next.off("loaded", checkResources)
46+
resolve()
47+
}
48+
}
49+
i18next.on("loaded", checkResources)
50+
// Check immediately in case resources are already loaded
51+
if (i18next.hasResourceBundle(targetLang, "translation")) {
52+
i18next.off("loaded", checkResources)
53+
resolve()
54+
}
55+
})
56+
setIsReady(true)
57+
}
58+
} catch (error) {
59+
// If something goes wrong, still set ready to avoid blocking the UI
60+
console.error("Error loading i18next resources:", error)
61+
setIsReady(true)
62+
}
63+
}
64+
65+
ensureResourcesLoaded()
66+
}, [lang])
67+
68+
return isReady
69+
}

src/styles/index.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,7 @@ h2.heading {
419419
.mermaid-diagram > img {
420420
height: auto;
421421
}
422+
423+
.mermaid {
424+
background: transparent;
425+
}

0 commit comments

Comments
 (0)