Tracking pageviews in single-page apps (SPA)

Jun 11, 2024

A single-page application (or SPA) dynamically loads content for new pages using JavaScript instead of loading new pages from the server. Ideally, this enables users to navigate around the app without waiting for new pages to load, providing a seamless user experience.

PostHog's JavaScript Web SDK automatically captures pageview events on page load. The problem with SPAs is that page loads don't happen beyond the initial one. This means user navigation in your SPA isn't tracked.

To fix this, you can implement pageview capture manually using custom events. This tutorial shows you how to do this for the most popular SPA frameworks like Next.js, Vue, Svelte, and Angular.

Prerequisite: Each of these requires you to have an app created and PostHog installed. To install the PostHog JavaScript Web SDK, run the following command for the package manager of your choice:

Terminal
yarn add posthog-js
# or
npm install --save posthog-js
# or
pnpm add posthog-js

Tracking pageviews in Next.js (app router)

To add PostHog to your Next.js app, we start by creating the PostHogProvider component in the app folder. We set capture_pageview: false because we will manually capture pageviews.

JavaScript
// app/providers.js
'use client'
import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
import { useEffect } from 'react'
export function PHProvider({ children }) {
useEffect(() => {
posthog.init('<ph_project_api_key>', {
api_host: 'https://us.i.posthog.com',
capture_pageview: false
})
}, []);
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
}

To capture pageviews, we create another pageview.js component in the app folder.

JavaScript
// app/pageview.js
'use client'
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { usePostHog } from 'posthog-js/react';
export default function PostHogPageView() {
const pathname = usePathname();
const searchParams = useSearchParams();
const posthog = usePostHog();
// Track pageviews
useEffect(() => {
if (pathname && posthog) {
let url = window.origin + pathname
if (searchParams.toString()) {
url = url + `?${searchParams.toString()}`
}
posthog.capture(
'$pageview',
{
'$current_url': url,
}
)
}
}, [pathname, searchParams, posthog])
return null
}

Finally, we import both and put them together in the app/layout.js file.

JavaScript
// app/layout.js
import "./globals.css";
import { PHProvider } from './providers'
import dynamic from 'next/dynamic'
const PostHogPageView = dynamic(() => import('./pageview'), {
ssr: false,
})
export default function RootLayout({ children }) {
return (
<html lang="en">
<PHProvider>
<body>
{children}
<PostHogPageView />
</body>
</PHProvider>
</html>
);
}

Make sure to dynamically import the PostHogPageView component or the useSearchParams hook will deopt the entire app into client-side rendering.

Tracking pageviews in React Router

If you are using React Router AKA react-router-dom, start by adding the PostHogProvider component in the app folder. Make sure to set capture_pageview: false because we will manually capture pageviews.

JavaScript
// app/providers.js
'use client'
import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
if (typeof window !== 'undefined') {
posthog.init('<ph_project_api_key>', {
api_host: 'https://us.i.posthog.com',
capture_pageview: false
})
}
export function PHProvider({ children }) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
}

To capture pageviews, we create another pageview.js component in the app folder.

JavaScript
// app/pageview.js
import { useEffect } from "react";
import { useLocation } from 'react-router-dom';
export default function PostHogPageView() {
let location = useLocation();
// Track pageviews
useEffect(() => {
if (posthog) {
posthog.capture(
'$pageview',
{
'$current_url': window.location.href,
}
)
}
}, [location])
return null
}

Finally, we import both and put them together in the app/layout.js file.

JavaScript
import * as React from 'react';
import { PHProvider } from './providers'
import { PostHogPageView } from './pageview'
function App() {
return (
<html lang="en">
<PHProvider>
<body>
{children}
<PostHogPageView />
</body>
</PHProvider>
</html>
);
}

Tracking pageviews in Vue

After creating a Vue app and setting up the vue-router, create a new folder in the src/components named plugins. In this folder, create a file named posthog.js. This is where we initialize PostHog.

JavaScript
// src/plugins/posthog.js
import posthog from "posthog-js";
export default {
install(app) {
app.config.globalProperties.$posthog = posthog.init(
"<ph_project_api_key>",
{
api_host: "https://us.i.posthog.com",
capture_pageview: false
}
);
},
};

After this, you can add the plugin to the main.js file and use it along with the router to capture pageviews afterEach route change.

JavaScript
// src/main.js
import { createApp, nextTick } from 'vue'
import App from './App.vue'
import router from './router'
import posthogPlugin from '../plugins/posthog';
const app = createApp(App);
app.use(posthogPlugin).use(router).mount('#app');
router.afterEach((to, from, failure) => {
if (!failure) {
nextTick(() => {
app.config.globalProperties.$posthog.capture(
'$pageview',
{ path: to.fullPath }
);
});
}
});

Tracking pageviews in Svelte

If you haven't already, start by creating a +layout.js file for your Svelte app in your src/routes folder. In it, add the code to initialize PostHog.

JavaScript
// src/routes/+layout.js
import posthog from 'posthog-js'
import { browser } from '$app/environment';
export const load = async () => {
if (browser) {
posthog.init(
'<ph_project_api_key>',
{
api_host: 'https://us.i.posthog.com',
capture_pageview: false
}
)
}
return
};

After that, create a +layout.svelte file in src/routes. In it, use the afterNavigate interceptor to capture pageviews.

Svelte
<!-- src/routes/+layout.svelte -->
<script>
import posthog from 'posthog-js'
import { browser } from '$app/environment';
import { beforeNavigate, afterNavigate } from '$app/navigation';
if (browser) {
afterNavigate(() => posthog.capture('$pageview'));
}
</script>
<slot></slot>

Tracking pageviews in Angular

To start tracking pageviews in Angular, begin by initializing PostHog in src/main.ts.

JavaScript
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import posthog from 'posthog-js';
posthog.init('<ph_project_api_key>',
{
api_host: 'https://us.i.posthog.com',
capture_pageview: false
}
);
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

After setting up your routes and router, you can capture pageviews by subscribing to navigationEnd events in app.component.ts.

JavaScript
import { Component } from '@angular/core';
import { RouterOutlet, Router, Event, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
import { Observable } from 'rxjs';
import posthog from 'posthog-js';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
title = 'angular-spa';
navigationEnd: Observable<NavigationEnd>;
constructor(public router: Router) {
this.navigationEnd = router.events.pipe(
filter((event: Event) => event instanceof NavigationEnd)
) as Observable<NavigationEnd>;
}
ngOnInit() {
this.navigationEnd.subscribe((event: NavigationEnd) => {
posthog.capture('$pageview');
});
}
}

Further reading

Subscribe to our newsletter

Product for Engineers

Read by 45,000+ founders and builders.

We'll share your email with Substack

Questions? Ask Max AI.

It's easier than reading through 609 docs articles.

Comments

  • Chris
    2 months ago

    It appears that this method does not register "Visitors" or "Views" in the "Web Analytics" tab.

    I can only get a page view to load in the "Web Analytics" tab if I directly load a specific URL.

    Is this correct, or am I missing something? In a perfect world, calling .capture() would also register data in the "Web Analytics" tab.

    I've attached a screenshot of the "Web Analytics" tab I'm referring to.

    For reference, here's my code using Solid.js

    const location = useLocation();
    createEffect(() => {
    if (posthogService) {
    posthogService.capture("$pageview", {
    $current_url: domain + location.pathname,
    });
    }
    });

    Screenshot 2024-12-31 at 1.10.40 PM.png

  • Chris
    2 months ago

    It appears that this method does not register "Visitors" or "Views" in the "Web Analytics" tab.

    I can only get a page view to load in the "Web Analytics" tab if I directly load a specific URL.

    Is this correct, or am I missing something? In a perfect world, calling .capture() would also register data in the "Web Analytics" tab.

    I've attached a screenshot of the "Web Analytics" tab I'm referring to.

    For reference, here's my code using Solid.js

    const location = useLocation();
    createEffect(() => {
    if (posthogService) {
    posthogService.capture("$pageview", {
    $current_url: domain + location.pathname,
    });
    }
    });

    Screenshot 2024-12-31 at 1.10.40 PM.png

    • Ian
      2 months ago

      Which method? You need to capture a $pageview event for it to show up. Current URL should automatically be added as a property when that event is captured.

    • Chris
      Author2 months ago

      @ian/29296 Sorry, for being unclear. I was referring to the .capture() method.

      I’m building a mobile app with CapacitorJS and SolidJS. Capacitor wraps a web app in a native WebView, enabling access to platform-specific APIs for cross-platform development.

      Since this is a mobile app, I’m using client-side rendering and routing.

      To track page views, I’ve created a <PostHogPageView> component that captures $pageview and $pageleave events. (Code below.) These events appear in the Activity section of my PostHog dashboard when I navigate within the app using links.

      However, these page views do not appear in the Web Analytics section. The only way I can get page views to register under Web Analytics is by bypassing client-side routing, typing the URL into the browser, and initiating a full page load.

      Is this expected behavior?

      Here’s the component:

      export default function PostHogPageView() {
      const location = useLocation();
      createEffect(() => {
      if (posthogService) {
      posthogService.capture("$pageview", {
      $current_url: domain + location.pathname,
      });
      }
      });
      useBeforeLeave(() => {
      if (posthogService) {
      posthogService.capture("$pageleave", {
      $current_url: domain + location.pathname,
      });
      }
      });
      return null;
      }
  • D
    10 months ago

    This guide needs be updated to use usePosthog hook as suggested in a few other places in the documentation

  • Oliver
    a year ago

    Is there a way to suppress the initial PageView event that usually captured after the HTML page has been loaded and let the frontend router capture the first PageView event?

  • Oliver
    a year ago

    Is there a way to suppress the initial PageView event that usually captured after the HTML page has been loaded and let the frontend router capture the first PageView event?

    • Ian
      a year agoSolution

      Yup, you can include capture_pageview: false in your PostHog initialization like this:

      posthog.init('<ph_project_api_key>', {
      api_host: '<ph_instance_address>',
      capture_pageview: false
      })
    • Oliver
      Authora year ago

      awesome, thanks!

  • Josh
    a year ago

    Do you need to turn autocapture off if you're triggering the $pageview event manually?

    • Marcus
      a year ago

      Hey Josh, that is not required in most cases, since the manual tracking is not going to capture the initial $pageview event.

  • Erik
    2 years ago

    app.posthog.com vs. eu.posthog.com

    In the instructions the init command for Posthog in Index.js has "app.posthog.com". But it should be eu.posthog.com if you are not registered on the american server. You should perhaps mention that. Took me a while to troubleshoot. :)

PostHog.com doesn't use third party cookies - only a single in-house cookie.

No data is sent to a third party.

Ursula von der Leyen, President of the European Commission