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.
Lya, 14 ans
@@ -1104,7 +1111,7 @@ export default function App() {
Guides
-
Avant de commencer, ou pour aller plus loin
+
Guides pour faire son arbre généalogique
{GUIDES.map(({ href, title, text }) => (
diff --git a/landing/src/entry-server.jsx b/landing/src/entry-server.jsx
new file mode 100644
index 0000000..c3ca820
--- /dev/null
+++ b/landing/src/entry-server.jsx
@@ -0,0 +1,12 @@
+import { StrictMode } from 'react'
+import { renderToString } from 'react-dom/server'
+import App from './App.jsx'
+
+// Rendu de la landing à la compilation (npm run build), injecté dans dist/index.html par scripts/prerender.mjs
+export function render() {
+ return renderToString(
+
+
+ ,
+ )
+}
diff --git a/landing/src/main.jsx b/landing/src/main.jsx
index 970b2fc..6d9412e 100644
--- a/landing/src/main.jsx
+++ b/landing/src/main.jsx
@@ -1,5 +1,5 @@
import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
+import { createRoot, hydrateRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import { analytics } from './analytics.js'
@@ -9,8 +9,14 @@ const deferInit = () => analytics.init()
if ('requestIdleCallback' in window) window.requestIdleCallback(deferInit, { timeout: 3000 })
else window.setTimeout(deferInit, 1500)
-createRoot(document.getElementById('root')).render(
+const container = document.getElementById('root')
+const app = (
- ,
+
)
+
+// En production le HTML est prérendu (scripts/prerender.mjs) : on l'hydrate.
+// En développement (vite dev) la racine est vide : rendu client classique.
+if (container.hasChildNodes()) hydrateRoot(container, app)
+else createRoot(container).render(app)