# Script blocked by Content Security Policy (CSP)

> No visits because a Content Security Policy blocks the MetricRaven script or its requests? Allow it in script-src and connect-src, with examples for Next.js, Vercel, Netlify, Nginx, Apache and WordPress.

Source: https://metricraven.com/docs/content-security-policy  
Markdown: https://metricraven.com/docs/content-security-policy.md  
Section: Troubleshooting

A Content Security Policy (CSP) tells the browser which scripts may run and where a page may send requests. If it doesn't list MetricRaven, the browser silently blocks the tracker and no visits arrive.

## Check whether CSP is the problem

1. **Open your live site with DevTools**

   Open the page in Chrome, Edge or Firefox and open DevTools (`F12`, or `Cmd+Option+I` on a Mac).

2. **Look in the Console**

   A CSP block reads like this:

   ```bash title="Console"
   Refused to load the script 'https://metricraven.com/js/script.js' because it violates the following
   Content Security Policy directive: "script-src 'self'".

   Refused to connect to 'https://metricraven.com/api/v1/collect' because it violates the following
   Content Security Policy directive: "connect-src 'self'".
   ```

   The first line means `script-src` blocks the script. The second means `connect-src` blocks the requests that send visits.

3. **Find the policy**

   In the **Network** tab, select the page document and look for a `Content-Security-Policy` response header, or search the page source for `http-equiv="Content-Security-Policy"`.

## Allow MetricRaven

Add `https://metricraven.com` to two directives: `script-src` (loads `/js/script.js`) and `connect-src` (sends visits and events to `/api/v1/collect`). Keep everything else in your policy as it is:

```bash title="The two directives to change"
script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;
```

> **Note:** If your policy has no `script-src` or `connect-src`, the browser uses `default-src` instead. Add both directives rather than loosening `default-src`.

## Next.js and Vercel

### Static headers in next.config

```typescript title="next.config.ts"
// next.config.ts
import type { NextConfig } from "next";

const csp = [
  "default-src 'self'",
  "script-src 'self' 'unsafe-inline' https://metricraven.com",
  "connect-src 'self' https://metricraven.com",
  "img-src 'self' data: blob:",
  "style-src 'self' 'unsafe-inline'",
].join("; ");

const nextConfig: NextConfig = {
  async headers() {
    return [{ source: "/(.*)", headers: [{ key: "Content-Security-Policy", value: csp }] }];
  },
};

export default nextConfig;
```

### vercel.json

For any framework deployed on Vercel, set the header in `vercel.json`:

```json title="vercel.json"
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;"
        }
      ]
    }
  ]
}
```

## Nonce-based policies (strict-dynamic)

With `'strict-dynamic'`, browsers ignore host allowlists in `script-src`, so adding the domain isn't enough: the script tag needs the request's nonce. `connect-src` still needs `https://metricraven.com`.

```typescript title="proxy.ts"
// proxy.ts: a fresh nonce per request ('strict-dynamic' ignores host allowlists)
import { NextResponse, type NextRequest } from "next/server";

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    "connect-src 'self' https://metricraven.com",
  ].join("; ");
  const headers = new Headers(request.headers);
  headers.set("x-nonce", nonce);
  headers.set("Content-Security-Policy", csp);
  const response = NextResponse.next({ request: { headers } });
  response.headers.set("Content-Security-Policy", csp);
  return response;
}
```

```typescript title="app/layout.tsx"
// app/layout.tsx
import Script from "next/script";
import { headers } from "next/headers";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const nonce = (await headers()).get("x-nonce") ?? undefined;
  return (
    <html lang="en">
      <head>
        <Script
          src="https://metricraven.com/js/script.js"
          data-site-id="YOUR_SITE_ID"
          strategy="afterInteractive"
          nonce={nonce}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}
```

Outside Next.js, print the same per-request nonce on the script tag:

```html
<script
  defer
  nonce="RANDOM_PER_REQUEST_VALUE"
  src="https://metricraven.com/js/script.js"
  data-site-id="YOUR_SITE_ID"
></script>
```

The [npm package](https://metricraven.com/docs/npm-package) takes a `nonce` option for the script tag it injects. A nonce must be random and new on every response; a fixed value defeats the policy.

## Netlify

```ini title="_headers"
# public/_headers (or the publish directory)
/*
  Content-Security-Policy: script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;
```

```ini title="netlify.toml"
# netlify.toml
[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;"
```

## HTML meta tag

If you can't set response headers, put the policy in a meta tag at the top of `<head>`:

```html
<meta
  http-equiv="Content-Security-Policy"
  content="script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;"
/>
```

A meta policy applies only to elements after it, and it is combined with any header policy: if the server also sends a CSP header, that header must allow MetricRaven too.

## Nginx

```nginx title="nginx.conf"
# server { … } block, then: sudo nginx -t && sudo systemctl reload nginx
add_header Content-Security-Policy "script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;" always;
```

`add_header` in a `location` block replaces the ones from the `server` block, so add it wherever your other headers live.

## Apache

```apache title=".htaccess"
# .htaccess or the VirtualHost (needs mod_headers)
<IfModule mod_headers.c>
  Header set Content-Security-Policy "script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;"
</IfModule>
```

## WordPress

Security plugins (Wordfence, Sucuri, WP Cerber, Really Simple Security, Headers Security Advanced & HSTS) and some hosts set the policy for you. Find their CSP or **HTTP headers** setting and add `https://metricraven.com` to the script and connect sources. If your theme sets the header itself:

```php title="functions.php"
// functions.php (child theme) or a small must-use plugin
add_action('send_headers', function () {
    header("Content-Security-Policy: script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;");
});
```

## Serving tracking from your own domain

With a [proxy](https://metricraven.com/docs/proxy), the script and its requests use your own domain, so `'self'` already covers them and you don't need to list MetricRaven:

```bash
script-src 'self'; connect-src 'self';
```

If the proxy lives on another subdomain (for example `stats.example.com`), list that origin in both directives instead.

> **Verify it works**
>
> Reload the page with the cache disabled. The Console shows no CSP errors, the Network tab shows `script.js` and a `/api/v1/collect` request with `ok: true`, and the visit appears in your site's **Traffic** tab.

## Still blocked?

| Symptom | Check |
| --- | --- |
| The header you changed isn't the one served | A CDN, host or plugin may add its own policy. Compare the header in DevTools with your config. |
| Two Content-Security-Policy headers | Browsers enforce every policy, so the strictest wins. Remove the duplicate or allow MetricRaven in both. |
| Only connect-src errors | The script loads but can't send visits: add `https://metricraven.com` (or your proxy origin) to `connect-src`. |
| Report-Only header | A `Content-Security-Policy-Report-Only` header only reports; it never blocks. Look for the enforcing header. |
| No CSP errors and still no visits | See [Troubleshooting](https://metricraven.com/docs/troubleshooting): domains, exclusions and blockers. |

## Details

- The tracker doesn't use `eval`, inline scripts or inline styles, so it needs no `'unsafe-inline'` or `'unsafe-eval'`.
- Crawler tracking with @metricraven/ai-crawl runs on your server and isn't affected by CSP.
