Rotary Districts/Rotary Clubs · ClubRunner CMS

PART 1
GTM & GA4 Configuration
for Websites Using ClubRunner CMS

Step-by-step instructions for correct web analytics, tailored to ClubRunner's multi-domain architecture.

Anders Tihkan (Värmdö Skärgård RC)  ·  Updated 18 August 2026

Do you have feedback on this guide? Leave feedback — it helps us improve the material.

Part 1 of 2  ·  This document describes how to configure GTM and GA4.
Part 2 — Traffic Analysis and Reporting — describes how the collected data is evaluated and compiled into a report.
The support page on Traffic Analysis (use translation buttons at the top of the page) explains why measurement and configuration are needed.

Changelog

18 August 2026Section 2h: the normalisation in the Canonical Page Location variable was extended to also strip a trailing slash (/clubsmap//clubsmap), not just case. Already implemented 2h? Add one line to your existing variable — see the section for the code. No new variables, triggers, or tags are required, and no other step in the document is affected.

18 August 2026Section 2h, Step 2: clarified that the page_location parameter must be set under Configuration settings on the Google tag — not under Shared event settings, which was previously (incorrectly) mentioned as equivalent. Shared event settings only apply to GA4 Event tags and never reach the automatic page_view. Check your setup if you added the parameter before this date: if it's under Shared event settings, case/trailing-slash variants will keep appearing as separate rows in "Pages and screens", even though steps 1 and 2 looked complete.

Contents
  1. Changelog
  2. ClubRunner — Register the GTM Container
  3. Google Analytics 4 — Settings
  4. Google Tag Manager — Tags and Triggers
  5. Known Limitations with ClubRunner
  6. Checklist
  7. National Portal — rotary.se (Measuring Onward Clicks)

Why ClubRunner Requires Special Configuration

ClubRunner uses five separate domains. Without the correct configuration, GA4 interprets navigation between the domains as external visits and creates new sessions with incorrect traffic sources. All five must be treated as internal.

Public site (alias)rotary[district].se
Portal / CMS backendportal.clubrunner.ca
Content APIcontent.clubrunner.ca
Admin interfaceadmin.clubrunner.ca
Member and club datacrsadmin.com

Files that visitors download via ClubRunner are additionally served from a separate blob domain: clubrunner.blob.core.windows.net. This requires its own trigger (see section 2g).

GTM and GA4 in English

Make sure that your language settings are in English, to match all menu items and field names in these instructions. Follow the steps below to make sure you see the same labels.

Google Tag Manager — add ?hl=en to the address bar:
https://tagmanager.google.com/?hl=en

Google Analytics 4 — add ?hl=en to the address bar:
https://analytics.google.com/?hl=en

For a district, configuring tracking is essential — see the support page on Traffic Analysis (use translation buttons at the top of the page).

Does this also apply to a club site?

The same configuration can be applied to a ClubRunner club. The mechanics are identical — domains, triggers and tags work the same way. The difference is that a typical club site has such low traffic volume that statistical measurements rarely yield meaningful results: segmentation, funnel analysis and traffic-source breakdowns require enough observations for the patterns to be reliable.

Several of the advanced tags in this document, however, are valuable regardless of volume, because they don't rely on statistics but instead provide a direct list of concrete events — even a handful of observations is useful:

Document downloads (2f/2g)Which minutes, bylaws or forms club members actually download.
Search term tracking (2i)What visitors are looking for — reveals content that is missing or hard to find in the menu.

Beyond these, the following headline metrics always provide a good basis, regardless of traffic volume:

TrendSessions and users over time — does traffic appear to be growing or shrinking?
ContentMost visited pages — what content attracts visitors?
Traffic sourcesOrganic search, direct traffic or referrals — how do people find the site?
Device typeMobile versus desktop — determines whether the layout works for the actual audience.
EngagementAverage engagement time per session — do visitors stay and read?
BounceEngaged sessions share — proportion of visits with genuine interaction.

Recommendation: set up the base tag, document downloads and search term tracking on a club site regardless of volume — they cost nothing extra and produce directly useful data.

0

ClubRunner — Register the GTM Container

0a

Enter the Container ID in ClubRunner's analytics settings

ClubRunner needs to know your GTM container ID in order to inject the tracking code automatically on every page. Without this, no data reaches GTM or GA4.

The page has two tracking fields:

Section Field What to do
Google Analytics Stream ID Must be empty — delete any existing value.
Google Tag Manager Container ID Enter your Container ID, e.g. GTM-XXXXXXX. Then click Save.
Stream ID must be empty — If you fill in a GA4 Stream ID alongside GTM, all traffic gets double-counted: every page view shows up as two separate hits in GA4. Leave the field empty and let GTM handle all tracking. If you previously tracked from ClubRunner directly to GA4, keep using the same GA4 ID in GTM.
1

Google Analytics 4 — Settings

1a

Add ClubRunner's domains as "Unwanted referrals"

This is the single most important setting for ClubRunner sites. It prevents traffic from ClubRunner's own domains from being interpreted as external visits and breaking ongoing sessions, and prevents clicks to these domains from being counted as outbound external link clicks.

1
Open GA4 Admin
Go to analytics.google.com, select the right GA4 property, click Admin (the gear icon at the bottom of the left-hand menu).
2
Open Data Streams
Under "Property settings" → click Data Streams → click your web data stream (it should show your site's URL).
3
Open Configure tag settings
Scroll down and click Configure tag settings (blue link at the bottom of the data stream view).
4
Open List unwanted referrals
Click Show all if the list is collapsed, then click List unwanted referrals.
5
Add the domains
Click Add condition and add one row per domain. Choose match type "Domain contains" for each. Save with Save.
// Domain contains — add one row per domain: clubrunner.ca crsadmin.com clubrunner.blob.core.windows.net
Tip: clubrunner.ca alone is enough to cover the portal, content and admin subdomains. It's still worth adding them individually for clarity. clubrunner.blob.core.windows.net needs to be added separately — it's the domain ClubRunner serves page files from.
1b

Turn off outbound clicks in Enhanced Measurement

GA4 can automatically track clicks on outbound links via Enhanced Measurement. Since we handle this in GTM with ClubRunner exceptions (see section 2b), the built-in feature must be turned off — otherwise duplicate events are created.

Enhanced Measurement feature Setting Comment
Page views ✅ On Always on
Scrolls ✅ On Fires at 90% scroll
Outbound clicks 🚫 Off Handled via GTM with ClubRunner exceptions
Site search Either (no effect) ClubRunner never sends the search term as a query parameter — the feature can never trigger here. Search tracking is handled entirely via the custom GTM setup, see section 2i
Video engagement ✅ On Captures YouTube embeds
File downloads 🚫 Off Handled via GTM (2f/2g). The blob files have file extensions and would otherwise be double-counted by the built-in tracking
1c

Register custom dimensions for event parameters

Events created in GTM can carry parameters (e.g. which URL was clicked). Parameters don't automatically appear in GA4 reports — they must be registered as "custom dimensions" to become searchable and reportable. The reports are built in the next section, 1d.

Create one row per parameter:

Dimension name (your choice) Scope Event parameter / User property Description (used by)
Link URL Event link_url outbound_click, bulletin_viewed
Link text Event link_text outbound_click, file_download (tags in 2f and 2g)
File name Event file_name file_download (tags in 2f and 2g)
Search term Event search_term view_search_results
Special Event Label Event special_event_label special_event (2j)
Note: It can take up to 24 hours before custom dimensions start populating in reports after being created. "Event parameter" is a dropdown — the parameter search_term only appears in the list once GA4 has received it from a real search on the published site. Registration is not retroactive.
1d

Build an Exploration with five tabs for custom events

GA4's standard reports show total counts per event but not what was clicked or searched for. That requires an Exploration (Explore). Build one shared exploration with five tabs — one per custom event — so all the detail data lives in one place. The settings below are a starting point; adjust them yourself to tailor the reports further.

Getting started

  1. Go to Explore → Blank exploration.
  2. Name the exploration, e.g. Custom events [period].
  3. Open the Variables panel (left) and click + next to Dimensions and Metrics respectively to add these shared variables for all tabs:
    Dimensions: Event name Link text Link URL File name Search term Special Event Label Metric: Event count
  4. Then create five tabs in the order below. Each tab is configured in the Tab Settings panel (right).
    Tip: After the first report, click the down arrow on the tab name and duplicate the tab — then you only need to make a few small changes to the new tab's Settings. Edit the tab name by clicking on it.

Tab 1 — Outbound clicks

Tab name: Outbound clicks Rows: Link text // primary row Link URL // nested row Nested rows: Yes Show rows: 100 Filter: Event name exactly matches outbound_click Sort: Event count descending

Tab 2 — Sitesearch CR

Tab name: Sitesearch CR Rows: Search term Nested rows: No Show rows: 25 Filter: Event name exactly matches view_search_results

Tab 3 — Downloads

Tab name: Downloads Rows: Link text File name Nested rows: No Show rows: 50 Filter: Event name exactly matches file_download Sort: File name descending
Two triggers, one event: the tag for 2f (document pages) and the tag for 2g (blob files) both use the event name file_download. They appear as one and the same event in the report — file_name contains the full URL and distinguishes the sources. Filter on clubrunner.blob.core.windows.net if you want to separate them.

Tab 4 — Bulletin viewed

Tab name: Bulletin viewed Rows: Link text // primary row Link URL // nested row Nested rows: Yes Show rows: 100 Filter: Event name exactly matches bulletin_viewed
UUID not readable? Each /bulletin/View/[uuid] is unique to a specific newsletter. Look up the UUID in ClubRunner under Bulletins to identify which issue each row refers to.

Tab 5 — Special Event

Tab name: Special Event Rows: Special Event Label Nested rows: No Show rows: 10 Filter: Event name exactly matches special_event Sort: Event count descending
No nested URL row here, unlike tab 1 and 4: each label is by design tied to one specific, selected link (2j), so multiple URL variants never occur under the same label. Only relevant once section 2j has been configured — the tab's filter simply matches no rows until then.
1e

Referral spam — domain list and hostname filter

There are two types of referral spam with entirely different mechanisms, and they require different responses.

Modern MP spammers (e.g. leadsgo.io) inject fake sessions directly against GA4's collection server via the Measurement Protocol — your gtag.js never runs. This means "List unwanted referrals" has no effect: the list is a client-side check that MP spam never passes through. The sessions are still recorded in GA4 with the spam domain intact as the source. The only effective protection against MP spam is the Web hostname traffic filter (see below).

Crawling spam (older bots, button and traffic spam) actually visits the site. Gtag.js loads, and the list prevents the spam domain from being written as the referral source — the session appears under direct rather than referral. The session is still recorded, but does not skew the traffic source report.

The list below is still worth maintaining as a monitoring layer for crawling spam and email proxies. GA4 allows a maximum of 50 domains per data stream — the list below contains around 30.

Known spam domains

Domain Note
leadsgo.io Most common in 2024–2025; "the new Semalt"
semalt.com Classic spammer, active since 2014
semalt.net Variant of the above
trafficpeak.io Actively reported in 2024–2025
news.grets.store Actively reported in 2024–2025
static.seders.website Actively reported in 2024–2025
info.seders.website Variant of the above
kar.razas.site Actively reported in 2024–2025

Button/widget spam (priority 2)

Poses as shared social buttons to attract clicks.

buttons-for-website.com
buttons-for-your-website.com
free-share-buttons.com
free-social-buttons.com
free-floating-buttons.com
floating-share-buttons.com
simple-share-buttons.com
share-buttons.xyz
social-buttons.com
social-widget.xyz

Traffic spam (priority 2)

Domains that sell or simulate website traffic.

darodar.com
priceg.com
blackhatworth.com
makemoneyonline.com
ilovevitaly.com
econom.co
anticrawler.org
ghostvisitor.com
gammatraffic.com
traffic-cash.xyz
bot-traffic.xyz
trafficbot.life
free-traffic.xyz
bottraffic4free.club
rapidhits.net

Email/newsletter proxies to watch

May be legitimate — investigate if you don't recognise the sender before blocking.

Domain Note
url41.mailanyone.net Email tracking proxy — add if you don't send newsletters via the service
statics.teams.cdn.office.net Microsoft Teams link preview — may be internal traffic
New spam domains appear continuously. Recommended routine: review the Source/Medium report filtered on medium = referral once a month. Domains with an unrealistically high engagement rate (>95%) and long session duration but zero conversions are typical spam signatures.

Web hostname traffic — effective protection against MP spam

GA4's Web hostname traffic filter removes hits where the hostname does not match your real domains. MP spam by definition lacks a valid hostname from your site, making the filter reliably effective against that type of spam traffic — unlike the domain list above.

Configure the filter under Admin → Data settings → Data filters. Enable it in test mode first and verify that no legitimate traffic is excluded before switching it to active mode.
1f

Connect Google Search Console to GA4

Google Search Console (GSC) shows which search terms in Google lead visitors to the site — the incoming search traffic. This complements the search term tracking in section 2i, which captures what visitors search for on the site once they're already there.

Prerequisite: The site must already be verified in Google Search Console. The easiest way to verify is with an HTML meta tag in GTM (the Google tag's Additional configuration → Custom fields → google-site-verification).
  1. Click Link.
  2. Select the Search Console property that matches the site.
  3. Select the data stream and confirm.

Once the link is active, data appears under Reports → Acquisition → Search Console with the following dimensions:

Report What it shows
Queries Which search terms in Google generated clicks and impressions
Google organic search pages Which pages ranked, with clicks, impressions, CTR and average position
2–3 day delay. GSC data in GA4 is not updated in real time — expect the most recent days to be missing from the report.

2

Google Tag Manager — Tags and Triggers

Order: Always create the trigger first, then the tag that references it. Always start by enabling the necessary built-in variables (2a) before creating triggers that rely on click data.
2a

Enable built-in variables for click tracking

GTM has built-in variables that capture click data (which URL, which link text, etc.). They must be enabled manually before they can be used in triggers and tags.

Tick the following under the "Clicks" section:

Click Element Click Classes Click ID Click Target Click URL Click Text

Close the configuration panel (the cross, top right). The variables are now available in all triggers and tags.

UI tip: When filling in conditions in a trigger and clicking directly from the variable field to the value field with the mouse, the variable selection is reset. Press Tab instead to move focus between fields — this preserves the selection.
2b

Google tag — GA4 base configuration

The central tag that initialises GA4. All other GA4 event tags reference this one. It must be of type Google tag (not the older "GA4 Configuration").

1
Choose tag type
Click "Tag Configuration" → choose Google tag (not "Google Analytics: GA4 Event").
2
Enter the Tag ID
Enter your GA4 Measurement ID, format G-XXXXXXXXXX. Found in GA4 under Admin → Data Streams → your stream. If you already use GA4, keep using the same ID even though tracking now goes via GTM.
3
Set the trigger
Click "Triggering" → choose Initialization – All Pages. Note: this is not "All Pages (Page View)" — the Google tag must be initialised early in the page load.
4
Name the tag and save
Name the tag, e.g. Google Analytics. Save with Save.
2c

Microsoft Clarity — tag

Microsoft Clarity is a free tool for session recording and heatmaps. It can also serve as an independent metric to compare against GA4 figures, for instance to estimate drop-off from declined cookies.

The Clarity project ID is found under Settings → Setup → Get tracking code in Microsoft Clarity at clarity.microsoft.com. It's a short alphanumeric string, e.g. abc123xyz.

Note: Microsoft Clarity is available as a ready-made tag template in GTM's template gallery. Search for "Clarity" under Tag Configuration → choose the template from Microsoft. Then you only need to paste the Clarity project ID into one field, without handling the HTML code below manually. If the template is unavailable, use the Custom HTML option.

Option A — Clarity template (recommended)

Tag type: Microsoft Clarity (from the template gallery) Project ID: YOUR_CLARITY_PROJECT_ID Trigger: Initialization – All Pages

Option B — Custom HTML

Tag type: Custom HTML Trigger: Initialization – All Pages // HTML to paste: <script type="text/javascript"> (function(c,l,a,r,i,t,y){ c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; t=l.createElement(r);t.async=1; t.src="https://www.clarity.ms/tag/"+i; y=l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t,y); })(window, document, "clarity", "script", "YOUR_CLARITY_PROJECT_ID"); </script>
2d

Outbound link clicks — trigger and tag

Registers clicks on external links. ClubRunner's own domains, bulletin links and page file downloads must be excluded so they aren't counted as external or double-counted against the download tags.

Step 1 — Create the trigger

Trigger type: Click – Just Links This trigger fires on: Some Link Clicks // Conditions — fires when ALL are met: // Gate: real http(s) links only – blocks filter/JS links with no href {{Click URL}} matches RegEx ^https?:// // Replace rotary[district].se with your public domain: {{Click URL}} does not start with https://rotary[district].se {{Click URL}} does not start with https://portal.clubrunner.ca {{Click URL}} does not start with https://content.clubrunner.ca {{Click URL}} does not start with https://admin.clubrunner.ca {{Click URL}} does not contain crsadmin.com {{Click URL}} does not contain clubrunner.blob.core.windows.net {{Click URL}} does not contain /bulletin/View/ {{Click URL}} does not contain view.officeapps.live.com Trigger name: Outbound link click
Why the blob domain and Office Viewer are excluded: Files in the "Documents" widget are served from clubrunner.blob.core.windows.net and are already captured by the download tag in section 2g. Without this row, the same click would also trigger outbound_click and be double-counted as external. view.officeapps.live.com is added as a defensive exception: Word, Excel and PowerPoint documents under /documents/sv-se/ are already covered by the trigger in 2f (ClubRunner's href always points to rotary2405.se and the redirect happens server-side), but if a link is accidentally hardcoded directly to Office Viewer, it would otherwise not be captured as outbound_click.
Important: Each row is its own condition. Click "+ Add condition" for every new row. All conditions in the list must be met simultaneously for the trigger to fire — that is the correct behaviour here.
Why the RegEx row is needed: A trigger with only negative conditions ("does not…") fires incorrectly on links with no real href — e.g. ClubRunner's "Filter" button on the events page, which filters via JavaScript. In that case {{Click URL}} is empty, and an empty string satisfies every negative condition at once. The condition Click URL matches RegEx ^https?:// requires a genuine http(s) address and suppresses the false match. (Triggers 2e–2g are not affected, since they rely on positive contains/starts with conditions.)

Step 2 — Create the tag

Tag type: Google Analytics: GA4 Event Configuration: [select your Google tag] Event name: outbound_click Event parameters: Parameter name: link_url Value: {{Click URL}} Parameter name: link_text Value: {{Click Text}} Trigger: Outbound link click Tag name: GA4 – Outbound link click
2e

Newsletter view (bulletin) — trigger and tag

ClubRunner does not include GTM on /bulletin/View/ pages. The solution is to capture the click on the link in the originating session instead, which produces an event tied to the active session.

The trigger must also be added as an exception in the "Outbound link click" trigger (section 2d) so that bulletin clicks aren't counted twice.

Case-sensitive: ClubRunner uses a capital V in the URL: /bulletin/View/. Wrong case in the condition means the trigger never fires.

Step 1 — Create the trigger

Trigger type: Click – Just Links This trigger fires on: Some Link Clicks {{Click URL}} contains /bulletin/View/ Trigger name: Bulletin-view

Step 2 — Create the tag

Tag type: Google Analytics: GA4 Event Configuration: [select your Google tag] Event name: bulletin_viewed Event parameters: Parameter name: link_url Value: {{Click URL}} Trigger: Bulletin-view Tag name: GA4 – Bulletin view
2f

Document download — trigger and tag

Captures clicks on ClubRunner's document links (files in Documents). Match on the path /documents/sv-se/ in the URL rather than the file extension — ClubRunner's document links don't always have a file extension, and path matching is simpler and more robust.

Step 1 — Create the trigger

Trigger type: Click – Just Links This trigger fires on: Some Link Clicks {{Click URL}} contains /documents/sv-se/ // Matches ClubRunner's document pages – simpler and more specific than RegEx Trigger name: Document download
Automatically covers Office documents: ClubRunner uses the same URL pattern (/documents/sv-se/) in the href attribute for all document types — PDF, Word, Excel and PowerPoint. The redirect to Microsoft Office Online Viewer (view.officeapps.live.com) happens server-side after the click; GTM captures the href attribute before the redirect. No separate trigger is needed for Office documents. Events that appear in GA4 as outbound clicks to view.officeapps.live.com are an artefact of Enhanced Measurement and disappear once Outbound clicks is turned off (section 1b).

Step 2 — Create the tag

Tag type: Google Analytics: GA4 Event Configuration: [select your Google tag] Event name: file_download Event parameters: Parameter name: file_name Value: {{Click URL}} Parameter name: link_text Value: {{Click Text}} Trigger: Document download Tag name: GA4 – Document download
2g

Page file downloads via ClubRunner blob storage — trigger and tag

Files uploaded in the Download Page Files widget are stored by ClubRunner on a separate domain: clubrunner.blob.core.windows.net. These files sit under a path prefix that starts with the district's/club's ClubRunner ID (e.g. /00000060099/). Since that ID is unique to the district/club, it's sufficient as a match — the trigger then becomes independent of language and folder structure. They require a separate trigger because the domain differs from your public site.

The blob domain must also be included in the exceptions in the "Outbound link click" trigger (section 2d) so these downloads aren't simultaneously counted as external clicks.

What's the difference from 2f? Both triggers match on path, not file type. 2f captures document links on your own domain (/documents/sv-se/), while 2g captures files that ClubRunner serves from blob storage on a different domain (clubrunner.blob.core.windows.net/[org-ID]/). They sit on different domains and therefore each need their own trigger.

Step 1 — Create the trigger

Trigger type: Click – Just Links This trigger fires on: Some Link Clicks {{Click URL}} starts with https://clubrunner.blob.core.windows.net/00000060099/ // The org ID is unique to your district/club and is sufficient as a match – nothing else in the path is needed. // Replace 00000060099 with your district/club ClubRunner organisation ID. // You can see your ClubRunner ID while logged in, in the address bar: https://admin.clubrunner.ca/[ID]/ Trigger name: Page file download

Step 2 — Create the tag

Tag type: Google Analytics: GA4 Event Configuration: [select your Google tag] Event name: file_download Event parameters: Parameter name: file_name Value: {{Click URL}} Parameter name: link_text Value: {{Click Text}} Trigger: Page file download Tag name: GA4 – Page file download
Note: Both download tags (2f and 2g) use the same event name file_download. In GA4 they appear as a single combined metric under Engagement → Events. The file_name parameter contains the full URL, and you can filter by domain to separate them in reports.
2h

Consolidating page views: renamed pages, case, and trailing slash (canonical path)

GA4 groups page views by page_location (the actual URL). On ClubRunner, this often causes the same page to be split across several rows for three reasons: renamed pages are reachable via an old alias URL alongside the clean address, case in the path (/Events vs /events, /ClubInfo/… vs /clubinfo/…) creates duplicates, and a trailing slash does the same (/clubsmap/ vs /clubsmap). The variable below fixes all three.

When a page is renamed, ClubRunner lets the old address keep working — but without a 301 redirect. The content is served on a long alias URL, e.g. /60099/Page/Show?ClassCode=Page&Slug=…, alongside the clean /page/<slug>. The result is two pages in the report for the same content.

Two pitfalls: (1) the alias URL does not 301-redirect — it responds with 200 — so GA4 registers both addresses as separate pages. (2) the canonical tag on the alias page points to the wrong host: portal.clubrunner.ca instead of the public domain. Copying the canonical straight into page_location therefore gives you a portal.clubrunner.ca row instead.

Here's what it actually looks like for the same page reached via two entry points:

Entry point (URL you click) Canonical sent by ClubRunner
rotary2405.se/page/support-logga-in-hjälp https://rotary2405.se/page/support-logga-in-hjälp
rotary2405.se/60099/Page/Show?…&Slug=support-logga-in-hjälp https://portal.clubrunner.ca/page/support-logga-in-hjälp ❌ wrong host

The point: the path /page/support-logga-in-hjälp is identical in both canonicals — only the host differs. That's why the variable builds page_location from the canonical path (correct in both cases) but always sets it on the public domain via location.origin.

Step 1 — Variable that builds a clean page_location

Variable type: Custom JavaScript Variable name: Canonical Page Location function() { var keep = ['utm_source','utm_medium','utm_campaign','utm_term', 'utm_content','utm_id','gclid','gbraid','wbraid', 'dclid','gclsrc','fbclid','msclkid','srsltid']; var path; try { var c = document.querySelector('link[rel="canonical"]'); path = c && c.href ? new URL(c.href).pathname : location.pathname; } catch (e) { path = location.pathname; } path = path.toLowerCase(); // ClubRunner is case-insensitive – merges /Events, /ClubInfo/… etc. if (path.length > 1 && path.charAt(path.length - 1) === '/') path = path.slice(0, -1); // trailing slash – /clubsmap/ and /clubsmap are the same page if (/^\/page\/showhomepage\/?$/.test(path)) path = '/'; // homepage's internal route → root var loc = location.origin + path; // public domain + canonical path var src = new URLSearchParams(location.search); var out = new URLSearchParams(); keep.forEach(function(k){ if (src.has(k)) out.set(k, src.get(k)); }); var qs = out.toString(); return qs ? loc + '?' + qs : loc; }

Canonical path (renamed pages). The variable takes the path from the canonical tag (the same clean /page/<slug> regardless of whether the visitor arrived via the alias URL or the clean address), sets it on the public domain via location.origin, and keeps only campaign parameters (utm_*, gclid, etc.) so attribution survives.

Case correction. The line path = path.toLowerCase() lowercases the path — ClubRunner is case-insensitive, so /Events, /ClubInfo/… and /ClubsMap are merged with their lowercase variants. In a year of raw data for rotary2405.se, the period immediately before the GTM clean-up (1 March 2025 – 28 February 2026), 12,681 of the period's 30,942 views (41%) were spread across 207 such duplicate groups — /clubinfo/* was often split into three or four case variants. Lowercasing also normalises the case of %-encoding (%C3%A4%c3%a4). Since only the path is lowercased, campaign parameter values are unaffected.

Trailing slash. The line that strips a trailing / (except on the root, which is left untouched) likewise merges /clubsmap/ with /clubsmap — ClubRunner responds identically whether the path ends with a slash or not. In the same year of data, trailing-slash duplicates accounted for a further 10,664 views spread across 106 groups. Combined, the case and trailing-slash corrections together merge 259 duplicate groups — 16,837 of the period's 30,942 views (54%). One concrete example: /ClubsMap, /clubsmap and /clubsmap/ were three separate rows in the report despite all pointing to the same page — 1,287 views between them.

Precondition for the normalisation: this is safe specifically because ClubRunner serves the same page regardless of URL case and trailing slash. On a server where paths are sensitive to either, the corresponding line could merge two different pages — in that case, that line could not be used.
Special case — the homepage: ClubRunner serves the homepage on two live addresses — the clean root / and the internal route /page/showhomepage (which logo/"Home" links often point to). Both have self-referencing canonicals, so canonical alone doesn't merge them; that's why the if line in the variable forces /page/showhomepage to the root. The bare route /60099/Page/Show without ?…&Slug=, however, is an error page ("Account not found", soft 404) — it should not be normalised away. If it turns up in reports, it's a useful signal of broken links (e.g. set an alert on page_title containing "General Error").

Step 2 — Override page_location in the Google tag

Open the Google tag from section 2b. Under Configuration settings, click Add parameter and fill in:

Configuration parameter: page_location Value: {{Canonical Page Location}}
Case-sensitive field name. The parameter name must be typed exactly page_location — all lowercase. That's what GA4's built-in field is called; if you type Page_location (capital P, a common instinct since GTM's own UI field is labelled "Page location") GA4 won't recognise it as the reserved field. Instead an entirely separate, unused custom parameter gets created, and page_location stays untouched — the override looks configured but does nothing.
Effect: The automatic page_view and all GA4 events that inherit the Google tag are now reported on the clean path, regardless of which alias URL the visitor arrived on. Since campaign parameters are preserved, traffic sources are unaffected.
Verify in the Network tab: visit a page with a known case variant in incognito (e.g. /ClubsMap), filter the Network tab for collect, and check the dl parameter in the GA4 request. If the path appears there in lowercase, the override is correctly wired up; if it's still in its original case, the parameter is still in the wrong place or the container version hasn't been published.
2i

The header search box — tracking search terms (site search)

ClubRunner's built-in search box (the field with id="searchString" in the header) doesn't send the search term as a query parameter (?q=…), which is what GA4's Enhanced Measurement expects. Instead, ClubRunner navigates to a path of the form /Search/<term>. This means the search term must be extracted from the path using a Custom JavaScript variable.

Confirmed behaviour: Before URL encoding, ClubRunner replaces the characters . % ? : / + * & " with the placeholders -~0- through -~8-. The variable below restores these to the original characters.

Step 1 — Variable that extracts the search term

Variable type: Custom JavaScript Variable name: JS-sitesearch CR function() { var path = window.location.pathname; var match = path.match(/^\/[Ss]earch\/(.+)$/); if (!match) return undefined; var term = decodeURIComponent(match[1]); // Restore ClubRunner's placeholders to the original characters (see doSearch()) var replacements = [ ['-~0-', '.'], ['-~1-', '%'], ['-~2-', '?'], ['-~3-', ':'], ['-~4-', '/'], ['-~5-', '+'], ['-~6-', '*'], ['-~7-', '&'], ['-~8-', '"'] ]; for (var i = 0; i < replacements.length; i++) { term = term.split(replacements[i][0]).join(replacements[i][1]); } return term; }

Step 2 — Trigger

Trigger type: Page View This trigger fires on: Some Page Views {{Page Path}} matches RegEx ^/[Ss]earch/ Trigger name: CR-sitesearch

Step 3 — Tag

Tag type: Google Analytics: GA4 Event Configuration: [select your Google tag] Event name: view_search_results // GA4's recommended event name for searches Event parameters: Parameter name: search_term Value: {{JS-sitesearch CR}} Trigger: CR-sitesearch Tag name: GA4 – sitesearch CR
UI trap in eventSettingsTable: when filling in the parameter value, type {{ and choose the variable from the list that pops up — confirm with Tab or by clicking outside the field. If you click directly from the variable field to the value field with the mouse, the selection is reset.
2j

Special Event — selected links and buttons with a custom label

Beyond the general categories above (outbound clicks, downloads, search) you sometimes want to track individual, selected links or buttons in reports — e.g. a sign-up link — with a readable label in GA4 (e.g. "Interest form") instead of just the link address. A shared event name, special_event, is used for all of these, regardless of how many different links/buttons are tracked. New special events normally require no new trigger or tag — just a row change in one of two shared components. On ClubRunner web pages, links come in three main types, depending on which widget is used for the link:

ClubRunner widget What can be edited Matched via
Text widget (link in body text) Link text only, no HTML URL list (Special Event – Link List)
Button widget ("Just a Button") Button text and link address, no HTML URL list (Special Event – Link List)
Custom widget Free-form HTML, incl. attributes data-ga-label attribute (Special Event – Attribute)
The button widget adds a trailing space to the link address (e.g. href="https://example.com "), which means an exact equals condition misses the match. The RegEx matching used in the configuration below does not require an exact end match and is therefore unaffected by the space.

Recommended solution: data-ga-label in a Custom widget

Use this pattern when the link/button sits in a Custom widget (HTML-editable) — simplest, and requires no future GTM changes.

Example code for a blue button that changes colour on mouse-over (same style and colours as ClubRunner's "Read more" buttons) whose label (Interest form) shows up clearly in the GA4 report:

<a href="https://example.com" data-ga-label="Interest form" onmouseout="this.style.backgroundColor='#0083EF'" onmouseover="this.style.backgroundColor='#0067BC'" style="display:inline-block; background-color:#0083EF; color:#ffffff; padding:4px 10px; border-radius:4px; text-decoration:none; font-weight:bold; font-family:Arial, sans-serif;" target="_blank">Fill in and submit here ...</a>

Text widgets and button widgets have no HTML editing and cannot carry the attribute — the method under "Configure your first special event" below is used instead.

Base setup — built once, regardless of which variant is used

Step 1 — Variable: Click Data Label

Variable type: Element Attribute Attribute Name: data-ga-label Variable name: Click Data Label

Step 2 — Variable: Special Event Label Lookup (created empty)

Input Variable: {{Click URL}} // Don't add any row yet — an empty table is risk-free, // it just returns "no value" until rows exist. Set Default Value: unchecked Variable name: Special Event Label Lookup

Step 3 — Variable: Special Event Label (fallback chain)

Variable name: Special Event Label function() { // 1. Custom widget: the attribute always wins if ({{Click Data Label}}) return {{Click Data Label}}; // 2. Text widget/button widget: look it up in the list var l = {{Special Event Label Lookup}}; if (l) return l; // 3. Last resort: visible link text/button text return {{Click Text}}; }

Step 4 — Trigger: Special Event – Attribute

Trigger type: Click – All Elements This trigger fires on: Some Clicks Click Element matches CSS selector [data-ga-label] Trigger name: Special Event – Attribute

Configure your first special event

Which of the steps below applies depends on the widget type (see the table above) — the base setup works the same regardless of whether your first case has a label or not.

If Custom widget (label): add data-ga-label to the HTML code as in the example above. Nothing more to do — Special Event – Attribute captures it automatically.

If text widget or button widget (no label possible):

Step 5 — Add a row to Special Event Label Lookup

Input: your link's full URL Output: desired label, e.g. "Interest form"

Step 6 — Trigger: Special Event – Link List

Trigger type: Click – Just Links This trigger fires on: Some Link Clicks {{Click URL}} matches RegEx (your URL) Trigger name: Special Event – Link List

Step 7 — Tag: GA4 – Special Event

Tag type: Google Analytics: GA4 Event Configuration: [select your Google tag] Event name: special_event Event parameters: Parameter name: special_event_label Value: {{Special Event Label}} Parameter name: link_url Value: {{Click URL}} Parameter name: link_text Value: {{Click Text}} Trigger: Special Event – Attribute and/or Special Event – Link List — both if you already have cases of both types now Tag name: GA4 – Special Event

Step 8 — GA4: register custom dimension

Dimension nameScopeEvent parameterDescription
Special Event LabelEventspecial_event_labelspecial_event
special_event does not affect the outbound click count. Special event links can be either internal or external. An external special event link is genuinely an outbound click and is already naturally included in the "Outbound link click" total (2d) — it is not counted extra or differently for also being a special event. An internal special event link is not affected by 2d at all, since that trigger only matches external domains.

Update — adding more special events later

  • Custom widget (with label): add data-ga-label="label" to the HTML code. Done, no GTM change.
  • Text widget/button widget (no label): add a row to Special Event Label Lookup. Does the Special Event – Link List trigger already exist (step 6)? Add |newURL to the end of the RegEx expression. Doesn't exist yet? Create it per step 6, and attach it to the GA4 – Special Event tag.
2k

Test and publish

Never publish changes without testing them in GTM's Preview mode. Every unsaved change in the workspace is a "Draft" — only after publishing do the changes become active and start sending data to GA4.

1
Start Preview mode
Click Preview (top right in GTM). Enter your site's URL and click Connect. A new tab opens with the site, and the GTM Tag Assistant panel is shown. Restarted Preview after a change and it still doesn't fire? Exit and fully restart Preview — an ongoing session doesn't always reflect saved changes.
2
Test each trigger
Click an external link → check that "GA4 – Outbound link click" appears in "Tags Fired". Click a bulletin link → "GA4 – Bulletin view". Click a link to a file from Documents → "GA4 – Document download", and a link to a page file (in the Page Download Files widget) → "GA4 – Page file download". Click a special event → "GA4 – Special Event".
3
Verify in GA4 DebugView
In GA4: Admin → DebugView. Events should appear within a few seconds. Check that event parameters (link_url, file_name) are populated correctly.
4
Publish the container
Exit Preview. Click Submit (top right) → Publish. Give the version a descriptive name, e.g. "GA4 + Clarity + outbound + bulletin + downloads".
5
Wait 24–48 hours
The GA4 Realtime report updates immediately. Standard reports under Engagement → Events can take up to 48 hours to populate.

3

Known Limitations with ClubRunner

3a

Pages without GTM coverage

ClubRunner does not add GTM code on the page types below. They are therefore not registered as page views in GA4, but we compensate with click events.

URL pattern Content Compensation
/bulletin/View/[uuid] Newsletter ✅ Click event in the originating session (section 2e)
/documents/[uuid] Document view ✅ Download event (section 2f/2g)
3b

GTM Tag Coverage — reading the report correctly

GTM's built-in "Tag Coverage" report (Admin → Tag Coverage) shows how many pages lack GTM tagging. For ClubRunner sites the figure looks dramatic, but the majority of untagged pages are bulletin pages, document pages and internal admin pages — not primarily public-facing content. The report updates automatically when the GTM container is published or when visitors generate new traffic.

If pages appear to remain untagged despite traffic, first check with Google Tag Assistant whether the page has GTM code at all. If the code is missing, report it to ClubRunner support.

Checklist — Quick Summary

Google Analytics 4

  • List unwanted referrals: add clubrunner.ca, crsadmin.com, clubrunner.blob.core.windows.net — plus the referral spam list in section 1e
  • Enhanced Measurement: turn off Outbound clicks and File downloads
  • Custom dimensions (Event scope): link_url, link_text, file_name, search_term, special_event_label
  • Exploration "Custom events [period]" with five tabs: Outbound clicks, Sitesearch CR, Downloads, Bulletin viewed, Special Event (section 1d)
  • Verify events in DebugView after publishing

Google Tag Manager — 8 tags

  • Google tag (GA4) — trigger: Initialization – All Pages
  • Microsoft Clarity — trigger: Initialization – All Pages
  • GA4 Event: outbound_click — gate ^https?:// + exclude ClubRunner domains and /bulletin/View/
  • GA4 Event: bulletin_viewed — click on /bulletin/View/
  • GA4 Event: file_download (2f) — path trigger for /documents/sv-se/
  • GA4 Event: file_download (2g) — blob URL trigger for ClubRunner page files
  • GA4 Event: view_search_results — regex ^/[Ss]earch/ on path
  • GA4 Event: special_event (2j) — Click Element [data-ga-label] or URL list, label via fallback chain

4

National Portal — rotary.se (Measuring Onward Clicks)

rotary.se is the national Rotary portal, whose purpose — as decided by the DG council for the Swedish districts — is to funnel visitors onward to the districts and to other Rotary sites (e.g. rotarystudent.se, rotaryfoundation.se).
Here, outbound clicks are the primary measurement point. This section is self-contained and can be followed independently for a portal.

The key difference from district/club

On a district or club website, you suppress outbound noise and keep all ClubRunner domains internal. On the portal, you instead want to count clicks out to districts and other Rotary sites. These are genuine external sites with their own GA4 properties — they must therefore not be added to unwanted referrals and must not be excluded in the trigger. Only ClubRunner's own infrastructure is kept internal.

4a

GA4 — List unwanted referrals (ClubRunner infrastructure only)

Since the portal itself runs ClubRunner, its own infrastructure should be kept internal, just as for district/club.

// Domain contains — add one row per domain: clubrunner.ca crsadmin.com clubrunner.blob.core.windows.net
4b

GA4 — Enhanced Measurement and custom dimensions

Turn off Outbound clicks in Enhanced Measurement (same reason as section 1b — we handle outbound clicks in GTM so we can attach the target domain). Other Enhanced Measurement features can stay on.

Register custom dimensions so clicks can be reported per site:

Dimension name Scope Event parameter / User property Purpose
Link domain Event link_domain Target domain — break down clicks per site
Link URL Event link_url Full destination URL
Link text Event link_text The link text that was clicked
Note: link_domain is what distinguishes the portal setup from district/club — it's the dimension you group the report on.
4c

GTM — base tags Google tag and Clarity, plus renamed pages

Detailed click steps are in section 2b (Google tag) and 2c (Clarity).
Also add the renamed-pages consolidation (section 2h), including the page_location rewrite in the portal's Google tag.

// Google tag — initialises GA4 Tag type: Google tag Tag ID: G-XXXXXXXXXX (rotary.se's own Measurement ID) Trigger: Initialization – All Pages Tag name: Google Analytics // Microsoft Clarity (optional – session recording + cookieless cross-check) Tag type: Microsoft Clarity (from the template gallery) Project ID: YOUR_CLARITY_PROJECT_ID Trigger: Initialization – All Pages
4d

GTM — variable that extracts the target domain

First enable the built-in click variables (Click URL, Click Text, etc.) as described in section 2a. Then create a variable that extracts just the hostname from the clicked link, so the report can be grouped per site instead of per full URL.

Variable type: Custom JavaScript Variable name: Click URL Hostname function() { try { return new URL({{Click URL}}).hostname.replace(/^www\./, ''); } catch (e) { return undefined; } }

The variable returns, for example, rotary2405.se or rotarystudent.se and strips a leading www. so the same site isn't split across two rows.

4e

GTM — trigger and tag for onward clicks

A single event that fires on every link that leaves rotary.se. It scales automatically as new district or other sites are added — no list to maintain.

Step 1 — Create the trigger

Trigger type: Click – Just Links This trigger fires on: Some Link Clicks // Fires on every link that leaves rotary.se: // Gate: real http(s) links only – blocks filter/JS links with no href {{Click URL}} matches RegEx ^https?:// {{Click URL}} does not start with https://rotary.se {{Click URL}} does not start with https://www.rotary.se {{Click URL}} does not contain clubrunner.ca {{Click URL}} does not contain clubrunner.blob.core.windows.net {{Click URL}} does not contain crsadmin.com Trigger name: Portal outbound click
Why does the exception work? The condition is "does not start with" — https://rotary2325.se does not start with https://rotary.se, so clicks to the district sites aren't blocked and are counted as onward clicks. The https://www.rotary.se row is a safety row: if the portal is reached via the www address, internal links shouldn't be counted as outbound. The ClubRunner domains are also excluded so that clicks to, for example, the login (portal.clubrunner.ca) aren't counted as onward clicks.

Step 2 — Create the tag

Tag type: Google Analytics: GA4 Event Configuration: [select your Google tag] Event name: outbound_click Event parameters: Parameter name: link_url Value: {{Click URL}} Parameter name: link_text Value: {{Click Text}} Parameter name: link_domain Value: {{Click URL Hostname}} Trigger: Portal outbound click Tag name: GA4 – Portal outbound click
4f

GA4 — Build an Exploration for portal traffic

The portal needs its own Exploration showing onward clicks to districts and other sites. The configuration is simpler than "Custom events" since there's only one event — outbound_click — but with link_domain as the primary dimension to break down clicks by target site.

Getting started

  1. Go to Explore → Blank exploration.
  2. Name the exploration Portal traffic.
  3. Add these shared variables in the Variables panel:
    Dimensions: Event name Link domain Link URL Metric: Event count
  4. Then create two tabs in the order below.

Tab 1 — Event sum

An overview of all event types — gives a quick picture of the traffic without filters.

Tab name: Event sum Rows: Event name Nested rows: No Show rows: 50 Cell type: Bar chart // No filters — shows all event types

Tab 2 — Outbound clicks

Clicks broken down by target site and URL — the portal's primary reporting tool.

Tab name: Outbound clicks Rows: Link domain // primary row — groups by site Link URL // nested row — shows specific page Event name // confirms it's outbound_click Nested rows: Yes Show rows: 50 Cell type: Bar chart Filter: Event name exactly matches outbound_click
Narrow it to the Rotary network: the tag captures all outbound clicks (including Facebook, rotary.org, etc.). If you only want to follow the Swedish Rotary sites, filter Link domain to the relevant domains in the report — no need to change the tag.