---
title: "Tracking User Pages"
description: "Monitor which pages users are viewing and invalidate specific routes."
canonical_url: "https://nuxtseo.com/docs/skew-protection/guides/route-tracking"
last_updated: "2026-05-25T15:22:38.359Z"
---

Track which pages your users are currently viewing in real-time. This enables targeted version invalidation for specific routes without affecting users on other pages.

<callout type="warning">

Route tracking requires `connectionTracking: true` and only works with `sse` or `ws` update strategies.

</callout>

<callout type="info">

Route data is only available to authorized connections for privacy. Regular users cannot see which pages other users are viewing.

</callout>

## Setup

Enable route tracking in your Nuxt config:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  skewProtection: {
    connectionTracking: true,
    routeTracking: true
  }
})
```

### Authorization Hook

The server only sends stats to connections that pass authorization. Implement the `skew:authorize-stats` hook:

```ts [server/plugins/skew-auth.ts]
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('skew:authorize-stats', async ({ event, authorize }) => {
    // With nuxt-auth-utils
    const session = await getUserSession(event)
    if (session.user?.role === 'admin') {
      authorize()
    }
  })
})
```

## Real-time Stats via useActiveConnections

For live updates, use the `useActiveConnections` composable on authorized pages:

```vue [pages/admin/dashboard.vue]
<script setup lang="ts">
const { total, versions, routes, authorized } = useActiveConnections()

const sortedRoutes = computed(() => {
  return Object.entries(routes.value)
    .sort(([, a], [, b]) => b - a)
    .slice(0, 10)
})
</script>

<template>
  <div v-if="authorized" class="live-pages">
    <h3>Most Active Pages ({{ total }} users)</h3>
    <table>
      <tr v-for="[route, count] in sortedRoutes" :key="route">
        <td>{{ route }}</td>
        <td>{{ count }} viewers</td>
      </tr>
    </table>
  </div>
  <div v-else>
    Not authorized for live stats
  </div>
</template>
```

## API Endpoint (Alternative)

For non-WebSocket access (CI/CD, external tools), expose stats via an API endpoint:

```ts [server/api/admin/stats.get.ts]
export default defineEventHandler(async (event) => {
  const authHeader = getHeader(event, 'authorization')
  if (authHeader !== `Bearer ${process.env.ADMIN_SECRET}`) {
    throw createError({ statusCode: 401 })
  }

  return new Promise((resolve) => {
    const nitroApp = useNitroApp()
    // @ts-expect-error custom hook
    nitroApp.hooks.callHook('skew:stats', (stats) => {
      resolve(stats)
    })
  })
})
```

The stats object contains:

```ts
interface Stats {
  total: number // Total active connections
  versions: Record<string, number> // Users per build version
  routes: Record<string, number> // Users per route path
}
```

## Invalidating Users on Specific Routes

Force users on a specific page to refresh when you deploy critical changes to that page.

### Server Plugin for Route Invalidation

```ts [server/plugins/route-invalidation.ts]
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitroApp) => {
  const connections = new Map<string, { route: string, send: (data: unknown) => void }>()

  nitroApp.hooks.hook('skew:connection:open', ({ id, route, send }) => {
    connections.set(id, { route: route || '/', send })
  })

  nitroApp.hooks.hook('skew:connection:route-update', ({ id, route }) => {
    const conn = connections.get(id)
    if (conn)
      conn.route = route
  })

  nitroApp.hooks.hook('skew:connection:close', ({ id }) => {
    connections.delete(id)
  })

  // Expose function to invalidate specific routes
  // @ts-expect-error extending global
  globalThis.invalidateRoute = (routePattern: string | RegExp) => {
    const version = useRuntimeConfig().app.buildId
    for (const [, conn] of connections) {
      const matches = typeof routePattern === 'string'
        ? conn.route === routePattern || conn.route.startsWith(routePattern)
        : routePattern.test(conn.route)

      if (matches) {
        conn.send({ type: 'version', version, force: true })
      }
    }
  }
})
```

### API Endpoint for Invalidation

```ts [server/api/admin/invalidate-route.post.ts]
export default defineEventHandler(async (event) => {
  const { route, secret } = await readBody(event)

  if (secret !== process.env.ADMIN_SECRET) {
    throw createError({ statusCode: 401 })
  }

  // @ts-expect-error global function from plugin
  globalThis.invalidateRoute?.(route)

  return { ok: true, route }
})
```

### Usage from CI/CD

After deploying changes to a specific page:

```bash
# Invalidate all users on /checkout
curl -X POST https://yoursite.com/api/admin/invalidate-route \
  -H "Content-Type: application/json" \
  -d '{"route": "/checkout", "secret": "your-admin-secret"}'

# Invalidate all users on /blog/* routes
curl -X POST https://yoursite.com/api/admin/invalidate-route \
  -H "Content-Type: application/json" \
  -d '{"route": "/blog/", "secret": "your-admin-secret"}'
```

## Performance Considerations

Route tracking sends a message to the server on every client-side navigation. For high-traffic sites:

- The module tracks routes per-connection, not per-pageview
- Messages are small (~50 bytes per navigation)
- Consider disabling for sites with rapid navigation patterns

## Limitations

- Same limitations as [connection tracking](/docs/skew-protection/guides/live-connections#limitations)
- Route paths only (the module does not track query strings or hashes)
- Client-side navigations only (full page loads reconnect with new route)
- Route data is server-side only (not exposed to clients for privacy)
