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
- Create a Next.js App Router project
- Place
<OpenPanelComponent clientId="..." trackScreenViews /> in a nested layout (not root app/layout.tsx)
- Load the page
- Check
window.op in browser console → undefined
- 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
Problem
OpenPanelComponentusesstrategy="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,
beforeInteractivescripts are only promoted to real<script>tags in the<head>when placed in the rootapp/layout.tsx. In nested layouts, theScriptcomponent withbeforeInteractiveis treated as a regular React component and serialized into the RSC wire format:This is just JSON data describing the component's props — not an executable
<script>tag. When React hydrates on the client, it seesstrategy: "beforeInteractive"and does nothing, becausebeforeInteractiveis designed to run before hydration, not after.Result:
<script src="https://openpanel.dev/op1.js">loads fine (no strategy =afterInteractivedefault)window.op = ...; window.op('init', ...)) never executeswindow.opisundefinedReproduction
<OpenPanelComponent clientId="..." trackScreenViews />in a nested layout (not rootapp/layout.tsx)window.opin browser console →undefinedview-source:→window.oponly appears inside the RSC JSON payload (self.__next_f.push), not as a<script>tagSuggested Fix
Change the init script strategy from
beforeInteractivetoafterInteractive:Or allow passing a
strategyprop to override the default.Workaround
Replace
OpenPanelComponentwith manual scripts:Versions
@openpanel/nextjs: 1.0.8 (also confirmed in latest 1.1.3)