Skip to content

feat: make strict-CSP hx-csp adoption ergonomic#1940

Merged
davidpoblador merged 1 commit into
mainfrom
csp-hx-csp-ergonomics
May 30, 2026
Merged

feat: make strict-CSP hx-csp adoption ergonomic#1940
davidpoblador merged 1 commit into
mainfrom
csp-hx-csp-ergonomics

Conversation

@davidpoblador

Copy link
Copy Markdown
Member

Summary

Closes #1938. Smooths the four friction points the issue hit when adopting the htmx hx-csp extension under a strict (no unsafe-eval) CSP.

1. Re-export the extension (friction #1 + #2)

  • New vibetuner-js/htmx-csp.js + "./htmx/csp" export, mirroring the existing htmx/preload, htmx/sse, htmx/live. Projects now import "@alltuner/vibetuner/htmx/csp"; with no direct htmx.org devDependency — which previously had to be pinned by hand and would drift from the framework's htmx the moment it bumped.
  • Verified the re-export bundles with bun (htmx.registerExtension("hx-csp") lands in the output).

2. body_attrs skeleton block (friction #4)

  • The <body> tag had no attribute hook, forcing a full skeleton override just to add the nonce. Added {% block body_attrs %} so projects stamp a single inherited nonce: hx-nonce:inherited="{{ csp_nonce }}". Verified against the htmx 4 source that :inherited resolves hx-nonce from ancestors and the extension reads it via the same attributeValue path — so one stamp on <body> covers every htmx element.

3. htmx_config skeleton block (friction #3)

  • htmx.config.safeEval must be set before the extension's init, but ESM hoists imports so htmx.config.safeEval = true in config.js runs too late. The htmx-native fix is the <meta name="htmx-config"> tag — but htmx reads that meta synchronously when its script evaluates (#initHtmxConfig in the constructor), not at DOMContentLoaded. The skeleton's head block renders after the bundle script, so it would be too late.
  • Added a dedicated {% block htmx_config %} positioned just before the bundle script, so a meta placed there is parsed first. Documented:
    {% block htmx_config %}
        <meta name="htmx-config" content='extensions:"hx-csp",safeEval:true'>
    {% endblock htmx_config %}

4. Docs

  • development-guide.md, llms.txt, llms-full.txt updated to the new import path, the body_attrs single-stamp pattern, and the htmx_config/meta safeEval approach.

Testing

  • test_skeleton_extension_points.py: new TestBodyAttrs (renders inside <body>, empty by default) and test_htmx_config_block_renders_before_bundle_js (ordering invariant — the whole point of the block).
  • Full unit suite: 902 passed. ruff, djlint, rumdl all clean (pre-commit green). node --check on the new JS, and a bun build of the re-export.

Notes

  • Docs reference "added in 10.20.0" as the version that introduces the @alltuner/vibetuner/htmx/csp subpath (next minor after the just-released 10.19.0).
  • The existing explicit hx-nonce on user/profile.html.jinja is left as-is — harmless, and it inherits correctly whether or not a project sets body_attrs.

🤖 Generated with Claude Code

Smooths the rough edges from #1938 for adopting the htmx hx-csp
extension under a strict (no unsafe-eval) CSP:

- Re-export the extension as `@alltuner/vibetuner/htmx/csp` (mirroring
  htmx/preload, htmx/sse, htmx/live) so projects enable it with one
  import and no direct htmx.org devDependency to drift from the
  framework's pinned version.
- Add a `body_attrs` block to the skeleton so projects can stamp a
  single inherited `hx-nonce:inherited="{{ csp_nonce }}"` on <body>
  (every htmx element inherits it) without forking the template.
- Add an `htmx_config` block positioned just before the bundle script.
  htmx reads its `<meta name="htmx-config">` when the script evaluates,
  not at DOMContentLoaded, so the meta (and thus safeEval) must precede
  the bundle — the head block came too late. This sidesteps the
  ESM import-ordering trap for `htmx.config.safeEval`.
- Update the CSP docs (development guide, llms.txt, llms-full.txt) to
  the new import path, the body_attrs single-stamp pattern, and the
  meta-tag safeEval approach.

Closes #1938

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@davidpoblador davidpoblador merged commit 80e4bd4 into main May 30, 2026
2 checks passed
@davidpoblador davidpoblador deleted the csp-hx-csp-ergonomics branch May 30, 2026 13:28
davidpoblador pushed a commit that referenced this pull request May 30, 2026
🤖 I have created a release *beep* *boop*
---


##
[10.20.0](v10.19.0...v10.20.0)
(2026-05-30)


### Features

* make strict-CSP hx-csp adoption ergonomic
([#1940](#1940))
([80e4bd4](80e4bd4))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make strict-CSP hx-csp adoption ergonomic: re-export the extension + a body-nonce hook

1 participant