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

# Astro, Nuxt, SvelteKit

> One middleware for every Fetch-API runtime, plus the few lines each framework needs to hand it a request.

`createFetchMiddleware` returns a function with this shape:

```ts theme={null}
(context: { request: Request }, next: () => Promise<Response>) => Promise<Response>
```

That is **exactly** Astro's middleware signature, so on Astro you export it directly. Every other framework has its own shape and needs two or three lines to bridge. They are all below.

<Warning>
  Do not `export const handle = createFetchMiddleware(beacon)` on SvelteKit, or use it as a Nitro handler on Nuxt. Those frameworks call middleware differently and the export will silently never run.
</Warning>

***

## Install

```bash theme={null}
npm install @snowseo/beacon
```

```bash .env theme={null}
SNOWSEO_BEACON_KEY=sb_live_...
```

<Warning>
  No `VITE_` or `PUBLIC_` prefix. Those are inlined into the client bundle, which publishes the key.
</Warning>

Create the beacon once and import it where you need it:

```ts src/lib/beacon.ts theme={null}
import { createBeacon } from "@snowseo/beacon";

export const beacon = createBeacon({
  siteUrl: "https://example.com",
  dir: "dist",
  analytics: { key: process.env.SNOWSEO_BEACON_KEY! },
});
```

***

## Wiring it up

<CodeGroup>
  ```ts Astro theme={null}
  // src/middleware.ts
  import { createFetchMiddleware } from "@snowseo/beacon";
  import { beacon } from "./lib/beacon";

  export const onRequest = createFetchMiddleware(beacon);
  ```

  ```ts SvelteKit theme={null}
  // src/hooks.server.ts
  import { createFetchMiddleware } from "@snowseo/beacon";
  import type { Handle } from "@sveltejs/kit";
  import { beacon } from "$lib/beacon";

  const middleware = createFetchMiddleware(beacon);

  export const handle: Handle = ({ event, resolve }) =>
    middleware({ request: event.request }, () => resolve(event));
  ```

  ```ts Hono theme={null}
  import { createFetchMiddleware } from "@snowseo/beacon";
  import { beacon } from "./lib/beacon";

  const middleware = createFetchMiddleware(beacon);

  // Hono's next() resolves to void, so read the response back off the context.
  app.use(async (c, next) => {
    c.res = await middleware({ request: c.req.raw }, async () => {
      await next();
      return c.res;
    });
  });
  ```

  ```ts Workers theme={null}
  import { createFetchMiddleware } from "@snowseo/beacon";
  import { beacon } from "./lib/beacon";

  const middleware = createFetchMiddleware(beacon);

  export default {
    async fetch(request: Request, env: Env, ctx: ExecutionContext) {
      return middleware(
        { request, waitUntil: (p) => ctx.waitUntil(p) },
        () => handleRequest(request, env),
      );
    },
  };
  ```

  ```ts Deno theme={null}
  import { createFetchMiddleware } from "@snowseo/beacon";
  import { beacon } from "./lib/beacon.ts";

  const middleware = createFetchMiddleware(beacon);

  Deno.serve((request) => middleware({ request }, () => handleRequest(request)));
  ```

  ```ts Netlify theme={null}
  import { createFetchMiddleware } from "@snowseo/beacon";
  import { beacon } from "./lib/beacon";

  const middleware = createFetchMiddleware(beacon);

  export default async (request: Request, context: Context) =>
    middleware(
      { request, waitUntil: (p) => context.waitUntil(p) },
      () => context.next(),
    );
  ```
</CodeGroup>

Verify any of them:

```bash theme={null}
curl -A "GPTBot/1.2" https://example.com/
curl -H "Accept: text/markdown" https://example.com/
```

***

## Nuxt

Nuxt is the awkward one. Nitro middleware runs *before* the route handler and cannot wrap its response, so `createFetchMiddleware` does not fit. Serving twins and reporting hits still work; advertising the twin on HTML responses has to be done by hand.

```ts server/middleware/beacon.ts theme={null}
import { appendLink, mergeVary } from "@snowseo/beacon";
import { beacon } from "../utils/beacon";

export default defineEventHandler(async (event) => {
  const request = toWebRequest(event);

  // Returning a Response short-circuits the route handler.
  const twin = await beacon.handle(request);
  if (twin) {
    return twin;
  }

  const path = new URL(request.url).pathname;
  if (await beacon.hasTwin(path, request)) {
    const link = `<${beacon.markdownUrlFor(path)}>; rel="alternate"; type="text/markdown"`;
    setResponseHeader(event, "Link", appendLink(getResponseHeader(event, "Link") as string ?? null, link));
    setResponseHeader(event, "Vary", mergeVary(getResponseHeader(event, "Vary") as string ?? null, "Accept"));
  }

  beacon.track(request, { format: "html", path });
});
```

<Note>
  The `Vary: Accept` line is not optional. Without it a cache can hand the HTML copy to a client that asked for Markdown, or the reverse.
</Note>

***

## Serverless runtimes

On Workers, Netlify Edge and similar, pass the platform's waiting context so a pending report is not killed when the response returns. The snippets above already do:

```ts theme={null}
{ request, waitUntil: (p) => ctx.waitUntil(p) }
```

Without it the report is cancelled mid-flight and the hit is simply lost, with no error anywhere.

***

## Options

```ts theme={null}
analytics: {
  key: process.env.SNOWSEO_BEACON_KEY!,
  endpoint: "https://beacon.example.com",  // your own collector
  disableCategories: ["training"],         // report only what you want
  onHit: (hit, match) => log.info(match.agent, hit.path),
  onError: (error) => log.warn(error),
}
```

`endpoint` accepts a bare origin; the ingest path is appended for you. See [self-hosting](/docs/beacon/self-hosting/overview) to run your own, and the [SDK reference](/docs/beacon/reference/sdk) for every option.
