From 9bf55a16f3bd4e07e582b588991328e942aefec9 Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Wed, 4 Nov 2026 19:01:07 +0100 Subject: [PATCH] =?UTF-8?q?landing=20:=20mesure=20d'audience=20des=20pages?= =?UTF-8?q?=20statiques,=20provenance=20gard=C3=A9e=20jusqu'=C3=A0=20l'app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les guides, l'aide, la vidéo et les pages vie privée (public/, copiés sans Vite) n'avaient aucune mesure. src/pages.js, construit par Vite avec une empreinte et posé au build dans chaque page par scripts/inject-pages.mjs, envoie les mêmes événements que la page d'accueil (page_viewed, scroll_depth_50 et 100, signup_cta_clicked) et video_played sur /video/. Les liens vers l'app gardent le ?source= d'arrivée au lieu de la provenance fixe de la page. Page vie privée (fr, en) complétée. Co-Authored-By: Claude Opus 5.5 --- landing/package.json | 2 +- .../public/donnees-et-vie-privee/index.html | 2 +- landing/public/en/privacy/index.html | 2 +- landing/scripts/inject-pages.mjs | 37 ++++++++ landing/src/pages.js | 85 +++++++++++++++++++ landing/vite.config.js | 6 ++ 6 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 landing/scripts/inject-pages.mjs create mode 100644 landing/src/pages.js diff --git a/landing/package.json b/landing/package.json index b7c9f05..f4712da 100644 --- a/landing/package.json +++ b/landing/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build && vite build --ssr src/entry-server.jsx --outDir dist-ssr && node scripts/prerender.mjs", + "build": "vite build && vite build --ssr src/entry-server.jsx --outDir dist-ssr && node scripts/prerender.mjs && node scripts/inject-pages.mjs", "preview": "vite preview", "test": "node --test" }, diff --git a/landing/public/donnees-et-vie-privee/index.html b/landing/public/donnees-et-vie-privee/index.html index f5995a9..2431ad0 100644 --- a/landing/public/donnees-et-vie-privee/index.html +++ b/landing/public/donnees-et-vie-privee/index.html @@ -96,7 +96,7 @@

Mesure d'audience

Parenthèse utilise PostHog pour compter les visites et voir ce qui est utilisé. PostHog est hébergé aux États-Unis, les données d'usage décrites ici y sont envoyées.

-

Ce qui est mesuré, et rien de plus. Sur le site, la visite d'une page, le défilement, les clics sur les boutons « Créer mon arbre », avec la provenance (page précédente, mobile ou ordinateur). Dans l'application, l'ouverture de l'écran de compte, la création d'un compte ou d'un arbre, et pour les arbres partagés, l'ouverture, le temps passé et le type de contenu consulté (texte, photo, vidéo), avec le type d'accès. L'arbre y est désigné par un code brouillé, jamais par son nom.

+

Ce qui est mesuré, et rien de plus. Sur le site, la visite d'une page, le défilement, la lecture de la vidéo de présentation, les clics sur les boutons « Créer mon arbre », avec la provenance (lien d'arrivée, page précédente, mobile ou ordinateur). Dans l'application, l'ouverture de l'écran de compte, la création d'un compte ou d'un arbre, et pour les arbres partagés, l'ouverture, le temps passé et le type de contenu consulté (texte, photo, vidéo), avec le type d'accès. L'arbre y est désigné par un code brouillé, jamais par son nom.

Il n'y a pas de capture automatique des clics ni des pages, et pas d'enregistrement des sessions. Aucun contenu d'arbre (nom, personne, photo) n'est envoyé, ni l'adresse de l'arbre, retirée des liens avant tout envoi. Quand vous êtes connecté à votre compte, un identifiant technique du compte (jamais votre adresse email) relie vos visites entre elles. Un cookie est posé sur parenthese.io et ses sous-domaines pour reconnaître la même visite entre le site et l'application.

Vos droits

diff --git a/landing/public/en/privacy/index.html b/landing/public/en/privacy/index.html index 09d78d3..5eb79e7 100644 --- a/landing/public/en/privacy/index.html +++ b/landing/public/en/privacy/index.html @@ -96,7 +96,7 @@

Audience measurement

Parenthèse uses PostHog to count visits and see what gets used. PostHog is hosted in the United States, and the usage data described here is sent there.

-

Here is what is measured, and nothing more. On the website, page views, scrolling, clicks on the “Start your tree” buttons, with where they came from (previous page, mobile or computer). In the app, opening the account screen, creating an account or a tree, and for shared trees, opening them, the time spent and the type of content viewed (text, photo, video), with the type of access. The tree is identified there by a scrambled code, never by its name.

+

Here is what is measured, and nothing more. On the website, page views, scrolling, playing the introduction video, clicks on the “Start your tree” buttons, with where they came from (link used to arrive, previous page, mobile or computer). In the app, opening the account screen, creating an account or a tree, and for shared trees, opening them, the time spent and the type of content viewed (text, photo, video), with the type of access. The tree is identified there by a scrambled code, never by its name.

There is no automatic capture of clicks or pages, and no session recording. No tree content (name, person, photo) is sent, and neither is the address of the tree, which is removed from links before anything is sent. When you are logged in to your account, a technical account identifier (never your email address) links your visits together. A cookie is set on parenthese.io and its subdomains to recognize the same visit across the site and the app.

Your rights

diff --git a/landing/scripts/inject-pages.mjs b/landing/scripts/inject-pages.mjs new file mode 100644 index 0000000..14b772a --- /dev/null +++ b/landing/scripts/inject-pages.mjs @@ -0,0 +1,37 @@ +// Pose la mesure d'audience (src/pages.js, nom à empreinte lu dans le manifeste de Vite) dans chaque page +// statique de dist/ : guides, aide, vidéo, vie privée. Ces pages viennent de public/ et ne passent pas par +// Vite. La page d'accueil (fr et en,
) a déjà sa propre mesure (App.jsx), elle est sautée. +// Lancé par `npm run build` après scripts/prerender.mjs. +import { readdir, readFile, rm, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const dist = fileURLToPath(new URL('../dist', import.meta.url)) +const manifestPath = join(dist, '.vite', 'manifest.json') +const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) +const entry = manifest['src/pages.js'] +if (!entry?.file) throw new Error('src/pages.js absent du manifeste de Vite') +const tag = `` + +async function* htmlFiles(dir) { + for (const item of await readdir(dir, { withFileTypes: true })) { + const path = join(dir, item.name) + if (item.isDirectory()) yield* htmlFiles(path) + else if (item.name === 'index.html') yield path + } +} + +let injected = 0 +for await (const path of htmlFiles(dist)) { + const html = await readFile(path, 'utf8') + if (html.includes('
')) continue + if (html.includes(tag)) continue + if (!html.includes('')) throw new Error(` introuvable dans ${path}`) + await writeFile(path, html.replace('', ` ${tag}\n`)) + injected++ +} +if (injected === 0) throw new Error('Aucune page statique trouvée pour la mesure d\'audience') + +// Le manifeste ne sert qu'au build, il n'a rien à faire en ligne +await rm(join(dist, '.vite'), { recursive: true, force: true }) +console.log(`Mesure d'audience posée dans ${injected} pages statiques (${entry.file})`) diff --git a/landing/src/pages.js b/landing/src/pages.js new file mode 100644 index 0000000..76457b7 --- /dev/null +++ b/landing/src/pages.js @@ -0,0 +1,85 @@ +// Mesure d'audience des pages statiques (guides, aide, vidéo, vie privée), injectée au build +// par scripts/inject-pages.mjs. Mêmes événements et mêmes propriétés que la page d'accueil (App.jsx), +// et rien de plus que ce que décrit la page « Données et vie privée ». +import { analytics } from './analytics.js' + +const params = new URLSearchParams(window.location.search) +// Provenance de l'arrivée (?source=newsletter…), sinon « direct », comme sur la page d'accueil +const arrival = params.get('source') || 'direct' +const lang = document.documentElement.lang?.startsWith('en') ? 'en' : 'fr' + +const eventProps = (extra = {}) => ({ + source: arrival, + referrer: document.referrer || 'none', + viewport: window.innerWidth < 768 ? 'mobile' : 'desktop', + lang, + ...extra, +}) + +// Les liens vers l'app gardent la provenance de l'arrivée : un lecteur venu de ?source=newsletter +// ouvre la démo en newsletter, pas en « video ». Sans provenance, le lien garde celle de la page. +function forwardSource() { + if (arrival === 'direct') return + for (const link of document.querySelectorAll('a[href^="https://app.parenthese.io"]')) { + const url = new URL(link.href) + url.searchParams.set('source', arrival) + link.href = url.toString() + } +} + +// Un clic vers la création de compte : le même événement que la page d'accueil (funnel PostHog) +function trackSignupClicks() { + document.addEventListener('click', (event) => { + const link = event.target.closest?.('a[href^="https://app.parenthese.io"]') + if (!link || !link.href.includes('account=register')) return + if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return + event.preventDefault() + analytics.captureThenNavigate('signup_cta_clicked', eventProps({ placement: 'page' }), link.href) + }) +} + +function trackScroll() { + const reached = { half: false, full: false } + const onScroll = () => { + const doc = document.documentElement + const max = Math.max(0, doc.scrollHeight - window.innerHeight) + const progress = max > 0 ? (window.scrollY || 0) / max : 1 + if (!reached.half && progress >= 0.5) { + reached.half = true + analytics.capture('scroll_depth_50', eventProps()) + } + if (!reached.full && progress >= 0.99) { + reached.full = true + analytics.capture('scroll_depth_100', eventProps()) + window.removeEventListener('scroll', onScroll) + } + } + window.addEventListener('scroll', onScroll, { passive: true }) +} + +// La vidéo de présentation (/video/) : une lecture par visite, avec le format regardé +function trackVideo() { + let played = false + for (const video of document.querySelectorAll('video')) { + video.addEventListener('play', () => { + if (played) return + played = true + analytics.capture('video_played', eventProps({ format: video.videoWidth > video.videoHeight ? '16x9' : '9x16' })) + }) + } +} + +function start() { + forwardSource() + trackSignupClicks() + // PostHog après le premier rendu, comme main.jsx : les événements d'ici là partent à l'init + if ('requestIdleCallback' in window) window.requestIdleCallback(() => analytics.init(), { timeout: 3000 }) + else window.setTimeout(() => analytics.init(), 1500) + analytics.capture('page_viewed', eventProps()) + trackScroll() + trackVideo() +} + +// Une page préchargée (speculation rules) n'est comptée que si on l'ouvre vraiment +if (document.prerendering) document.addEventListener('prerenderingchange', start, { once: true }) +else start() diff --git a/landing/vite.config.js b/landing/vite.config.js index 3c522b6..45a70b2 100644 --- a/landing/vite.config.js +++ b/landing/vite.config.js @@ -9,5 +9,11 @@ export default defineConfig({ target: ['chrome107', 'edge107', 'firefox104', 'safari16'], outDir: 'dist', emptyOutDir: true, + // Le manifeste donne le nom à empreinte de src/pages.js, que scripts/inject-pages.mjs pose + // dans les pages statiques de public/ (guides, aide, vidéo), copiées sans passer par Vite + manifest: true, + rollupOptions: { + input: { main: 'index.html', pages: 'src/pages.js' }, + }, }, })