Implicit, Angular livrează aplicația ca CSR (client-side rendering): serverul trimite un <app-root></app-root> gol, iar browserul trebuie să descarce, să parseze și să ruleze tot JavaScript-ul — apoi să mai ceară și datele — înainte să arate ceva util. SSR (randare pe server, la fiecare request) și SSG (prerender, la build) mută prima randare în altă parte: utilizatorul primește direct HTML cu conținut.
Cu @angular/ssr nu alegi o dată pentru toată aplicația, ci pe rută: fișierul app.routes.server.ts spune pentru fiecare path dacă e RenderMode.Prerender, RenderMode.Server sau RenderMode.Client. Asta se numește hybrid rendering. API-ul de rute de server a apărut în v19 și e stabil din v20.
- Randarea în browser (CSR), pe server (SSR) sau la build (prerender) se alege per rută, cu
RenderModeînapp.routes.server.ts. - Prerender pentru paginile care arată la fel pentru toți, SSR pentru cele personalizate sau cu date care se schimbă des, CSR pentru zonele de după login.
- SSR îmbunătățește prima afișare și ce vede un crawler, dar adaugă un server de întreținut și cod care trebuie să meargă și pe server.
- Rutele cu parametri se pot prerandarea dacă le dai lista de valori, cu
getPrerenderParams. - Cache-ul de la CDN contează adesea mai mult decât viteza serverului.
SSR nu e un comutator gratuit de performanță. Câștigi LCP, SEO și preview-uri corecte pe rețelele sociale; plătești cu un server Node de operat, cu cod care trebuie să ruleze și fără window și cu o fereastră în care pagina se vede, dar încă nu răspunde la click. Decizia bună se ia per rută, nu per proiect.
Trei moduri de randare
CSR · Client | SSR · Server | SSG · Prerender | |
|---|---|---|---|
| HTML-ul apare | în browser, după JS | pe server, la fiecare request | la build, o singură dată |
| Serverul, per request | servește un fișier static (shell-ul gol) | pornește aplicația, cere datele, randează | servește un fișier static |
| TTFB | mic | include randarea și API-urile chemate de ea | minim, mai ales de pe CDN |
| LCP | după JS + bootstrap + request API | imediat ce sosește HTML-ul | imediat ce sosește HTML-ul |
| SEO, preview-uri | slab: depinde de JS | excelent | excelent |
| Conținut per utilizator | da | da | nu — HTML identic pentru toți |
| Hosting | orice hosting static / CDN | runtime Node (sau alt runtime JS) | orice hosting static / CDN |
| Cost pe cerere | ≈ 0 | CPU + memorie pentru o randare completă | ≈ 0 |
Ce face concret serverul în modul Server: pentru fiecare request, @angular/ssr creează o platformă nouă, apelează funcția de bootstrap din main.server.ts, lasă routerul să navigheze la URL, așteaptă ca aplicația să devină stabilă (toate request-urile HTTP și PendingTasks terminate), serializează DOM-ul în HTML și distruge aplicația. Consecința practică: TTFB-ul unei pagini SSR e cel puțin cât cel mai lent API chemat în timpul randării.
Într-o aplicație hibridă reală, modurile conviețuiesc: homepage-ul și paginile de marketing sunt prerandate, căutarea e randată pe server, iar zona de cont rulează doar în browser.
Ce se întâmplă la un request
Cinci momente contează, în ordinea în care le trăiește utilizatorul:
- TTFB (time to first byte) — primul octet al răspunsului HTML.
- FCP (first contentful paint) — primul text sau prima imagine pe ecran.
- LCP (largest contentful paint) — apare elementul principal (titlul, imaginea produsului). E metrica pe care o simte utilizatorul ca „s-a încărcat”.
- Hidratarea — JS-ul a sosit, Angular preia HTML-ul existent și atașează listenerii.
- Interactiv — un click chiar face ceva. Din acest moment contează INP (cât de repede răspunde pagina).
La SSR și SSG, între LCP și „interactiv” există o fereastră în care pagina arată gata, dar butoanele nu au încă listener. Pe un telefon modest, cu un bundle de câteva sute de KB, fereastra are ușor 1–3 secunde. Rulează simularea și compară cele trei moduri pe aceeași rețea.
SSR: TTFB include munca serverului (care așteaptă API-ul), apoi conținutul apare imediat. Banda galbenă = conținut vizibil, dar fără listeneri până la hidratare.
Observă ce nu se schimbă: main.js are aceeași dimensiune în toate cele trei moduri. SSR nu micșorează JavaScript-ul, deci interactivitatea vine cam în același timp ca la CSR — uneori chiar puțin mai târziu, pentru că HTML-ul e mai mare și hidratarea costă și ea CPU. Câștigul real e LCP-ul. Ca să micșorezi fereastra neinteractivă: mai puțin JS inițial (rute lazy, @defer), hidratare incrementală și event replay, care nu pierde click-urile din fereastră.
Ce vede un crawler
Un crawler pornește de la HTML-ul primit (ce vezi cu View source, nu în tab-ul Elements). La CSR, acel HTML e identic pentru orice URL. Googlebot poate rula JavaScript, dar într-o etapă ulterioară și cu limite; bot-urile care generează preview-uri în chat-uri și rețele sociale citesc de regulă doar HTML-ul inițial. Mai e un detaliu ignorat des: status code-ul. Un server static nu știe că /produse/9999 nu există, așa că răspunde 200 — un „soft 404”.
HTTP/1.1 200 OK<!doctype html><html lang="ro"><head> <title>Magazin</title> <link rel="stylesheet" href="styles-5INURTSO.css"></head><body> <app-root></app-root> <script src="main-HG7KQWZB.js" type="module"></script></body></html>
- ✓Status HTTP corect200
- ✕<title> specific paginiiMagazin
- ✕meta description / og:*lipsă
- ✕Text vizibil în HTML0 cuvinte
- ✕Link-uri <a href> de urmat0
Același HTML pentru orice URL: tot conținutul apare abia după ce rulează JavaScript-ul. Bot-urile de preview nu-l rulează; Googlebot îl rulează, dar într-o etapă ulterioară și cu limite.
Title și meta tag-urile se setează cu serviciile Title și Meta din @angular/platform-browser. Ele lucrează prin DOCUMENT, deci funcționează și pe server: ce setezi în timpul randării ajunge în HTML-ul trimis. Pentru pagini fără date ajunge proprietatea title a rutei (sau un TitleStrategy propriu, dacă vrei un format comun).
import { Component, computed, effect, inject, input } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { Meta, Title } from '@angular/platform-browser';
interface Product { id: string; name: string; description: string; imageUrl: string }
@Component({
selector: 'app-product-page',
template: `
@if (product.hasValue()) {
<h1>{{ product.value().name }}</h1>
<p>{{ product.value().description }}</p>
}
`,
})
export default class ProductPage {
readonly id = input.required<string>(); // din ruta produse/:id (withComponentInputBinding)
private readonly title = inject(Title);
private readonly meta = inject(Meta);
protected readonly product = httpResource<Product>(() => `/api/products/${this.id()}`);
constructor() {
// Rulează și pe server: tag-urile ajung în HTML-ul trimis crawler-elor.
effect(() => {
if (!this.product.hasValue()) return;
const p = this.product.value();
this.title.setTitle(`${p.name} — Magazin`);
this.meta.updateTag({ name: 'description', content: p.description });
this.meta.updateTag({ property: 'og:title', content: p.name });
this.meta.updateTag({ property: 'og:description', content: p.description });
this.meta.updateTag({ property: 'og:image', content: p.imageUrl });
});
}
}Pune logica într-un serviciu SeoService.update({ title, description, image, canonical }) și apelează-l din paginile de detaliu. Pentru <link rel="canonical"> ai nevoie de DOCUMENT — detalii în Cod compatibil cu serverul.
Setup: ce generează ng add
# proiect nou, cu SSR + prerender configurate de la început
ng new magazin --ssr
# proiect existent: adaugă @angular/ssr, express și fișierele de server
ng add @angular/ssr
# build de producție → dist/magazin/browser + dist/magazin/server
ng build
# pornește serverul Node (script adăugat de ng add în package.json)
npm run serve:ssr:magazin # = node dist/magazin/server/server.mjsComanda adaugă dependențele (@angular/ssr, express), patru fișiere și câteva opțiuni în angular.json. Merită să știi ce face fiecare, pentru că pe ele le vei modifica:
server.ts— serverul Express. Servește fișierele statice dindist/…/browser, iar restul request-urilor le dă luiAngularNodeAppEngine.handle(), care întoarce unResponse(saunulldacă URL-ul nu e o rută Angular). ExportulreqHandlere folosit de CLI:ng serverandează și el pe server.main.server.ts— funcția de bootstrap apelată de server la fiecare randare.app.config.server.ts—provideServerRendering(withRoutes(serverRoutes)), combinat cu config-ul de browser prinmergeApplicationConfig.app.routes.server.ts— modul de randare pe rută. Implicit, totul ePrerender.- În
app.config.tsapareprovideClientHydration(), ca browserul să refolosească HTML-ul în loc să-l refacă.
import {
AngularNodeAppEngine,
createNodeRequestHandler,
isMainModule,
writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { join } from 'node:path';
const browserDistFolder = join(import.meta.dirname, '../browser');
const app = express();
const angularApp = new AngularNodeAppEngine(); // o singură instanță pentru tot serverul
// 1. (opțional) API-urile tale: app.get('/api/{*splat}', (req, res) => { ... });
// 2. Fișierele statice (JS/CSS/imagini au hash în nume → cache de un an)
app.use(express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false }));
// 3. Orice altceva: Angular decide — HTML prerandat, SSR sau shell-ul CSR
app.use((req, res, next) => {
angularApp
.handle(req)
.then((response) => (response ? writeResponseToNodeResponse(response, res) : next()))
.catch(next);
});
if (isMainModule(import.meta.url) || process.env['pm_id']) {
const port = process.env['PORT'] || 4000;
app.listen(port, (error) => {
if (error) throw error;
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// folosit de ng serve / ng build (și de unele platforme serverless)
export const reqHandler = createNodeRequestHandler(app);Detaliu din server.ts: express.static are index: false, deci nici măcar / nu e servit direct de pe disc. Orice pagină trece prin Angular, care aplică render mode-ul, headerele și status-ul configurate pe rută.
Pentru a preveni SSRF, serverul validează header-ul Host. Din v22, un host necunoscut primește 400 Bad Request (înainte, serverul cădea pe CSR). ng add pune "allowedHosts": []: completează domeniile tale în angular.json (sunt acceptate și wildcard-uri ca *.magazin.ro), în opțiunile AngularNodeAppEngine sau în variabila de mediu NG_ALLOWED_HOSTS. În spatele unui load balancer, headerele X-Forwarded-* sunt ignorate până nu le permiți explicit cu trustProxyHeaders. Detalii în Securitate.
Dacă ai un proiect mai vechi al cărui server.ts folosește CommonEngine: API-ul e deprecated din v22, înlocuitorul fiind AngularNodeAppEngine (Node) sau AngularAppEngine (runtime-uri bazate pe Web API Request/Response).
RenderMode pe rută
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
// Identic pentru toți, se schimbă doar la deploy → HTML generat la build (SSG)
{ path: '', renderMode: RenderMode.Prerender },
{ path: 'despre', renderMode: RenderMode.Prerender },
// Public, indexat, date care se schimbă des → randat la fiecare request (SSR)
{ path: 'cautare', renderMode: RenderMode.Server },
// În spatele login-ului: fără SEO, HTML personal → doar în browser (CSR)
{ path: 'cont/**', renderMode: RenderMode.Client },
// Plasa de siguranță pentru orice rută Angular nelistată mai sus
{ path: '**', renderMode: RenderMode.Server },
];Regulile care contează în practică:
- Potrivirea: un segment exact bate un parametru (
:id), care bate**. Deci**e plasa de siguranță, nu o regulă care „suprascrie”. - Două direcții de validare: fiecare rută din router trebuie să se potrivească cu o rută de server, iar fiecare rută de server (în afară de cele terminate în
/**) trebuie să existe în router. Altfel build-ul se oprește cu un mesaj explicit. - Path-ul nu începe cu „/” —
'/despre'e eroare la build. - Ce primește browserul:
Prerender→ fișierul generat la build (cuETag);Server→ HTML randat acum;Client→index.csr.html, shell-ul gol, servit tot de serverul tău.
- HTML-ul e identic pentru toți și stabil: îl generezi o dată, la build.
- Servit ca fișier static (ideal de pe CDN): TTFB minim, cost aproape zero per request.
- Datele se schimbă zilnic → ai nevoie de un build programat (ex. nightly în CI) care re-generează paginile.
- Mii de pagini adaugă minute la build; măsoară timpul și dimensiunea deploy-ului.
import { PrerenderFallback, RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: 'produse/:id',
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
const api = inject(CatalogApi); // sincron, înainte de await
const items = await api.mostVisited(500);
return items.map((x) => ({ id: String(x.id) }));
},
fallback: PrerenderFallback.Server,
},
// ...
];Headere, status și redirect-uri
Fiecare ServerRoute poate avea headers și status. Excepția: rutele Prerender acceptă headers, dar nu status — tipul ServerRoutePrerender îl exclude, pentru că fișierul e generat o singură dată. Headerele unei pagini prerandate se aplică doar când ea e servită prin serverul Node, nu de pe un hosting static.
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
// HTML public: CDN-ul îl poate ține 60 s și servi versiunea veche cât reîmprospătează
path: 'cautare',
renderMode: RenderMode.Server,
headers: { 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300' },
},
{
// redirect permanent: { path: 'oferte', redirectTo: 'promotii' } în app.routes.ts
path: 'oferte',
renderMode: RenderMode.Server,
status: 301, // fără status: 302
},
{
// prerender: headers da, status NU (tipul ServerRoutePrerender îl exclude)
path: 'despre',
renderMode: RenderMode.Prerender,
headers: { 'X-Frame-Options': 'DENY' },
},
{ path: '**', renderMode: RenderMode.Server },
];Un redirectTo din router devine pe server un redirect HTTP real (302 implicit; poți pune status: 301, 303, 307 sau 308), răspuns fără să mai randeze aplicația. La fel, dacă un guard redirecționează în timpul randării (vezi Guards & resolvers), URL-ul final diferă de cel cerut și serverul răspunde cu redirect. Cu outputMode: 'static', unde nu există server, redirect-urile sunt prerandate ca „soft redirects” cu <meta http-equiv="refresh"> în HTML.
Status și headere decise la runtime
Când status-ul depinde de date (produsul nu există), îl setezi din componentă prin RESPONSE_INIT. Pentru cookie-uri, limbă sau user-agent citești request-ul prin REQUEST. Ambele vin din @angular/core și sunt null în browser, la prerender și la build — tratează mereu cazul null.
import { Component, RESPONSE_INIT, inject } from '@angular/core';
@Component({
selector: 'app-not-found',
template: `<h1>Pagina nu există</h1>`,
})
export default class NotFound {
constructor() {
// RESPONSE_INIT e null în browser, la prerender și la build → verifică mereu.
const response = inject(RESPONSE_INIT);
if (response) response.status = 404; // crawler-ul vede 404, nu „200 cu text de eroare”
}
}Prerender cu parametri
Rutele fără parametri sunt descoperite și prerandate automat. Pentru produse/:id, Angular nu poate ghici ce ID-uri există, așa că îi dai o funcție getPrerenderParams. Ea rulează la build, într-un injection context (poți folosi inject()), și întoarce o listă de obiecte parametru → valoare. O rută Prerender cu parametri, dar fără getPrerenderParams, oprește build-ul.
import { inject } from '@angular/core';
import { PrerenderFallback, RenderMode, ServerRoute } from '@angular/ssr';
import { ProductApi } from './products/product-api';
export const serverRoutes: ServerRoute[] = [
{
path: 'produse/:id',
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
const api = inject(ProductApi); // ✓ inject() sincron, ÎNAINTE de orice await
const ids = await api.topProductIds(500); // rulează la BUILD, nu la request
return ids.map((id) => ({ id: String(id) })); // valorile trebuie să fie string-uri
},
// /produse/9999 nu a fost prerandat → ce facem la cerere?
fallback: PrerenderFallback.Server, // implicit: SSR la cerere
},
{
path: 'blog/:slug/**', // parametri + catch-all
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
return [{ slug: 'ssr', '**': 'partea/1' }]; // → /blog/ssr/partea/1
},
},
{ path: '**', renderMode: RenderMode.Server },
];Ce se întâmplă cu un URL care nu a fost generat la build depinde de fallback:
| PrerenderFallback | Pentru /produse/9999 (neprerandat) | Când îl alegi |
|---|---|---|
Server (implicit) | randat la cerere pe server | catalog mare: prerandezi top N, restul la cerere |
Client | primește shell-ul CSR, randat în browser | vrei SEO doar pentru paginile generate și nu vrei SSR |
None | Angular nu răspunde (request-ul merge mai departe, de obicei 404) | lista de la build e completă prin definiție |
inject() funcționează doar sincron, la începutul funcției. Un inject() scris după await nu mai e în injection context și aruncă eroare. Ia toate dependențele în primele rânduri, apoi fă request-urile.
Costul e la build: fiecare URL e o randare completă. Zece mii de produse înseamnă zece mii de randări la fiecare deploy și zece mii de fișiere de încărcat. Dacă datele se schimbă zilnic, ai nevoie și de un build programat care să le regenereze.
Deploy, cache și costuri
outputMode din angular.json decide ce produce build-ul. Cu "server" (ce pune ng add) primești dist/app/browser plus dist/app/server/server.mjs și ai nevoie de un proces Node. Cu "static" primești doar fișiere: pagini prerandate și shell-ul CSR, deployabile pe orice CDN. O rută RenderMode.Server în modul static e eroare la build.
{
"projects": {
"magazin": {
"architect": {
"build": {
"options": {
"outputMode": "static"
}
}
}
}
}
}Straturile de cache
| Ce | Cum | Atenție |
|---|---|---|
| JS, CSS, imagini cu hash | maxAge: '1y' în express.static / CDN | numele se schimbă la fiecare build, deci cache-ul lung e sigur |
| HTML prerandat | fișier static, ETag → 304 Not Modified | se schimbă doar la deploy |
| HTML SSR public | headers pe rută: public, s-maxage=60, stale-while-revalidate=300 | doar pentru conținut identic pentru toți |
| HTML SSR personal | private, no-store | niciodată public: CDN-ul l-ar servi altui utilizator |
| Date HTTP din randare | transfer cache: răspunsurile GET/HEAD ajung în HTML și sunt refolosite la hidratare | request-urile cu Authorization/Cookie sunt excluse implicit |
Transfer cache-ul e motivul pentru care, în timeline, SSR nu mai face request-ul la API din browser: datele aduse de HttpClient pe server sunt serializate în <script id="ng-state"> și citite la pornire. Opțiunile lui (withHttpTransferCacheOptions, withNoHttpTransferCache) sunt pe pagina Hidratare. Tot aici: pe server, cu backend-ul fetch implicit, fiecare răspuns HTTP e limitat la 1 MB (eroarea NG02825); limita se schimbă cu provideServerRendering({ maxResponseBodySize }, …).
Costul operațional
- CPU per request: fiecare randare SSR e un bootstrap complet. Fără cache la CDN, traficul de la bot-uri se plătește în servere.
- TTFB legat de backend: serverul așteaptă stabilitatea, deci un API lent îți încetinește toate paginile. Pune timeout-uri și mută datele secundare în @defer (pe server, un bloc fără trigger
hydraterandează doar placeholder-ul). - Memorie partajată între request-uri: codul e evaluat o singură dată per proces, deci variabilele de modul și provider-ii
useValuesunt comuni tuturor utilizatorilor. - Încă un sistem în producție: loguri, monitorizare, scalare, versiune de Node, cold start pe serverless.
import { InjectionToken } from '@angular/core';
// ✗ Stare la nivel de modul: pe server, modulul e evaluat O SINGURĂ DATĂ,
// deci variabila e comună tuturor request-urilor (și tuturor utilizatorilor).
let currentUser: string | null = null;
// ✗ useValue la nivel de aplicație: aceeași valoare până la restartul serverului.
export const RENDERED_AT = new InjectionToken<number>('RENDERED_AT');
const bad = { provide: RENDERED_AT, useValue: Date.now() };
// ✓ useFactory: rulează la fiecare randare (fiecare request are injectorul lui).
const good = { provide: RENDERED_AT, useFactory: () => Date.now() };
// ✓ Starea per utilizator stă în servicii (instanțiate per request), nu în variabile globale.Când DA, când NU
- Pagini publice care trebuie indexate și distribuite: produse, articole, landing pages.
Prerenderdacă le poți genera la build,Serverdacă nu. - LCP-ul se traduce în bani (e-commerce, media), iar o parte din utilizatori au telefoane și rețele modeste.
- Conținut stabil (documentație, blog, marketing):
Prerender+outputMode: 'static', fără niciun server de întreținut. - Conținut public cu date proaspete și trafic mare:
Server+ cache scurt la CDN. - Ai nevoie de status code-uri corecte (404, 301) pentru SEO.
- Aplicații în spatele login-ului (dashboard, admin, back-office):
Client. Investește în bundle mic, rute lazy și @defer. - Nu ai cine să opereze un server Node: rămâi la CSR sau la
PrerendercuoutputMode: 'static'. - „Aplicația e lentă după încărcare”: SSR nu ajută după hidratare. Problema e în JS sau în change detection → Performanță, Change detection.
- Rute construite pe API-uri de browser (hărți, canvas, editoare):
RenderMode.Clientpe ruta respectivă. - HTML personalizat cu trafic mare: nu randa per utilizator. Prerandează partea comună și încarcă bucățile personale în browser.
Capcane frecvente
Cod care atinge window, document sau localStorage în constructor, în ngOnInit sau la nivel de modul crapă pe server. Soluția: afterNextRender, token-ul DOCUMENT și implementări de serviciu separate pentru server și browser — toate pe pagina Cod compatibil cu serverul.
O variabilă let currentUser la nivel de modul sau un useValue calculat la pornire trăiesc cât procesul Node, nu cât request-ul. Pune starea în servicii (instanțiate per randare) și folosește useFactory pentru valori calculate per request.
- Cache public pe HTML personalizat → un utilizator vede pagina altuia. Pe rutele
Servercu date personale puneCache-Control: private, no-store. allowedHostsgol în producție → 400 pe toate paginile (v22). Configurează domeniile sauNG_ALLOWED_HOSTS.- „Soft 404”: componenta „not found” răspunde 200. Setează
status = 404prinRESPONSE_INIT. - Fallback SPA greșit pe hosting static: cu prerender,
index.htmle pagina rutei/. RuteleClienttrebuie să cadă peindex.csr.html. - Aplicația nu devine stabilă: un
PendingTaskfără cleanup (sau, cu Zone.js, unsetIntervalpornit în zona Angular) ține randarea pe loc, iar request-ul nu se mai termină. - CDN care minifică HTML-ul și șterge comentariile → hidratarea eșuează cu NG0507. Comentariile adăugate de Angular sunt necesare; dezactivează opțiunea pe CDN.
- „Am pus SSR și TTI a rămas la fel” — e normal: JS-ul e același. Măsoară LCP, nu TTI, și redu JS-ul inițial separat.
Fișă rapidă
// ng new app --ssr | ng add @angular/ssr | ng build | node dist/app/server/server.mjs
// app.config.server.ts
provideServerRendering(withRoutes(serverRoutes));
provideServerRendering({ maxResponseBodySize: 5 * 1024 * 1024 }, withRoutes(serverRoutes)); // implicit 1 MB
// app.config.ts (browser)
provideClientHydration();
// app.routes.server.ts — import { RenderMode, PrerenderFallback, ServerRoute } from '@angular/ssr'
export const serverRoutes: ServerRoute[] = [
{ path: 'despre', renderMode: RenderMode.Prerender }, // HTML la build (SSG)
{
path: 'produse/:id',
renderMode: RenderMode.Prerender,
getPrerenderParams: async () => [{ id: '1' }, { id: '2' }], // rulează la build
fallback: PrerenderFallback.Server, // | Client | None — implicit Server
},
{ path: 'cautare', renderMode: RenderMode.Server, headers: { 'Cache-Control': 'no-store' } },
{ path: 'cont/**', renderMode: RenderMode.Client }, // shell gol, randare în browser
{ path: '**', renderMode: RenderMode.Server }, // plasa de siguranță
];
// @angular/core — toate sunt null în browser, la prerender și la build
inject(REQUEST); // Request: headere, cookie-uri, URL
inject(RESPONSE_INIT); // ResponseInit: status / headere setate dinamic
inject(REQUEST_CONTEXT); // al doilea argument din angularApp.handle(req, context)
// angular.json → architect.build.options
// "outputMode": "server" | "static" static = fără Node: doar Prerender + Client
// "security": { "allowedHosts": ["exemplu.ro"] } host necunoscut → 400 Bad Request (v22)