Skip to main content

Single Page Application (SPA) Support

The tracker automatically detects navigation in Single Page Applications. No configuration required.

How It Works

The tracker intercepts:

  1. history.pushState() - Triggered when navigating to a new URL
  2. history.replaceState() - Triggered when replacing current URL
  3. popstate event - Triggered when user clicks back/forward

Each URL change triggers a new pageview event automatically.

Supported Frameworks

Works out of the box with:

FrameworkRouterTested
Reactreact-router v5/v6Yes
Vuevue-router v3/v4Yes
Angular@angular/routerYes
Next.jsApp Router, Pages RouterYes
Nuxt.jsnuxt/routerYes
Sveltesvelte-routing, SvelteKitYes
Remix@remix-run/reactYes

Any router that uses the History API is automatically supported.

Installation

Same as regular installation:

<script src="https://t.sealmetrics.com/t.js?id=YOUR_ACCOUNT_ID" defer></script>

React Example

With react-router

// App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/checkout" element={<Checkout />} />
</Routes>
</BrowserRouter>
);
}

No additional setup needed. Navigation between routes is tracked automatically.

Tracking Conversions

// CheckoutSuccess.jsx
import { useEffect } from 'react';

function CheckoutSuccess({ order }) {
useEffect(() => {
// Pageview is automatic, just track conversion
if (typeof sealmetrics !== 'undefined') {
sealmetrics.conv('purchase', order.total, {
order_id: order.id,
currency: order.currency
});
}
}, [order]);

return <div>Thank you for your order!</div>;
}

With Content Grouping

// Use the group parameter in the script tag per page type,
// or set it dynamically:

function ProductPage({ product }) {
useEffect(() => {
if (typeof sealmetrics !== 'undefined') {
sealmetrics({ group: 'product' });
}
}, [product.id]);

return <div>{product.name}</div>;
}

Next.js Example

App Router (Next.js 13+)

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }) {
return (
<html>
<head>
<Script
src="https://t.sealmetrics.com/t.js?id=YOUR_ACCOUNT_ID"
strategy="afterInteractive"
/>
</head>
<body>{children}</body>
</html>
);
}

Pages Router

// pages/_app.tsx
import Script from 'next/script';

export default function MyApp({ Component, pageProps }) {
return (
<>
<Script
src="https://t.sealmetrics.com/t.js?id=YOUR_ACCOUNT_ID"
strategy="afterInteractive"
/>
<Component {...pageProps} />
</>
);
}

Dynamic Content Grouping

// app/blog/[slug]/page.tsx
'use client';

import { useEffect } from 'react';

export default function BlogPost({ params }) {
useEffect(() => {
if (typeof window !== 'undefined' && typeof sealmetrics !== 'undefined') {
sealmetrics({ group: 'blog' });
}
}, [params.slug]);

return <article>...</article>;
}

Vue Example

With vue-router

// main.js
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';

const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/products', component: Products },
{ path: '/products/:id', component: ProductDetail }
]
});

createApp(App).use(router).mount('#app');

Tracking Events in Components

<!-- ProductDetail.vue -->
<template>
<div>
<h1>{{ product.name }}</h1>
<button @click="addToCart">Add to Cart</button>
</div>
</template>

<script>
export default {
methods: {
addToCart() {
if (typeof sealmetrics !== 'undefined') {
sealmetrics.micro('add_to_cart', {
product_id: this.product.id,
product_name: this.product.name
});
}
// ... add to cart logic
}
}
};
</script>

Angular Example

Module Setup

// app.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'products', component: ProductsComponent },
{ path: 'products/:id', component: ProductDetailComponent }
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}

Tracking Service

// analytics.service.ts
import { Injectable } from '@angular/core';

declare global {
interface Window {
sealmetrics: any;
}
}

@Injectable({ providedIn: 'root' })
export class AnalyticsService {
trackConversion(type: string, amount: number, properties?: object) {
if (typeof window.sealmetrics !== 'undefined') {
window.sealmetrics.conv(type, amount, properties);
}
}

trackMicro(type: string, properties?: object) {
if (typeof window.sealmetrics !== 'undefined') {
window.sealmetrics.micro(type, properties);
}
}
}

Nuxt.js Example

Nuxt 3

// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
script: [
{
src: 'https://t.sealmetrics.com/t.js?id=YOUR_ACCOUNT_ID',
defer: true
}
]
}
}
});

Composable for Tracking

// composables/useAnalytics.ts
export function useAnalytics() {
const trackConversion = (type: string, amount: number, props?: object) => {
if (process.client && typeof sealmetrics !== 'undefined') {
sealmetrics.conv(type, amount, props);
}
};

const trackMicro = (type: string, props?: object) => {
if (process.client && typeof sealmetrics !== 'undefined') {
sealmetrics.micro(type, props);
}
};

return { trackConversion, trackMicro };
}

TypeScript Declarations

Add type declarations for better IDE support:

// types/sealmetrics.d.ts
interface SealmetricsOptions {
group?: string;
}

interface SealmetricsFunction {
(options?: SealmetricsOptions): void;
conv(type: string, amount: number, properties?: Record<string, string>): void;
micro(type: string, properties?: Record<string, string>): void;
}

declare global {
const sealmetrics: SealmetricsFunction;
const sm: SealmetricsFunction;
const _sm: SealmetricsFunction;
}

export {};

Troubleshooting

Duplicate Pageviews

If you see duplicate pageviews, check:

  1. Script is not included multiple times
  2. Not calling sealmetrics() manually when automatic tracking is sufficient
  3. No other analytics wrapper is re-triggering events

Missing Pageviews on Route Change

The tracker uses the History API. If your framework uses a custom navigation method that bypasses pushState/replaceState, pageviews won't be tracked.

Solution: Call sealmetrics() manually after navigation:

router.afterEach(() => {
if (typeof sealmetrics !== 'undefined') {
sealmetrics();
}
});

Hash-Based Routing

If your SPA uses hash-based routing (/#/path), the tracker will not automatically detect changes.

Solution: Listen for hashchange and track manually:

window.addEventListener('hashchange', function() {
if (typeof sealmetrics !== 'undefined') {
sealmetrics();
}
});

Server-Side Rendering (SSR)

Always check if sealmetrics exists before calling it:

if (typeof sealmetrics !== 'undefined') {
sealmetrics.conv('purchase', 99.99);
}

Or for Next.js/Nuxt:

if (typeof window !== 'undefined' && typeof sealmetrics !== 'undefined') {
sealmetrics.conv('purchase', 99.99);
}