diff --git a/site/netlify.toml b/site/netlify.toml
index 71eee1eb2..83e9417fe 100644
--- a/site/netlify.toml
+++ b/site/netlify.toml
@@ -148,8 +148,8 @@ Content-Security-Policy = "default-src 'self' mon-entreprise.fr; style-src 'self
status = 200
[[redirects]]
- from = ":SITE_FR/simulateurs/dirigeant-sasu"
- to = "/prerender/mon-entreprise/simulateurs/dirigeant-sasu/index.html"
+ from = ":SITE_FR/simulateurs/sasu"
+ to = "/prerender/mon-entreprise/simulateurs/sasu/index.html"
status = 200
[[redirects]]
diff --git a/site/package.json b/site/package.json
index c3a5a3f2c..fc25fafb3 100644
--- a/site/package.json
+++ b/site/package.json
@@ -24,12 +24,12 @@
"start": "vite dev",
"build": "NODE_OPTIONS='--max-old-space-size=6144'; yarn build:sitemap && vite build && yarn build:iframe-script",
"build:ssr": "NODE_OPTIONS='--max-old-space-size=4096'; vite build --ssr ./source/entry-server.tsx --outDir ./dist/server --emptyOutDir && echo '{\"module\": \"commonjs\"}' > dist/package.json",
- "build:prerender": "node prerender.cjs",
+ "build:prerender": "ts-node-esm prerender.ts",
"build:iframe-script": "NODE_OPTIONS='--max-old-space-size=4096'; vite build --config vite-iframe-script.config.ts",
"build:preview": "VITE_FR_BASE_URL=http://localhost:8888; VITE_EN_BASE_URL=http://localhost:8889; yarn build && yarn build:ssr && yarn build:prerender",
"build:sitemap": "ts-node-esm scripts/build-sitemap.ts",
- "preview:mon-entreprise": "sed 's|:SITE_FR||g' netlify.toml | sed 's|:API_URL|http://localhost:3004|g' > dist/netlify.toml && cd dist && npx netlify-cli dev -d ./ -p 8888",
- "preview:infrance": "sed 's|:SITE_EN||g' | sed 's|:API_URL|http://localhost:3004|g' netlify.toml > dist/netlify.toml && cd dist && npx netlify-cli dev -d ./ -p 8889",
+ "preview:mon-entreprise": "sed 's|:SITE_EN|_|g' netlify.toml | sed 's|:SITE_FR||g' | sed 's|:API_URL|http://localhost:3004|g' > dist/netlify.toml && cd dist && npx netlify-cli dev -d ./ -p 8888",
+ "preview:infrance": " sed 's|:SITE_EN||g' netlify.toml | sed 's|:SITE_FR|_|g' | sed 's|:API_URL|http://localhost:3004|g' > dist/netlify.toml && cd dist && npx netlify-cli dev -d ./ -p 8889",
"typecheck:watch": "tsc --skipLibCheck --noEmit --watch",
"test": "vitest",
"test:dev-e2e:mon-entreprise": "cypress open --e2e",
diff --git a/site/prerender-worker.js b/site/prerender-worker.js
new file mode 100644
index 000000000..26310945e
--- /dev/null
+++ b/site/prerender-worker.js
@@ -0,0 +1,38 @@
+import { render } from './dist/server/entry-server.js'
+
+import { promises as fs, readFileSync } from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'url'
+
+const dirname = path.dirname(fileURLToPath(import.meta.url))
+
+const cache = {}
+
+const htmlBodyStart = ''
+const htmlBodyEnd = ''
+const headTagsStart = ''
+const headTagsEnd = ''
+
+const regexHTML = new RegExp(htmlBodyStart + '[\\s\\S]+' + htmlBodyEnd, 'm')
+const regexHelmet = new RegExp(headTagsStart + '[\\s\\S]+' + headTagsEnd, 'm')
+
+export default async ({ site, url, lang }) => {
+ // TODO: Add CI test to enforce meta tags on SSR pages
+ const { html, styleTags, helmet } = render(url, lang)
+
+ const template =
+ cache[site] ??
+ readFileSync(path.join(dirname, `./dist/${site}.html`), 'utf-8')
+
+ cache[site] = template
+
+ const page = template
+ .replace(regexHTML, html)
+ .replace('', styleTags)
+ .replace(regexHelmet, helmet.title.toString() + helmet.meta.toString())
+
+ const dir = path.join(dirname, 'dist/prerender', site, url)
+
+ await fs.mkdir(dir, { recursive: true })
+ await fs.writeFile(path.join(dir, 'index.html'), page)
+}
diff --git a/site/prerender.cjs b/site/prerender.cjs
deleted file mode 100644
index 9a752e41f..000000000
--- a/site/prerender.cjs
+++ /dev/null
@@ -1,69 +0,0 @@
-/* eslint-env node */
-// TODO: Move to ESModule. But it was easier to make this script work with
-// CommonJS when I wrote it.
-// cf. https://github.com/vitejs/vite/blob/133fcea5223263b0ae08ac9a0422b55183ebd266/packages/vite/src/node/build.ts#L495
-// cf. https://github.com/vitejs/vite/pull/2157
-// cf. https://github.com/vitejs/vite/pull/6812
-
-// TODO: We could use something like https://github.com/Aslemammad/tinypool to
-// prerender all pages in parallel (used by vitest). Or move to SSR with a
-// lambda and immutable caching.
-
-const { readFileSync, promises: fs } = require('fs')
-const path = require('path')
-const { render } = require('./dist/server/entry-server.js')
-
-const pagesToPrerender = {
- 'mon-entreprise': [
- '/',
- '/créer',
- '/gérer',
- '/simulateurs',
- '/simulateurs/salaire-brut-net',
- '/simulateurs/chômage-partiel',
- '/simulateurs/auto-entrepreneur',
- '/simulateurs/indépendant',
- '/simulateurs/dirigeant-sasu',
- '/simulateurs/artiste-auteur',
- '/iframes/simulateur-embauche',
- '/iframes/pamc',
- ],
- infrance: ['/', '/calculators/salary', '/iframes/simulateur-embauche'],
-}
-
-const templates = Object.fromEntries(
- Object.keys(pagesToPrerender).map((siteName) => [
- siteName,
- readFileSync(path.join(__dirname, `./dist/${siteName}.html`), 'utf-8'),
- ])
-)
-
-;(async function () {
- await Promise.all(
- Object.entries(pagesToPrerender).flatMap(([site, urls]) =>
- urls.map((url) => prerenderUrl(url, site))
- )
- )
-})()
-
-const htmlBodyStart = ''
-const htmlBodyEnd = ''
-const headTagsStart = ''
-const headTagsEnd = ''
-
-async function prerenderUrl(url, site) {
- const lang = site === 'mon-entreprise' ? 'fr' : 'en'
- // TODO: Add CI test to enforce meta tags on SSR pages
- const { html, styleTags, helmet } = await render(url, lang)
- const page = templates[site]
- .replace(new RegExp(htmlBodyStart + '[\\s\\S]+' + htmlBodyEnd, 'm'), html)
- .replace('', styleTags)
- .replace(
- new RegExp(headTagsStart + '[\\s\\S]+' + headTagsEnd, 'm'),
- helmet.title.toString() + helmet.meta.toString()
- )
-
- const dir = path.join(__dirname, 'dist/prerender', site, url)
- await fs.mkdir(dir, { recursive: true })
- await fs.writeFile(path.join(dir, 'index.html'), page)
-}
diff --git a/site/prerender.ts b/site/prerender.ts
new file mode 100644
index 000000000..9731f1fc0
--- /dev/null
+++ b/site/prerender.ts
@@ -0,0 +1,53 @@
+import { writeFileSync } from 'node:fs'
+import Tinypool from 'tinypool'
+import { constructLocalizedSitePath } from './source/sitePaths.js'
+
+const filename = new URL('./prerender-worker.js', import.meta.url).href
+const pool = new Tinypool({ filename })
+
+const sitePathFr = constructLocalizedSitePath('fr')
+const sitePathEn = constructLocalizedSitePath('en')
+
+export const pagesToPrerender: {
+ 'mon-entreprise': string[]
+ infrance: string[]
+} = {
+ 'mon-entreprise': [
+ sitePathFr.index,
+ sitePathFr.créer.index,
+ sitePathFr.gérer.index,
+ sitePathFr.simulateurs.index,
+ sitePathFr.simulateurs.salarié,
+ sitePathFr.simulateurs['chômage-partiel'],
+ sitePathFr.simulateurs['auto-entrepreneur'],
+ sitePathFr.simulateurs.indépendant,
+ sitePathFr.simulateurs.sasu,
+ sitePathFr.simulateurs['artiste-auteur'],
+ '/iframes/simulateur-embauche',
+ '/iframes/pamc',
+ ],
+ infrance: [
+ sitePathEn.index,
+ sitePathEn.simulateurs.salarié,
+ '/iframes/simulateur-embauche',
+ ],
+}
+
+if (process.env.GENERATE_PRERENDER_PATHS_JSON) {
+ // This json file is used in e2e cypress test
+ writeFileSync(
+ 'cypress/prerender-paths.json',
+ JSON.stringify(pagesToPrerender)
+ )
+ console.log('cypress/prerender-paths.json was generated!')
+
+ process.exit()
+}
+
+await Promise.all(
+ Object.entries(pagesToPrerender).flatMap(([site, urls]) =>
+ urls.map((url) =>
+ pool.run({ site, url, lang: site === 'mon-entreprise' ? 'fr' : 'en' })
+ )
+ )
+)
diff --git a/site/scripts/preCommit/index.ts b/site/scripts/preCommit/index.ts
index d0574abdd..be2dfd042 100644
--- a/site/scripts/preCommit/index.ts
+++ b/site/scripts/preCommit/index.ts
@@ -13,6 +13,10 @@ const results = await execOnFileChange({
],
run: 'yarn build:yaml-to-dts',
},
+ {
+ paths: ['./prerender.ts'],
+ run: 'GENERATE_PRERENDER_PATHS_JSON=true yarn build:prerender',
+ },
],
})
diff --git a/site/source/entry-server.tsx b/site/source/entry-server.tsx
index 7a53e1178..343fba9be 100644
--- a/site/source/entry-server.tsx
+++ b/site/source/entry-server.tsx
@@ -16,7 +16,7 @@ export function render(url: string, lang: 'fr' | 'en') {
console.error(err)
)
- const html = ReactDOMServer.renderToString(
+ const element = (
@@ -28,6 +28,11 @@ export function render(url: string, lang: 'fr' | 'en') {
)
+ // first render
+ ReactDOMServer.renderToString(element)
+ // second render with the configured engine
+ const html = ReactDOMServer.renderToString(element)
+
const styleTags = sheet.getStyleTags()
return { html, styleTags, helmet: helmetContext.helmet }
diff --git a/site/source/sitePaths.ts b/site/source/sitePaths.ts
index d5f59349d..c364c3244 100644
--- a/site/source/sitePaths.ts
+++ b/site/source/sitePaths.ts
@@ -200,38 +200,47 @@ const checkedSitePathsFr: RequiredPath & typeof rawSitePathsFr = rawSitePathsFr
// If there is a type error here, check rawSitePathsEn object matches the metadata-src.ts pathId
const checkedSitePathsEn: RequiredPath & typeof rawSitePathsEn = rawSitePathsEn
-type SitePathObject = {
- [K in keyof T]: T[K] extends string ? string : SitePathObject
-} & {
- index: string
+type SitePathsFr = typeof checkedSitePathsFr
+type SitePathsEn = typeof checkedSitePathsEn
+
+type SitePath = { [key: string]: string | SitePath } & { index: string }
+
+type SitePathBuilt = {
+ [K in keyof T]: T[K] extends string
+ ? K extends 'index'
+ ? `${Root}${T[K]}`
+ : T extends { index: string }
+ ? `${Root}${T['index']}${T[K]}`
+ : `${Root}${T[K]}`
+ : SitePathBuilt<
+ T[K] extends SitePath ? T[K] : never,
+ T extends { index: string } ? `${Root}${T['index']}` : `${Root}`
+ >
}
-function constructSitePaths(
- root: string,
- { index, ...sitePaths }: SitePathObject
-): SitePathObject {
- const entries = Object.entries(sitePaths) as [
- string,
- string | SitePathObject
- ][]
+function constructSitePaths(
+ obj: SitePath,
+ root = ''
+): SitePathBuilt {
+ const { index } = obj
+ const entries = Object.entries(obj)
- return {
- index: root + index,
- ...Object.fromEntries(
- entries.map(([k, value]) => [
- k,
- typeof value === 'string'
- ? root + index + value
- : constructSitePaths(root + index, value),
- ])
- ),
- } as SitePathObject
+ return Object.fromEntries(
+ entries.map(([k, value]) => [
+ k,
+ typeof value === 'string'
+ ? root + (k === 'index' ? value : index + value)
+ : constructSitePaths(value, root + index),
+ ])
+ ) as SitePathBuilt
}
-export const constructLocalizedSitePath = (language: 'en' | 'fr') => {
- const sitePaths = language === 'fr' ? checkedSitePathsFr : checkedSitePathsEn
-
- return constructSitePaths('', sitePaths)
+export const constructLocalizedSitePath = (
+ language: T
+) => {
+ return constructSitePaths(
+ language === 'fr' ? checkedSitePathsFr : checkedSitePathsEn
+ )
}
export type SitePathsType = ReturnType
diff --git a/site/tsconfig.json b/site/tsconfig.json
index 3c215f2e9..baa0540cc 100644
--- a/site/tsconfig.json
+++ b/site/tsconfig.json
@@ -27,6 +27,7 @@
"scripts",
"test/**/*.ts",
"vite.config.ts",
- "vite-iframe-script.config.ts"
+ "vite-iframe-script.config.ts",
+ "prerender.ts"
]
}