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.