From fcfb7d1494626691a04961e5b306d0168266b6a0 Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Tue, 27 Oct 2026 18:05:12 +0100 Subject: [PATCH] =?UTF-8?q?Landing=20:=20pages=20pr=C3=A9charg=C3=A9es=20a?= =?UTF-8?q?u=20survol=20d'un=20lien=20(speculationrules)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Règles de préchargement dans l'accueil et les 15 guides : une page du site est préparée quand on survole son lien (appui sur téléphone), et s'ouvre presque tout de suite. Pages seulement, pas les fichiers ; l'application n'est pas concernée. Ignoré par les navigateurs qui ne connaissent pas ces règles. - Une page préchargée ne compte pas de visite avant d'être vraiment ouverte (PostHog attend prerenderingchange), et l'animation du logo se joue à l'ouverture, pas pendant le préchargement. - CSP de la landing (deploy/nginx.conf) : empreinte des règles, et nouvelle empreinte du script du logo. Le mot-clé 'inline-speculation-rules' ne suffisait pas à Chrome. - Test : chaque script en ligne des pages a son empreinte dans la CSP, chaque page a ses règles. Co-Authored-By: Claude Opus 5.5 --- deploy/nginx.conf | 6 ++- landing/index.html | 6 ++- landing/public/aide/index.html | 4 ++ .../application-arbre-genealogique/index.html | 4 ++ .../arbre-genealogique-a-remplir/index.html | 4 ++ .../arbre-genealogique-avec-photos/index.html | 4 ++ .../index.html | 4 ++ landing/public/cousinade/index.html | 4 ++ .../public/donnees-et-vie-privee/index.html | 4 ++ landing/public/en/family-reunion/index.html | 4 ++ landing/public/en/family-tree-app/index.html | 4 ++ .../en/family-tree-with-photos/index.html | 4 ++ landing/public/en/help/index.html | 4 ++ .../en/how-to-make-a-family-tree/index.html | 4 ++ .../en/printable-family-tree/index.html | 4 ++ landing/public/en/privacy/index.html | 4 ++ .../en/tell-your-family-story/index.html | 4 ++ .../raconter-histoire-de-famille/index.html | 4 ++ landing/src/main.jsx | 11 ++++-- landing/test/csp-empreintes.test.mjs | 39 +++++++++++++++++++ 20 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 landing/test/csp-empreintes.test.mjs diff --git a/deploy/nginx.conf b/deploy/nginx.conf index 6da594e..9978615 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -33,12 +33,16 @@ map $host $parenthese_app_csp { # script-src 'sha256-...' script en ligne du (animation du logo, dans index.html et en/index.html). # S'il change, recalculer l'empreinte : base64(sha256(contenu exact entre # )), sinon l'animation ne se joue plus (sans autre effet). +# 2e 'sha256-...' règles de préchargement des pages ( + Parenthèse : l'arbre généalogique vivant de votre famille @@ -72,6 +72,10 @@ ] } + + diff --git a/landing/public/aide/index.html b/landing/public/aide/index.html index 6d04e81..416b631 100644 --- a/landing/public/aide/index.html +++ b/landing/public/aide/index.html @@ -51,6 +51,10 @@ ] } + +
diff --git a/landing/public/application-arbre-genealogique/index.html b/landing/public/application-arbre-genealogique/index.html index 9d0ec80..3c84127 100644 --- a/landing/public/application-arbre-genealogique/index.html +++ b/landing/public/application-arbre-genealogique/index.html @@ -49,6 +49,10 @@ ] } + +
diff --git a/landing/public/arbre-genealogique-a-remplir/index.html b/landing/public/arbre-genealogique-a-remplir/index.html index 68f077f..ecfeb97 100644 --- a/landing/public/arbre-genealogique-a-remplir/index.html +++ b/landing/public/arbre-genealogique-a-remplir/index.html @@ -63,6 +63,10 @@ ] } + +
diff --git a/landing/public/arbre-genealogique-avec-photos/index.html b/landing/public/arbre-genealogique-avec-photos/index.html index deece9a..3fdb9c0 100644 --- a/landing/public/arbre-genealogique-avec-photos/index.html +++ b/landing/public/arbre-genealogique-avec-photos/index.html @@ -48,6 +48,10 @@ ] } + +
diff --git a/landing/public/comment-faire-un-arbre-genealogique/index.html b/landing/public/comment-faire-un-arbre-genealogique/index.html index 31e6472..6ae11a6 100644 --- a/landing/public/comment-faire-un-arbre-genealogique/index.html +++ b/landing/public/comment-faire-un-arbre-genealogique/index.html @@ -65,6 +65,10 @@ ] } + +
diff --git a/landing/public/cousinade/index.html b/landing/public/cousinade/index.html index 146e4ef..bd8e652 100644 --- a/landing/public/cousinade/index.html +++ b/landing/public/cousinade/index.html @@ -54,6 +54,10 @@ ] } + +
diff --git a/landing/public/donnees-et-vie-privee/index.html b/landing/public/donnees-et-vie-privee/index.html index e4b0ae9..cd33d07 100644 --- a/landing/public/donnees-et-vie-privee/index.html +++ b/landing/public/donnees-et-vie-privee/index.html @@ -35,6 +35,10 @@ "publisher": { "@type": "Person", "name": "Ilies Allali", "email": "pro.allali.ilies@gmail.com" } } + +
diff --git a/landing/public/en/family-reunion/index.html b/landing/public/en/family-reunion/index.html index 8a3e418..be55ccf 100644 --- a/landing/public/en/family-reunion/index.html +++ b/landing/public/en/family-reunion/index.html @@ -54,6 +54,10 @@ ] } + +
diff --git a/landing/public/en/family-tree-app/index.html b/landing/public/en/family-tree-app/index.html index e8c8851..cf98c6c 100644 --- a/landing/public/en/family-tree-app/index.html +++ b/landing/public/en/family-tree-app/index.html @@ -49,6 +49,10 @@ ] } + +
diff --git a/landing/public/en/family-tree-with-photos/index.html b/landing/public/en/family-tree-with-photos/index.html index 728ddf6..d99ee5a 100644 --- a/landing/public/en/family-tree-with-photos/index.html +++ b/landing/public/en/family-tree-with-photos/index.html @@ -48,6 +48,10 @@ ] } + +
diff --git a/landing/public/en/help/index.html b/landing/public/en/help/index.html index ff5c217..ce53f28 100644 --- a/landing/public/en/help/index.html +++ b/landing/public/en/help/index.html @@ -51,6 +51,10 @@ ] } + +
diff --git a/landing/public/en/how-to-make-a-family-tree/index.html b/landing/public/en/how-to-make-a-family-tree/index.html index 4d0ad88..8acd568 100644 --- a/landing/public/en/how-to-make-a-family-tree/index.html +++ b/landing/public/en/how-to-make-a-family-tree/index.html @@ -65,6 +65,10 @@ ] } + +
diff --git a/landing/public/en/printable-family-tree/index.html b/landing/public/en/printable-family-tree/index.html index 2b24bb1..65d4dd8 100644 --- a/landing/public/en/printable-family-tree/index.html +++ b/landing/public/en/printable-family-tree/index.html @@ -63,6 +63,10 @@ ] } + +
diff --git a/landing/public/en/privacy/index.html b/landing/public/en/privacy/index.html index eedce00..374d31e 100644 --- a/landing/public/en/privacy/index.html +++ b/landing/public/en/privacy/index.html @@ -35,6 +35,10 @@ "publisher": { "@type": "Person", "name": "Ilies Allali", "email": "pro.allali.ilies@gmail.com" } } + +
diff --git a/landing/public/en/tell-your-family-story/index.html b/landing/public/en/tell-your-family-story/index.html index 46cfaee..601e663 100644 --- a/landing/public/en/tell-your-family-story/index.html +++ b/landing/public/en/tell-your-family-story/index.html @@ -47,6 +47,10 @@ ] } + +
diff --git a/landing/public/raconter-histoire-de-famille/index.html b/landing/public/raconter-histoire-de-famille/index.html index f10b24c..38f1a79 100644 --- a/landing/public/raconter-histoire-de-famille/index.html +++ b/landing/public/raconter-histoire-de-famille/index.html @@ -47,6 +47,10 @@ ] } + +
diff --git a/landing/src/main.jsx b/landing/src/main.jsx index c804f71..ade7f07 100644 --- a/landing/src/main.jsx +++ b/landing/src/main.jsx @@ -5,10 +5,15 @@ import App from './App.jsx' import { analytics } from './analytics.js' import { localeFromPath } from './i18n.js' -// PostHog après le premier rendu : les événements émis avant sont mis en file et envoyés à l'init +// PostHog après le premier rendu : les événements émis avant sont mis en file et envoyés à l'init. +// Page préchargée (survol d'un lien, speculationrules) : rien ne part avant qu'on l'ouvre vraiment. const deferInit = () => analytics.init() -if ('requestIdleCallback' in window) window.requestIdleCallback(deferInit, { timeout: 3000 }) -else window.setTimeout(deferInit, 1500) +const scheduleInit = () => { + if ('requestIdleCallback' in window) window.requestIdleCallback(deferInit, { timeout: 3000 }) + else window.setTimeout(deferInit, 1500) +} +if (document.prerendering) document.addEventListener('prerenderingchange', scheduleInit, { once: true }) +else scheduleInit() const container = document.getElementById('root') const app = ( diff --git a/landing/test/csp-empreintes.test.mjs b/landing/test/csp-empreintes.test.mjs new file mode 100644 index 0000000..00f1ff4 --- /dev/null +++ b/landing/test/csp-empreintes.test.mjs @@ -0,0 +1,39 @@ +// Les scripts en ligne de la landing (animation du logo, règles de préchargement des pages) ne passent la CSP de +// nginx que par leur empreinte exacte (deploy/nginx.conf). Un script modifié sans recalculer son empreinte ne +// casse rien de visible : l'animation ou le préchargement s'arrêtent sans bruit. Ce test le dit tout de suite. +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { readFileSync, readdirSync, statSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const landing = fileURLToPath(new URL('..', import.meta.url)) +const csp = readFileSync(path.join(landing, '../deploy/nginx.conf'), 'utf8').match(/map \$host \$parenthese_landing_csp \{\s*default "([^"]+)"/)[1] +const autorisees = new Set([...csp.matchAll(/'sha256-([^']+)'/g)].map((m) => m[1])) +const empreinte = (texte) => createHash('sha256').update(texte, 'utf8').digest('base64') + +function pages(dossier) { + return readdirSync(dossier).flatMap((nom) => { + const chemin = path.join(dossier, nom) + if (statSync(chemin).isDirectory()) return pages(chemin) + return nom === 'index.html' ? [chemin] : [] + }) +} +const toutes = [path.join(landing, 'index.html'), ...pages(path.join(landing, 'public'))] + +test('chaque script en ligne des pages a son empreinte dans la CSP de la landing', () => { + for (const fichier of toutes) { + const html = readFileSync(fichier, 'utf8') + for (const [, attributs, contenu] of html.matchAll(/]*)>([\s\S]*?)<\/script>/g)) { + if (/\bsrc=|application\/ld\+json/.test(attributs)) continue + assert.ok(autorisees.has(empreinte(contenu)), `${path.relative(landing, fichier)} : script en ligne sans empreinte dans deploy/nginx.conf (${empreinte(contenu)})`) + } + } +}) + +test('chaque page a ses règles de préchargement', () => { + for (const fichier of toutes) { + assert.match(readFileSync(fichier, 'utf8'), /