Integrate

Next.js (deprecated)

<ComicStrip> behind an App-Router-safe client boundary, plus an SSR placeholder helper, now built into @toonstrip/react and @toonstrip/cli directly.

Deprecated

@toonstrip/next used to wrap two things Next.js App Router needs: a 'use client' boundary and a server-only PNG-placeholder helper. Both now live directly on the packages that do the work, so the wrapper adds nothing on top of them. Existing installs keep working (it’s a re-export shim, not removed); new code should import from @toonstrip/react and @toonstrip/cli instead, as shown below.

<ComicStrip>

// app/page.tsx: a Server Component; no "use client" needed here
import { ComicStrip } from "@toonstrip/react";

<ComicStrip document={doc} packs={["/packs/comic-chat/"]} />

Same props as @toonstrip/react’s <ComicStrip>. The component itself carries a 'use client' directive, the boundary React Server Components require around anything using useRef/useEffect. You can import it directly from a Server Component; you don’t need to mark your own page 'use client'.

SSR placeholder

// app/page.tsx
import { ComicStrip } from "@toonstrip/react";

export default async function Page() {
  // Dynamic + webpackIgnore, not a static import; see the callout below.
  const { renderPlaceholder } = await import(/* webpackIgnore: true */ "@toonstrip/cli");
  const placeholder = await renderPlaceholder(doc, { width: 640 });
  return <ComicStrip document={doc} packs={["/packs/comic-chat/"]} placeholder={placeholder} />;
}

renderPlaceholder reuses @toonstrip/cli’s headless-canvas render path (the same one behind toonstrip render) to produce a PNG on the server, sized in CSS pixels to match the width you give it. <ComicStrip> renders that as a light-DOM <img slot="placeholder"> inside <comic-strip>, present in the raw server HTML with no client JS, so it shows with JavaScript disabled and in “view source”. Once client JS hydrates and the live canvas is ready, @toonstrip/element hides the placeholder and shows the live strip itself; no code drives that swap directly.

Match the width you pass to renderPlaceholder to the CSS width your <ComicStrip> is actually laid out at. A mismatch produces a placeholder sized differently than the live strip, which shows up as a layout shift on hydration.

renderPlaceholder pulls in @toonstrip/cli, which loads @napi-rs/canvas, a native Node addon that must never end up in a client bundle. Only call it from a Server Component or other server-only code.

Warning

@toonstrip/cli’s native dependency needs two things in your app’s own next.config: list @napi-rs/canvas and @toonstrip/cli under serverExternalPackages, and add @toonstrip/cli as your app’s own direct dependency (not only a transitive one). Even with both, a static import of renderPlaceholder still makes webpack try to bundle the native binaries when @toonstrip/cli is a workspace-symlinked local package (as in this monorepo’s own example), hence the webpackIgnored dynamic import() above. See examples/next-app’s next.config.mjs and app/page.tsx for a working setup.

Worked example

examples/next-app in the repo is an App Router app: a Server Component page renders a strip with a placeholder, scripts/copy-pack-assets.mjs copies @toonstrip/pack-comic-chat’s files into public/packs/comic-chat/ for the client-side cast fetch (same script examples/react-vite and examples/vue-vite use), and renderPlaceholder reads the pack straight from node_modules for the server-side render. Run it locally:

pnpm --filter next-app-example build && pnpm --filter next-app-example start

Current limitations

  • Tested against Next.js 15 (App Router) and React 18. Pages Router isn’t covered.
  • No Nuxt-equivalent wrapper yet. See Vue reference’s note on that.

Tip

@toonstrip/element’s module no longer throws when imported on a server. That guard (dev plan §13.4) is what makes <ComicStrip>’s client boundary safe to import from a Server Component in the first place.