PageSourceSearch

https://coffee-labo.co.jp/wp-content/themes/coffee-swell_child/assets/js/link-tracking.js?ver=1.6.59

js coffee-labo.co.jp collected 2026-10-02 10:35:55 UTC 13,329 bytes, 278 lines download raw bytes

1/**
2 * ショップ導線のクリック計測(UTMの代替)
3 *
4 * 経緯: 自サイト内リンクとショップ宛リンクに
5 *   utm_source=Google&utm_medium=organic&utm_content=<設置場所>
6 * を付けて設置場所別のクリックを見ていたが、GA4はランディング時にutm_*を見つけると
7 * セッションを切って獲得元を上書きする。shop.coffee-labo.co.jp は媒体側と同じGA4
8 * プロパティ(307320527)で計測しているため、この付与は「別プロパティへの送客計測」に
9 * ならず、単にセッションを分断して本来の獲得元(自然検索等)を消していた。
10 * 記事→ショップ→購入の売上帰属もそこで途切れていた。
11 *
12 * 本スクリプトはUTMを使わずに同じ情報をGA4イベントとして送る。セッションは切れない。
13 * リンク側に属性を足す必要がないよう、設置場所はDOMから導出する(記事99件を
14 * 書き換えずに済ませるため)。
15 *
16 * 送信するイベント(paramsは3イベント共通):
17 *   select_content : 同一ドメインの /shop/・/products/・/collections/、旧Shopifyホスト、
18 *                    /lp/ 配下(ショップ導線)
19 *   affiliate_click: ASP・モール宛の外部クリック(1.6.18新設。おすすめ3選や
20 *                    Amazon/楽天/Yahoo枠の実クリックをショップ導線と同じ土俵で比較する)
21 *   outbound_click : 上記以外の外部ドメイン宛クリック(引用元・SNS等)
22 *   content_type : 設置場所(global_menu / info_bar / widget / article_body / footer / other)
23 *   content_id   : select_content は遷移先パス(例 /products/fruity-blend)、
24 *                  外部2イベントはホスト込み(例 af.moshimo.com/af/c/click)100字まで
25 *   link_url     : 原則href全体。Checkout/accountの機密導線は署名path・queryを除いた固定URL
26 *   link_text    : リンク文言(80字まで)
27 *   link_region  : ウィジェットID等、設置場所をさらに特定できる文字列
28 *
29 * content_type と content_id は GA4 の標準ディメンション contentType / contentId に
30 * そのまま入るため登録不要。item_id は使わない(あれはeコマースの items 配列用の
31 * 予約名で、イベント単体に付けても contentId 相当のディメンションには入らない)。
32 * link_url / link_text / link_region はカスタムディメンション登録が必要。
33 */
34(function () {
35	'use strict';
36
37	var LEGACY_SHOP_HOST = 'shop.coffee-labo.co.jp';
38	var CHECKOUT_HOST = 'checkout.coffee-labo.co.jp';
39	var SHOPIFY_STORE_ID = '59722727561';
40
41	/* Checkout URLには署名付きpath tokenやkey/_gl等が含まれる。GA4へは導線種別だけを
42	   送れば十分なので、CheckoutとShopifyアカウントは固定の分析用URLへ縮約する。 */
43	function trackedAnalyticsDestination(url) {
44		// Inbox専用ページは購入手続きと分離し、追加クエリは計測へ渡さない。
45		if (url.hostname === CHECKOUT_HOST && url.pathname === '/' && url.searchParams.get('clc_chat') === '1') {
46			return { contentId: '/chat', linkUrl: 'https://' + CHECKOUT_HOST + '/?clc_chat=1' };
47		}
48		var isLegacyCheckout = url.hostname === LEGACY_SHOP_HOST && (
49			url.pathname === '/checkout' || url.pathname.indexOf('/checkout/') === 0
50			|| url.pathname === '/checkouts' || url.pathname.indexOf('/checkouts/') === 0
51			|| url.pathname === '/cart/c' || url.pathname.indexOf('/cart/c/') === 0
52		);
53		var isLegacyAccount = url.hostname === LEGACY_SHOP_HOST && (
54			url.pathname === '/account' || url.pathname.indexOf('/account/') === 0
55			|| url.pathname === '/customer_authentication' || url.pathname.indexOf('/customer_authentication/') === 0
56		);
57		var shopifyAccountPath = '/' + SHOPIFY_STORE_ID + '/account';
58
59		if (url.hostname === CHECKOUT_HOST || isLegacyCheckout) {
60			return {
61				contentId: '/checkout',
62				linkUrl: url.origin + '/checkout'
63			};
64		}
65
66		if (
67			isLegacyAccount || (
68				url.hostname === 'shopify.com'
69				&& (url.pathname === shopifyAccountPath || url.pathname.indexOf(shopifyAccountPath + '/') === 0)
70			)
71		) {
72			return {
73				contentId: '/account',
74				linkUrl: 'https://shopify.com/' + SHOPIFY_STORE_ID + '/account'
75			};
76		}
77
78		return {
79			contentId: url.pathname || '/',
80			linkUrl: url.href
81		};
82	}
83
84	/* 設置場所の判定順は「外側の器から先に」ではなく「具体的なものから先に」。
85	   ヘッダー内のウィジェットは widget ではなく global_menu として数えたいので
86	   info_bar → global_menu → footer → widget → article_body の順に見る。 */
87	var REGIONS = [
88		{ type: 'info_bar', selector: '.c-infoBar, .p-infoBar' },
89		/* PC追従バナー。functions.php側の個別ハンドラは二重計上になるため撤去済み */
90		{ type: 'labocafe_banner', selector: '#labocafe-footer-banner' },
91		/* スマホの追従フッターバーはヘッダーメニューと出方も押され方も違うので分けて数える */
92		{ type: 'fix_bottom_menu', selector: '#fix_bottom_menu, .p-fixBottomMenu' },
93		{ type: 'global_menu', selector: '.l-header, #header, .c-gnav, .p-spMenu, .l-fixHeader' },
94		{ type: 'footer', selector: '.l-footer, #footer' },
95		/* coffee-labo-commerce プラグイン(WP統合ストア)のUI(1.6.48)。
96		   クラス接頭辞 clc- はプラグイン側の規約に依存する(変更時はここも追随)。
97		   従来はストア内クリックが全て other に落ちて最大勢力になっていた
98		   (2026-08-14〜28実測: other 154件/select_content中1位)。
99		   プラグイン自身は select_item / add_to_cart 等のeコマースイベントを別途送って
100		   おり、ここでの分類は「導線クリックの発生場所」ラベルとして共存する
101		   (同一クリックが両系列に載るのは仕様)。.clc-storefront はページ全体を包む
102		   受け皿なので、より具体的な領域を必ず先に並べる */
103		{ type: 'store_cart', selector: '.clc-cart-dialog, .clc-cart-reminder' },
104		{ type: 'store_chat', selector: '.clc-inbox-floating' },
105		{ type: 'store_quiz', selector: '.clc-quiz-dialog' },
106		{ type: 'product_card', selector: '.clc-product-card' },
107		{ type: 'store_nav', selector: '.clc-shop-header, .clc-announcement, .clc-nav-panel' },
108		{ type: 'store_footer', selector: '.clc-shop-footer' },
109		{ type: 'store_page', selector: '.clc-storefront' },
110		/* 目次下のコンパクト3選(1.6.20)。目次ブロック内に挿入されるため
111		   w-beforeToc/widget と article_body より先に判定して独立集計する */
112		{ type: 'toc_reco', selector: '.toc-reco' },
113		/* 産地・デカフェ記事の自動CTA(functions.php priority 98)。本文末尾に挿入される
114		   ため widget/article_body より先に判定して独立集計する */
115		{ type: 'origin_cta', selector: '.origin-shop-cta' },
116		{ type: 'widget', selector: '.widget, .c-widget, .w-beforeToc, .w-afterToc, .w-cta' },
117		{ type: 'article_body', selector: '.post_content, .c-postContent, .entry-content' }
118	];
119
120	function detectRegion(anchor) {
121		for (var i = 0; i < REGIONS.length; i++) {
122			var hit = anchor.closest(REGIONS[i].selector);
123			if (hit) {
124				return { type: REGIONS[i].type, node: hit };
125			}
126		}
127		return { type: 'other', node: null };
128	}
129
130	/* ウィジェットIDなど、同じ設置場所の中でさらに区別できる手がかりを拾う。
131	   旧utm_contentの w-beforeToc_left / c-widget__title-side 相当の粒度を維持する。 */
132	function regionDetail(anchor, region) {
133		if (!region.node) return '';
134		if (region.node.id) return region.node.id;
135		var cls = region.node.className;
136		if (typeof cls !== 'string') return '';
137		return cls.trim().split(/\s+/).slice(0, 3).join(' ');
138	}
139
140	/* textContent は <noscript> の中身を「文字列」として拾う。lazyload が挿入する
141	   <noscript><img ...></noscript> のせいで link_text がHTMLの断片になるため、
142	   複製から noscript/script/style を外してから文字を取る。 */
143	function linkText(anchor) {
144		var clone = anchor.cloneNode(true);
145		var junk = clone.querySelectorAll('noscript, script, style');
146		for (var i = 0; i < junk.length; i++) {
147			junk[i].parentNode.removeChild(junk[i]);
148		}
149		var text = (clone.textContent || '').replace(/\s+/g, ' ').trim();
150		if (text) return text.slice(0, 80);
151		/* 画像だけのバナーは代替テキストで代用する */
152		var img = anchor.querySelector('img[alt]');
153		return img ? ('[img] ' + img.alt).slice(0, 80) : '';
154	}
155
156	function isCommercePath(pathname) {
157		return pathname === '/shop' || pathname.indexOf('/shop/') === 0
158			|| pathname === '/products' || pathname.indexOf('/products/') === 0
159			|| pathname === '/collections' || pathname.indexOf('/collections/') === 0;
160	}
161
162	function isTracked(url) {
163		/* 旧ホストの残存リンクも移行監視のため計測を継続する。 */
164		if (url.hostname === LEGACY_SHOP_HOST) return true;
165		/* WordPress統合後のCommerce URLと、ショップへ転送する旧LPを同じ導線として数える。 */
166		if (url.hostname === location.hostname && (isCommercePath(url.pathname) || url.pathname.indexOf('/lp/') === 0)) return true;
167		/* 正規化後のCheckout・ポリシー・Shopifyアカウント宛リンクも、移行前は旧ホスト宛で
168		   select_content として計上されていたショップ導線。outbound_click へ落とさず継続する(1.6.47)。 */
169		if (url.hostname === CHECKOUT_HOST) return true;
170		if (url.hostname === 'shopify.com' && url.pathname.indexOf('/' + SHOPIFY_STORE_ID + '/') === 0) return true;
171		return false;
172	}
173
174	/* アフィリエイト経路(ASPリダイレクタとモール直リンク)。末尾一致で判定する。
175	   記事内の実リンクは af.moshimo.com / px.a8.net / h.accesstrade.net 経由と
176	   Amazon・Yahoo!ショッピング直リンク(2026-08 実地調査)。 */
177	var AFFILIATE_HOSTS = [
178		'moshimo.com',
179		'a8.net',
180		'accesstrade.net',
181		'afi-b.com',
182		'affitch.com',
183		'felmat.net',
184		'rentracks.jp',
185		'valuecommerce.com',
186		'rakuten.co.jp',
187		'r10.to',
188		'amazon.co.jp',
189		'amzn.to',
190		'amzn.asia',
191		'shopping.yahoo.co.jp'
192	];
193
194	function isAffiliateHost(hostname) {
195		for (var i = 0; i < AFFILIATE_HOSTS.length; i++) {
196			var domain = AFFILIATE_HOSTS[i];
197			if (hostname === domain || hostname.slice(-(domain.length + 1)) === '.' + domain) return true;
198		}
199		return false;
200	}
201
202	document.addEventListener('click', function (event) {
203		/* 他のハンドラが stopPropagation しても拾えるよう capture 段階で受ける */
204		var target = event.target;
205		if (!target || typeof target.closest !== 'function') return;
206
207		var anchor = target.closest('a[href]');
208		if (!anchor) return;
209
210		var url;
211		try {
212			url = new URL(anchor.href, location.href);
213		} catch (e) {
214			return;
215		}
216		var eventName = null;
217		var contentId = null;
218		var linkUrl = url.href;
219		if (isTracked(url)) {
220			var destination = trackedAnalyticsDestination(url);
221			eventName = 'select_content';
222			contentId = destination.contentId;
223			linkUrl = destination.linkUrl;
224		} else if (
225			(url.protocol === 'http:' || url.protocol === 'https:') &&
226			url.hostname &&
227			url.hostname !== location.hostname
228		) {
229			/* 外部ドメイン宛。GA4のイベントparam上限に合わせ content_id は100字まで */
230			eventName = isAffiliateHost(url.hostname) ? 'affiliate_click' : 'outbound_click';
231			contentId = (url.hostname + url.pathname).slice(0, 100);
232		}
233		if (!eventName) return;
234		if (typeof window.gtag !== 'function') return;
235
236		var region = detectRegion(anchor);
237		var text = linkText(anchor);
238
239		window.gtag('event', eventName, {
240			content_type: region.type,
241			content_id: contentId,
242			link_url: linkUrl,
243			link_text: text,
244			link_region: regionDetail(anchor, region)
245		});
246	}, true);
247
248	/* GA4の自動form_submitは送信試行も含み得る。Contact Form 7がメール送信成功時だけ
249	   発火するイベントから、事業成果用のgenerate_leadを別に送る。 */
250	document.addEventListener('wpcf7mailsent', function (event) {
251		var detail = event.detail || {};
252		var formId = detail.contactFormId || detail.contact_form_id || '';
253		window.dataLayer = window.dataLayer || [];
254		if (typeof window.gtag !== 'function') {
255			window.gtag = function () {
256				window.dataLayer.push(arguments);
257			};
258		}
259		window.gtag('event', 'generate_lead', {
260			content_type: 'contact_form_7',
261			content_id: formId ? String(formId) : 'unknown'
262		});
263	});
264
265	/* 目次下コンパクト3選の開閉(1.6.20)。toggle はバブリングしないため
266	   キャプチャ段階で受け、ページ内で最初に開いた1回だけ送る。 */
267	document.addEventListener('toggle', function (event) {
268		var details = event.target;
269		if (!details || !details.classList || !details.classList.contains('toc-reco')) return;
270		if (!details.open || details.getAttribute('data-reco-opened')) return;
271		details.setAttribute('data-reco-opened', '1');
272		if (typeof window.gtag !== 'function') return;
273		window.gtag('event', 'toc_reco_open', {
274			content_type: 'toc_reco',
275			content_id: location.pathname
276		});
277	}, true);
278})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.