---
title: Quickstart
description: Add a c15t consent banner, preference dialog and consent-gated
  scripts to an Astro 5, 6 or 7 site with the c15t integration and Inth, on
  static or server output.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Before you start

[Create an Inth project](https://inth.com) and set it up before you touch the
code:

1. Add policy rules that cover the `measurement` category. This guide loads
   PostHog on measurement.
2. Add your site's origin to the project's trusted origins.
3. Copy the project's backend URL.

Then decide how Astro builds your pages. The integration supports both, and
[Rendering and deployment](/docs/frameworks/astro/rendering) covers mixed and
cached setups.

|Your site|Astro output|c15t mode|
|--|--|--|
|Static pages on any static host, no adapter|`output: 'static'`|`hosted()`|
|Pages rendered on each request, with a server adapter|`output: 'server'`|`manifest()`, the default|

For server output, the setup below bundles your policy into the server at
build time, the recommended production setup. Rebuild after changing policies,
translations or vendors. With static output, the browser asks Inth for the
policy on each visit.

[`examples/astro-static`](https://github.com/c15t/c15t/tree/v3/examples/astro-static)
and [`examples/astro`](https://github.com/c15t/c15t/tree/v3/examples/astro)
hold the finished code for each.

## Install the packages

|Package manager|Command|
|:--|:--|
|npm|`npm install c15t@alpha @astrojs/svelte svelte @c15t/integrations@alpha`|
|pnpm|`pnpm add c15t@alpha @astrojs/svelte svelte @c15t/integrations@alpha`|
|yarn|`yarn add c15t@alpha @astrojs/svelte svelte @c15t/integrations@alpha`|
|bun|`bun add c15t@alpha @astrojs/svelte svelte @c15t/integrations@alpha`|

The banner is plain `.astro` markup and ships no framework JavaScript. The
preference dialog is an island that loads when a visitor first reaches for it.
The `c15t` package includes the dialog for Svelte, React and Vue, and the
integration renders it with the one of those three your site registers.

Use the framework your site already ships, so visitors do not download a second
one for a single dialog. This guide uses Svelte. For React, install
`@astrojs/react react react-dom` instead of `@astrojs/svelte svelte` and
register `react()`. For Vue, install `@astrojs/vue vue` and register `vue()`.
With more than one of them registered, set `ui: 'react'` or `ui: 'vue'` on
`c15t()`; otherwise it uses Svelte.

## Set the backend URL

Set `PUBLIC_C15T_BACKEND_URL` to your project's backend URL, including any
path prefix, in `.env` or wherever you build:

```sh title=".env"
PUBLIC_C15T_BACKEND_URL=https://your-project.inth.app
```

If your app already sets `PUBLIC_INTH_PROJECT_URL` for other Inth SDKs, that works
too. When both are set, `PUBLIC_C15T_BACKEND_URL` wins.

`c15t()` reads it from the environment, or from `.env` or `.env.local` in the
project root, when `astro.config.mjs` loads, so the config needs no
`backendURL`. The URL must be absolute.

## Configure the integration

Pick the file that matches your output. Register the framework integration
before `c15t()`.

### Static output

```js title="astro.config.mjs"
import svelte from '@astrojs/svelte';
import { defineConfig } from 'astro/config';
import c15t, { hosted } from 'c15t/astro';

export default defineConfig({
	integrations: [svelte(), c15t({ mode: hosted() })],
});
```

`hosted()` sends the browser straight to your Inth backend's `/init`, so no
server is needed. A static page is the same for every visitor, which means
the banner appears once the browser's `/init` request returns, not in the
first HTML.

### Server output

```js title="astro.config.mjs"
import node from '@astrojs/node';
import svelte from '@astrojs/svelte';
import { defineConfig } from 'astro/config';
import c15t from 'c15t/astro';

export default defineConfig({
	adapter: node({ mode: 'standalone' }),
	integrations: [svelte(), c15t()],
	output: 'server',
});
```

`c15t()` with no `mode` uses `manifest()`. It fetches your project's public
policy once, when `astro build` or `astro dev` starts, and embeds it in the
server build. The server resolves each visitor from that snapshot and the
request's location and language headers, so the first HTML already contains
the banner, or no banner at all for a visitor who has chosen. Consent saves
still go to Inth. [Data fetching](/docs/concepts/data-fetching) compares
this with calling the backend on every request.

`manifest()` injects one route, `/api/c15t/[...path]`, which answers
`/api/c15t/init` and `/api/c15t/manifest`, so it needs a server adapter. Use
the adapter for your host in place of `@astrojs/node`. A server-rendered page
ships only the code that saves consent; the code that resolves a policy in
the browser loads when a page needs it.

The snapshot is fixed at build time:

* Rebuild after changing policies, translations or vendors. If your CI caches
  build output, force a fresh build.
* Consent choices still go to the backend.

The build reads the backend URL from your public backend URL variable when
the config doesn't pass one: `NEXT_PUBLIC_C15T_BACKEND_URL` in Next.js,
`NUXT_PUBLIC_C15T_BACKEND_URL` in Nuxt, `PUBLIC_C15T_BACKEND_URL` in Astro,
Svelte and SvelteKit, and `VITE_C15T_BACKEND_URL` in TanStack Start and other
Vite apps. Each also reads the matching Inth variable, such as
`NEXT_PUBLIC_INTH_PROJECT_URL`, when the c15t one is unset. See
[set the backend URL](/docs/concepts/modes#set-the-backend-url).

The fetch waits at most 10 seconds. When it fails, or no backend URL is set,
every framework does the same thing:

|Command|Default when the fetch fails|
|--|--|
|Production build: `next build`, `vite build`, `nuxt build`, `astro build`|The build stops with an error.|
|Dev: `next dev`, `vite dev`, `nuxt dev`, `astro dev`|A warning, and the server fetches the policy at runtime.|

Set `onBuildError` to use one behaviour for both. `'fail'` stops dev too.
`'runtime'` lets a production build finish, and the server fetches the policy
at runtime. The `C15T_ON_BUILD_ERROR` environment variable overrides the
option, so you can deploy during a backend outage without a code change:

```sh
C15T_ON_BUILD_ERROR=runtime npm run build
```

Turborepo's strict environment mode hides undeclared variables from tasks, so
list `C15T_ON_BUILD_ERROR` in the build task's `passThroughEnv` there.

The build skips the fetch, without an error, when it can't use a snapshot,
for example when the backend URL is relative. With `onBuildError: 'fail'`, a
relative URL stops the build. [Consent modes](/docs/concepts/modes#what-happens-when-the-download-fails)
lists every case.

The built server keeps the backend URL from build time, so setting
`PUBLIC_C15T_BACKEND_URL` when you start it changes nothing. Rebuild to point
at another project. To apply policy edits without rebuilding, set
`mode: manifest({ source: 'runtime' })`, with `manifest` imported from
`c15t/astro`. The server then fetches and caches the policy at runtime.

The integration writes its options into the page as JSON. `c15t()` throws if
an option holds a function, and names the option. Callbacks, vendor helpers
and anything else with a function belong in `src/c15t.client.ts`. Keep
secrets out of both.

## Register vendor scripts

Remove any existing PostHog snippet first, so PostHog loads once. Then list
the scripts in `src/c15t.client.ts`, which the integration finds on its own:

```ts title="src/c15t.client.ts"
import { posthog } from '@c15t/integrations/posthog';
import type { C15tClientOptionsExtension } from 'c15t/astro';

export default {
	scripts: [
		posthog({
			id: 'phc_your_project_key',
			initOptions: { cookieless_mode: 'never' },
			loadMode: 'after-consent',
		}),
	],
} satisfies C15tClientOptionsExtension;
```

Replace `phc_your_project_key` with your PostHog project key. PostHog waits
for measurement permission and does not fall back to cookieless capture.

Other vendors work the same way. See
[integrations](/docs/integrations/overview) for the list, and
[Scripts](/docs/frameworks/astro/scripts),
[Embeds](/docs/frameworks/astro/embeds) and
[Network blocker](/docs/frameworks/astro/network-blocker) for inline scripts,
iframes and tracking requests.

## Add the consent components to your layout

```astro title="src/layouts/base.astro"
---
import { ClientRouter } from 'astro:transitions';
import {
	ConsentBanner,
	ConsentDialog,
	ConsentDialogLink,
	ConsentScript,
} from 'c15t/astro/components';

interface Props {
	title: string;
}

const { title } = Astro.props;
---

<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta content="width=device-width, initial-scale=1" name="viewport" />
		<title>{title}</title>
		<ConsentScript />
		<ClientRouter />
	</head>
	<body>
		<slot />
		<footer>
			<ConsentDialogLink>Privacy settings</ConsentDialogLink>
		</footer>
		<ConsentBanner />
		<ConsentDialog />
	</body>
</html>
```

Wrap every page in this layout. `ConsentScript` goes in `<head>`, where it
hands the browser the server's decision and sets the color scheme before first
paint. `ConsentBanner` renders the banner, `ConsentDialog` reserves a place for
the preference dialog, and `ConsentDialogLink` is the link visitors use to
change their mind later. `ConsentScript` inlines the banner's rules into the
page, so you do not import a stylesheet.

`ClientRouter` is optional. With it, the consent runtime survives page
navigation. Without it, each page load starts the runtime again from the stored
choice.

## Type `Astro.locals.c15t`

The integration adds the type of the consent context it puts on every request
to `.astro/types.d.ts`, so TypeScript knows `Astro.locals.c15t` without an
`env.d.ts` line.

## Check that it works

Build and preview the site, then open it in a private window with DevTools open
on the Network tab:

1. Before you choose, the banner shows and there are no requests to
   `posthog.com`.
2. Select **Reject All** and reload. The banner stays closed and PostHog does
   not load.
3. Open **Privacy settings**, turn on **Analytics** (the `measurement`
   category) and select **Save Settings**. PostHog loads.
4. Turn **Analytics** off and save. The page reloads, and PostHog does not load
   again.

On server output, the page source of a first visit contains the banner markup.
On static output it does not, and the banner appears after the `/init`
request. Follow [Verify consent](/docs/guides/verify-consent) before you
ship.

## Next steps

* [Rendering and deployment](/docs/frameworks/astro/rendering) covers
  prerendered pages in a server build, cached pages with a server island, and
  offline mode for development.
* [Integration options](/docs/frameworks/astro/integration) lists every
  `c15t()` option, and [Components](/docs/frameworks/astro/components) every
  component and prop.
* [Client API](/docs/frameworks/astro/client-api) and
  [Server API](/docs/frameworks/astro/server) cover reading consent from your
  own scripts and from `Astro.locals.c15t`.
* [Customize](/docs/frameworks/astro/customize) and
  [Translations](/docs/frameworks/astro/translations) cover theme tokens, copy,
  dark mode and languages.
* [IAB TCF](/docs/frameworks/astro/iab) and
  [troubleshooting](/docs/frameworks/astro/troubleshooting).
