Prerender use second render + prerender is done in a worker
parent
0802c79cff
commit
1460080595
|
@ -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]]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 = '<!--app-html:start-->'
|
||||
const htmlBodyEnd = '<!--app-html:end-->'
|
||||
const headTagsStart = '<!--app-helmet-tags:start-->'
|
||||
const headTagsEnd = '<!--app-helmet-tags:end-->'
|
||||
|
||||
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('<!--app-style-->', 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)
|
||||
}
|
|
@ -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 = '<!--app-html:start-->'
|
||||
const htmlBodyEnd = '<!--app-html:end-->'
|
||||
const headTagsStart = '<!--app-helmet-tags:start-->'
|
||||
const headTagsEnd = '<!--app-helmet-tags:end-->'
|
||||
|
||||
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('<!--app-style-->', 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)
|
||||
}
|
|
@ -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' })
|
||||
)
|
||||
)
|
||||
)
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ export function render(url: string, lang: 'fr' | 'en') {
|
|||
console.error(err)
|
||||
)
|
||||
|
||||
const html = ReactDOMServer.renderToString(
|
||||
const element = (
|
||||
<HelmetProvider context={helmetContext}>
|
||||
<SSRProvider>
|
||||
<StyleSheetManager sheet={sheet.instance}>
|
||||
|
@ -28,6 +28,11 @@ export function render(url: string, lang: 'fr' | 'en') {
|
|||
</HelmetProvider>
|
||||
)
|
||||
|
||||
// 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 }
|
||||
|
|
|
@ -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<T> = {
|
||||
[K in keyof T]: T[K] extends string ? string : SitePathObject<T[K]>
|
||||
} & {
|
||||
index: string
|
||||
type SitePathsFr = typeof checkedSitePathsFr
|
||||
type SitePathsEn = typeof checkedSitePathsEn
|
||||
|
||||
type SitePath = { [key: string]: string | SitePath } & { index: string }
|
||||
|
||||
type SitePathBuilt<T extends SitePath, Root extends string = ''> = {
|
||||
[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<T>(
|
||||
root: string,
|
||||
{ index, ...sitePaths }: SitePathObject<T>
|
||||
): SitePathObject<T> {
|
||||
const entries = Object.entries(sitePaths) as [
|
||||
string,
|
||||
string | SitePathObject<T>
|
||||
][]
|
||||
function constructSitePaths<T extends SitePath>(
|
||||
obj: SitePath,
|
||||
root = ''
|
||||
): SitePathBuilt<T> {
|
||||
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<T>
|
||||
return Object.fromEntries(
|
||||
entries.map(([k, value]) => [
|
||||
k,
|
||||
typeof value === 'string'
|
||||
? root + (k === 'index' ? value : index + value)
|
||||
: constructSitePaths(value, root + index),
|
||||
])
|
||||
) as SitePathBuilt<T>
|
||||
}
|
||||
|
||||
export const constructLocalizedSitePath = (language: 'en' | 'fr') => {
|
||||
const sitePaths = language === 'fr' ? checkedSitePathsFr : checkedSitePathsEn
|
||||
|
||||
return constructSitePaths<typeof sitePaths>('', sitePaths)
|
||||
export const constructLocalizedSitePath = <T extends 'fr' | 'en'>(
|
||||
language: T
|
||||
) => {
|
||||
return constructSitePaths<T extends 'fr' ? SitePathsFr : SitePathsEn>(
|
||||
language === 'fr' ? checkedSitePathsFr : checkedSitePathsEn
|
||||
)
|
||||
}
|
||||
|
||||
export type SitePathsType = ReturnType<typeof constructLocalizedSitePath>
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"scripts",
|
||||
"test/**/*.ts",
|
||||
"vite.config.ts",
|
||||
"vite-iframe-script.config.ts"
|
||||
"vite-iframe-script.config.ts",
|
||||
"prerender.ts"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue