diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml
new file mode 100644
index 0000000..1db5b32
--- /dev/null
+++ b/.github/workflows/nextjs.yml
@@ -0,0 +1,48 @@
+name: Next.js PDF starter
+on:
+ pull_request:
+ paths: ['examples/nextjs/**', '.github/workflows/nextjs.yml']
+ push:
+ branches: [main]
+ paths: ['examples/nextjs/**', '.github/workflows/nextjs.yml']
+ workflow_dispatch:
+permissions:
+ contents: read
+concurrency:
+ group: nextjs-${{ github.ref }}
+ cancel-in-progress: true
+jobs:
+ standalone:
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - { os: ubuntu-latest, node: '22' }
+ - { os: ubuntu-latest, node: '24' }
+ - { os: windows-latest, node: '24' }
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 15
+ defaults:
+ run:
+ working-directory: examples/nextjs
+ env:
+ NEXT_TELEMETRY_DISABLED: '1'
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v6
+ with:
+ node-version: ${{ matrix.node }}
+ cache: npm
+ cache-dependency-path: examples/nextjs/package-lock.json
+ - run: npm ci --ignore-scripts --no-audit --no-fund
+ - run: npm run preview:pdf
+ - run: npm run build
+ - run: npm run verify
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: nextjs-${{ matrix.os }}-${{ matrix.node }}
+ path: |
+ examples/nextjs/output/
+ examples/nextjs/public/invoice.png
+ retention-days: 14
diff --git a/README.md b/README.md
index 0bc7b0a..9525687 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,14 @@ npm run example
Open `output/invoice/invoice.pdf` and `page-1.png`. The [three-page report
sources](examples/report.html) demonstrate pagination, tables, and vector artwork.
+## Use it in Next.js
+
+For a web application, start with the [Next.js PDF download example](examples/nextjs).
+It includes a designed invoice, an App Router handler, production build settings
+for the worker and bundled assets, and a standalone-server verification script.
+The route bounds rendering and returns private PDF attachments from fictional
+data; connect your own authenticated record lookup when adapting it.
+
## Fonts and assets
Supply file bytes explicitly. Additional fonts are registered alongside the
diff --git a/examples/nextjs/.gitignore b/examples/nextjs/.gitignore
new file mode 100644
index 0000000..ec3b8a6
--- /dev/null
+++ b/examples/nextjs/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+.next/
+output/
+.env*.local
diff --git a/examples/nextjs/LICENSE b/examples/nextjs/LICENSE
new file mode 100644
index 0000000..4c99bbd
--- /dev/null
+++ b/examples/nextjs/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Fullbleed Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/examples/nextjs/README.md b/examples/nextjs/README.md
new file mode 100644
index 0000000..aab6ac9
--- /dev/null
+++ b/examples/nextjs/README.md
@@ -0,0 +1,101 @@
+# Fullbleed PDF downloads in Next.js
+
+A runnable App Router starter that serves a designed invoice from a Node.js
+route. It includes the HTML/CSS, an actual PDF preview, and an isolated
+standalone-server check. All invoice data is fictional; the public fixture
+lookup is not an authentication system.
+
+## Run it
+
+Use Node.js 22 or newer. From this directory:
+
+```sh
+npm ci --ignore-scripts
+npm run build
+npm start
+```
+
+Open `http://127.0.0.1:3000` and select **Download sample PDF**. The route
+`/api/invoices/NS-1042` returns the one-page invoice as an attachment. Unknown
+IDs return 404. Use `npm run dev` while editing the app. `PORT` changes the port;
+`FULLBLEED_HOST` changes the production server's loopback binding.
+
+The lockfile pins Next.js 16.3.8, React 19.3.0 and the released Fullbleed Node
+0.1.1 tarball (engine 2.5.5). Fullbleed does not yet have a configured npm
+registry publisher; the dependency uses the verified GitHub release URL.
+Installing this starter needs no Python, Rust, system fonts or browser renderer.
+
+## Adapt the document
+
+Edit `templates/invoice.html` and `templates/invoice.css`. The sample uses
+bundled Inter, DM Serif Display and Bebas Neue. `lib/invoice.mjs` inserts an
+escaped customer name from the fictional fixture. Amounts and dates are sample
+content, not an invoicing ledger or tax calculation.
+
+After changing the template, regenerate and inspect the PDF and preview:
+
+```sh
+npm run preview:pdf
+```
+
+Open `output/invoice.pdf` and `public/invoice.png`, then rebuild the app. The
+checked-in image is output from Fullbleed, not a browser screenshot or mockup.
+
+## Keep the renderer on the server
+
+The route declares `runtime = 'nodejs'`. It uses filesystem reads, worker
+threads and WebAssembly, so this integration needs the Node runtime.
+`serverExternalPackages: ['fullbleed']` keeps Next.js from bundling the engine's
+Node entrypoint. Explicit output tracing includes the worker, WASM, fonts,
+WASI dependency and template files needed by the standalone server.
+See Next.js's [external-package configuration](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages)
+and [standalone output documentation](https://nextjs.org/docs/app/api-reference/config/next-config-js/output).
+
+`npm run build` copies the public preview and browser chunks into
+`.next/standalone`, after Next generates it. That folder is the deployment
+artifact. Start its `server.js` with Node on a host that supports workers and
+WebAssembly; set the host's normal `PORT` and `HOSTNAME` environment variables.
+The local start script defaults to loopback. Hosting-provider limits and
+serverless or Edge deployments have not been verified by this starter.
+
+## Adapt the route for real accounts
+
+Replace `getDemoInvoice(id)` with a session-authenticated lookup that verifies
+the invoice belongs to that account **before rendering**. Keep order fields
+escaped. Never accept arbitrary HTML, filesystem paths or remote asset URLs
+from this public endpoint.
+
+The example renders at most one document at a time per server process. Extra
+requests receive `503 BUSY` and `Retry-After: 2`; they do not enter an unbounded
+queue. Successful PDFs have `private, no-store`, a fixed filename, a complete
+content length and `nosniff`. Jobs have a 15-second rendering deadline and a
+five-page cap, and receive the request's abort signal. No generated PDF is
+written to application storage by the route.
+
+Use shared rate/usage controls for multiple processes or replicas. Measure your
+host's memory and CPU before increasing capacity; each render worker can grow
+to the package's 512 MiB WASM ceiling. Private document delivery also requires
+your application's normal access controls, monitoring and retention policies.
+
+## Verify the production artifact
+
+After building:
+
+```sh
+npm run verify
+```
+
+The check copies the standalone artifact to a fresh temporary directory outside
+the app and starts it on an available loopback port. It compares a real HTTP
+PDF to the direct engine output, checks headers and missing IDs, sends a
+concurrent burst, then verifies recovery from actual missing-glyph and page-cap
+failures. Client JavaScript is checked for accidental engine/worker inclusion.
+Results and the downloaded PDF are retained under `output/`.
+
+Temporary artifact directories use the `fullbleed-nextjs-` prefix and are kept
+for inspection. The verification script closes its own server when it finishes.
+These checks cover a fictional fixture; they do not establish application
+authorization, PDF standards compliance, or a hosting provider's limits.
+
+[Fullbleed Node API](https://github.com/fullbleed-engine/fullbleed-node) ·
+[Documentation](https://docs.fullbleed.dev/) · [MIT license](LICENSE)
diff --git a/examples/nextjs/app/api/invoices/[id]/route.js b/examples/nextjs/app/api/invoices/[id]/route.js
new file mode 100644
index 0000000..28ec29a
--- /dev/null
+++ b/examples/nextjs/app/api/invoices/[id]/route.js
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: MIT
+import { renderPdf, FullbleedError } from 'fullbleed';
+import { getDemoInvoice, invoiceTemplate } from '../../../../lib/invoice.mjs';
+
+export const runtime = 'nodejs';
+export const dynamic = 'force-dynamic';
+
+// One active render per process; there is no unbounded waiting queue.
+// Add authentication and a shared rate/usage limit for a deployed application.
+let active = 0;
+const privateHeaders = {
+ 'Cache-Control': 'private, no-store',
+ 'X-Content-Type-Options': 'nosniff',
+};
+const errorResponse = (status, code, message, extra = {}) => Response.json(
+ { error: { code, message } }, { status, headers: { ...privateHeaders, ...extra } },
+);
+
+export async function GET(request, { params }) {
+ const { id } = await params;
+ const invoice = getDemoInvoice(id);
+ if (!invoice) return errorResponse(404, 'NOT_FOUND', 'Sample invoice not found.');
+ if (active >= 1) return errorResponse(503, 'BUSY', 'A document is being prepared. Try again shortly.', { 'Retry-After': '2' });
+ active++;
+ try {
+ const template = await invoiceTemplate(invoice);
+ const { pdf } = await renderPdf({ ...template, maxPages: 5, timeoutMs: 15_000, signal: request.signal });
+ return new Response(pdf, {
+ headers: {
+ ...privateHeaders,
+ 'Content-Type': 'application/pdf',
+ 'Content-Disposition': `attachment; filename="invoice-${invoice.id}.pdf"`,
+ 'Content-Length': String(pdf.length),
+ },
+ });
+ } catch (error) {
+ const code = error instanceof FullbleedError ? error.code : 'UNEXPECTED';
+ // Log an operational code only; never log customer data or document input.
+ console.error('Fullbleed invoice render failed:', code);
+ if (code === 'TIMEOUT') return errorResponse(504, code, 'Document preparation timed out.');
+ if (code === 'ABORTED') return errorResponse(408, code, 'Document preparation was cancelled.');
+ return errorResponse(500, 'RENDER_FAILED', 'The document could not be prepared.');
+ } finally {
+ active--;
+ }
+}
diff --git a/examples/nextjs/app/globals.css b/examples/nextjs/app/globals.css
new file mode 100644
index 0000000..04bdfa4
--- /dev/null
+++ b/examples/nextjs/app/globals.css
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: MIT */
+:root { color-scheme: light; --ink: #203b32; --muted: #627169; --paper: #f7f5ee; --accent: #bb4e2c; }
+* { box-sizing: border-box; }
+body { margin: 0; background: var(--paper); color: var(--ink); font: 16px/1.6 system-ui, sans-serif; }
+a { color: inherit; text-underline-offset: .2em; }
+.nav, main, footer { max-width: 1240px; margin: auto; padding-left: 40px; padding-right: 40px; }
+.nav { min-height: 104px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #cdd2c8; }
+.wordmark { font-weight: 850; font-size: 30px; letter-spacing: -1.5px; text-decoration: none; }
+.wordmark span { color: var(--accent); }
+.eyebrow { font-size: 10px; font-weight: 750; letter-spacing: 2px; }
+.hero { display: grid; grid-template-columns: 1fr .88fr; align-items: center; gap: 76px; padding: 62px 0 76px; }
+h1 { font: 56px/1.06 Georgia, serif; letter-spacing: -2px; margin: 22px 0; }
+h1 em { color: var(--accent); font-weight: 400; }
+.lede { max-width: 430px; color: var(--muted); font-size: 17px; line-height: 1.7; margin-bottom: 30px; }
+.button { display: inline-flex; justify-content: space-between; gap: 38px; align-items: center; background: var(--ink); color: white; padding: 15px 22px; font-weight: 650; text-decoration: none; }
+.button:hover { background: #345949; }
+a:focus-visible { outline: 3px solid var(--accent); outline-offset: 5px; }
+.caption { font-size: 11px; color: var(--muted); margin-top: 13px; }
+.note { border-top: 1px solid #cdd2c8; max-width: 430px; padding-top: 28px; margin-top: 45px; }
+.note span, .steps article > span { font-size: 10px; font-weight: 750; letter-spacing: 1.3px; }
+.note p { font-size: 13px; color: var(--muted); margin-bottom: 0; }
+.paper { margin: 0; padding: 18px 18px 0; background: #dedfd3; border: 1px solid #cdd2c8; }
+.paper img { display: block; width: 100%; height: auto; box-shadow: 0 7px 18px #203b3214; }
+.paper figcaption { font-size: 10px; text-align: center; padding: 12px 0; color: #4b5d51; }
+.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 42px; padding: 35px 0 52px; border-top: 1px solid #cdd2c8; }
+.steps article > span { color: var(--accent); }
+.steps h2 { font-size: 17px; line-height: 1.4; margin: 12px 0; }
+.steps p { color: var(--muted); font-size: 13px; }
+footer { border-top: 1px solid #cdd2c8; display: flex; gap: 28px; padding-top: 24px; padding-bottom: 28px; font-size: 11px; }
+footer span { margin-right: auto; }
+@media (max-width: 850px) { .hero { gap: 30px; } h1 { font-size: 43px; } }
+@media (max-width: 650px) {
+ .nav, main, footer { padding-left: 22px; padding-right: 22px; }
+ .nav { min-height: 82px; } .hero { grid-template-columns: 1fr; gap: 35px; padding: 36px 0; }
+ h1 { font-size: 44px; } .lede { font-size: 16px; } .note { margin-top: 30px; padding-top: 20px; }
+ .steps { grid-template-columns: 1fr; gap: 24px; } .steps p { margin-bottom: 0; }
+ footer { flex-wrap: wrap; gap: 14px 25px; } footer span { width: 100%; }
+}
diff --git a/examples/nextjs/app/layout.js b/examples/nextjs/app/layout.js
new file mode 100644
index 0000000..4e3b2d8
--- /dev/null
+++ b/examples/nextjs/app/layout.js
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: MIT
+import './globals.css';
+
+export const metadata = {
+ title: 'A better PDF download | Fullbleed + Next.js',
+ description: 'A runnable Next.js starter serving a designed invoice with Fullbleed. Fictional sample data.',
+};
+
+export default function Layout({ children }) {
+ return
{children};
+}
diff --git a/examples/nextjs/app/page.js b/examples/nextjs/app/page.js
new file mode 100644
index 0000000..06c6c33
--- /dev/null
+++ b/examples/nextjs/app/page.js
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT
+export default function Page() {
+ return <>
+
+
+
+
+
A SMALL ROUTE. A BETTER DOCUMENT.
+
Make the downloadworth opening.
+
Serve a designed PDF from your Next.js app. Start with this studio invoice, then make the HTML and CSS your own.
+
Download sample PDF ↗
+
One page · Fictional invoice NS-1042 · No sign-in
+
YOUR APP. YOUR DOCUMENTS. The PDF is generated on your Node.js server with bundled fonts. Your HTML and CSS define the printed page.
+
+ Actual PDF output · Northstar Studio
+
+
+ 01 / DESIGN Keep your HTML & CSS. Edit the included template. Use the bundled typefaces or supply your own font files.
+ 02 / CONNECT Use your application data. The sample route looks up a fictional invoice. Connect your own authorized record lookup when adapting it.
+ 03 / DELIVER Send a real PDF. A normal download link calls the server route. Rendering has a page limit, deadline and capacity limit.
+
+
+
+ >;
+}
diff --git a/examples/nextjs/lib/invoice.mjs b/examples/nextjs/lib/invoice.mjs
new file mode 100644
index 0000000..b112f39
--- /dev/null
+++ b/examples/nextjs/lib/invoice.mjs
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT
+import { readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+
+// Fictional public fixture, not an authorization implementation. In your app,
+// replace this with an authenticated lookup scoped to the requesting account.
+const invoices = new Map([
+ ['NS-1042', { id: 'NS-1042', customer: 'Maple & Finch' }],
+]);
+
+export function getDemoInvoice(id) {
+ return invoices.get(id);
+}
+
+const escapeHtml = value => String(value).replace(/[&<>"']/g, char => ({
+ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
+})[char]);
+
+export async function invoiceTemplate(invoice) {
+ const [html, css] = await Promise.all([
+ readFile(join(process.cwd(), 'templates/invoice.html'), 'utf8'),
+ readFile(join(process.cwd(), 'templates/invoice.css'), 'utf8'),
+ ]);
+ return { html: html.replace('{{customer}}', () => escapeHtml(invoice.customer)), css };
+}
diff --git a/examples/nextjs/next.config.mjs b/examples/nextjs/next.config.mjs
new file mode 100644
index 0000000..5cfa072
--- /dev/null
+++ b/examples/nextjs/next.config.mjs
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT
+import { fileURLToPath } from 'node:url';
+
+const root = fileURLToPath(new URL('.', import.meta.url));
+
+export default {
+ output: 'standalone',
+ poweredByHeader: false,
+ serverExternalPackages: ['fullbleed'],
+ outputFileTracingRoot: root,
+ turbopack: { root },
+ outputFileTracingIncludes: {
+ '/api/invoices/*': [
+ './templates/**/*',
+ './node_modules/fullbleed/**/*',
+ './node_modules/@bjorn3/browser_wasi_shim/**/*',
+ ],
+ },
+};
diff --git a/examples/nextjs/package-lock.json b/examples/nextjs/package-lock.json
new file mode 100644
index 0000000..02fcffa
--- /dev/null
+++ b/examples/nextjs/package-lock.json
@@ -0,0 +1,1035 @@
+{
+ "name": "fullbleed-nextjs-starter",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "fullbleed-nextjs-starter",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "fullbleed": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.1.1/fullbleed-0.1.1.tgz",
+ "next": "16.3.8",
+ "react": "19.3.0",
+ "react-dom": "19.3.0"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@bjorn3/browser_wasi_shim": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.4.2.tgz",
+ "integrity": "sha512-/iHkCVUG3VbcbmEHn5iIUpIrh7a7WPiwZ3sHy4HZKZzBdSadwdddYDZAII2zBvQYV0Lfi8naZngPCN7WPHI/hA==",
+ "license": "MIT OR Apache-2.0"
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz",
+ "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz",
+ "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-freebsd-wasm32": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz",
+ "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.5"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz",
+ "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz",
+ "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz",
+ "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz",
+ "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz",
+ "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz",
+ "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz",
+ "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz",
+ "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz",
+ "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz",
+ "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz",
+ "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz",
+ "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz",
+ "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz",
+ "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz",
+ "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz",
+ "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz",
+ "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz",
+ "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz",
+ "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==",
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.11.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-webcontainers-wasm32": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz",
+ "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.5"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz",
+ "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz",
+ "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz",
+ "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@next/env": {
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.8.tgz",
+ "integrity": "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==",
+ "license": "MIT"
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.8.tgz",
+ "integrity": "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.8.tgz",
+ "integrity": "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.8.tgz",
+ "integrity": "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.8.tgz",
+ "integrity": "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.8.tgz",
+ "integrity": "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.8.tgz",
+ "integrity": "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.8.tgz",
+ "integrity": "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.8.tgz",
+ "integrity": "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.23",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
+ "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.27",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz",
+ "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001814",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz",
+ "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/client-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fullbleed": {
+ "version": "0.1.1",
+ "resolved": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.1.1/fullbleed-0.1.1.tgz",
+ "integrity": "sha512-kWVXlLp0ZYlXRcaiOg0GMJyM8ll0+1R1dV5LokzaoBZwzsNZ2Lz84g5ZH0/PvIN3kFd5An4ZvwGKn5VUR00TtA==",
+ "license": "MIT",
+ "dependencies": {
+ "@bjorn3/browser_wasi_shim": "0.4.2"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.19",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
+ "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/next": {
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.3.8.tgz",
+ "integrity": "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@next/env": "16.3.8",
+ "@swc/helpers": "0.5.23",
+ "baseline-browser-mapping": "^2.9.19",
+ "caniuse-lite": "^1.0.30001579",
+ "postcss": "8.5.23",
+ "styled-jsx": "5.1.6"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "16.3.8",
+ "@next/swc-darwin-x64": "16.3.8",
+ "@next/swc-linux-arm64-gnu": "16.3.8",
+ "@next/swc-linux-arm64-musl": "16.3.8",
+ "@next/swc-linux-x64-gnu": "16.3.8",
+ "@next/swc-linux-x64-musl": "16.3.8",
+ "@next/swc-win32-arm64-msvc": "16.3.8",
+ "@next/swc-win32-x64-msvc": "16.3.8",
+ "sharp": "^0.35.4"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "@playwright/test": "^1.51.1",
+ "babel-plugin-react-compiler": "*",
+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+ "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@playwright/test": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
+ "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.16",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
+ "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
+ "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.28.0"
+ },
+ "peerDependencies": {
+ "react": "^19.3.0"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
+ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.35.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz",
+ "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/colour": "^1.1.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.8.5"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.35.5",
+ "@img/sharp-darwin-x64": "0.35.5",
+ "@img/sharp-freebsd-wasm32": "0.35.5",
+ "@img/sharp-libvips-darwin-arm64": "1.3.4",
+ "@img/sharp-libvips-darwin-x64": "1.3.4",
+ "@img/sharp-libvips-linux-arm": "1.3.4",
+ "@img/sharp-libvips-linux-arm64": "1.3.4",
+ "@img/sharp-libvips-linux-ppc64": "1.3.4",
+ "@img/sharp-libvips-linux-riscv64": "1.3.4",
+ "@img/sharp-libvips-linux-s390x": "1.3.4",
+ "@img/sharp-libvips-linux-x64": "1.3.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.4",
+ "@img/sharp-linux-arm": "0.35.5",
+ "@img/sharp-linux-arm64": "0.35.5",
+ "@img/sharp-linux-ppc64": "0.35.5",
+ "@img/sharp-linux-riscv64": "0.35.5",
+ "@img/sharp-linux-s390x": "0.35.5",
+ "@img/sharp-linux-x64": "0.35.5",
+ "@img/sharp-linuxmusl-arm64": "0.35.5",
+ "@img/sharp-linuxmusl-x64": "0.35.5",
+ "@img/sharp-webcontainers-wasm32": "0.35.5",
+ "@img/sharp-win32-arm64": "0.35.5",
+ "@img/sharp-win32-ia32": "0.35.5",
+ "@img/sharp-win32-x64": "0.35.5"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/styled-jsx": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
+ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
+ "license": "MIT",
+ "dependencies": {
+ "client-only": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ }
+ }
+}
diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json
new file mode 100644
index 0000000..238786c
--- /dev/null
+++ b/examples/nextjs/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "fullbleed-nextjs-starter",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "license": "MIT",
+ "engines": { "node": ">=22" },
+ "scripts": {
+ "dev": "next dev --hostname 127.0.0.1",
+ "build": "next build",
+ "postbuild": "node tools/copy-public.mjs",
+ "start": "node tools/start.mjs",
+ "preview:pdf": "node tools/render-preview.mjs",
+ "verify": "node tools/verify.mjs"
+ },
+ "dependencies": {
+ "fullbleed": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.1.1/fullbleed-0.1.1.tgz",
+ "next": "16.3.8",
+ "react": "19.3.0",
+ "react-dom": "19.3.0"
+ }
+}
diff --git a/examples/nextjs/public/invoice.png b/examples/nextjs/public/invoice.png
new file mode 100644
index 0000000..82d648f
Binary files /dev/null and b/examples/nextjs/public/invoice.png differ
diff --git a/examples/nextjs/templates/invoice.css b/examples/nextjs/templates/invoice.css
new file mode 100644
index 0000000..235023f
--- /dev/null
+++ b/examples/nextjs/templates/invoice.css
@@ -0,0 +1,48 @@
+@page { size: A4; margin: 0; }
+* { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; }
+body { font-family: Inter; font-size: 10pt; line-height: 1.45; color: #17382e; }
+h1, h2, h3, p, figure { margin: 0; }
+h1, h2, h3 { font-weight: 400; }
+.page { width: 210mm; height: 296.9mm; padding: 15mm 16mm; position: relative; break-after: page; background: #f8f5ec; }
+.page:last-child { break-after: auto; }
+.kicker { font-size: 7.2pt; font-weight: 600; letter-spacing: 1.4pt; text-transform: uppercase; }
+.serif { font-family: 'DM Serif Display'; font-weight: 400; }
+.display { font-family: 'Bebas Neue'; font-weight: 400; }
+.muted { color: #5b6c64; }
+.small { font-size: 8pt; }
+.rule { border-top: .6pt solid #9eafa4; }
+.row { display: flex; justify-content: space-between; gap: 16pt; }
+.grid-2 { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 24pt; }
+.grid-2 > * { min-width: 0; }
+.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16pt; }
+.footer { position: absolute; left: 16mm; right: 16mm; bottom: 12mm; border-top: .6pt solid #9eafa4; padding-top: 8pt; display: flex; justify-content: space-between; gap: 12pt; font-size: 6.6pt; letter-spacing: .45pt; }
+table { width: 100%; border-collapse: collapse; font-size: 9pt; }
+th { font-size: 7.5pt; font-weight: 600; text-align: left; }
+th, td { padding: 10pt 0; border-bottom: .55pt solid #bdc8bf; vertical-align: top; }
+.number { text-align: right; white-space: nowrap; }
+.money { font-variant-numeric: tabular-nums; }
+strong { font-weight: 600; }
+a { color: inherit; }
+
+.invoice .masthead { display: grid; grid-template-columns: 1.05fr .8fr 1fr; gap: 18pt; align-items: center; padding-bottom: 22pt; border-bottom: 1pt solid #17382e; }
+.invoice .brand { font: 31pt/.88 'DM Serif Display'; letter-spacing: -1.3pt; }
+.invoice .brand span { display: block; line-height: .98; }
+.invoice .brand small { display: block; font: 6.5pt Inter; letter-spacing: 2.1pt; margin-top: 11pt; }
+.invoice .brand-note { border-left: 1pt solid #c24929; padding: 4pt 0 4pt 16pt; line-height: 1.8; }
+.invoice .address { font-size: 7.5pt; line-height: 1.7; border-left: 1pt solid #c24929; padding-left: 16pt; }
+.invoice .title-row { display: grid; grid-template-columns: 3.3fr 1fr; gap: 20pt; align-items: center; padding: 22pt 0 16pt; }
+.invoice h1 { font: 91pt/.9 'Bebas Neue'; letter-spacing: .3pt; }
+.invoice .title-note { color: #b44123; font: 14pt/1.2 'Bebas Neue'; letter-spacing: .5pt; }
+.invoice .details { border-top: .6pt solid #9eafa4; border-bottom: .6pt solid #9eafa4; padding: 15pt 0; margin-bottom: 10pt; }
+.invoice .details p { margin-top: 5pt; font-size: 8.2pt; }
+.invoice .details > div + div { border-left: .6pt solid #c7cdc4; padding-left: 14pt; }
+.invoice th { color: #52665a; }
+.invoice td { padding: 12pt 0; }
+.invoice .item-note { color: #617069; font-size: 8pt; margin-top: 4pt; }
+.invoice .amounts { display: grid; grid-template-columns: 1fr 1.25fr; gap: 27pt; margin-top: 23pt; }
+.invoice .thanks { font: italic 22pt/1.2 'DM Serif Display'; color: #b44123; padding-top: 8pt; }
+.invoice .summary-line { display: flex; justify-content: space-between; padding: 5pt 0; font-size: 8.5pt; }
+.invoice .total { background: #17382e; color: #fff8e8; padding: 16pt; margin-top: 9pt; border-left: 4pt solid #d96238; }
+.invoice .total .value { font: 38pt/1.1 'Bebas Neue'; margin-top: 7pt; }
+.invoice .payment { margin-top: 20pt; padding-top: 12pt; border-top: .6pt solid #9eafa4; display: grid; grid-template-columns: 1fr 1fr; gap: 24pt; font-size: 8pt; }
diff --git a/examples/nextjs/templates/invoice.html b/examples/nextjs/templates/invoice.html
new file mode 100644
index 0000000..bb0d7e9
--- /dev/null
+++ b/examples/nextjs/templates/invoice.html
@@ -0,0 +1,12 @@
+Northstar Studio — Invoice NS-1042
+
+
+INVOICE GOOD WORK. CLEAR DETAILS. LET'S MAKE WHAT'S NEXT.
+Prepared for {{customer}} Attn. Avery Morgan Portland, Oregon
The project Autumn launch Brand workshop, digital design & handoff
Invoice NS-1042 Issued · 01 October 2026 Due · 31 October 2026 Currency · USD
+DESCRIPTION HOURS RATE AMOUNT
+Discovery & brand workshop Positioning, priorities, and a clear creative direction
8 $125.00 $1,000.00 Digital design & implementation A cohesive visual system, from layout to launch
6 $95.00 $570.00 Review & studio handoff Final refinements, documentation, and delivery
2 $150.00 $300.00
+
+A little clarity. A lot of possibility.
Thank you for inviting us to help shape your next chapter.
Subtotal $1,870.00
Tax · 0% in this sample $0.00
+Payment details Reference NS-1042 with your payment. Payment instructions are supplied separately.
A note from the studio Questions about a line item? Write to hello@example.invalid.
+
+
diff --git a/examples/nextjs/tools/copy-public.mjs b/examples/nextjs/tools/copy-public.mjs
new file mode 100644
index 0000000..3fa2158
--- /dev/null
+++ b/examples/nextjs/tools/copy-public.mjs
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: MIT
+import { cp } from 'node:fs/promises';
+// Next's standalone output does not copy public files or browser chunks itself.
+await cp('public', '.next/standalone/public', { recursive: true });
+await cp('.next/static', '.next/standalone/.next/static', { recursive: true });
diff --git a/examples/nextjs/tools/render-preview.mjs b/examples/nextjs/tools/render-preview.mjs
new file mode 100644
index 0000000..d203792
--- /dev/null
+++ b/examples/nextjs/tools/render-preview.mjs
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: MIT
+import { mkdir, writeFile } from 'node:fs/promises';
+import { createHash } from 'node:crypto';
+import { renderPdf, version } from 'fullbleed';
+import { getDemoInvoice, invoiceTemplate } from '../lib/invoice.mjs';
+
+const result = await renderPdf({ ...await invoiceTemplate(getDemoInvoice('NS-1042')), previewDpi: 96, maxPages: 5, timeoutMs: 15_000 });
+if (result.pages !== 1 || result.missingGlyphs !== 0) throw new Error('Review the sample layout before publishing.');
+await mkdir('public', { recursive: true });
+await mkdir('output', { recursive: true });
+await writeFile('public/invoice.png', result.previews[0]);
+await writeFile('output/invoice.pdf', result.pdf);
+const hash = value => createHash('sha256').update(value).digest('hex');
+const record = { nodePackage: version, engineVersion: result.engineVersion, pages: result.pages, missingGlyphs: result.missingGlyphs, pdfSha256: hash(result.pdf), previewSha256: hash(result.previews[0]) };
+await writeFile('output/preview.json', JSON.stringify(record, null, 2) + '\n');
+console.log(JSON.stringify(record));
diff --git a/examples/nextjs/tools/start.mjs b/examples/nextjs/tools/start.mjs
new file mode 100644
index 0000000..59b410f
--- /dev/null
+++ b/examples/nextjs/tools/start.mjs
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: MIT
+import { resolve } from 'node:path';
+import { pathToFileURL } from 'node:url';
+
+process.env.HOSTNAME = process.env.FULLBLEED_HOST || '127.0.0.1';
+process.env.NODE_ENV = 'production';
+await import(pathToFileURL(resolve('.next/standalone/server.js')).href);
diff --git a/examples/nextjs/tools/verify.mjs b/examples/nextjs/tools/verify.mjs
new file mode 100644
index 0000000..3ca0671
--- /dev/null
+++ b/examples/nextjs/tools/verify.mjs
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: MIT
+// Exercise the production artifact with fictional data, outside the source tree.
+import assert from 'node:assert/strict';
+import { spawn } from 'node:child_process';
+import { createHash } from 'node:crypto';
+import { cp, mkdir, mkdtemp, readFile, readdir, writeFile } from 'node:fs/promises';
+import { createServer } from 'node:net';
+import { tmpdir, platform } from 'node:os';
+import { join, resolve } from 'node:path';
+import { setTimeout as delay } from 'node:timers/promises';
+import { renderPdf, version, engineVersion } from 'fullbleed';
+import { getDemoInvoice, invoiceTemplate } from '../lib/invoice.mjs';
+
+const checks = [];
+const check = (name, condition) => { assert.ok(condition, name); checks.push({ name, passed: true }); console.log(`${name}: passed`); };
+const hash = data => createHash('sha256').update(data).digest('hex');
+const project = process.cwd();
+const folder = await mkdtemp(join(tmpdir(), 'fullbleed-nextjs-'));
+await mkdir('output', { recursive: true });
+await cp('.next/standalone', folder, { recursive: true, dereference: true });
+await cp('public', join(folder, 'public'), { recursive: true });
+await cp('.next/static', join(folder, '.next/static'), { recursive: true });
+check('standalone artifact is isolated from the source application', !resolve(folder).startsWith(resolve(project)));
+const manifest = JSON.parse(await readFile(join(folder, 'node_modules/fullbleed/dist/build.json'), 'utf8'));
+for (const [name, expected] of Object.entries(manifest.files)) {
+ const bytes = await readFile(join(folder, 'node_modules/fullbleed', name));
+ assert.equal(hash(bytes), expected.sha256, name);
+}
+check('standalone tracing retains the verified engine and all bundled fonts', manifest.engineVersion === engineVersion);
+
+const socket = createServer();
+await new Promise(done => socket.listen(0, '127.0.0.1', done));
+const port = socket.address().port;
+await new Promise(done => socket.close(done));
+const base = `http://127.0.0.1:${port}`;
+const child = spawn(process.execPath, ['server.js'], { cwd: folder, windowsHide: true, env: { ...process.env, NODE_ENV: 'production', NEXT_TELEMETRY_DISABLED: '1', HOSTNAME: '127.0.0.1', PORT: String(port) }, stdio: ['ignore', 'pipe', 'pipe'] });
+let logs = '';
+for (const stream of [child.stdout, child.stderr]) stream.on('data', data => { logs = (logs + data.toString()).slice(-50000); });
+const stopped = new Promise((done, reject) => { child.once('error', reject); child.once('close', (code, signal) => done({ code, signal })); });
+stopped.catch(() => {});
+const stop = async () => { if (child.exitCode === null && child.signalCode === null) child.kill(); await stopped; };
+const get = (path, options = {}) => fetch(base + path, { ...options, signal: AbortSignal.timeout(25000) });
+let serving = false;
+try {
+ let ready = false;
+ for (let attempt = 0; attempt < 120; attempt++) {
+ if (child.exitCode !== null) throw new Error(`Standalone server stopped.\n${logs}`);
+ try { ready = (await get('/')).status === 200; } catch {}
+ if (ready) break;
+ await delay(250);
+ }
+ check('isolated production server starts on loopback', ready);
+ const html = await (await get('/')).text();
+ check('app exposes the sample download without a prefetched render', html.includes('Download sample PDF') && html.includes('href="/api/invoices/NS-1042"') && !html.includes('rel="prefetch"'));
+ const image = await get('/invoice.png');
+ check('standalone server includes the actual PDF preview asset', image.status === 200 && hash(Buffer.from(await image.arrayBuffer())) === hash(await readFile('public/invoice.png')));
+ const expected = await renderPdf({ ...await invoiceTemplate(getDemoInvoice('NS-1042')), maxPages: 5, timeoutMs: 15000 });
+ const url = '/api/invoices/NS-1042';
+ async function pdfResponse(response) {
+ const bytes = Buffer.from(await response.arrayBuffer());
+ assert.equal(response.status, 200);
+ assert.equal(response.headers.get('content-type'), 'application/pdf');
+ assert.equal(response.headers.get('content-disposition'), 'attachment; filename="invoice-NS-1042.pdf"');
+ assert.equal(response.headers.get('content-length'), String(bytes.length));
+ assert.match(response.headers.get('cache-control'), /private.*no-store/);
+ assert.equal(response.headers.get('x-content-type-options'), 'nosniff');
+ assert.equal(hash(bytes), hash(expected.pdf));
+ return bytes;
+ }
+ const bytes = await pdfResponse(await get(url));
+ await writeFile('output/route.pdf', bytes);
+ check('production route returns complete private PDF bytes with a useful filename', true);
+ const missing = await get('/api/invoices/NS-9999');
+ check('unknown invoice IDs return a private JSON error', missing.status === 404 && /no-store/.test(missing.headers.get('cache-control')) && (await missing.json()).error.code === 'NOT_FOUND');
+ check('unsupported writes are rejected by the route', (await get(url, { method: 'POST', body: '{}' })).status === 405);
+
+ const burst = await Promise.all(Array.from({ length: 12 }, async () => {
+ const response = await get(url);
+ if (response.status === 200) { await pdfResponse(response); return 200; }
+ assert.equal(response.status, 503);
+ assert.equal(response.headers.get('retry-after'), '2');
+ assert.match(response.headers.get('cache-control'), /no-store/);
+ assert.equal((await response.json()).error.code, 'BUSY');
+ return 503;
+ }));
+ check('a concurrent burst receives explicit capacity errors instead of an unbounded queue', burst.includes(200) && burst.includes(503));
+ await pdfResponse(await get(url));
+ check('normal PDF downloads recover after a capacity burst', true);
+
+ const templatePath = join(folder, 'templates/invoice.html');
+ const original = await readFile(templatePath);
+ try {
+ await writeFile(templatePath, 'Integral: \u2a0c
');
+ const failure = await get(url);
+ const body = await failure.json();
+ check('a real missing-glyph failure returns a generic non-PDF error', failure.status === 500 && body.error.code === 'RENDER_FAILED' && !JSON.stringify(body).includes('Integral'));
+ } finally { await writeFile(templatePath, original); }
+ await pdfResponse(await get(url));
+ check('a valid document renders after an engine failure', true);
+ try {
+ await writeFile(templatePath, 'Page limit sample
'.repeat(7));
+ const failure = await get(url);
+ check('the route enforces its page cap against a real multipage document', failure.status === 500 && (await failure.json()).error.code === 'RENDER_FAILED' && logs.includes('PAGE_LIMIT'));
+ } finally { await writeFile(templatePath, original); }
+ await pdfResponse(await get(url));
+ check('a valid document renders after the page limit rejects a job', true);
+
+ async function jsFiles(directory) {
+ const result = [];
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
+ const path = join(directory, entry.name);
+ if (entry.isDirectory()) result.push(...await jsFiles(path));
+ else if (entry.name.endsWith('.js')) result.push(path);
+ }
+ return result;
+ }
+ const chunks = await jsFiles(join(folder, '.next/static'));
+ for (const file of chunks) assert.doesNotMatch(await readFile(file, 'utf8'), /browser_wasi_shim|engine\.wasm|node:worker_threads/);
+ check('client JavaScript excludes the server PDF engine and worker', chunks.length > 0);
+ const info = JSON.parse(await readFile('package.json', 'utf8'));
+ const record = { checkedAt: new Date().toISOString(), node: process.version, platform: platform(), next: info.dependencies.next, nodePackage: version, engineVersion,
+ packageLockSha256: hash(await readFile('package-lock.json')), base, standaloneFolder: folder, pages: expected.pages, pdfSha256: hash(bytes), burst, checks,
+ scope: 'Real next build standalone server with released Fullbleed tarball and fictional data. No hosted platform or application-authentication claim.' };
+ await writeFile('output/verification.json', JSON.stringify(record, null, 2) + '\n');
+ await writeFile('output/server.log', logs);
+ if (process.argv.includes('--serve')) {
+ serving = true;
+ console.log(`FULLBLEED_NEXT_PREVIEW_READY ${base}`);
+ process.once('SIGINT', async () => { await stop(); process.exit(0); });
+ process.once('SIGTERM', async () => { await stop(); process.exit(0); });
+ await stopped;
+ }
+} catch (error) {
+ await writeFile('output/failure.json', JSON.stringify({ error: error.stack, checks, base, standaloneFolder: folder }, null, 2) + '\n');
+ await writeFile('output/server.log', logs);
+ throw error;
+} finally {
+ if (!serving) await stop();
+}