Content Security Policy
When you need this
Only if your site already sends a Content Security Policy. If it does not, do not add one for Gutternote. A new policy can block other parts of your site.
To find the policy the browser gets, check the response headers and the page source:
curl -sI https://staging.example.com/ | grep -i content-security-policyA page can get more than one policy, from the framework, host, CDN or a
<meta> tag. Each is enforced on its own, so each must allow Gutternote.
Directives
Merge these into the directives you already have:
script-src https://cdn.gutternote.com;connect-src https://api.gutternote.com https://storage.googleapis.com;img-src https://storage.googleapis.com blob:;style-src 'unsafe-inline';font-src https://cdn.gutternote.com;font-src is optional. Without it, the widget uses the system font.
If your policy has only default-src, a new directive replaces that fallback
for its type. Copy your existing sources into each directive you add.
Sources by feature
| Directive | Source | Needed for |
|---|---|---|
script-src | cdn.gutternote.com | widget.js and the bundles it loads later: page capture, emoji, markdown, themes. |
connect-src | api.gutternote.com | Configuration, sessions, threads, uploads. |
connect-src | storage.googleapis.com | Image uploads. They go straight to storage. |
img-src | storage.googleapis.com | Showing attachments. |
img-src | blob: | The preview before upload. |
style-src | 'unsafe-inline' | The widget’s stylesheet in its shadow root. |
font-src | cdn.gutternote.com | Optional web fonts. |
Google avatars come from lh3.googleusercontent.com. If that is blocked, the
widget shows initials. Google sign-in opens a popup, so no frame-src is
needed.
Nonces and strict-dynamic
The widget’s <style> cannot take your nonce. Browsers ignore
'unsafe-inline' when the same directive has a nonce or hash. If production
needs nonced styles, load Gutternote only on preview or staging with its own
policy.
With 'strict-dynamic', browsers ignore host sources in script-src. Put your
nonce on the Gutternote script tag. Then check that the later bundles still
load.
Examples
Next.js, adding the sources only on previews:
const preview = process.env.VERCEL_ENV === 'preview'const gn = (s) => (preview ? ` ${s}` : '')
const policy = [ "default-src 'self'", `script-src 'self'${gn('https://cdn.gutternote.com')}`, `connect-src 'self'${gn('https://api.gutternote.com https://storage.googleapis.com')}`, `img-src 'self' data:${gn('https://storage.googleapis.com blob:')}`, `style-src 'self'${gn("'unsafe-inline'")}`,].join('; ')
module.exports = { async headers() { return [{ source: '/(.*)', headers: [{ key: 'Content-Security-Policy', value: policy }] }] },}A meta tag, placed before the resources it covers:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.gutternote.com; connect-src 'self' https://api.gutternote.com https://storage.googleapis.com; img-src 'self' https://storage.googleapis.com blob:; style-src 'self' 'unsafe-inline'"/>Test with Report-Only
Send the new policy as Content-Security-Policy-Report-Only first. The browser
logs what it would block and blocks nothing. When the console is clear on the
pages reviewers use, switch to Content-Security-Policy.
Test the parts that load later:
- Attach an image or take a capture. This tests storage.
- Open the reaction picker. This tests a later bundle.
- Reload an attachment. This tests
img-src.
What a missing source looks like
| Missing | Result |
|---|---|
CDN in script-src | No widget. Or capture, emoji search or formatting fails. |
API in connect-src | No widget. It cannot read its configuration. |
Storage in connect-src | Image upload fails. |
Storage in img-src | Attachments show as broken images. |
blob: in img-src | The preview before upload is blank. |
'unsafe-inline' | The widget has no styles, or does not show. |
CDN in font-src | System font. Nothing else breaks. |
If it still blocks
- Read the console. It names the directive. Check whether the message came from the enforced policy or the Report-Only one.
- Check every policy the page gets, not just the one you edited.
- After each change, reload, clear the console, and repeat the action that failed.
- No CSP error and no widget: check the key and the origin. See The widget does not appear.
Self-hosted
Replace cdn.gutternote.com and api.gutternote.com with your own origins,
and the storage origin with your object store. Keep widget.js, its later
bundles and the fonts/ folder together.