Next.js Integration Guide

Install IssueCapture in Next.js (App Router & Pages Router)

Add bug reporting to your Next.js app with server-rendered user pre-fill and secure build-time env inlining. Works with App Router, Pages Router, Turbopack, and Next.js 13/14/15+.

Quick Summary

Time: ~10 minutes (incl. Jira OAuth)
Prerequisites: Next.js 13+, Jira Cloud account
Method: <script type="module"> in root layout
Works with: App Router, Pages Router, Turbopack

Prerequisites

  • Next.js 13 or higher
  • Jira Cloud account (Software or JSM)
  • IssueCapture account (free — includes 10 issues/month)

Step-by-Step Installation

Follow these steps to get up and running in minutes.

1

Get Your API Key

Sign up for IssueCapture and grab your widget API key

  • Go to issuecapture.com/signup and create a free account
  • Connect your Jira instance via OAuth (about 30 seconds)
  • Open the Widgets page and create a new widget
  • Click the "Keys" tab on your widget and copy the API key (starts with "ic_")
2

Add the API key to .env.local (build-time inlining)

Store the key in env so it doesn't end up in the repo — Next.js inlines it at build time

  • Next.js prefixes NEXT_PUBLIC_ variables are inlined into the JavaScript bundle at build time (not runtime)
  • This is safer than runtime reading — the key is baked into the production bundle during `next build`, not fetched from disk at serve-time
  • Local development uses .env.local; production builds use .env.production if it exists, else .env.local
  • Make sure .env.local and .env.*.local are in .gitignore (Next.js scaffolds this by default)
  • Restart `next dev` after adding or changing env vars — the dev server only re-evaluates env vars on start
  • Verify the key is inlined: check that `process.env.NEXT_PUBLIC_ISSUECAPTURE_API_KEY` is a string literal in the browser bundle (use Ctrl+F in the DevTools Sources tab for the key value)
# .env.local
NEXT_PUBLIC_ISSUECAPTURE_API_KEY=ic_your_api_key_here

# .env.production (optional, for CI/CD)
NEXT_PUBLIC_ISSUECAPTURE_API_KEY=ic_your_production_key

# .gitignore (already set by Next.js create-next-app)
.env.local
.env.*.local
3

Install in app/layout.tsx (App Router — Recommended)

Drop the widget into your root layout — server-rendered, client-side initialization

  • app/layout.tsx is a Server Component by default — it can read env vars and render server-side, but the widget script runs in the browser
  • The widget is an ES module — it must be loaded with `<script type="module">`. Next.js's built-in `<Script>` component doesn't support type="module", so use a raw `<script>` with dangerouslySetInnerHTML
  • The apiKey is inlined into the HTML at render time — it's visible in the page source (that's fine; it's public-facing)
  • Place it at the end of `<body>`, after {children}, so it never blocks first paint or hydration
  • The default `trigger: "auto"` and styling come from the dashboard — you only need apiKey in init()
  • Works with App Router (Next.js 13+) and Turbopack
// app/layout.tsx (Server Component, Next.js 13+)
import type { ReactNode } from 'react'

export default function RootLayout({
  children,
}: {
  children: ReactNode
}) {
  const apiKey = process.env.NEXT_PUBLIC_ISSUECAPTURE_API_KEY

  return (
    <html lang="en">
      <body>
        {children}

        {/* IssueCapture Widget — ESM module, client-side only */}
        <script
          type="module"
          dangerouslySetInnerHTML={{
            __html: `
              import IssueCapture from 'https://issuecapture.com/widget.js';
              IssueCapture.init({ apiKey: '${apiKey}' });
            `,
          }}
        />
      </body>
    </html>
  )
}
4

Option: use your own button

Skip the floating button and trigger the widget from an element you already render

  • `trigger` accepts any CSS selector — a single element or a class that matches many
  • When you set `trigger` to a selector, the floating button is not injected
  • Clicking any matched element opens the widget modal
// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const apiKey = process.env.NEXT_PUBLIC_ISSUECAPTURE_API_KEY

  return (
    <html lang="en">
      <body>
        {children}

        {/* Your existing feedback button somewhere in the layout */}
        <button id="my-feedback-btn">Report an Issue</button>

        <script
          type="module"
          dangerouslySetInnerHTML={{
            __html: `
              import IssueCapture from 'https://issuecapture.com/widget.js';
              IssueCapture.init({
                apiKey: '${apiKey}',
                trigger: '#my-feedback-btn',
              });
            `,
          }}
        />
      </body>
    </html>
  )
}
5

Programmatic open from a Client Component

Open the widget from your own React code (e.g. an error boundary)

  • `window.IssueCapture` is defined as soon as the widget script runs — guard with `?.` in case the script hasn't loaded yet
  • The same component pattern works in React 18 and React 19
  • For a full TypeScript declaration of every method (init, on, updateConfig, etc.) see the troubleshooting section
// components/BugReportButton.tsx
'use client'

declare global {
  interface Window {
    IssueCapture?: {
      open: () => void
      close: () => void
      isOpen: () => boolean
    }
  }
}

export default function BugReportButton() {
  const open = () => window.IssueCapture?.open()
  return (
    <button onClick={open} className="px-4 py-2 bg-blue-600 text-white rounded">
      Report Issue
    </button>
  )
}
6

Pre-fill the logged-in user (Server Component auth)

Use server-side auth to inline user data — no client-side secrets needed

  • Server Components can call `await auth()` — the user data is fetched server-side and inlined into the HTML
  • The user object reaches the browser as a string literal in the init call — no secrets exposed
  • Works with NextAuth.js, Clerk, Supabase, Firebase, or any auth provider with a server-side session getter
  • If the user signs in or changes identity client-side later (e.g. OAuth flow in a popup), call `IssueCapture.updateConfig({ user: { name, email } })` instead of re-initializing
  • Don't call `IssueCapture.init()` more than once per page — it replaces the config entirely
// app/layout.tsx (Server Component)
import { auth } from '@/lib/auth' // NextAuth.js, Clerk, Supabase, etc.
import type { ReactNode } from 'react'

export default async function RootLayout({
  children,
}: {
  children: ReactNode
}) {
  const apiKey = process.env.NEXT_PUBLIC_ISSUECAPTURE_API_KEY
  const session = await auth() // async server call — fetches session server-side

  const userJson = JSON.stringify({
    name: session?.user?.name ?? '',
    email: session?.user?.email ?? '',
  })

  return (
    <html lang="en">
      <body>
        {children}

        {/* User data inlined at render time — no client fetch needed */}
        <script
          type="module"
          dangerouslySetInnerHTML={{
            __html: `
              import IssueCapture from 'https://issuecapture.com/widget.js';
              IssueCapture.init({
                apiKey: '${apiKey}',
                user: ${userJson},
              });
            `,
          }}
        />
      </body>
    </html>
  )
}

/* For client-side sign-in (e.g. OAuth popup, later in lifecycle):
   After user signs in, update the widget without re-initializing:

   IssueCapture.updateConfig({
     user: { name: user.name, email: user.email }
   })

   Never call init() again — it replaces the whole config.
*/
7

Verify it loaded

Quick sanity check before you ship it

  • Start the dev server: `next dev`
  • Open the page — you should see the floating Report Issue button
  • Open DevTools console and type `IssueCapture` — you should see the widget object (confirms the script loaded even if an ad blocker is hiding the button)
  • Click the button, fill in a test issue, and submit
  • Open Jira and confirm the issue landed in the configured project

Troubleshooting

Common integration issues and how to solve them.

TypeScript: Property 'IssueCapture' does not exist on type 'Window'

Add a global declaration to your project (e.g. types/issuecapture.d.ts)

  • Create `types/issuecapture.d.ts` and declare `IssueCapture` on the global `Window` interface
  • Reference it in `tsconfig.json` under `include` (Next.js picks up `types/**` by default)
  • See the IssueCaptureAPI surface in the docs for the full method list (init, open, close, on, updateConfig, destroy)

Widget script placed in Edge Middleware or a Server Component never appears

The widget is browser-only — cannot run in Edge Functions, Server Components, or middleware

  • Don't initialize the widget in middleware.ts or an Edge Function — there's no DOM there
  • In App Router, the layout.tsx file is a Server Component (can read auth, DB, etc.) but the script tag itself runs in the browser — that's correct
  • If you're trying to render IssueCapture.open() from a Server Component, that won't work — either move the call to a Client Component or use a pre-rendered trigger
  • For auth/user data in a Server Component: fetch it server-side in layout.tsx, then inline it into the script tag as shown in Step 6 — the widget script runs client-side
  • Quick check: if your code is in middleware.ts or uses `use server`, the widget code won't execute (only the inlined script tag does)

Widget script doesn't load (no `IssueCapture` global)

  • Make sure you used `<script type="module">` — the widget is ESM. A plain `<Script src=...>` will not execute it correctly
  • Check the browser Network tab for a 200 on `widget.js` — if it 404s, the CDN is down or the URL is wrong
  • Verify `NEXT_PUBLIC_ISSUECAPTURE_API_KEY` is exposed at build time — log `process.env.NEXT_PUBLIC_ISSUECAPTURE_API_KEY` in your layout to confirm
  • Restart `next dev` if you just added the env var — the dev server only evaluates env vars on start
  • Disable ad blockers when testing — some block any script that mentions "widget"

"Domain not allowed" error in the console

  • In the IssueCapture dashboard, open your widget and click the Domains tab
  • Add `localhost:3000` for local dev (or whichever port you use)
  • Add your production and preview deployment domains (see the Vercel guide for wildcard tips)
  • Domains match on the browser's Origin header — subdomains need to be listed individually

Pages Router (legacy `_app.tsx`)

  • The same `<script type="module">` pattern works — put it in `_app.tsx` inside the root component's JSX or in `_document.tsx`
  • Use `useEffect` if you need to render the script tag only on the client (though at root level it usually executes fine)
  • App Router is the recommended setup for new projects — Pages Router is in maintenance mode

Ready to get started?

Free plan includes 10 issues/month. No card needed — connect Jira and you're done.