> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stage.systems/llms.txt
> Use this file to discover all available pages before exploring further.

# Campaign landing

> A request carrying a campaign code is counted, stamped with a first-party cookie, and redirected to the clean URL. Sites get it by re-exporting the middleware.

What a marketer sees is in [Guides: Campaign links](/campaigns/links-and-attribution). This is the mechanism.

## The code on the wire

A post link is the real page plus `?c=<code>`. The campaign link is the site root plus `?c=<campaign code>`. Codes are minted at publish: the campaign's title slugified and made unique per org, with per-post suffixes (`.x1`, `.li1`, `.ig1`, `-2` for a second link in the same post). A minted code and its destination are immutable.

Underneath, the stored destination is the real URL decorated with standard UTMs: `utm_source=<channel>`, `utm_medium=social`, `utm_campaign=<campaign ref>`, `utm_content=<post ref>`. The campaign link uses `utm_source=campaign`, `utm_content=link`. A URL already carrying `utm_campaign` is left alone. Minting can never block posting; on failure the long UTM URL goes out instead.

## The landing

`campaignLanding` in the SDK middleware handles `?c=<code>` and the legacy `/l/<slug>` path:

<Steps>
  <Step title="Resolve">
    One public, uncached API call counts the hit and returns the destination in the same statement. Unknown code: `404`, and the visitor still lands clean.
  </Step>

  <Step title="Stamp">
    The destination's UTMs are read into the `stage_ref` cookie: compact `source=…&campaign=…&content=…`, 30 days, `Path=/`, `SameSite=Lax`. An unknown code clears any stale cookie so it cannot claim the visit.
  </Step>

  <Step title="Redirect">
    `302` to the same URL with `c` and every `utm_*` parameter removed. The hop is not a page view. Targets must be `http(s)`.
  </Step>
</Steps>

Then the ordinary visit logging runs on the landing page. `logVisit` takes UTMs from the URL if present, else from `stage_ref`, so every page in the session for 30 days carries the campaign and the analytics rows look exactly as they always did.

## Two counters

| Counter   | Where                              | Counts                                        |
| --------- | ---------------------------------- | --------------------------------------------- |
| Link hits | The link row                       | Every resolve, bot-inclusive, consent-free    |
| Visits    | Visit rows with the campaign's ref | Attributed, consented, human, one per session |

## Reserved on every site

`c` is a reserved query parameter and `/l/*` a reserved path. A page that needs its own `?c=` should pick another name.

## Adopting

Nothing to write. Sites already re-export the middleware from a root `middleware.ts`, so taking the SDK release is the whole rollout. The org needs a canonical `site_url` (with `www` if that is where the site lives) or no links can be minted. Under the dev server the template's dev-only middleware applies the same landing so `localhost:4321/news/post?c=<code>` redirects and sets the cookie.

## Next steps

<CardGroup cols={2}>
  <Card title="Tracking" href="/developers/analytics/tracking">
    The visit that follows the landing.
  </Card>

  <Card title="Middleware" href="/developers/reference/middleware">
    Everything the middleware does.
  </Card>
</CardGroup>
