Skip to content

OpenPanelComponent: init script never executes in nested Next.js App Router layouts #290

@stoicamarcus

Description

@stoicamarcus

Problem

OpenPanelComponent uses strategy="beforeInteractive" for the inline init script. In Next.js App Router, when the component is placed in a nested layout (e.g. app/[locale]/[params]/layout.tsx), the init script is serialized as RSC payload data but never rendered as an actual <script> tag in the HTML. The browser never executes it.

Root Cause

In Next.js App Router, beforeInteractive scripts are only promoted to real <script> tags in the <head> when placed in the root app/layout.tsx. In nested layouts, the Script component with beforeInteractive is treated as a regular React component and serialized into the RSC wire format:

self.__next_f.push([1,"...[\"$\",\"$L5c\",null,{\"strategy\":\"beforeInteractive\",\"dangerouslySetInnerHTML\":{\"__html\":\"window.op = ...\"}}]...

This is just JSON data describing the component's props — not an executable <script> tag. When React hydrates on the client, it sees strategy: "beforeInteractive" and does nothing, because beforeInteractive is designed to run before hydration, not after.

Result:

  • <script src="https://openpanel.dev/op1.js"> loads fine (no strategy = afterInteractive default)
  • The inline init script (window.op = ...; window.op('init', ...)) never executes
  • window.op is undefined
  • No events are tracked

Reproduction

  1. Create a Next.js App Router project
  2. Place <OpenPanelComponent clientId="..." trackScreenViews /> in a nested layout (not root app/layout.tsx)
  3. Load the page
  4. Check window.op in browser console → undefined
  5. Check view-source:window.op only appears inside the RSC JSON payload (self.__next_f.push), not as a <script> tag

Suggested Fix

Change the init script strategy from beforeInteractive to afterInteractive:

- r.createElement(p, { strategy: "beforeInteractive", dangerouslySetInnerHTML: { __html: `...` } })
+ r.createElement(p, { id: "openpanel-init", strategy: "afterInteractive", dangerouslySetInnerHTML: { __html: `...` } })

Or allow passing a strategy prop to override the default.

Workaround

Replace OpenPanelComponent with manual scripts:

import Script from 'next/script'

<Script
  id="openpanel-init"
  strategy="afterInteractive"
  dangerouslySetInnerHTML={{
    __html: `window.op = window.op || function(...args) {(window.op.q = window.op.q || []).push(args)};
window.op('init', ${JSON.stringify({ clientId: 'your-client-id', trackScreenViews: true, sdk: 'nextjs', sdkVersion: '1.0.8' })});`,
  }}
/>
<Script src="https://openpanel.dev/op1.js" strategy="afterInteractive" />

Versions

  • @openpanel/nextjs: 1.0.8 (also confirmed in latest 1.1.3)
  • Next.js: 16

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions