Astro
Quickstart
Before you start
Create an Inth project and set it up before you touch the code:
- Add policy rules that cover the
measurementcategory. This guide loads PostHog on measurement. - Add your site's origin to the project's trusted origins.
- Copy the project's backend URL.
Then decide how Astro builds your pages. The integration supports both, and Rendering and deployment 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
and examples/astro
hold the finished code for each.
Install the packages
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:
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
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
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 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.
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:
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
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:
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 for the list, and Scripts, Embeds and Network blocker for inline scripts, iframes and tracking requests.
Add the consent components to your layout
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:
- Before you choose, the banner shows and there are no requests to
posthog.com. - Select Reject All and reload. The banner stays closed and PostHog does not load.
- Open Privacy settings, turn on Analytics (the
measurementcategory) and select Save Settings. PostHog loads. - 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 before you
ship.
Next steps
- Rendering and deployment covers prerendered pages in a server build, cached pages with a server island, and offline mode for development.
- Integration options lists every
c15t()option, and Components every component and prop. - Client API and
Server API cover reading consent from your
own scripts and from
Astro.locals.c15t. - Customize and Translations cover theme tokens, copy, dark mode and languages.
- IAB TCF and troubleshooting.