feat(datasheet): Product-page links carry UTM attribution and report the click
Both Buy renderers (picker hover panel + Part Inspector) now append utm_source=velxio&utm_medium=simulator&utm_campaign=datasheet with the component id in utm_content, so vendors can attribute the referral. trackProductPageClick fires the GA4 event and dispatches a 'velxio:product-page-click' CustomEvent — an overlay build can persist the click server-side without this tree knowing about it.
This commit is contained in:
parent
75842e3668
commit
4b9fa3f0d0
|
|
@ -24,7 +24,8 @@ import { useTranslation } from 'react-i18next';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import type { PropertyDescriptor } from '../types/component-metadata';
|
import type { PropertyDescriptor } from '../types/component-metadata';
|
||||||
import { loadDoc, type ComponentDoc } from './componentDocs';
|
import { loadDoc, productPageHref, type ComponentDoc } from './componentDocs';
|
||||||
|
import { trackProductPageClick } from '../utils/analytics';
|
||||||
import './ComponentInfoPanel.css';
|
import './ComponentInfoPanel.css';
|
||||||
|
|
||||||
export interface PanelData {
|
export interface PanelData {
|
||||||
|
|
@ -168,7 +169,13 @@ export const ComponentInfoBody: React.FC<{
|
||||||
|
|
||||||
{buyHref && (
|
{buyHref && (
|
||||||
<div className="cip-footer">
|
<div className="cip-footer">
|
||||||
<a className="cip-buy" href={buyHref} target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
className="cip-buy"
|
||||||
|
href={productPageHref(buyHref, data.id)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={() => trackProductPageClick(data.id, doc?.brand, buyHref)}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
height="14"
|
height="14"
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,25 @@ export function registerComponentDoc(id: string, raw: string): void {
|
||||||
cache.delete(id);
|
cache.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorate a datasheet Buy/Product-page URL with UTM attribution so the
|
||||||
|
* vendor sees the referral came from Velxio. `componentId` lands in
|
||||||
|
* utm_content so partner dashboards can tell WHICH part drove the visit.
|
||||||
|
* Existing query params on the vendor URL are preserved.
|
||||||
|
*/
|
||||||
|
export function productPageHref(url: string, componentId: string): string {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
u.searchParams.set('utm_source', 'velxio');
|
||||||
|
u.searchParams.set('utm_medium', 'simulator');
|
||||||
|
u.searchParams.set('utm_campaign', 'datasheet');
|
||||||
|
u.searchParams.set('utm_content', componentId);
|
||||||
|
return u.toString();
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split an optional `---`-delimited front-matter block off the top of a
|
* Split an optional `---`-delimited front-matter block off the top of a
|
||||||
* Markdown string. Only a leading block counts, so table separators
|
* Markdown string. Only a leading block counts, so table separators
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ import {
|
||||||
safeHref,
|
safeHref,
|
||||||
type PanelData,
|
type PanelData,
|
||||||
} from '../ComponentInfoPanel';
|
} from '../ComponentInfoPanel';
|
||||||
|
import { productPageHref } from '../componentDocs';
|
||||||
|
import { trackProductPageClick } from '../../utils/analytics';
|
||||||
import {
|
import {
|
||||||
layoutInspectorPins,
|
layoutInspectorPins,
|
||||||
type InspectorPinInput,
|
type InspectorPinInput,
|
||||||
|
|
@ -813,10 +815,13 @@ export const PartInspectorDialog: React.FC<PartInspectorDialogProps> = ({
|
||||||
<div className="pid-actions-right">
|
<div className="pid-actions-right">
|
||||||
<a
|
<a
|
||||||
className="pid-buy-button"
|
className="pid-buy-button"
|
||||||
href={buyHref}
|
href={productPageHref(buyHref, componentMetadata.id)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
title={buyHref}
|
title={buyHref}
|
||||||
|
onClick={() =>
|
||||||
|
trackProductPageClick(componentMetadata.id, doc?.brand, buyHref)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,24 @@ export function trackVisitDiscord(): void {
|
||||||
fireEvent('visit_discord', { event_category: 'external_link' });
|
fireEvent('visit_discord', { event_category: 'external_link' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when a user clicks a datasheet "Product page" link. Besides the GA4
|
||||||
|
* event, dispatches a DOM CustomEvent so an overlay build can persist the
|
||||||
|
* click server-side (partner attribution) without this tree knowing about it.
|
||||||
|
*/
|
||||||
|
export function trackProductPageClick(componentId: string, brand?: string, href?: string): void {
|
||||||
|
fireEvent('product_page_click', {
|
||||||
|
event_category: 'external_link',
|
||||||
|
component_type: componentId,
|
||||||
|
brand: brand ?? '(none)',
|
||||||
|
});
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('velxio:product-page-click', {
|
||||||
|
detail: { componentId, brand: brand ?? null, href: href ?? null },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── CTA / Conversion ────────────────────────────────────────────────────────
|
// ── CTA / Conversion ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Fired when a user clicks a CTA button on a landing/SEO page. */
|
/** Fired when a user clicks a CTA button on a landing/SEO page. */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue