Pre-rendering avec Angular et les components standalone (sans Express)

Nous allons voir comment créer une application Angular et tirer parti du rendu côté serveur (SSR) pour faire du pré-rendu de nos pages.


Créer une nouvelle application

ng new angular-standalone --minimal --style=css --routing --standalone --ssr=false

Parmi les options ci-dessus, standalone est la seule qui nous intéresse pour cet article, ssr=false est là pour éviter d'avoir à supprimer le code généré par défaut qui contient déjà du SSR avec Express, nous allons le faire nous-même from scratch.


Configurer notre application pour le pré-rendu

Installer le package @angular/platform-server :

npm install -S @angular/platform-server

Créer le fichier tsconfig.ssr.json (grosso modo une copie de tsconfig.app.json avec "types": ["node"] et "files": ["prerender.ts"]) :

tsconfig.ssr.json
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": ["node"] }, "files": ["prerender.ts"], "include": ["src/**/*.d.ts"] }

Ajouter le builder @angular-devkit/build-angular:server dans le fichier angular.json :

angular.json
"server": { "builder": "@angular-devkit/build-angular:server", "options": { "outputPath": "dist/angular-standalone/server", "main": "prerender.ts", "tsConfig": "tsconfig.ssr.json" } },

Ajouter le script "build:ssr": "ng run angular-standalone:server" dans le fichier package.json :

package.json
"scripts": { /* ... */ "build:ssr": "ng run angular-standalone:server" }

Ce script va nous permettre de générer le code de notre application pour le pré-rendu.


Ajouter le script "prerender": "node dist/angular-standalone/server/main.js" dans le fichier package.json :

package.json
"scripts": { /* ... */ "prerender": "node dist/angular-standalone/server/main.js" }

Ce script va nous permettre de lancer le pré-rendu de notre application.


Créer notre entrypoint pour le pré-rendu

prerender.ts
import "zone.js/node"; // Zone.js pour Node import { ApplicationConfig } from "@angular/core"; import { provideServerRendering } from "@angular/platform-server"; import { provideRouter } from "@angular/router"; import { routes } from "./src/app/app.routes"; /* Configuration de l'application pour le SSR */ // Note: ng new ... --ssr crée un fichier src/app/app.config.server.ts équivalent à ce bloc, // mais il merge la configuration server avec celle du browser en utilisant mergeApplicationConfig, // nous voulons ici une configuration server-only, donc on ne merge pas la configuration browser. const config: ApplicationConfig = { providers: [ provideRouter(routes), // provideServerRendering(), ], }; /* Main SSR app */ import { AppComponent } from "./src/app/app.component"; import { renderApplication } from "@angular/platform-server"; import { bootstrapApplication } from "@angular/platform-browser"; // Note: ng new ... --ssr crée un fichier src/main.server.ts équivalent à ce bloc. const bootstrap = () => bootstrapApplication(AppComponent, config); /* Lecture de index.html */ import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join, resolve } from "node:path"; const distFolder = dirname(fileURLToPath(import.meta.url)); const browserFolder = resolve(distFolder, "dist/angular-standalone/browser"); // Nous avons besoin de lire le fichier index.html pour injecter le // contenu de l'application lors du rendu du HTML. const document = readFileSync(join(browserFolder, "index.html"), "utf8"); function render(url: string) { return renderApplication(bootstrap, { url, document }); } /* Génération du HTML */ render("/") .then((html) => console.log("Result:", html.length, "bytes")) .catch((err) => console.error("Error while rendering route", err));

On peut aussi itérer sur les routes de notre application pour générer les pages une par une :

prerender.ts
for (const route of routes) { // On ignore les routes sont des wildcards (**) ou qui n'ont pas de path. if (!route.path || route.path === "**") { continue; } render(route.path) .then((html) => console.log(`Result for ${route.path}:`, html.length, "bytes")) .catch((err) => console.error(`Error while rendering route "${route.path}"`, err)); }

Lancer le build et le pré-rendu

npm run build npm run build:ssr npm run prerender

Conclusion

Et voilà, on a notre application Angular qui est prête à être pré-rendue !

Il vous rester à décider quoi faire de ces pages pré-rendues: les stocker en cache, les servir directement, les mettre dans un CDN, etc.

Jusqu'ici, on s'est contenté de les afficher dans la console, mais on peut aussi les écrire dans des fichiers :

prerender.ts
import { promises as fs } from "fs"; render("/") .then((html) => fs.writeFile(join(browserFolder, "prerendered.html"), html)) .catch((err) => console.error(`Error while rendering route "/"`, err));