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# ornpm install --save posthog-js# orpnpm 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.
// 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',person_profiles: 'identified_only',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.
// 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 pageviewsuseEffect(() => {if (pathname && posthog) {let url = window.origin + pathnameif (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.
// app/layout.jsimport "./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 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.
// src/plugins/posthog.jsimport 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",person_profiles: 'identified_only',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.
// src/main.jsimport { 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.
// src/routes/+layout.jsimport 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',person_profiles: 'identified_only',capture_pageview: false})}return};
After that, create a +layout.svelte
file in src/routes
. In it, use the afterNavigate
interceptor to capture pageviews.
<!-- 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
.
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',person_profiles: 'identified_only',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
.
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
- What to do after installing PostHog in 5 steps
- What engineers get wrong about analytics
- Complete guide to event tracking
The React Router (v6 or below) example is quite confusing. There is no react-router used in the example at all? So no idea where the provider should be placed exactly. For example, placing it on the top-level only tracks the initial page view.
For tracking all page views on React Router v5, I did this instead:
function TrackPosthogPageView() {const location = useLocation();const posthog = usePostHog();useEffect(() => {if (posthog) {posthog.capture('$pageview', {path: location.pathname,});}}, [location, posthog]);return null;}function App() {return (<BrowserRouter><TrackPosthogPageView /><Switch>{/* my routes */}</Switch></BrowserRouter>)}which seems to work, and tracks all page views
I'd like to know if $pageleave events are necessary and if posthog won't calculate some metrics correctly without it. Also a guidance on implementation would be nice - should we call it in useEffect return callback? Or is there a better approach?
The linked video nor the examples don't mention pageleave at all, so I assume it is not necessary. But is it really?
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,});}});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,});}});- Samuel4 months ago
Can someone please answer this?
This guide needs be updated to use
usePosthog
hook as suggested in a few other places in the documentationIt's on our roadmap 😁: https://github.com/PostHog/posthog.com/issues/8244
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?
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?
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})awesome, thanks!
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. :)
(Max length for these comments)
posthog.init( // new '', { api_host: '' } )
https://posthog.com/tutorials/single-page-app-pageviews#setting-up-posthog