Next.js
App router + static export. The two pitfalls to know up front, plus a working pattern.
Use Next.js when you want file-based routing, SSG, React server components, or any ecosystem integration.
Next.js does not run on DiLink 5.0 (L5, L5 Ultra, Song Plus). Its static export is modern chunked ES-module JS; the ~2022 Di5.0 WebView can't execute it and the page blanks with no error. Next.js here is L8 (Di5.1) only. Need L5? Use the Build for L5 + L8 classic-bundle path.
Three pitfalls to know up front:
- Di5.0 / L5 is unsupported. Next's
/_next/chunk loader is an ES module; Di5.0 WebViews silently don't run it (see the callout above and Targets matrix). - The host bridge lives on
window. It does not exist during SSR. Any code that reads it must run in a Client Component insideuseEffect/componentDidMount. - For publishing, you want a fully static output (
output: 'export'). The host doesn't run a Next.js server — your mini-app is shipped as bytes to the CDN.
Install
pnpm add i99dashThat's it — one package. The runtime client + types live at the
root entry, the React bindings under the i99dash/react subpath,
the CLI at i99dash/cli, the dev-server at i99dash/dev-server.
The i99dash binary is on $PATH after install.
i99dash/react is optional but pulls its weight: one provider
- one hook per family, with a
fallbackthat handles SSR / no-host out of the box. The plain SDK example below also works.
Static export
next.config.mjs:
export default {
output: 'export',
trailingSlash: true,
images: { unoptimized: true },
};sdk.config.json
{
"appRoot": "./out",
"mocksDir": "./mocks",
"buildCommand": "next build"
}next build with output: 'export' drops the static site in ./out;
the SDK copies that to dist/ and tarballs it.
Client-component pattern
With i99dash/react (recommended)
Mount the provider once at the top of your client tree; every hook below picks it up.
app/Providers.client.tsx:
'use client';
import { useEffect, useState } from 'react';
import { MiniAppProvider } from 'i99dash/react';
import { createClientOrSSR, type MiniAppClient } from 'i99dash';
export function Providers({ children }: { children: React.ReactNode }) {
// Start null on both server and client first paint — keeps the
// hydrated tree byte-identical to SSR. The effect runs once on mount
// and produces the single real client instance for the app's lifetime.
const [client, setClient] = useState<MiniAppClient | null>(null);
useEffect(() => setClient(createClientOrSSR()), []);
return <MiniAppProvider client={client}>{children}</MiniAppProvider>;
}app/layout.tsx:
import { Providers } from './Providers.client';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}app/fuel/Fuel.client.tsx:
'use client';
import { useMiniAppContext } from 'i99dash/react';
import type { MiniAppContext } from 'i99dash';
const fallback = {
userId: '',
activeCarId: '',
locale: 'en',
isDark: false,
appVersion: '0.0.0',
appId: '',
} satisfies MiniAppContext;
export default function Fuel() {
const { data: ctx, loading } = useMiniAppContext({ fallback });
if (loading) return <p>loading…</p>;
return <pre>{JSON.stringify(ctx, null, 2)}</pre>;
}Without i99dash/react (plain SDK)
app/fuel/Fuel.client.tsx:
'use client';
import { useEffect, useState } from 'react';
import { MiniAppClient, type MiniAppContext } from 'i99dash';
export default function Fuel() {
const [ctx, setCtx] = useState<MiniAppContext | null>(null);
useEffect(() => {
const client = MiniAppClient.fromWindow();
client
.getContext()
.then(setCtx)
.catch((e) => console.error(e));
}, []);
if (!ctx) return <p>loading…</p>;
return <pre>{JSON.stringify(ctx, null, 2)}</pre>;
}app/fuel/page.tsx:
import Fuel from './Fuel.client';
export default function Page() {
return <Fuel />;
}Running both dev servers
# terminal 1 — Next.js
pnpm next dev --port 3000
# terminal 2 — i99dash dev pointing at a static build
pnpm next build && pnpm next export
i99dash dev --port 5173Or inline the shim during dev: add
<script src="http://127.0.0.1:5173/_sdk/bridge.js"> to your root
layout behind a process.env.NODE_ENV === 'development' check.
Working example
A complete Next.js 15 app-router project — including driving-state
banner, RTL flip on Arabic context, and a fetch() against a declared
manifest.network origin — lives at
examples/nextjs-example/
in the SDK repo. Clone, pnpm install, pnpm dev.
Common gotchas
| Symptom | Fix |
|---|---|
window is not defined build error | You called MiniAppClient.fromWindow() outside useEffect. Wrap it. |
next build errors with "page is not exportable" | A Server Component imports the SDK runtime. Move the import into a 'use client' file. |
| Hydration mismatch between server-rendered and client | You're rendering context-derived markup before useEffect runs. Render a stable placeholder (null or <p>loading…</p>) on first paint. |
Missing host bridge in dev when running next dev | Use the inline shim trick above, or run i99dash dev against the static export instead. |
| Blank screen on L5 / L5 Ultra / Song Plus (works on L8) | Expected — Next's ES-module output can't run on the Di5.0 WebView. Re-bundle as a classic IIFE per Build for L5 + L8. |
Related
- Best practices — including SSR/CSR splits.
- Local development — fixture grammar.
- Type-only imports — for SSR pages
that only need the shape of
MiniAppContext.