diff --git a/apps/portal/src/App.js b/apps/portal/src/App.js index 2ecc2ae675..669b94eee9 100644 --- a/apps/portal/src/App.js +++ b/apps/portal/src/App.js @@ -195,6 +195,7 @@ export default class App extends React.Component { }); } } + this.setupRecommendationButtons(); } catch (e) { /* eslint-disable no-console */ console.error(`[Portal] Failed to initialize:`, e); @@ -888,6 +889,35 @@ export default class App extends React.Component { }; } + getRecommendationButtons() { + const customTriggerSelector = '[data-recommendation]'; + return document.querySelectorAll(customTriggerSelector) || []; + } + + /** Setup click tracking for recommendation buttons */ + setupRecommendationButtons() { + // Handler for custom buttons + const clickHandler = (event) => { + // Send beacons for recommendation clicks + const recommendationId = event.currentTarget.dataset.recommendation; + + if (recommendationId) { + this.dispatchAction('trackRecommendationClicked', { + recommendationId + // eslint-disable-next-line no-console + }).catch(console.error); + } else { + // eslint-disable-next-line no-console + console.warn('[Portal] Invalid usage of data-recommendation attribute'); + } + }; + + const elements = this.getRecommendationButtons(); + for (const element of elements) { + element.addEventListener('click', clickHandler, {passive: true}); + } + } + render() { if (this.state.initStatus === 'success') { return ( diff --git a/apps/portal/src/actions.js b/apps/portal/src/actions.js index df96cd95e8..68663094c1 100644 --- a/apps/portal/src/actions.js +++ b/apps/portal/src/actions.js @@ -502,6 +502,18 @@ async function oneClickSubscribe({data: {siteUrl}, state}) { } function trackRecommendationClicked({data: {recommendationId}, api}) { + try { + const existing = localStorage.getItem('ghost-recommendations-clicked'); + const clicked = existing ? JSON.parse(existing) : []; + if (clicked.includes(recommendationId)) { + // Already tracked + return; + } + clicked.push(recommendationId); + localStorage.setItem('ghost-recommendations-clicked', JSON.stringify(clicked)); + } catch (e) { + // Ignore localstorage errors (browser not supported or in private mode) + } api.recommendations.trackClicked({ recommendationId }); diff --git a/ghost/core/core/frontend/helpers/tpl/recommendations.hbs b/ghost/core/core/frontend/helpers/tpl/recommendations.hbs index b748cdf9d9..4f262c38d6 100644 --- a/ghost/core/core/frontend/helpers/tpl/recommendations.hbs +++ b/ghost/core/core/frontend/helpers/tpl/recommendations.hbs @@ -2,7 +2,7 @@
{{rec.reason}}
diff --git a/ghost/core/test/unit/frontend/helpers/recommendations.test.js b/ghost/core/test/unit/frontend/helpers/recommendations.test.js index cb8248a49b..70097435fa 100644 --- a/ghost/core/test/unit/frontend/helpers/recommendations.test.js +++ b/ghost/core/test/unit/frontend/helpers/recommendations.test.js @@ -41,11 +41,11 @@ describe('{{#recommendations}} helper', function () { sinon.stub(api, 'recommendationsPublic').get(() => { return { browse: sinon.stub().resolves({recommendations: [ - {title: 'Recommendation 1', url: 'https://recommendations1.com', favicon: 'https://recommendations1.com/favicon.ico', reason: 'Reason 1'}, - {title: 'Recommendation 2', url: 'https://recommendations2.com', favicon: 'https://recommendations2.com/favicon.ico', reason: 'Reason 2'}, - {title: 'Recommendation 3', url: 'https://recommendations3.com', favicon: 'https://recommendations3.com/favicon.ico', reason: 'Reason 3'}, - {title: 'Recommendation 4', url: 'https://recommendations4.com', favicon: 'https://recommendations4.com/favicon.ico', reason: 'Reason 4'}, - {title: 'Recommendation 5', url: 'https://recommendations5.com', favicon: 'https://recommendations5.com/favicon.ico', reason: 'Reason 5'} + {id: '1', title: 'Recommendation 1', url: 'https://recommendations1.com', favicon: 'https://recommendations1.com/favicon.ico', reason: 'Reason 1'}, + {id: '2', title: 'Recommendation 2', url: 'https://recommendations2.com', favicon: 'https://recommendations2.com/favicon.ico', reason: 'Reason 2'}, + {id: '3', title: 'Recommendation 3', url: 'https://recommendations3.com', favicon: 'https://recommendations3.com/favicon.ico', reason: 'Reason 3'}, + {id: '4', title: 'Recommendation 4', url: 'https://recommendations4.com', favicon: 'https://recommendations4.com/favicon.ico', reason: 'Reason 4'}, + {id: '5', title: 'Recommendation 5', url: 'https://recommendations5.com', favicon: 'https://recommendations5.com/favicon.ico', reason: 'Reason 5'} ], meta: meta}) }; }); @@ -71,35 +71,35 @@ describe('{{#recommendations}} helper', function () { const expected = trimSpaces(html`