Next.js Integration Guide

Install IssueCapture in Next.js

Add bug reporting to your Next.js app. 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

Store the key in env so it doesn't end up in the repo

  • Create .env.local in your project root (if it doesn't exist)
  • Use NEXT_PUBLIC_ so the value is inlined for the browser bundle
  • Make sure .env.local is in .gitignore (Next.js scaffolds this by default)
  • Restart `next dev` after adding or changing env vars
# .env.local
NEXT_PUBLIC_ISSUECAPTURE_API_KEY=ic_your_api_key_here
3

Install in app/layout.tsx (App Router)

Drop the widget into your root layout so it loads on every page

  • The widget is an ES module — it has to be loaded with `<script type="module">`. Next.js's `<Script>` component doesn't support type="module", so use a raw `<script>` with dangerouslySetInnerHTML
  • Place it at the end of `<body>`, after children, so it never blocks first paint
  • The default `trigger: "auto"` and styling come from the dashboard — you only need apiKey here
  • Works with App Router, Pages Router (in `_app.tsx`), Next.js 13/14/15+, and Turbopack
// 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}

        {/* IssueCapture Widget — ESM module, runs after hydration */}
        <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 (optional)

Save your customers the typing — pass their name/email through

  • The values are inlined at render time — works in a server-rendered layout
  • If the user signs in client-side later, call `IssueCapture.updateConfig({ user: { name, email } })` instead of re-initialising
  • Don't call `IssueCapture.init()` more than once per page — use `updateConfig` for changes
// app/layout.tsx
import { auth } from '@/lib/auth' // your auth helper

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const apiKey = process.env.NEXT_PUBLIC_ISSUECAPTURE_API_KEY
  const session = await auth()
  const userJson = JSON.stringify({
    name: session?.user?.name ?? '',
    email: session?.user?.email ?? '',
  })

  return (
    <html lang="en">
      <body>
        {children}
        <script
          type="module"
          dangerouslySetInnerHTML={{
            __html: `
              import IssueCapture from 'https://issuecapture.com/widget.js';
              IssueCapture.init({
                apiKey: '${apiKey}',
                user: ${userJson},
              });
            `,
          }}
        />
      </body>
    </html>
  )
}
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 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`
  • 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
  • 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
  • Use `useEffect` if you need to render the script tag only on the client
  • App Router is the recommended setup for new projects

Ready to get started?

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