|
8 | 8 | in the form `@databiosphere/findable-ui/lib/<path>`, where `<path>` is the path of the file within the `lib` |
9 | 9 | folder. |
10 | 10 |
|
| 11 | +## Consumer setup for Next.js 16 (Pages Router, static export) |
| 12 | + |
| 13 | +Consuming apps on Next.js 16 must wire up MUI's Emotion cache helpers in `_app.tsx` and `_document.tsx`. Without this wiring, the browser will report React hydration warnings on components styled by MUI + Emotion (which is most of findable-ui). |
| 14 | + |
| 15 | +### Terminology note |
| 16 | + |
| 17 | +This setup involves a package called `@mui/material-nextjs` and a helper called `documentGetInitialProps`. The naming uses "SSR" / "document" terminology because the package targets Next.js generally, including deployments that do run a runtime HTTP server. **It still applies to static-export deployments.** All the work described below runs at `next build` time, during the same step that generates the static `.html` files. There is no runtime server involved. |
| 18 | + |
| 19 | +When the docs say "extract styles on the server," read it as "extract styles during the static HTML build step." |
| 20 | + |
| 21 | +### Why this is needed |
| 22 | + |
| 23 | +- findable-ui components are styled with MUI + Emotion (CSS-in-JS). |
| 24 | +- Emotion generates `css-XXXXXXX` class names by hashing styles in the order they are first encountered during the React render tree walk. |
| 25 | +- Next.js builds the static HTML by running the React tree once during `next build`, then the browser runs the same tree again during hydration. For hydration to succeed, both passes must produce the same class names. |
| 26 | +- Under Next 16 (Turbopack is the default bundler), the module evaluation order during the build can diverge from the order in the browser, so the same component can be assigned a different hash on each pass. React 19 reports this as a hydration mismatch. |
| 27 | +- The fix: capture Emotion's style cache during the build's HTML generation step and inject the resulting `<style>` tags into the static HTML. The browser then hydrates against the exact class names the build wrote. |
| 28 | + |
| 29 | +### Required packages |
| 30 | + |
| 31 | +Install in the consuming project: |
| 32 | + |
| 33 | +```bash |
| 34 | +npm install @mui/material-nextjs @emotion/server |
| 35 | +``` |
| 36 | + |
| 37 | +- `@mui/material-nextjs` — MUI's official integration package; provides the cache provider and the document-head extractor |
| 38 | +- `@emotion/server` — peer dependency of the document helper; used during the build to flush Emotion styles into the HTML |
| 39 | + |
| 40 | +### Opt out of Turbopack (required for now) |
| 41 | + |
| 42 | +Next 16 makes Turbopack the default bundler. Turbopack + Pages Router + MUI is currently broken — Emotion class names diverge between the static HTML and the browser hydration pass, producing the exact hydration mismatch this section is meant to fix. The wiring above is still required, but it does not work under Turbopack. |
| 43 | + |
| 44 | +Until the upstream fix lands ([vercel/next.js#82607](https://github.com/vercel/next.js/issues/82607)), pin every `next dev` / `next build` invocation to webpack: |
| 45 | + |
| 46 | +```jsonc |
| 47 | +// package.json |
| 48 | +"scripts": { |
| 49 | + "dev": "next dev --webpack", |
| 50 | + "build": "next build --webpack" |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +Webpack is still fully supported in Next 16 — Turbopack was promoted to default, not "webpack removed." The deprecation timeline for the webpack fallback hasn't been published. |
| 55 | + |
| 56 | +This is a Next.js / Turbopack bug, not a findable-ui one. Re-enable Turbopack when [vercel/next.js#82607](https://github.com/vercel/next.js/issues/82607) is closed. |
| 57 | + |
| 58 | +### `_app.tsx` |
| 59 | + |
| 60 | +Wrap the app in `AppCacheProvider`: |
| 61 | + |
| 62 | +```tsx |
| 63 | +import { EmotionCache } from "@emotion/react"; |
| 64 | +import { AppCacheProvider } from "@mui/material-nextjs/v16-pagesRouter"; |
| 65 | +import type { AppProps } from "next/app"; |
| 66 | + |
| 67 | +type MyAppProps = AppProps & { |
| 68 | + emotionCache?: EmotionCache; |
| 69 | +}; |
| 70 | + |
| 71 | +function MyApp(props: MyAppProps): JSX.Element { |
| 72 | + const { Component, emotionCache, pageProps } = props; |
| 73 | + return ( |
| 74 | + <AppCacheProvider emotionCache={emotionCache}> |
| 75 | + {/* existing theme providers and layout */} |
| 76 | + <Component {...pageProps} /> |
| 77 | + </AppCacheProvider> |
| 78 | + ); |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +### `_document.tsx` |
| 83 | + |
| 84 | +Add `DocumentHeadTags` inside `<Head>` and assign `documentGetInitialProps` as the static `getInitialProps`: |
| 85 | + |
| 86 | +```tsx |
| 87 | +import { |
| 88 | + documentGetInitialProps, |
| 89 | + DocumentHeadTags, |
| 90 | + DocumentHeadTagsProps, |
| 91 | +} from "@mui/material-nextjs/v16-pagesRouter"; |
| 92 | +import Document, { |
| 93 | + DocumentContext, |
| 94 | + Head, |
| 95 | + Html, |
| 96 | + Main, |
| 97 | + NextScript, |
| 98 | +} from "next/document"; |
| 99 | + |
| 100 | +class MyDocument extends Document<DocumentHeadTagsProps> { |
| 101 | + render(): JSX.Element { |
| 102 | + return ( |
| 103 | + <Html> |
| 104 | + <Head> |
| 105 | + <DocumentHeadTags {...this.props} /> |
| 106 | + {/* other head content */} |
| 107 | + </Head> |
| 108 | + <body> |
| 109 | + <Main /> |
| 110 | + <NextScript /> |
| 111 | + </body> |
| 112 | + </Html> |
| 113 | + ); |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +MyDocument.getInitialProps = async (ctx: DocumentContext) => { |
| 118 | + return await documentGetInitialProps(ctx); |
| 119 | +}; |
| 120 | + |
| 121 | +export default MyDocument; |
| 122 | +``` |
| 123 | + |
| 124 | +### Note on `next-mdx-remote@6` |
| 125 | + |
| 126 | +Consumers using `next-mdx-remote` to render MDX content must pass `blockJS: false` to `serialize()`. Version 6 added a `blockJS: true` default that strips all JavaScript expressions from MDX during compilation — including JSX attribute expressions like `<Breadcrumbs breadcrumbs={[...]} />`. With the default, those props are silently dropped at build time and the receiving component renders with no props. Setting `blockJS: false` preserves expression-valued attributes. The narrower `blockDangerousJS: true` setting (default) still blocks `eval` / `new Function` / etc., so the safety net for actually-dangerous patterns is retained. |
| 127 | + |
| 128 | +```ts |
| 129 | +import { serialize } from "next-mdx-remote/serialize"; |
| 130 | + |
| 131 | +const mdxSource = await serialize(content, { |
| 132 | + blockJS: false, |
| 133 | + // ...your existing options |
| 134 | +}); |
| 135 | +``` |
| 136 | + |
| 137 | +If you use findable-ui's `buildStaticProps` helper (`@databiosphere/findable-ui/lib/utils/mdx/staticGeneration/staticProps`), `blockJS: false` is already the default — no consumer change needed. |
| 138 | + |
11 | 139 | ## Developing findable-ui alongside a consuming app |
12 | 140 |
|
13 | 141 | Use `scripts/link.sh` to build and install a local copy of findable-ui |
|
0 commit comments