From 70af00e6361b725ea7528321f6786959003f9d2e Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Fri, 25 Sep 2026 11:41:47 +0200 Subject: [PATCH] =?UTF-8?q?Landing=20pr=C3=A9rendue=20=C3=A0=20la=20compil?= =?UTF-8?q?ation=20et=20textes=20orient=C3=A9s=20arbre=20g=C3=A9n=C3=A9alo?= =?UTF-8?q?gique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Textes de la landing (pastille, lede, H2 des étapes, de la démo et des guides, Pour qui) et métadonnées og/twitter/JSON-LD sans « galaxie » - npm run build fait le build client, un build SSR (src/entry-server.jsx, renderToString) puis scripts/prerender.mjs injecte le HTML dans dist/index.html ; hydrateRoot côté client - Rendu serveur sans accès window : source et isMobile posés après montage - noscript rend visibles les blocs data-reveal sans JavaScript - App : robots.txt en Allow pour que la meta noindex soit lue Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 1 + frontend/public/robots.txt | 4 +++- landing/index.html | 10 ++++++---- landing/package.json | 2 +- landing/scripts/prerender.mjs | 21 +++++++++++++++++++++ landing/src/App.jsx | 27 +++++++++++++++++---------- landing/src/entry-server.jsx | 12 ++++++++++++ landing/src/main.jsx | 12 +++++++++--- 8 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 landing/scripts/prerender.mjs create mode 100644 landing/src/entry-server.jsx diff --git a/.gitignore b/.gitignore index 40075cd..48eae82 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Build outputs **/dist/ +**/dist-ssr/ **/build/ **/.next/ **/coverage/ diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt index 1f53798..a8566ce 100644 --- a/frontend/public/robots.txt +++ b/frontend/public/robots.txt @@ -1,2 +1,4 @@ +# L'app n'est pas à indexer. L'exploration reste permise pour que les moteurs lisent +# la balise de index.html (un Disallow la masque). User-agent: * -Disallow: / +Allow: / diff --git a/landing/index.html b/landing/index.html index a15606b..78239d1 100644 --- a/landing/index.html +++ b/landing/index.html @@ -15,7 +15,7 @@ - + @@ -24,13 +24,13 @@ - + - + @@ -63,7 +63,7 @@ "applicationCategory": "LifestyleApplication", "operatingSystem": "Web", "inLanguage": "fr-FR", - "description": "Web app pour construire et partager la mémoire d'une famille : arbre en forme de galaxie, photos, anecdotes, vidéos et voix, partage par lien.", + "description": "Web app pour créer l'arbre généalogique d'une famille en ligne, avec photos, anecdotes, vidéos et voix, partageable en un lien sans compte.", "offers": { "@type": "Offer", "price": "0", "priceCurrency": "EUR" }, "publisher": { "@id": "https://parenthese.io/#organization" } } @@ -72,6 +72,8 @@ + +
diff --git a/landing/package.json b/landing/package.json index 33d3240..d484aef 100644 --- a/landing/package.json +++ b/landing/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "vite build && vite build --ssr src/entry-server.jsx --outDir dist-ssr && node scripts/prerender.mjs", "preview": "vite preview", "test": "node --test" }, diff --git a/landing/scripts/prerender.mjs b/landing/scripts/prerender.mjs new file mode 100644 index 0000000..f1eda58 --- /dev/null +++ b/landing/scripts/prerender.mjs @@ -0,0 +1,21 @@ +// Injecte le HTML de la landing, rendu par React à la compilation, dans dist/index.html. +// Lancé par `npm run build` après le build client (dist/) et le build SSR (dist-ssr/). +import { readFile, rm, writeFile } from 'node:fs/promises' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const root = fileURLToPath(new URL('..', import.meta.url)) +const indexPath = `${root}dist/index.html` +const ssrDir = `${root}dist-ssr` +const MARKER = '
' + +const { render } = await import(pathToFileURL(`${ssrDir}/entry-server.js`).href) +const appHtml = render() +if (!appHtml || !appHtml.includes('${appHtml}`)) +await rm(ssrDir, { recursive: true, force: true }) + +console.log(`Prérendu injecté dans dist/index.html (${Math.round(appHtml.length / 1024)} Ko de HTML)`) diff --git a/landing/src/App.jsx b/landing/src/App.jsx index 17f8480..94e71d2 100644 --- a/landing/src/App.jsx +++ b/landing/src/App.jsx @@ -788,7 +788,8 @@ function InlineDemo() { const [active, setActive] = useState(false) const [loaded, setLoaded] = useState(false) const [shouldLoad, setShouldLoad] = useState(false) - const [isMobile, setIsMobile] = useState(() => window.matchMedia('(max-width: 768px)').matches) + // Faux au prérendu comme au premier rendu client (hydratation identique), corrigé par l'effet ci-dessous + const [isMobile, setIsMobile] = useState(false) const sectionRef = useRef(null) // L'iframe de l'app (500 Ko de JS + la galaxie) ne se charge qu'à l'approche de la section, @@ -824,8 +825,8 @@ function InlineDemo() {
Un exemple -

Toute une famille sur une page

-

Cliquez sur un visage, sa fiche s'ouvre. C'est l'arbre de démonstration, vous pouvez l'explorer sans créer de compte.

+

Un arbre généalogique avec photos, à explorer

+

Cliquez sur un visage pour ouvrir sa fiche. C'est un arbre de démonstration, sans compte à créer.

@@ -904,14 +905,20 @@ const GUIDES = [ }, ] +const readSource = () => new URLSearchParams(window.location.search).get('source') || 'direct' + export default function App() { const [headerElevated, setHeaderElevated] = useState(false) const [sharedReturnUrl, setSharedReturnUrl] = useState('') const scrollMilestonesRef = useRef({ half: false, full: false }) - const source = new URLSearchParams(window.location.search).get('source') || 'direct' + // La page est prérendue à la compilation, sans URL : le premier rendu est toujours la variante + // par défaut, puis la variante `app-shared` est posée après le montage (pas d'écart d'hydratation). + const [source, setSource] = useState('direct') + useEffect(() => { setSource(readSource()) }, []) + const buildEventProps = (extra = {}) => ({ - source, + source: readSource(), referrer: document.referrer || 'none', viewport: window.innerWidth < 768 ? 'mobile' : 'desktop', ...extra, @@ -1001,14 +1008,14 @@ export default function App() {
- Gratuit, sans compte pour lire + Arbre généalogique gratuit, en ligne

L'histoire de votre famille, racontée par ceux qui l'ont vécue

{source === 'app-shared' ? "Vous venez de découvrir Parenthèse à travers le lien d'une famille. Créez la vôtre en quelques minutes." - : 'Des visages, des voix, des souvenirs rangés par génération. Un lien suffit pour que toute la famille y ait accès.'} + : "Créez l'arbre généalogique de votre famille en ligne, avec des photos, des voix et des souvenirs. Un lien suffit pour que toute la famille le consulte."}

@@ -1030,7 +1037,7 @@ export default function App() {
Comment ça marche -

Trois gestes, pas plus

+

Créer un arbre généalogique, en trois gestes

@@ -1081,7 +1088,7 @@ export default function App() {
Pour qui

Du petit-fils de 8 ans à la grand-mère de 80

-

Parenthèse se lit comme un album. Pas de jargon, pas d'arborescence à comprendre. On reconnaît quelqu'un, on clique.

+

Parenthèse se lit comme un album de famille. On reconnaît un visage, on clique, on écoute.