Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,29 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
const htmxEvents = [
'sseError', 'sseOpen', 'swapError', 'targetError', 'timeout',
'validation:validate', 'validation:failed', 'validation:halted',
'xhr:abort', 'xhr:loadend', 'xhr:loadstart'
];
htmxEvents.forEach(event => {
const funcName = 'func ' + event;
if (typeof window[funcName] === 'undefined') {
window[funcName] = function() {
console.warn('HTMX event handler "' + funcName + '" was called but not defined. Providing safety fallback.');
};
}
Comment on lines +78 to +84
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback function logs a warning every time an extension triggers the missing handler, which can spam the console (and, in some setups, log aggregation) during SSE streams. Since the intent is resilience, not observability, this should ideally warn once per handler (or be gated behind a development check).

Suggestion

Rate-limit or dedupe the warning per handler (or only warn in development). For example:

const warned = new Set();
// ...
window[funcName] = function() {
  if (!warned.has(funcName)) {
    warned.add(funcName);
    console.warn(`HTMX event handler "${funcName}" was called but not defined. Providing safety fallback.`);
  }
};

Alternatively, log only when process.env.NODE_ENV !== 'production'.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

});
})();
`,
}}
/>
</head>
Comment on lines +68 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script uses dangerouslySetInnerHTML without any scoping/guards and will run on every page load, potentially in environments where window isn’t available (or before extensions/HTMX are ready). While the code references window only at runtime, it’s still worth explicitly guarding for non-browser contexts and avoiding unnecessary work when document/window aren’t present.

Also, because this is a root layout in Next.js, you should be deliberate about when the script runs. If the goal is to prevent early calls from extensions, consider next/script with an appropriate strategy (e.g., beforeInteractive) and a stable id to avoid duplication/rehydration surprises.

Suggestion

Consider switching to next/script and adding explicit guards:

  • Use <Script id="htmx-func-fallback" strategy="beforeInteractive"> to ensure the fallback exists as early as possible.
  • Add if (typeof window === 'undefined') return; inside the IIFE to harden against non-browser execution.

Example:

import Script from 'next/script'

// ...
<head>
  <Script id="htmx-func-fallback" strategy="beforeInteractive">
    {`(function(){
      if (typeof window === 'undefined') return;
      const htmxEvents = [/* ... */];
      for (const event of htmxEvents) {
        const funcName = 'func ' + event;
        if (typeof window[funcName] === 'undefined') {
          window[funcName] = function() {};
        }
      }
    })();`}
  </Script>
</head>

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.

Comment on lines +68 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Inline script may be blocked by CSP; prefer external script or nonce.

If script-src disallows unsafe-inline, this script won’t run and the fallback won’t be installed. Consider moving the bootstrap to a static file loaded with next/script (beforeInteractive) or apply a CSP nonce if you use nonces.

🛠️ Proposed refactor (external script)
+import Script from 'next/script'
...
-      <head>
-        <script
-          dangerouslySetInnerHTML={{
-            __html: `
-              (function() {
-                const htmxEvents = [
-                  'sseError', 'sseOpen', 'swapError', 'targetError', 'timeout',
-                  'validation:validate', 'validation:failed', 'validation:halted',
-                  'xhr:abort', 'xhr:loadend', 'xhr:loadstart'
-                ];
-                htmxEvents.forEach(event => {
-                  const funcName = 'func ' + event;
-                  if (typeof window[funcName] === 'undefined') {
-                    window[funcName] = function() { 
-                      console.warn('HTMX event handler "' + funcName + '" was called but not defined. Providing safety fallback.');
-                    };
-                  }
-                });
-              })();
-            `,
-          }}
-        />
-      </head>
+      <head>
+        <Script src="/htmx-fallback.js" strategy="beforeInteractive" />
+      </head>

Then add public/htmx-fallback.js:

(function () {
  const htmxEvents = [
    'sseError', 'sseOpen', 'swapError', 'targetError', 'timeout',
    'validation:validate', 'validation:failed', 'validation:halted',
    'xhr:abort', 'xhr:loadend', 'xhr:loadstart'
  ];
  htmxEvents.forEach(event => {
    const funcName = 'func ' + event;
    if (typeof window[funcName] === 'undefined') {
      window[funcName] = function () {
        console.warn(
          'HTMX event handler "' + funcName + '" was called but not defined. Providing safety fallback.'
        );
      };
    }
  });
})();
🧰 Tools
🪛 ast-grep (0.40.5)

[warning] 69-69: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.3.13)

[error] 70-70: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🤖 Prompt for AI Agents
In `@app/layout.tsx` around lines 68 - 90, Move the inline HTMX fallback script
out of app/layout.tsx into an external static file (e.g.,
public/htmx-fallback.js) that contains the IIFE creating the htmxEvents array
and the window['func ' + event] safety handlers, then load that file with
Next.js's next/script using strategy="beforeInteractive" in layout.tsx (or
alternatively generate and apply a CSP nonce and add it to the inline script) so
the bootstrap runs when script-src disallows unsafe-inline; update references to
the IIFE/htmxEvents and the window handler names (the 'func ' + event handlers)
to ensure the same behavior after moving the code.

<body
className={cn(
'font-sans antialiased',
Expand Down