Web SDKv1.0.0

Next.js, Nuxt, SvelteKit and Astro: mount on the client, tear down cleanly

The package imports cleanly on the server, but a viewer is browser-only. Render the component on the client alone, keep init() in one module, and release both the viewer and the document on unmount.
FrameworksKernstra.init()dynamic(…, { ssr: false }).client.vueviewer.destroy()doc.close()
Nothing touches window until Kernstra.init() or a viewer runs, so importing the package on the server is fine. What must stay on the client is the module whose top level calls init() and the component that builds the viewer.

What you get

  • No viewer is built during server rendering.
  • Kernstra.init() is idempotent: a second call with the same options gets the same promise back, so a remount or hot reload needs no guard.
  • The viewer and the document are released when the person leaves.

1. One module owns init

Every component calls initKernstra() from this module. A second init() call with the same options gets the same promise back (a remount, a new pdfUrl, React's StrictMode); a call with other options rejects with INVALID_ARGUMENT, which is why one module holds them. It only defines a function, so importing it from server-rendered code runs nothing. Read the key the way your framework exposes variables to the browser: import.meta.env.KERNSTRA_LICENSE with Vite, as here (add envPrefix: ['VITE_', 'KERNSTRA_']), process.env.KERNSTRA_LICENSE in Next.js, and in Nuxt a composable instead of this module; the steps below show each.
kernstra.tsTypeScript
import { Kernstra } from '@kernstra/pdf-web-sdk';

export const initKernstra = () =>
  Kernstra.init({ license: import.meta.env.KERNSTRA_LICENSE });

2. Next.js: load the component with ssr: false

Load the component with ssr: false, and call initKernstra() inside an effect, never during render. In the App Router, ssr: false is allowed only in a client component: put the dynamic() call in a file that starts with 'use client' and render it from the page. PdfEditor is the React integration. In kernstra.ts read process.env.KERNSTRA_LICENSE, exposed to the browser by env: { KERNSTRA_LICENSE: process.env.KERNSTRA_LICENSE } in next.config.ts: the key ships to the browser by design. For the render worker, put render.worker.js under public/ and pass workerUrl: '/render.worker.js'.
app/report/PdfEditorClient.tsxTSX
'use client';
import dynamic from 'next/dynamic';

// Render <PdfEditorClient pdfUrl="/report.pdf" /> from app/report/page.tsx.
export const PdfEditorClient = dynamic(() => import('./PdfEditor'), { ssr: false });

3. Nuxt: .client.vue or <ClientOnly>

Give the component the .client.vue suffix (or wrap a plain component in <ClientOnly>), and keep the Vue integration's onMounted / onBeforeUnmount pair. In Nuxt the module that owns init is a composable, app/composables/useKernstra.ts, which reads the key with useRuntimeConfig().public.kernstraLicense, filled from NUXT_PUBLIC_KERNSTRA_LICENSE. Call it in setup and await what it returns in onMounted.
app/pages/report.vueVue
<template>
  <PdfEditor pdf-url="/report.pdf" />
</template>

4. SvelteKit: key the component by the route

onMount never runs on the server, so the Svelte component needs no wrapper. Key it by the route parameter so a client navigation to another id mounts a fresh viewer. SvelteKit 3 replaced the $lib alias with #lib.
src/routes/documents/[id]/+page.svelteSvelte
<script lang="ts">
  import { page } from '$app/state';
  // SvelteKit 2: '$lib/PdfEditor.svelte'
  import PdfEditor from '#lib/PdfEditor.svelte';
</script>

{#key page.params.id}
  <PdfEditor pdfUrl={`/api/documents/${page.params.id}.pdf`} />
{/key}

5. Astro: a client-only island

In Astro, a framework component rendered with client:only is skipped on the server and hydrated on the client alone, which is the same job ssr: false does in Next.js. Use the React, Vue or Svelte integration as the island, with the matching value (client:only="react", "vue" or "svelte"; a .svelte or .vue import keeps its extension). Astro passes only PUBLIC_ variables to the browser, so expose the key in astro.config.mjs with vite: { envPrefix: ['PUBLIC_', 'KERNSTRA_'] } (keep PUBLIC_), or declare it with astro:env (envField.string({ context: 'client', access: 'public' })) and import it from astro:env/client.
src/pages/report.astroAstro
---
import PdfEditor from '../components/PdfEditor';
---

<PdfEditor client:only="react" pdfUrl="/report.pdf" />

6. Tear down in the right order

viewer.destroy() removes everything the viewer added to the page but does not close the underlying PDFDocument: call doc.close() yourself, before or after, or the document's memory is never released. An effect canceled while its document is still opening (StrictMode's first run, a route change) closes the document it opened, since nothing will clean it up later; a loader placed by that run is removed in the cleanup too.
PdfEditor.tsxTSX
return () => {
  canceled = true;
  removeLoader();
  viewer?.destroy();
  void doc?.close();
};

Gotchas

  • A viewer destroyed before it is ready rejects whenReady() with ABORTED, which is no error to report.
  • The ?url import for the render worker is Vite's. A bundler without it (webpack 5, the Angular CLI, esbuild, Rollup) serves the file as a static asset instead.
  • Kernstra.events is kept across Kernstra.destroy(): a handler registered at module level hears the next init()'s initialized without subscribing again.