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

# Page views SDK middleware

> Report every page-content request your site serves with drop-in middleware for TypeScript, Python, Go, and Ruby: what it forwards, how it composes with Agentify, and the fail-open guarantee that a beacon never delays or breaks a page.

Every [seller SDK](/sdks/overview) ships Page views middleware for its language's dominant boundary: a fetch-handler wrapper in TypeScript, ASGI and WSGI adapters in Python, `net/http` middleware in Go, and Rack middleware in Ruby. Mount it around the handler that serves your marketing site, and each page load is reported to [Page views](/page-views/overview) in the background. Agents, crawlers, and people all get your site exactly as before.

All four implement one contract:

* Only `GET` requests to non-asset paths are reported; assets and every other method pass through unobserved.
* The handler runs first, so the beacon carries the real response status and the request duration. The visitor's `User-Agent`, `Accept`, and connecting IP are forwarded for [server-side classification](/page-views/overview#how-a-viewer-is-classified).
* It records the response's **representation** (`html` or `markdown`, read from the response's content-type) and, when [Agentify](/agentify/overview) attached one, the **journey id** that stitches this view to the agentified read. Both are captured automatically; you set nothing.
* **The beacon is fire-and-forget and fails open.** A missing key, a network error, a non-204, a timeout, or a malformed report is handed to `onError` and swallowed. The middleware never delays or breaks the page.

Only the request's **path** is reported, never its query string, so tracking parameters never reach ZeroClick.

## Configure the key

Reporting uses a `zc_` key with the [`page-views:write` scope](/page-views/overview#the-page-views-key-scope). TypeScript is standalone: the key rides inline on the middleware as `apiKey`, with no `createSeller` and no signing secrets. The other three SDKs read it from a `page_views_key` / `PageViewsKey` field on the client you already build for the guard; when that field is unset, they fall back to the combined API key.

<CodeGroup>
  ```ts TypeScript theme={null}
  import { withPageViews } from "@zeroclickai/sellers";

  // Standalone: no createSeller, no signing secrets. The key rides inline
  // on the middleware. See "Mount the middleware" below.
  const handler = withPageViews(renderPage, {
    seller: process.env.ZEROCLICK_SELLER_ID!,
    apiKey: process.env.ZEROCLICK_PAGE_VIEWS_KEY!,
  });
  ```

  ```python Python theme={null}
  # Set page_views_key on the client (create_seller or create_async_seller);
  # the middleware reads it from there.
  zeroclick = create_async_seller(
      api_key=os.environ["ZEROCLICK_API_KEY"],
      page_views_key=os.environ["ZEROCLICK_PAGE_VIEWS_KEY"],
  )
  ```

  ```go Go theme={null}
  seller, err := sellers.New(sellers.Config{
  	APIKey:       os.Getenv("ZEROCLICK_API_KEY"),
  	PageViewsKey: os.Getenv("ZEROCLICK_PAGE_VIEWS_KEY"),
  })
  ```

  ```ruby Ruby theme={null}
  config.zeroclick.api_key = ENV.fetch("ZEROCLICK_API_KEY")
  config.zeroclick.page_views_key = ENV.fetch("ZEROCLICK_PAGE_VIEWS_KEY")
  ```
</CodeGroup>

Delivery is fire-and-forget on a short timeout: Go's `PageViewsTimeout` defaults to `DefaultPageViewTimeout` (2 seconds), and the other SDKs use an equally short page-view default (`timeoutMs` in TypeScript, `page_view_timeout_seconds` in Ruby), long enough to send the beacon, short enough never to sit on a page.

## Mount the middleware

<CodeGroup>
  ```ts TypeScript theme={null}
  import { withPageViews } from "@zeroclickai/sellers";

  // Wrap the fetch handler that serves your site. Works anywhere the SDK
  // works: Node.js servers, Hono, Next.js route handlers, edge runtimes.
  const handler = withPageViews(renderPage, {
    seller: process.env.ZEROCLICK_SELLER_ID!,
    apiKey: process.env.ZEROCLICK_PAGE_VIEWS_KEY!,
  });

  export default { fetch: handler };
  ```

  ```python Python theme={null}
  # ASGI (Starlette, FastAPI). Pass the async client and your seller id:
  from zeroclick_sellers.adapters import PageViewsAsgiMiddleware

  app.add_middleware(PageViewsAsgiMiddleware, seller=zeroclick, seller_id="sel_x")

  # WSGI (Flask, Django). Pass the blocking client from create_seller:
  from zeroclick_sellers.adapters import PageViewsWsgiMiddleware

  app.wsgi_app = PageViewsWsgiMiddleware(app.wsgi_app, zeroclick, "sel_x")
  ```

  ```go Go theme={null}
  // A plain func(http.Handler) http.Handler, like Meter and Agentify: it
  // composes with net/http, chi, gorilla/mux, and gin's WrapH.
  mux.Handle("/", seller.PageViews(sellers.PageViewsOptions{})(site))
  ```

  ```ruby Ruby theme={null}
  # Rails: mount it in config/application.rb. Omit `seller:` and the
  # middleware resolves the process-wide client on the first request.
  config.middleware.use ZeroClick::Sellers::Middleware::PageViews, seller_id: "sel_x"

  # Outside Rails, pass the client explicitly:
  use ZeroClick::Sellers::Middleware::PageViews, seller: SELLER, seller_id: "sel_x"
  ```
</CodeGroup>

Mount it in front of the pages people and agents read (your marketing routes), not in front of your billed API, which already reports usage through the guard. If your marketing site runs as its own process, configure a client there the same way; Page views only ever needs the seller id and the `page-views:write` key.

## Combining with Agentify

[Page views](/page-views/overview) and [Agentify](/agentify/sdk-middleware) are independent opt-ins: run either alone or both. When you run both, **Page views must be the outer middleware, wrapping Agentify**, and this ordering is load-bearing, not cosmetic.

* **It is the only way Page views sees agent traffic at all.** The Agentify middleware short-circuits agent requests: on a match it returns markdown and never calls the inner handler. A Page views beacon mounted *inside* Agentify would therefore never fire for those requests, silently dropping exactly the agent page views you most want to measure. Only the outer layer observes them.
* **It stitches the journey.** Being outside is also how the page view picks up the journey id Agentify attaches to its response, tying the marketing view to the agentified read on one journey.

Wrap Agentify with Page views in each language's idiom:

<CodeGroup>
  ```ts TypeScript theme={null}
  // withPageViews on the outside, wrapping withAgentify.
  const handler = withPageViews(withAgentify(zeroClick, renderPage), {
    seller: process.env.ZEROCLICK_SELLER_ID!,
    apiKey: process.env.ZEROCLICK_PAGE_VIEWS_KEY!,
  });

  export default { fetch: handler };
  ```

  ```python Python theme={null}
  # WSGI: the LAST wrap is the outermost, so apply Agentify first and
  # Page views second. Page views ends up outside Agentify.
  app.wsgi_app = AgentifyWsgiMiddleware(app.wsgi_app, seller=zeroclick)
  app.wsgi_app = PageViewsWsgiMiddleware(app.wsgi_app, zeroclick, "sel_x")

  # ASGI: add Agentify first and Page views last, so Page views is outermost.
  ```

  ```go Go theme={null}
  // PageViews wraps Agentify wraps your site.
  mux.Handle("/", seller.PageViews(sellers.PageViewsOptions{})(
  	seller.Agentify(sellers.AgentifyOptions{})(site)))
  ```

  ```ruby Ruby theme={null}
  # In Rack the FIRST `use` is the OUTERMOST layer, so mount Page views
  # before Agentify to keep Page views on the outside.
  use ZeroClick::Sellers::Middleware::PageViews, seller_id: "sel_x"
  use ZeroClick::Sellers::Middleware::Agentify
  ```
</CodeGroup>

The direction differs by framework because the composition primitives do. TypeScript and Go nest explicitly, so the outer call is Page views. Python's WSGI wrapping and Rack's `use` build a stack instead: in WSGI the **last** wrap is outermost, so Page views is applied second; in Rack the **first** `use` is outermost, so Page views is mounted first. In every case the result is the same: Page views sits on the outside of Agentify.

## Options

Each middleware takes the same options, named in its language's idiom:

| Option                                            | TypeScript   | Python        | Go           | Ruby          |
| ------------------------------------------------- | ------------ | ------------- | ------------ | ------------- |
| The seller whose site this is (`sel_…`); required | `seller`     | `seller_id`   | `Seller`     | `seller_id`   |
| Override the public URL whose path is reported    | `resolveUrl` | `resolve_url` | `ResolveURL` | `resolve_url` |
| Observe the fail-open path; never re-raised       | `onError`    | `on_error`    | `OnError`    | `on_error`    |

The key itself is not an option here: TypeScript passes `apiKey` inline on the middleware, while Python, Go, and Ruby configure it once on the client as `page_views_key` / `PageViewsKey`. Behind a proxy that rewrites the scheme or host, set the URL override so the reported path comes from the public URL rather than the one your process sees: only the path is ever reported.

## Report without the middleware

Any language or framework can report by calling the [REST endpoint](/page-views/rest) directly, once per page-content `GET`. Each SDK also exposes a one-shot reporter for when the middleware boundary does not fit (a background job, a custom router, a hand-rolled handler):

| SDK        | Reporter                                                                             |
| ---------- | ------------------------------------------------------------------------------------ |
| TypeScript | `reportPageView(input, { apiKey })`                                                  |
| Python     | `client.report_page_view(ReportPageViewInput(...))` (`await` it on the async client) |
| Go         | `seller.ReportPageView(ctx, sellers.ReportPageViewInput{...})`                       |
| Ruby       | `client.report_page_view(seller:, path:, ...)`                                       |

A direct reporter carries the same body the middleware sends, and can set `representation` and the journey id explicitly rather than deriving them from a response it never saw.

## Failing open, observably

The middleware exists to add a measurement, never an outage or a delay: any error on the reporting path leaves the response untouched. That silence is deliberate, and it means a misconfigured key looks exactly like a working page, so wire the error hook to your logger:

```ts theme={null}
const handler = withPageViews(renderPage, {
  seller: process.env.ZEROCLICK_SELLER_ID!,
  apiKey: process.env.ZEROCLICK_PAGE_VIEWS_KEY!,
  onError: (error, request) =>
    logger.warn({ error, url: request.url }, "page-view beacon failed"),
});
```

In Go, a nil `OnError` falls back to the client's configured `Logger`.

## Next steps

<Columns cols={2}>
  <Card title="REST endpoint" icon="rocket" href="/page-views/rest">
    The underlying request: body, response, and errors.
  </Card>

  <Card title="Overview" icon="book-open" href="/page-views/overview">
    What is captured, how viewers are classified, and the key scope.
  </Card>
</Columns>
