---
title: "How to add web analytics to an Astro site"
description: "Track page views, sessions, and UTMs on an Astro site, keep campaign data through redirects, and read traffic by channel and campaign."
canonical: "https://amplitude.com/en-us/build/astro-web-analytics"
---

# How to add web analytics to an Astro site

Track page views, sessions, and UTMs on an Astro site, keep campaign data through redirects, and read traffic by channel and campaign.

Source: https://amplitude.com/en-us/build/astro-web-analytics

---

# How to add web analytics to an Astro site

Configure the Amplitude Unified SDK so every Astro page load and \<ClientRouter /> navigation becomes one page view, every visit carries its UTM and referrer data, and sessions feed bounce rate and session length. Then keep campaign parameters alive through prerendered and on-demand redirects, and open the Marketing Analytics views that read them. About 15 minutes, all copy-paste.

- Astro 6+
- Static or on-demand
- Unified SDK 1.x
- 15 min

Chris Tam · Last updated 19 October 2026

Astro is built for content: blogs, docs, and marketing sites that ship as static HTML. [Web analytics](https://amplitude.com/web-analytics) tells you where those readers come from, which campaigns bring them, and which pages keep them reading.

In this tutorial, you'll configure Amplitude for traffic data, keep campaign parameters through Astro redirects, and set up the channel and campaign views. It builds on [the Astro product analytics page](/build/astro-product-analytics).

## What you'll build

An Astro site that sends `[Amplitude] Page Viewed` once per page, whether a link triggers a full page load or a `<ClientRouter />` swap, starts and ends sessions automatically, and stores `utm_source`, `utm_medium`, `utm_campaign`, referrer, and click IDs as both user properties and event properties, including on visits that land on a URL you moved or a page that redirects signed-out visitors. By the end you'll see a test campaign in your event stream and a Marketing Analytics view configured to read it.

## Prerequisites

- The setup from [How to add product analytics to an Astro site](/build/astro-product-analytics): `@amplitude/unified` installed, `PUBLIC_AMPLITUDE_API_KEY` in `.env`, `src/scripts/amplitude.ts` loaded by `<Amplitude />` in `src/layouts/Layout.astro`, and `src/middleware.ts` setting `Astro.locals.user` (from your auth helper or the page's `src/lib/auth.ts` stub).
- For the sign-in guard in step 4, an adapter and at least one page rendered on demand. A fully prerendered site skips `src/pages/account.astro`; the other redirects still work.
- An Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General.
- An Admin or Manager role in the Amplitude project for step 5. The Member role can't edit Marketing Analytics settings.

No Amplitude account yet? A free account covers the tracking on this page. [Sign up for free](https://app.amplitude.com/signup) to get your project API key. Custom Marketing Analytics views need a Growth or Enterprise plan; [compare plans](https://amplitude.com/pricing).

## 1. Install the SDK and add the key

The [Quickstart](https://amplitude.com/docs/get-started/amplitude-quickstart) installs `@amplitude/unified` for browser apps that use npm, and the [Astro product analytics page](/build/astro-product-analytics) uses it, so this setup changes the same `src/scripts/amplitude.ts`. If you already followed that page, the SDK is installed and `.env` holds `PUBLIC_AMPLITUDE_API_KEY`; skip to step 2. Otherwise install it, then add the key to `.env` as that page's step 1 shows.

Terminal

```sh
npm install @amplitude/unified
```

## 2. Configure autocapture for traffic data

Attribution, page views, and sessions are on by default, and the base page already names them in `src/scripts/amplitude.ts`, so nobody turns one off later without knowing what it feeds. This page changes one option: `pageViews` gets `trackHistoryChanges`. Page URL enrichment is on by default too, so there's nothing to add for it.

This is the complete file. The first comment names the lines other Astro pages add here; if you followed any of them, keep those lines and change only `pageViews`.

src/scripts/amplitude.ts

```typescript
// Keep lines other Astro pages added here: debugMode and privacyConfig
// (/build/astro-session-replay); the amplitude-cookies import, the device ID
// handoff lines, the deviceId spread, and the experiment block
// (/build/astro-feature-flags); the engagement block with renderCssInDom
// (/build/astro-guides-surveys); the optional webExperiment plugin lines below
// the amplitudeReady export (/build/astro-web-experiment). This page changes only
// pageViews. Import this module only from <script> tags, never from component
// frontmatter.
import * as amplitude from "@amplitude/unified";

export * from "@amplitude/unified";

const apiKey = import.meta.env.PUBLIC_AMPLITUDE_API_KEY;

if (!apiKey) {
  console.warn("PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.");
}

// Resolves once initAll finishes. Feature flags and guides and surveys
// wait on it; nothing on this page needs to.
export const amplitudeReady: Promise<void> = apiKey
  ? amplitude
      .initAll(apiKey, {
        analytics: {
          autocapture: {
            // UTM, referrer, and click ID capture. Feeds Traffic by Channel and Campaign.
            attribution: true,
            // [Amplitude] Page Viewed on every page load and every path change.
            // "pathOnly" ignores changes to only the query string or #hash.
            pageViews: { trackHistoryChanges: "pathOnly" },
            // [Amplitude] Start Session and End Session. Feeds bounce rate and session length.
            sessions: true,
            formInteractions: true,
            fileDownloads: true,
            elementInteractions: true,
          },
        sessionReplay: {
          // Every session while you build; about 1% in production.
          sampleRate: import.meta.env.DEV ? 1 : 0.01,
        },
      })
      .catch((error) => {
        console.warn("Amplitude failed to initialize", error);
      })
  : Promise.resolve();
```

**Why.** The module runs in the browser, so the SDK reads campaign data from the URL the browser actually loaded. Anything that happened before that, such as a redirect, already decided which URL that is. That's why step 4 fixes redirects and not this file. Keep `initAll` out of component frontmatter, middleware, and endpoints; they run on the server or during the build.

`@amplitude/unified` turns on session replay when `sessionReplay.sampleRate` is above 0. This config records every session in development and about 1% in production, which is where Amplitude recommends starting; raise it over a few days. Set it to `0` to turn session replay off. Web analytics doesn't need it; it's here because the base page set it.

From Browser SDK 2.45.0, the SDK stores campaign parameters two ways by default: as user properties (including first-touch `initial_utm_source` and friends, set once) and as event properties on events sent from the landing URL. Later pages and custom events such as `Project Created` don't carry them, so segment those on the user properties. If the Verify check below shows no `utm_` values in `event_properties`, update `@amplitude/unified` to a release that bundles Browser SDK 2.45.0 or later. For referrer exclusions and the other attribution options, see [Track marketing attribution](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2#track-marketing-attribution).

The rest of autocapture already suits a content site: `formInteractions` records newsletter and contact form starts and submits, `fileDownloads` records clicks on links to files such as a PDF guide, and `elementInteractions` records clicks on calls to action.

## 3. Let autocapture count page views

There's no page view code on this site, and there shouldn't be. With `pageViews` on, the SDK sends `[Amplitude] Page Viewed` when it initializes and again when the URL changes in the browser. Each event carries `[Amplitude] Page Path`, `[Amplitude] Page Title`, `[Amplitude] Page URL`, `[Amplitude] Page Location`, and `[Amplitude] Page Counter`. That covers both ways an Astro site navigates:

- Without `<ClientRouter />`, every link is a full page load. `src/scripts/amplitude.ts` runs again, `initAll` runs again, and the new page view is sent on init. The device ID, session, and first-touch campaign carry over, because the SDK stores them in the browser. Internal links don't overwrite the campaign either: by default the SDK excludes your own domain as a referrer.
- With `<ClientRouter />` in your layout's `<head>`, the router swaps the page in place and pushes the new URL to history. The module doesn't run again, and the SDK sends the page view on the URL change. Back and forward buttons count the same way, but their page view carries the previous page's `[Amplitude] Page Title`, because the browser fires the history event before the router swaps in the new page. On a `<ClientRouter />` site, break Page Engagement down by `[Amplitude] Page Path`, not title.

`trackHistoryChanges: "pathOnly"` sends a page view when the path changes, such as `/blog` to `/blog/launch-notes`, and skips changes to only the query string or hash. With the default, `"all"`, any URL change counts, so a reader clicking down a docs page's table of contents (`#install`, `#configure`) or a blog index that rewrites `?tag=astro` would add a page view per click, which inflates page views per session and skews entry and exit pages. On a site without `<ClientRouter />`, page loads carry most of the count and the option only touches those in-page URL changes. Use `"all"` if your query strings mark different pages, such as `/blog?page=2`.

Keep internal links on the router's default history behavior. A link with `data-astro-history="replace"`, or `navigate(url, { history: "replace" })` from `astro:transitions/client`, rewrites the current history entry instead of pushing a new one; if one of those navigations shows no page view in Verify, switch it back to the default.

Don't add an `astro:page-load` listener that calls `track('Page Viewed')`. It produces a second, differently named page view on every navigation, and Marketing Analytics only counts the one it's configured to read.

## 4. Keep UTM parameters through redirects

The SDK can only record campaign data that's in the URL, and it only records a new campaign when the referrer isn't your own domain. Astro redirects break both, in different ways depending on how the page renders:

- A redirect on a prerendered page, either an entry in `redirects` in `astro.config.mjs` or `Astro.redirect()` in a page that isn't rendered on demand, builds to an HTML file with a `<meta http-equiv="refresh">` tag and a fixed target URL. The query string is gone on arrival, and that file has no layout, so the SDK never runs on it. A newsletter link to `/plans?utm_source=newsletter` lands on a bare `/pricing`, and the visit records no campaign at all, not even first-touch `initial_utm_source`.
- A redirect on a page rendered on demand, `Astro.redirect("/login")` or `context.redirect("/login")` in middleware, is a real HTTP redirect, but it goes exactly where you point it, and a plain path has no query string.
- With an adapter, configured redirects can be written to your host's redirect config instead. Whether those keep the query string depends on the host, so test one with a `?utm_source=test` URL. `@astrojs/node` doesn't: it answers `308` with the bare path.

Amplitude's docs call the general case out for single-page apps: UTM parameters can drop during SPA redirects.

For redirects that run on the server, carry the campaign parameters across with one small helper. It copies only the UTM and click ID parameters the SDK reads, so it never drags unrelated query keys onto the new page:

src/lib/campaign.ts

```typescript
// The UTM and click ID parameters the Browser SDK reads for attribution.
const CAMPAIGN_KEYS = [
  "utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content", "utm_id",
  "gclid", "fbclid", "dclid", "gbraid", "wbraid", "ko_click_id",
  "li_fat_id", "msclkid", "rdt_cid", "ttclid", "twclid",
];

// Astro.redirect("/login") on its own drops ?utm_source=...
// Same-origin paths only.
export function withCampaign(path: string, from: URL): string {
  const target = new URL(path, from);
  for (const key of CAMPAIGN_KEYS) {
    const value = from.searchParams.get(key);
    if (value !== null && !target.searchParams.has(key)) {
      target.searchParams.set(key, value);
    }
  return target.pathname + target.search + target.hash;
}
```

Use it in every guard on a page rendered on demand, passing `Astro.url`. This one reads the user the base page's middleware put on `locals`:

src/pages/account.astro

```html
---
import Layout from "../layouts/Layout.astro";
import { withCampaign } from "../lib/campaign";

// Rendered on demand, so the guard sees the request. Needs an adapter.
export const prerender = false;

if (!Astro.locals.user) {
  return Astro.redirect(withCampaign("/login", Astro.url));
}
---

<Layout>
  <main>
    <h1>Account</h1>
    <p>Your account</p>
  </main>
</Layout>
```

The same goes for `context.redirect()` in middleware: pass `withCampaign(path, context.url)`. If you send people on from the browser after sign-in, wrap the path the same way: `navigate(withCampaign("/welcome", new URL(location.href)))` with `navigate` from `astro:transitions/client`, or `location.assign(...)` on a site without the router.

A URL you moved on a prerendered site needs a different fix, because the meta refresh page can't read the query string. Replace the `redirects` entry with a small page that redirects in the browser and keeps the query string. A `redirects` entry for the same path wins over a page file, in `astro dev`, in a static build, and with an adapter, so delete the `/plans` entry from `astro.config.mjs` or the page never runs.

src/pages/plans.astro

```html
---
// Old campaign URL, moved to /pricing. Replaces "/plans" in the redirects
// option of astro.config.mjs, which drops ?utm_source=... in a static build.
// No layout on purpose: the SDK runs once, on /pricing.
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="robots" content="noindex" />
    <!-- Without this, /pricing sees your own domain as the referrer, and the
         SDK skips the campaign. -->
    <meta name="referrer" content="no-referrer" />
    <title>Moved to /pricing</title>
    <script is:inline>
      location.replace("/pricing" + location.search + location.hash);
    </script>
  </head>
  <body>
    <a href="/pricing">This page moved to /pricing.</a>
  </body>
</html>
```

The script needs `is:inline`: it must run before anything else on that page and import nothing. It also keeps the whole query string, not only campaign keys, which is what a moved URL should do. The trade-off is the original referrer: a visitor from a search result with no UTM parameters arrives at `/pricing` as direct traffic. If your adapter writes real HTTP redirects to your host's config and the Verify check shows the query string survives, keep the `redirects` entry instead, since an HTTP redirect also keeps the referrer.

**Why.** Event property tracking re-reads campaign parameters on page load and on every in-page navigation, so whatever is in the URL after the redirect is what gets attached to the events sent from that URL. For the same reason, don't strip UTM parameters from the address bar with `history.replaceState` to tidy the URL.

Give the guard and the moved URL somewhere to land. Skip either stub if you already have the page.

src/pages/pricing.astro

```html
---
import Layout from "../layouts/Layout.astro";
---

<Layout>
  <main>
    <h1>Pricing</h1>
  </main>
</Layout>
```

src/pages/login.astro

```html
---
// Stub until you add your sign-in form.
import Layout from "../layouts/Layout.astro";
---

<Layout>
  <main>
    <h1>Sign in</h1>
  </main>
</Layout>
```

Check every redirect on your site, configured or in code, with a `?utm_source=test` URL before you trust your channel numbers.

## 5. Set up the Marketing Analytics views

In Amplitude, web analytics lives under Marketing Analytics in the navigation bar. It uses Browser SDK events by default, so this step is configuration, not code.

1. Create the channel classifier. Go to Data > Properties > Channels. Opening that page creates `[Amplitude] Default Channels`, which maps UTM parameters and referrer data into GA4-aligned channels such as Paid Search and Organic Social. If your org predates it, click Build with the new logic when prompted.
2. Confirm the page view event. Open Marketing Analytics, click Customize, and select Page View and Filter. Choose `[Amplitude] Page Viewed` as the event. Set a domain filter if the project also receives events from another site, such as your app on a different domain.
3. Define a conversion funnel. Under Conversion Funnel, choose the events that make up your funnel. If you followed the Astro product analytics page, `[Amplitude] Page Viewed` followed by `Project Created` works as a first funnel.
4. Read the views. Traffic by Channel, Traffic by Campaign, Page Engagement, and Conversion each show a time series and a table. Page Engagement covers visitors, page views, bounce rate, page views per session, and entry and exit rate per page, which is where a blog or docs site finds its best and worst landing pages. Ad Performance needs an ad network source such as Google Ads connected first.

For every setting, see [Out-of-the-box Marketing Analytics](https://amplitude.com/docs/analytics/ootb-marketing-analytics).

**MCP.** Working in Cursor or Claude Code? [Amplitude's MCP server](https://amplitude.com/docs/amplitude-ai/amplitude-mcp) lets your agent look up the events already in your project before it writes `track` calls, so a conversion event matches a name you already use instead of inventing `projectCreated` next to `Project Created`.

## Verify it worked

Redirects on prerendered pages only behave like production in a production build, so run `npm run build` and `npm run preview`. Open `http://localhost:4321/plans?utm_source=build-test&utm_medium=email&utm_campaign=astro-web-analytics` in a fresh private window, click through two pages (type another page's path into the address bar if your stubs have no links), then check in this order:

1. **Network tab.** Turn on Preserve log first. The document request for `/plans` should load the small redirect page, followed by a document request for `/pricing?utm_source=build-test&utm_medium=email&utm_campaign=astro-web-analytics`, and the address bar should keep those parameters. With an adapter, also open `/account?utm_source=build-test` while signed out (with the auth stub, everyone is): it should return 302 with `utm_source` still in the `Location` header for `/login`. If your adapter has no preview command, check `/account` with `npm run dev`. If either loses its parameters, the fix is step 4, and nothing downstream will work until it is. Then filter for `api2.amplitude.com`. You should see a POST returning 200. The SDK compresses request bodies of 2 KB or more, so read the events in the [Instrumentation Explorer](https://amplitude.com/docs/analytics/debug-analytics#instrumentation-explorer) Chrome extension instead: the `[Amplitude] Page Viewed` event for `/pricing` should carry `utm_source: build-test` in `event_properties`, and an identify event should set `utm_source` and `initial_utm_source` as user properties.

On `localhost` the SDK doesn't treat your own domain as an internal referrer, so the first internal link sends a second identify that clears `utm_source`, and a missing `no-referrer` tag won't show up. Repeat the `/plans` check on a deployed preview URL before you trust it: there, clicking through sends no second identify, and the profile keeps `utm_source: build-test`.

1. **Ingestion Debugger.** In Amplitude, click Data, select Sources, then open the Ingestion Debugger tab. The successful requests chart should show your uploads. It shows counts, not individual events.
2. **User lookup.** Go to Users > User Profiles and search your device ID (the Instrumentation Explorer shows it as `device_id`). Within ten seconds to a minute, the event stream shows `[Amplitude] Start Session` (`session_start` in the Instrumentation Explorer), then one `[Amplitude] Page Viewed` per page, and the profile shows `initial_utm_source: build-test`.
3. **Marketing Analytics.** Open Traffic by Campaign for today. `astro-web-analytics` should appear as a campaign.

Using `<ClientRouter />`? In the same window, click two internal links, use the back button once, and click a `#heading` link on a long page. The Instrumentation Explorer should show one `[Amplitude] Page Viewed` per path change and none for the heading link.

To watch payloads in the console, add `logLevel: amplitude.Types.LogLevel.Debug` to the `analytics` block in development.

## Troubleshooting

### Campaign traffic shows up as Direct

The UTM parameters were gone before the SDK started. Open a `?utm_source=test` URL with Preserve log on and follow every redirect: a `redirects` entry or `Astro.redirect()` on a prerendered page builds to a meta refresh with a fixed URL, and `Astro.redirect()` or `context.redirect()` on demand drops the query unless you wrap the path with `withCampaign`. A `navigate()` or `location.assign()` that doesn't pass the query does the same in the browser. The Channels docs also note that inconsistent `set()` and `unset()` calls on UTM user properties can classify traffic as Other.

### UTM values on events, but no `initial_utm_source` on the profile

The visitor arrived through a redirect page on your own domain, so the referrer is your domain, and by default the SDK doesn't treat a same-domain referrer as a new campaign. Keep the `<meta name="referrer" content="no-referrer" />` line in any browser-side redirect page like `src/pages/plans.astro`.

### `NoAdapterInstalled` during the build

`src/pages/account.astro` renders on demand. Add an adapter (for example `npx astro add node`) or delete the file; the `/plans` fix doesn't need one.

### Two page view events per navigation

An `astro:page-load` listener or a page view helper is running alongside autocapture. Delete it. If both events are `[Amplitude] Page Viewed`, check that only `src/scripts/amplitude.ts` calls `initAll` and that nothing loads it with an `is:inline` script.

### A page view for every heading link or filter click

The page writes to the hash or the query string, and `trackHistoryChanges` is `"all"`. Set `pageViews: { trackHistoryChanges: "pathOnly" }` as in step 2.

### Every page in Page Engagement has the same title

Your layout hardcodes `<title>`, so every `[Amplitude] Page Title` is the same. Give `src/layouts/Layout.astro` a `title` prop, render `<title>{title}</title>`, and pass it from each page, or break the view down by `[Amplitude] Page Path` under Customize > Breakdown. If your layout has no frontmatter yet (the `create astro` starter's doesn't), add the `---` fences at the top of the file. On a `<ClientRouter />` site, back and forward views also carry the previous page's title, so use `[Amplitude] Page Path` there.

### The same page shows up twice in Page Engagement

Visitors reach it as both `/blog` and `/blog/`, and each is its own `[Amplitude] Page Path`. Pick one form with `trailingSlash` in `astro.config.mjs` and link to it consistently. Your host decides trailing slashes for prerendered pages, so check its settings too.

### Page views stopped even though the code enables them

Remote configuration overrides local autocapture settings feature by feature, and Browser SDK 2.16.1 and later fetch it by default. Check Data > Settings > Autocapture and make sure page views and sessions aren't turned off there. To keep autocapture under code control, add `remoteConfig: { fetchRemoteConfig: false }` to the `analytics` block.

### Requests fail with ERR\_BLOCKED\_BY\_CLIENT

An ad blocker is dropping the call. Route Amplitude traffic through a proxy on your own domain and point every endpoint at it: `serverUrl` (events) and `remoteConfig.serverUrl` (remote configuration) in the `analytics` block, plus `trackServerUrl` and `configServerUrl` in the `sessionReplay` block.

## Full code reference

Every file this tutorial adds or changes, in one block, the version to hand to an agent. 6 files. Shared files start with a comment naming the lines other Astro pages add there; merge into your file and keep those lines. Unchanged from earlier pages; keep your versions: `.env`, `src/layouts/Layout.astro`, `src/components/Amplitude.astro`, `src/middleware.ts`, `src/env.d.ts`, `src/lib/auth.ts`, `src/components/SignOutButton.astro`, `src/components/CreateProjectForm.astro`, `src/pages/projects/new.astro`.

6 files

```html
// -- src/scripts/amplitude.ts --
// Keep lines other Astro pages added here: debugMode and privacyConfig
// (/build/astro-session-replay); the amplitude-cookies import, the device ID
// handoff lines, the deviceId spread, and the experiment block
// (/build/astro-feature-flags); the engagement block with renderCssInDom
// (/build/astro-guides-surveys); the optional webExperiment plugin lines below
// the amplitudeReady export (/build/astro-web-experiment). This page changes only
// pageViews. Import this module only from <script> tags, never from component
// frontmatter.
import * as amplitude from "@amplitude/unified";

export * from "@amplitude/unified";

const apiKey = import.meta.env.PUBLIC_AMPLITUDE_API_KEY;

if (!apiKey) {
  console.warn("PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.");
}

// Resolves once initAll finishes. Feature flags and guides and surveys
// wait on it; nothing on this page needs to.
export const amplitudeReady: Promise<void> = apiKey
  ? amplitude
      .initAll(apiKey, {
        analytics: {
          autocapture: {
            // UTM, referrer, and click ID capture. Feeds Traffic by Channel and Campaign.
            attribution: true,
            // [Amplitude] Page Viewed on every page load and every path change.
            // "pathOnly" ignores changes to only the query string or #hash.
            pageViews: { trackHistoryChanges: "pathOnly" },
            // [Amplitude] Start Session and End Session. Feeds bounce rate and session length.
            sessions: true,
            formInteractions: true,
            fileDownloads: true,
            elementInteractions: true,
          },
        sessionReplay: {
          // Every session while you build; about 1% in production.
          sampleRate: import.meta.env.DEV ? 1 : 0.01,
        },
      })
      .catch((error) => {
        console.warn("Amplitude failed to initialize", error);
      })
  : Promise.resolve();

// -- src/lib/campaign.ts --
// The UTM and click ID parameters the Browser SDK reads for attribution.
const CAMPAIGN_KEYS = [
  "utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content", "utm_id",
  "gclid", "fbclid", "dclid", "gbraid", "wbraid", "ko_click_id",
  "li_fat_id", "msclkid", "rdt_cid", "ttclid", "twclid",
];

// Astro.redirect("/login") on its own drops ?utm_source=...
// Same-origin paths only.
export function withCampaign(path: string, from: URL): string {
  const target = new URL(path, from);
  for (const key of CAMPAIGN_KEYS) {
    const value = from.searchParams.get(key);
    if (value !== null && !target.searchParams.has(key)) {
      target.searchParams.set(key, value);
    }
  return target.pathname + target.search + target.hash;
}

// -- src/pages/account.astro --
---
import Layout from "../layouts/Layout.astro";
import { withCampaign } from "../lib/campaign";

// Rendered on demand, so the guard sees the request. Needs an adapter.
export const prerender = false;

if (!Astro.locals.user) {
  return Astro.redirect(withCampaign("/login", Astro.url));
}
---

<Layout>
  <main>
    <h1>Account</h1>
    <p>Your account</p>
  </main>
</Layout>

// -- src/pages/plans.astro --
---
// Old campaign URL, moved to /pricing. Replaces "/plans" in the redirects
// option of astro.config.mjs, which drops ?utm_source=... in a static build.
// No layout on purpose: the SDK runs once, on /pricing.
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="robots" content="noindex" />
    <!-- Without this, /pricing sees your own domain as the referrer, and the
         SDK skips the campaign. -->
    <meta name="referrer" content="no-referrer" />
    <title>Moved to /pricing</title>
    <script is:inline>
      location.replace("/pricing" + location.search + location.hash);
    </script>
  </head>
  <body>
    <a href="/pricing">This page moved to /pricing.</a>
  </body>
</html>

// -- src/pages/pricing.astro --
---
import Layout from "../layouts/Layout.astro";
---

<Layout>
  <main>
    <h1>Pricing</h1>
  </main>
</Layout>

// -- src/pages/login.astro --
---
// Stub until you add your sign-in form.
import Layout from "../layouts/Layout.astro";
---

<Layout>
  <main>
    <h1>Sign in</h1>
  </main>
</Layout>
```

## Hand it to your agent

Prefer to have Cursor, Claude Code, or Codex CLI do the wiring? This prompt points the agent at this page and pins the decisions that matter.

Using Cursor or Claude Code? Paste this prompt

```text
Complete https://amplitude.com/build/astro-product-analytics first, or
confirm this Astro site already has its setup; create its src/lib/auth.ts
stub, src/middleware.ts, and src/pages/projects/new.astro if they're
missing. Then set up Amplitude web analytics by following
https://amplitude.com/build/astro-web-analytics exactly. Merge into existing
shared files (src/scripts/amplitude.ts) and never delete lines another
Amplitude Build page added; each file's first comment names them, including
any debugMode or privacyConfig lines, the experiment block, and Web
Experiment plugin lines. In src/scripts/amplitude.ts change only
analytics.autocapture.pageViews, to { trackHistoryChanges: "pathOnly" }, and
keep initAll called once there. Never import the SDK in component
frontmatter, middleware, or endpoints, and don't add an astro:page-load page
view listener. Add the withCampaign helper in src/lib/campaign.ts with the
page's 6 UTM and 11 click ID keys. Find every Astro.redirect() and
context.redirect() in pages rendered on demand and middleware, and every
navigate() or location.assign() after sign-in, and make each one carry the
UTM and click ID parameters from Astro.url, context.url, or location.href.
Find every entry in the redirects option of astro.config.mjs and every
Astro.redirect() on a prerendered page, list them for me, and replace each
static one with a page like the page's src/pages/plans.astro (is:inline
location.replace keeping location.search and location.hash, and a
no-referrer meta tag), unless I tell you my host's redirects keep the
query string. When you replace one, delete the matching redirects entry;
the entry wins over the page. Add the page's demo files so the Verify steps run:
src/pages/plans.astro (to /pricing), src/pages/account.astro (prerender =
false, redirects to /login when Astro.locals.user is null), and stub pages
for /pricing and /login if they don't exist. If the site has no adapter,
skip src/pages/account.astro and tell me. Don't hardcode the key and don't
invent event names. Report every file you changed and every redirect you
checked.
```

## What this unlocks

- [Session replay](https://amplitude.com/session-replay), already on from step 2, lets you watch what visitors with `utm_campaign: astro-web-analytics` did after they landed on `/pricing`.
- [Heatmaps](https://amplitude.com/heatmaps), built from those session replays, show where visitors click on `/pricing`, and a segment on `initial_utm_source` splits paid from organic.
- [Web Experiment](https://amplitude.com/web-experimentation) can test a new `/pricing` layout, launched from the Page Engagement table, with your conversion funnel as the metric.
- [Guides and surveys](https://amplitude.com/guides-and-surveys) can ask visitors whose `utm_medium` is `email` what brought them back.

One integration; everything else reads from the same events.

## Next steps

[Web frameworksHow to add session replay to an Astro siteWatch the sessions behind your campaign traffic, with masking and sampling tuned for production.](/build/astro-session-replay)[Web frameworksHow to add Web Experiment to an Astro siteTest a page change without a deploy, measured against the same traffic data.](/build/astro-web-experiment)[Web frameworksHow to add heatmaps to an Astro siteSee where readers click on your landing pages, built from the session replays you already capture.](/build/astro-heatmaps)

Reference docs

[Browser Unified SDK →](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk)[Browser SDK 2: track marketing attribution →](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2#track-marketing-attribution)[Channels →](https://amplitude.com/docs/data/channels)[Out-of-the-box Marketing Analytics →](https://amplitude.com/docs/analytics/ootb-marketing-analytics)[Debug analytics →](https://amplitude.com/docs/analytics/debug-analytics)[Amplitude MCP server →](https://amplitude.com/docs/amplitude-ai/amplitude-mcp)

Amplitude is the leading [all-in-one analytics platform](https://amplitude.com/) for building better products. Product and engineering teams use Amplitude to connect [product analytics](https://amplitude.com/amplitude-analytics) and [web analytics](https://amplitude.com/web-analytics) with [session replay](https://amplitude.com/session-replay), [heatmaps](https://amplitude.com/heatmaps), [experimentation](https://amplitude.com/amplitude-experiment), [feature flags](https://amplitude.com/feature-management), and [guides and surveys](https://amplitude.com/guides-and-surveys) in one unified workflow, with [AI agents](https://amplitude.com/ai-agents) working across all of it. Learn how Amplitude is building [self-improving products](https://amplitude.com/self-improving-products) with [Wave](https://amplitude.com/wave), helping teams turn product data into PRs.

For agents

[View raw .md](/build/astro-web-analytics.md)[llms.txt](/llms.txt)

## Ship the instrumentation, then read the data

Once events are flowing, Amplitude turns them into funnels, retention curves, and session replays without any further setup.

[Create a free account](https://app.amplitude.com/signup)[Browse all Build tutorials](https://amplitude.com/build)
