goatcounter icon indicating copy to clipboard operation
goatcounter copied to clipboard

goatcounter does not count pageviews when using Next.js Link

Open pahhh-blo opened this issue 3 years ago • 5 comments

I recently started using goatcounter and I think that count.js does not take into account the method used by the Next.js Link component to navigate.

The default behavior of the Link component is to push a new URL into the history stack.

With this component, navigating does not trigger a DOMContentLoaded event, causing pageviews only to be counted either when the user visits the website for the first time, or a page is reloaded.

// count.js (Line 106-112)
// Run function after DOM is loaded.
	var on_load = function(f) {
		if (document.body === null)
			document.addEventListener('DOMContentLoaded', function() { f() }, false)
		else
			f()
	}

Is this behavior the best and only way to count pageviews, or can count.js be modified to take into account this type of navigation? What about using the DOMContentLoaded OR document.location property to detect when then url changes and trigger the count? Or perhaps using the History API to detect when a new item is pushed to the history.

I wish I could have created a pull request, but I have more questions than answers right now.

pahhh-blo avatar May 05 '21 14:05 pahhh-blo

Right now you'll need to add a wee bit of custom code for this which calls goatcounter.count() manually: https://www.goatcounter.com/code/spa

This can (and should) be integrated in the count.js script as well, but I want to make sure it works well with most/all common SPA framework; I'm not really sure if just hooking in to the History API is enough and/or works well for all of them as I'm not familiar with most of them. So while it's a small change in lines-of-code, it's a bit more work to test and ensure it works well everywhere. To be completely honest, I've been kind of procrastinating on this as I don't really like dealing with this kind of JS-heavy SPA stuff 😅

arp242 avatar May 05 '21 14:05 arp242

Thanks! I actually don't want to count changes in location.hash or location.search. I just want to make sure every change in the location.pathname is counted. I found the exact solution to this problem in this post.

I will have the following script in my website:

(function (history) {
  var pushState = history.pushState
  history.pushState = function (state) {
    window.goatcounter.count({
        path: location.pathname,
      })
    return pushState.apply(history, arguments)
  }
})(window.history)

Now I'm just concerned about double counting, because I still want to count DOMContentLoaded events, but I'm not completely sure if it's going to double count pageviews at any point. I think it won't, but please let me know if you think I can make it better.

It would be nice to have this included by default.

pahhh-blo avatar May 05 '21 15:05 pahhh-blo

Ehm, yeah; location.hash is the old-fashioned way of doing this SPA stuff; although I think some still use it.

Now I'm just concerned about double counting, because I still want to count DOMContentLoaded events, but I'm not completely sure if it's going to double count pageviews at any point. I think it won't, but please let me know if you think I can make it better.

Unless you call history.pushState on load, it should be fine I think? If not, you can use no_onload on data-goatcounter-settings on the script tag:

<script data-goatcounter="https://EXAMPLE.goatcounter.com/count"
        data-goatcounter-settings='{"no_onload": true}'
        async src="//gc.zgo.at/count.js"></script>

This is actually one of those things that makes the SPA integration tricky, because some frameworks may call pushState on load and others may not.

arp242 avatar May 05 '21 15:05 arp242

FWIW, I just ran into this same issue and settled on the following solution by following the Next.js Script component documentation page and adapting the code found on the GoatCounter SPA help page ever so slightly.

Note that I, like you, didn't care so much about the location.hash and location.search parts so just using the path given by the 'routeChangeComplete' event gave the path I needed.

Here's what my _app.js looks like:

import Head from 'next/head';
import Script from 'next/script';
import { useEffect } from 'react';
import { useRouter } from 'next/router';

function App({ Component, pageProps }) {
  const router = useRouter();
  useEffect(function sendGoatCounterEventsOnRoute() {
    const handleRouteChange = (path) => {
      window?.goatcounter?.count?.({
        path,
      })
    }
    router.events.on('routeChangeComplete', handleRouteChange)
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange)
    }
  }, [router.events])

  return (
    <>
      <Head>
        <title>Page Title</title>
      </Head>
      <Component {...pageProps} />

      <Script
        async
        data-goatcounter="https://YOURID.goatcounter.com/count"
        src="//gc.zgo.at/count.js"
        strategy='afterInteractive'
      />
    </>
  );
};

export default App;

spencerbyw avatar Jan 19 '22 03:01 spencerbyw

Thanks! I actually don't want to count changes in location.hash or location.search. I just want to make sure every change in the location.pathname is counted. I found the exact solution to this problem in this post.

I will have the following script in my website:

(function (history) {
  var pushState = history.pushState
  history.pushState = function (state) {
    window.goatcounter.count({
        path: location.pathname,
      })
    return pushState.apply(history, arguments)
  }
})(window.history)

Now I'm just concerned about double counting, because I still want to count DOMContentLoaded events, but I'm not completely sure if it's going to double count pageviews at any point. I think it won't, but please let me know if you think I can make it better.

It would be nice to have this included by default.

@diegoramosxyz Where exactly did you put this code in your Next.js app?

pankajparashar avatar Jun 09 '22 02:06 pankajparashar