PageSourceSearch

https://madamefigaro.jp/wp-content/themes/madamefigaro-jp/assets/js/article-read-tracking.js?ver=1.0.153

js madamefigaro.jp collected 2026-09-24 19:19:23 UTC 7,756 bytes, 262 lines download raw bytes

1/**
2 * Article Read Tracking for GTM
3 * 記事本文の80%が表示されたタイミングでarticle_readイベントを送信
4 *
5 * @package madameFIGARO.jp
6 */
7
8(function() {
9	'use strict';
10
11	/**
12	 * 記事読了トラッキングクラス
13	 */
14	class ArticleReadTracking {
15		constructor() {
16			// 送信済み記事IDを記録(重複防止)
17			this.trackedArticles = new Set();
18
19			// IntersectionObserverを初期化
20			this.initObserver();
21
22			// 無限スクロールで新しい記事が追加された際の対応
23			document.addEventListener('contentAdded', (event) => {
24				// 少し遅延させてから新しい記事を監視
25				setTimeout(() => this.observeAllArticles(), 100);
26			});
27		}
28
29		/**
30		 * IntersectionObserverを初期化
31		 */
32		initObserver() {
33			this.observer = new IntersectionObserver(
34				(entries) => this.handleIntersection(entries),
35				{
36					threshold: 0,        // マーカー要素が1pxでも表示されたら発火
37					rootMargin: '0px'
38				}
39			);
40
41			// 全記事を監視対象に追加
42			this.observeAllArticles();
43		}
44
45		/**
46		 * 全記事の本文エリアを監視対象に追加
47		 */
48		observeAllArticles() {
49			// .b-article.editor-styles-wrapper 要素を取得
50			const articleContents = document.querySelectorAll('.b-article.editor-styles-wrapper');
51
52			articleContents.forEach(contentElement => {
53				// 親のarticle要素を取得
54				const articleElement = contentElement.closest('article.p-detail');
55				if (!articleElement) {
56					console.warn('Article Read Tracking: article.p-detail element not found');
57					return;
58				}
59
60				// 記事IDを取得
61				const postId = articleElement.dataset.postId;
62				if (!postId) {
63					console.warn('Article Read Tracking: data-post-id not found');
64					return;
65				}
66
67				// 既に送信済みの場合はスキップ
68				if (this.trackedArticles.has(postId)) {
69					return;
70				}
71
72				// 本文エリアの80%地点にマーカー要素を挿入
73				const marker = this.createMarker(postId, contentElement);
74				if (marker) {
75					this.observer.observe(marker);
76				}
77			});
78		}
79
80		/**
81		 * 本文エリアの80%地点にマーカー要素を作成・挿入
82		 *
83		 * @param {string} postId 記事ID
84		 * @param {HTMLElement} contentElement 本文エリア要素
85		 * @returns {HTMLElement|null} マーカー要素
86		 */
87		createMarker(postId, contentElement) {
88			// 既にマーカーが存在する場合はスキップ
89			const existingMarker = contentElement.querySelector('.article-read-marker');
90			if (existingMarker) {
91				return existingMarker;
92			}
93
94			// 本文エリアの高さを取得
95			const contentHeight = contentElement.scrollHeight;
96			if (contentHeight === 0) {
97				console.warn('Article Read Tracking: content height is 0');
98				return null;
99			}
100
101			// 80%の位置を計算
102			const eightyPercentPosition = contentHeight * 0.8;
103
104			// デバッグ情報を出力
105			console.log(`Article Read Tracking [Post ID: ${postId}]:`, {
106				contentHeight: contentHeight + 'px',
107				eightyPercentPosition: eightyPercentPosition + 'px',
108				percentage: '80%'
109			});
110
111			// マーカー要素を作成(目に見えない1pxの要素)
112			const marker = document.createElement('div');
113			marker.className = 'article-read-marker';
114			marker.dataset.postId = postId;
115			marker.style.cssText = 'position: absolute; height: 1px; width: 100%; opacity: 0; pointer-events: none;';
116			marker.style.top = eightyPercentPosition + 'px';
117
118			// 本文エリアにposition:relativeを追加(マーカーの絶対位置指定のため)
119			const computedStyle = window.getComputedStyle(contentElement);
120			if (computedStyle.position === 'static') {
121				contentElement.style.position = 'relative';
122			}
123
124			// マーカーを挿入
125			contentElement.appendChild(marker);
126
127			return marker;
128		}
129
130		/**
131		 * Intersection Observerのコールバック
132		 *
133		 * @param {IntersectionObserverEntry[]} entries
134		 */
135		handleIntersection(entries) {
136			entries.forEach(entry => {
137				// 表示された場合のみ処理
138				if (!entry.isIntersecting) return;
139
140				const marker = entry.target;
141				const postId = marker.dataset.postId;
142
143				// 既に送信済みの場合はスキップ
144				if (this.trackedArticles.has(postId)) {
145					this.observer.unobserve(marker);
146					return;
147				}
148
149				// 送信済みとしてマーク
150				this.trackedArticles.add(postId);
151
152				// dataLayerが存在しない場合は初期化
153				window.dataLayer = window.dataLayer || [];
154
155				// 記事読了イベントを送信
156				// GTM_SETUP_GUIDE.md 4.4 に準拠
157				this.sendArticleReadEvent(postId);
158
159				// 監視解除(一度だけ発火させる)
160				this.observer.unobserve(marker);
161			});
162		}
163
164		/**
165		 * 記事読了イベントをdataLayerに送信
166		 *
167		 * @param {string} postId 記事ID
168		 */
169		sendArticleReadEvent(postId) {
170			// dataLayerから現在の記事情報を取得
171			const currentData = this.getCurrentArticleData();
172
173			// イベントデータを構築
174			const eventData = {
175				event: 'article_read',
176				article_id: currentData.article_id || postId,
177				article_category: currentData.article_category || '',
178				article_author: currentData.article_author || '',
179				article_read_count: 1,
180				page_location: window.location.href,
181				page_path: window.location.pathname,
182				page_title: document.title
183			};
184
185			// dataLayerにpush
186			window.dataLayer.push(eventData);
187
188			console.log('article_read event pushed for post ID:', postId, eventData);
189		}
190
191		/**
192		 * dataLayerから現在の記事情報を取得
193		 *
194		 * @returns {Object} 記事情報
195		 */
196		getCurrentArticleData() {
197			const data = {
198				article_id: '',
199				article_category: '',
200				article_author: ''
201			};
202
203			// dataLayerから最新の記事情報を取得
204			if (window.dataLayer && Array.isArray(window.dataLayer)) {
205				// dataLayerを逆順で走査(最新の値を優先)
206				for (let i = window.dataLayer.length - 1; i >= 0; i--) {
207					const layer = window.dataLayer[i];
208					if (layer.article_id && !data.article_id) {
209						data.article_id = layer.article_id;
210					}
211					if (layer.article_category && !data.article_category) {
212						data.article_category = layer.article_category;
213					}
214					if (layer.article_author && !data.article_author) {
215						data.article_author = layer.article_author;
216					}
217
218					// 全ての情報が揃ったら終了
219					if (data.article_id && data.article_category && data.article_author) {
220						break;
221					}
222				}
223			}
224
225			return data;
226		}
227	}
228
229	/**
230	 * 初期化関数
231	 */
232	function init() {
233		// 記事ページのみ実行
234		if (document.body.classList.contains('single-post') ||
235		    document.body.classList.contains('single-cityguide') ||
236		    document.body.classList.contains('single-yukari-weekly') ||
237		    document.body.classList.contains('single-yukari-yearly') ||
238		    document.body.classList.contains('single-yukari-sora') ||
239		    document.body.classList.contains('single-celebrity-weekly') ||
240		    document.body.classList.contains('single-celebrity-yearly') ||
241		    document.body.classList.contains('single-celebrity-zodiac') ||
242		    document.body.classList.contains('single-gallery') ||
243		    document.body.classList.contains('single')) {
244
245			// .b-article.editor-styles-wrapper 要素が存在するか確認
246			const articleContent = document.querySelector('.b-article.editor-styles-wrapper');
247			if (articleContent) {
248				new ArticleReadTracking();
249				console.log('Article Read Tracking initialized');
250			} else {
251				console.log('Article Read Tracking: .b-article.editor-styles-wrapper not found');
252			}
253		}
254	}
255
256	// DOM読み込み完了後に初期化
257	if (document.readyState === 'loading') {
258		document.addEventListener('DOMContentLoaded', init);
259	} else {
260		init();
261	}
262})();

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.