Prerender use second render + prerender is done in a worker

pull/2175/head
Jérémy Rialland 2022-06-21 13:34:44 +02:00 committed by Jérémy Rialland
parent 0802c79cff
commit 1460080595
9 changed files with 144 additions and 103 deletions

View File

@ -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]]

View File

@ -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",

38
site/prerender-worker.js Normal file
View File

@ -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)
}

View File

@ -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)
}

53
site/prerender.ts Normal file
View File

@ -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' })
)
)
)

View File

@ -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',
},
],
})

View File

@ -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 }

View File

@ -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>

View File

@ -27,6 +27,7 @@
"scripts",
"test/**/*.ts",
"vite.config.ts",
"vite-iframe-script.config.ts"
"vite-iframe-script.config.ts",
"prerender.ts"
]
}