[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6F2ZSD9WTHD3K3PEZ9QSSCD":3,"skill-same:01M6F2ZSD9WTHD3K3PEZ9QSSCD":68},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":40,"linkOnly":41,"markdownContent":42,"markdownBody":43,"frontMatter":44,"refName":45,"commitSha":46,"skillDirectory":47,"forksCount":48,"repositoryHtmlUrl":49,"repositoryHomepageUrl":50,"ownerHtmlUrl":51,"ownerBlogUrl":6,"ownerDescription":52,"updatedAt":53,"install":54,"resources":67},"01M6F2ZSD9WTHD3K3PEZ9QSSCD","angular-ssr",null,"Implement server-side rendering and hydration in Angular v20+ using @angular\u002Fssr. Use for SSR setup, hydration strategies, prerendering static pages, and handling browser-only APIs. Triggers on SSR configuration, fixing hydration mismatches, prerendering routes, or making code SSR-compatible.","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",66,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fanalogjs\u002Fangular-skills\u002Fangular-ssr\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39],"ssr","angular","angular ssr","implement","setup","code","browser","server","use","only","configuration","strategies","server-side","pages","triggers","rendering","2026-11-10 10:12:30",false,"---\nname: angular-ssr\ndescription: Implement server-side rendering and hydration in Angular v20+ using @angular\u002Fssr. Use for SSR setup, hydration strategies, prerendering static pages, and handling browser-only APIs. Triggers on SSR configuration, fixing hydration mismatches, prerendering routes, or making code SSR-compatible.\n---\n\n# Angular SSR\n\nImplement server-side rendering, hydration, and prerendering in Angular v20+.\n\n## Setup\n\n### Add SSR to Existing Project\n\n```bash\nng add @angular\u002Fssr\n```\n\nThis adds:\n- `@angular\u002Fssr` package\n- `server.ts` - Express server\n- `src\u002Fmain.server.ts` - Server bootstrap\n- `src\u002Fapp\u002Fapp.config.server.ts` - Server providers\n- Updates `angular.json` with SSR configuration\n\n### Project Structure\n\n```\nsrc\u002F\n├── app\u002F\n│   ├── app.config.ts          # Browser config\n│   ├── app.config.server.ts   # Server config\n│   └── app.routes.ts\n├── main.ts                     # Browser bootstrap\n├── main.server.ts              # Server bootstrap\nserver.ts                       # Express server\n```\n\n## Configuration\n\n### app.config.server.ts\n\n```typescript\nimport { ApplicationConfig, mergeApplicationConfig } from '@angular\u002Fcore';\nimport { provideServerRendering } from '@angular\u002Fplatform-server';\nimport { provideServerRoutesConfig } from '@angular\u002Fssr';\nimport { appConfig } from '.\u002Fapp.config';\nimport { serverRoutes } from '.\u002Fapp.routes.server';\n\nconst serverConfig: ApplicationConfig = {\n  providers: [\n    provideServerRendering(),\n    provideServerRoutesConfig(serverRoutes),\n  ],\n};\n\nexport const config = mergeApplicationConfig(appConfig, serverConfig);\n```\n\n### Server Routes Configuration\n\n```typescript\n\u002F\u002F app.routes.server.ts\nimport { RenderMode, ServerRoute } from '@angular\u002Fssr';\n\nexport const serverRoutes: ServerRoute[] = [\n  {\n    path: '',\n    renderMode: RenderMode.Prerender, \u002F\u002F Static at build time\n  },\n  {\n    path: 'products',\n    renderMode: RenderMode.Prerender,\n  },\n  {\n    path: 'products\u002F:id',\n    renderMode: RenderMode.Server, \u002F\u002F Dynamic SSR\n  },\n  {\n    path: 'dashboard',\n    renderMode: RenderMode.Client, \u002F\u002F Client-only (SPA)\n  },\n  {\n    path: '**',\n    renderMode: RenderMode.Server,\n  },\n];\n```\n\n### Render Modes\n\n| Mode | Description | Use Case |\n|------|-------------|----------|\n| `RenderMode.Prerender` | Static HTML at build time | Marketing pages, blogs |\n| `RenderMode.Server` | Dynamic SSR per request | User-specific content |\n| `RenderMode.Client` | Client-side only (SPA) | Authenticated dashboards |\n\n## Hydration\n\n### Default Hydration\n\nHydration is enabled by default with `provideClientHydration()`:\n\n```typescript\n\u002F\u002F app.config.ts\nimport { provideClientHydration } from '@angular\u002Fplatform-browser';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideClientHydration(),\n    \u002F\u002F ...\n  ],\n};\n```\n\n### Incremental Hydration\n\nDefer hydration of specific components:\n\n```typescript\n@Component({\n  template: `\n    \u003C!-- Hydrate when visible -->\n    @defer (hydrate on viewport) {\n      \u003Capp-comments [postId]=\"postId\" \u002F>\n    } @placeholder {\n      \u003Cdiv class=\"comments-placeholder\">Loading comments...\u003C\u002Fdiv>\n    }\n    \n    \u003C!-- Hydrate on interaction -->\n    @defer (hydrate on interaction) {\n      \u003Capp-interactive-chart [data]=\"chartData\" \u002F>\n    }\n    \n    \u003C!-- Hydrate on idle -->\n    @defer (hydrate on idle) {\n      \u003Capp-recommendations \u002F>\n    }\n    \n    \u003C!-- Never hydrate (static only) -->\n    @defer (hydrate never) {\n      \u003Capp-static-footer \u002F>\n    }\n  `,\n})\nexport class Post {\n  postId = input.required\u003Cstring>();\n  chartData = input.required\u003CChartData>();\n}\n```\n\n### Hydration Triggers\n\n| Trigger | Description |\n|---------|-------------|\n| `hydrate on viewport` | When element enters viewport |\n| `hydrate on interaction` | On click, focus, or input |\n| `hydrate on idle` | When browser is idle |\n| `hydrate on immediate` | Immediately after load |\n| `hydrate on timer(ms)` | After specified delay |\n| `hydrate when condition` | When expression is true |\n| `hydrate never` | Never hydrate (static) |\n\n### Event Replay\n\nCapture user events before hydration completes:\n\n```typescript\nimport { provideClientHydration, withEventReplay } from '@angular\u002Fplatform-browser';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideClientHydration(withEventReplay()),\n  ],\n};\n```\n\n## Browser-Only Code\n\n### Platform Detection\n\n```typescript\nimport { PLATFORM_ID, inject } from '@angular\u002Fcore';\nimport { isPlatformBrowser, isPlatformServer } from '@angular\u002Fcommon';\n\n@Component({...})\nexport class My {\n  private platformId = inject(PLATFORM_ID);\n  \n  ngOnInit() {\n    if (isPlatformBrowser(this.platformId)) {\n      \u002F\u002F Browser-only code\n      window.addEventListener('scroll', this.onScroll);\n    }\n  }\n}\n```\n\n### afterNextRender \u002F afterRender\n\nRun code only in browser after rendering:\n\n```typescript\nimport { afterNextRender, afterRender } from '@angular\u002Fcore';\n\n@Component({...})\nexport class Chart {\n  constructor() {\n    \u002F\u002F Runs once after first render (browser only)\n    afterNextRender(() => {\n      this.initChart();\n    });\n    \n    \u002F\u002F Runs after every render (browser only)\n    afterRender(() => {\n      this.updateChart();\n    });\n  }\n  \n  private initChart() {\n    \u002F\u002F Safe to use DOM APIs here\n    const canvas = document.getElementById('chart');\n    new Chart(canvas, this.config);\n  }\n}\n```\n\n### Inject Browser APIs Safely\n\n```typescript\n\u002F\u002F tokens.ts\nimport { InjectionToken, PLATFORM_ID, inject } from '@angular\u002Fcore';\nimport { isPlatformBrowser } from '@angular\u002Fcommon';\n\nexport const WINDOW = new InjectionToken\u003CWindow | null>('Window', {\n  providedIn: 'root',\n  factory: () => {\n    const platformId = inject(PLATFORM_ID);\n    return isPlatformBrowser(platformId) ? window : null;\n  },\n});\n\nexport const LOCAL_STORAGE = new InjectionToken\u003CStorage | null>('LocalStorage', {\n  providedIn: 'root',\n  factory: () => {\n    const platformId = inject(PLATFORM_ID);\n    return isPlatformBrowser(platformId) ? localStorage : null;\n  },\n});\n\n\u002F\u002F Usage\n@Injectable({ providedIn: 'root' })\nexport class Storage {\n  private storage = inject(LOCAL_STORAGE);\n  \n  get(key: string): string | null {\n    return this.storage?.getItem(key) ?? null;\n  }\n  \n  set(key: string, value: string): void {\n    this.storage?.setItem(key, value);\n  }\n}\n```\n\n## Prerendering\n\n### Static Routes\n\n```typescript\n\u002F\u002F app.routes.server.ts\nexport const serverRoutes: ServerRoute[] = [\n  { path: '', renderMode: RenderMode.Prerender },\n  { path: 'about', renderMode: RenderMode.Prerender },\n  { path: 'contact', renderMode: RenderMode.Prerender },\n  { path: 'blog', renderMode: RenderMode.Prerender },\n];\n```\n\n### Dynamic Routes with getPrerenderParams\n\n```typescript\n\u002F\u002F app.routes.server.ts\nimport { RenderMode, ServerRoute, PrerenderFallback } from '@angular\u002Fssr';\n\nexport const serverRoutes: ServerRoute[] = [\n  {\n    path: 'products\u002F:id',\n    renderMode: RenderMode.Prerender,\n    async getPrerenderParams() {\n      \u002F\u002F Fetch product IDs to prerender\n      const response = await fetch('https:\u002F\u002Fapi.example.com\u002Fproducts');\n      const products = await response.json();\n      return products.map((p: Product) => ({ id: p.id }));\n    },\n    fallback: PrerenderFallback.Server, \u002F\u002F SSR for non-prerendered\n  },\n  {\n    path: 'blog\u002F:slug',\n    renderMode: RenderMode.Prerender,\n    async getPrerenderParams() {\n      const posts = await fetchBlogPosts();\n      return posts.map(post => ({ slug: post.slug }));\n    },\n    fallback: PrerenderFallback.Client, \u002F\u002F SPA for non-prerendered\n  },\n];\n```\n\n### Prerender Fallback Options\n\n| Fallback | Description |\n|----------|-------------|\n| `PrerenderFallback.Server` | SSR for non-prerendered routes |\n| `PrerenderFallback.Client` | Client-side rendering |\n| `PrerenderFallback.None` | 404 for non-prerendered routes |\n\n## HTTP Caching\n\n### TransferState\n\nAutomatically transfer HTTP responses from server to client:\n\n```typescript\nimport { provideClientHydration, withHttpTransferCacheOptions } from '@angular\u002Fplatform-browser';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideClientHydration(\n      withHttpTransferCacheOptions({\n        includePostRequests: true,\n        includeRequestsWithAuthHeaders: false,\n        filter: (req) => !req.url.includes('\u002Fapi\u002Frealtime'),\n      })\n    ),\n  ],\n};\n```\n\n### Manual TransferState\n\n```typescript\nimport { TransferState, makeStateKey } from '@angular\u002Fcore';\n\nconst PRODUCTS_KEY = makeStateKey\u003CProduct[]>('products');\n\n@Injectable({ providedIn: 'root' })\nexport class Product {\n  private http = inject(HttpClient);\n  private transferState = inject(TransferState);\n  private platformId = inject(PLATFORM_ID);\n  \n  getProducts(): Observable\u003CProduct[]> {\n    \u002F\u002F Check if data was transferred from server\n    if (this.transferState.hasKey(PRODUCTS_KEY)) {\n      const products = this.transferState.get(PRODUCTS_KEY, []);\n      this.transferState.remove(PRODUCTS_KEY);\n      return of(products);\n    }\n    \n    return this.http.get\u003CProduct[]>('\u002Fapi\u002Fproducts').pipe(\n      tap(products => {\n        \u002F\u002F Store for transfer on server\n        if (isPlatformServer(this.platformId)) {\n          this.transferState.set(PRODUCTS_KEY, products);\n        }\n      })\n    );\n  }\n}\n```\n\n## Build and Deploy\n\n### Build Commands\n\n```bash\n# Build with SSR\nng build\n\n# Output structure\ndist\u002F\n├── my-app\u002F\n│   ├── browser\u002F      # Client assets\n│   └── server\u002F       # Server bundle\n```\n\n### Run SSR Server\n\n```bash\n# Development\nnpm run serve:ssr:my-app\n\n# Production\nnode dist\u002Fmy-app\u002Fserver\u002Fserver.mjs\n```\n\n### Deploy to Node.js Host\n\n```javascript\n\u002F\u002F server.ts (generated)\nimport { APP_BASE_HREF } from '@angular\u002Fcommon';\nimport { CommonEngine } from '@angular\u002Fssr\u002Fnode';\nimport express from 'express';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport bootstrap from '.\u002Fsrc\u002Fmain.server';\n\nconst serverDistFolder = dirname(fileURLToPath(import.meta.url));\nconst browserDistFolder = resolve(serverDistFolder, '..\u002Fbrowser');\nconst indexHtml = join(serverDistFolder, 'index.server.html');\n\nconst app = express();\nconst commonEngine = new CommonEngine();\n\napp.get('*', express.static(browserDistFolder, { maxAge: '1y', index: false }));\n\napp.get('*', (req, res, next) => {\n  commonEngine\n    .render({\n      bootstrap,\n      documentFilePath: indexHtml,\n      url: req.originalUrl,\n      publicPath: browserDistFolder,\n      providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],\n    })\n    .then((html) => res.send(html))\n    .catch((err) => next(err));\n});\n\napp.listen(4000, () => {\n  console.log('Server listening on http:\u002F\u002Flocalhost:4000');\n});\n```\n\nFor advanced patterns, see [references\u002Fssr-patterns.md](references\u002Fssr-patterns.md).\n\n","# Angular SSR\n\nImplement server-side rendering, hydration, and prerendering in Angular v20+.\n\n## Setup\n\n### Add SSR to Existing Project\n\n```bash\nng add @angular\u002Fssr\n```\n\nThis adds:\n- `@angular\u002Fssr` package\n- `server.ts` - Express server\n- `src\u002Fmain.server.ts` - Server bootstrap\n- `src\u002Fapp\u002Fapp.config.server.ts` - Server providers\n- Updates `angular.json` with SSR configuration\n\n### Project Structure\n\n```\nsrc\u002F\n├── app\u002F\n│   ├── app.config.ts          # Browser config\n│   ├── app.config.server.ts   # Server config\n│   └── app.routes.ts\n├── main.ts                     # Browser bootstrap\n├── main.server.ts              # Server bootstrap\nserver.ts                       # Express server\n```\n\n## Configuration\n\n### app.config.server.ts\n\n```typescript\nimport { ApplicationConfig, mergeApplicationConfig } from '@angular\u002Fcore';\nimport { provideServerRendering } from '@angular\u002Fplatform-server';\nimport { provideServerRoutesConfig } from '@angular\u002Fssr';\nimport { appConfig } from '.\u002Fapp.config';\nimport { serverRoutes } from '.\u002Fapp.routes.server';\n\nconst serverConfig: ApplicationConfig = {\n  providers: [\n    provideServerRendering(),\n    provideServerRoutesConfig(serverRoutes),\n  ],\n};\n\nexport const config = mergeApplicationConfig(appConfig, serverConfig);\n```\n\n### Server Routes Configuration\n\n```typescript\n\u002F\u002F app.routes.server.ts\nimport { RenderMode, ServerRoute } from '@angular\u002Fssr';\n\nexport const serverRoutes: ServerRoute[] = [\n  {\n    path: '',\n    renderMode: RenderMode.Prerender, \u002F\u002F Static at build time\n  },\n  {\n    path: 'products',\n    renderMode: RenderMode.Prerender,\n  },\n  {\n    path: 'products\u002F:id',\n    renderMode: RenderMode.Server, \u002F\u002F Dynamic SSR\n  },\n  {\n    path: 'dashboard',\n    renderMode: RenderMode.Client, \u002F\u002F Client-only (SPA)\n  },\n  {\n    path: '**',\n    renderMode: RenderMode.Server,\n  },\n];\n```\n\n### Render Modes\n\n| Mode | Description | Use Case |\n|------|-------------|----------|\n| `RenderMode.Prerender` | Static HTML at build time | Marketing pages, blogs |\n| `RenderMode.Server` | Dynamic SSR per request | User-specific content |\n| `RenderMode.Client` | Client-side only (SPA) | Authenticated dashboards |\n\n## Hydration\n\n### Default Hydration\n\nHydration is enabled by default with `provideClientHydration()`:\n\n```typescript\n\u002F\u002F app.config.ts\nimport { provideClientHydration } from '@angular\u002Fplatform-browser';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideClientHydration(),\n    \u002F\u002F ...\n  ],\n};\n```\n\n### Incremental Hydration\n\nDefer hydration of specific components:\n\n```typescript\n@Component({\n  template: `\n    \u003C!-- Hydrate when visible -->\n    @defer (hydrate on viewport) {\n      \u003Capp-comments [postId]=\"postId\" \u002F>\n    } @placeholder {\n      \u003Cdiv class=\"comments-placeholder\">Loading comments...\u003C\u002Fdiv>\n    }\n    \n    \u003C!-- Hydrate on interaction -->\n    @defer (hydrate on interaction) {\n      \u003Capp-interactive-chart [data]=\"chartData\" \u002F>\n    }\n    \n    \u003C!-- Hydrate on idle -->\n    @defer (hydrate on idle) {\n      \u003Capp-recommendations \u002F>\n    }\n    \n    \u003C!-- Never hydrate (static only) -->\n    @defer (hydrate never) {\n      \u003Capp-static-footer \u002F>\n    }\n  `,\n})\nexport class Post {\n  postId = input.required\u003Cstring>();\n  chartData = input.required\u003CChartData>();\n}\n```\n\n### Hydration Triggers\n\n| Trigger | Description |\n|---------|-------------|\n| `hydrate on viewport` | When element enters viewport |\n| `hydrate on interaction` | On click, focus, or input |\n| `hydrate on idle` | When browser is idle |\n| `hydrate on immediate` | Immediately after load |\n| `hydrate on timer(ms)` | After specified delay |\n| `hydrate when condition` | When expression is true |\n| `hydrate never` | Never hydrate (static) |\n\n### Event Replay\n\nCapture user events before hydration completes:\n\n```typescript\nimport { provideClientHydration, withEventReplay } from '@angular\u002Fplatform-browser';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideClientHydration(withEventReplay()),\n  ],\n};\n```\n\n## Browser-Only Code\n\n### Platform Detection\n\n```typescript\nimport { PLATFORM_ID, inject } from '@angular\u002Fcore';\nimport { isPlatformBrowser, isPlatformServer } from '@angular\u002Fcommon';\n\n@Component({...})\nexport class My {\n  private platformId = inject(PLATFORM_ID);\n  \n  ngOnInit() {\n    if (isPlatformBrowser(this.platformId)) {\n      \u002F\u002F Browser-only code\n      window.addEventListener('scroll', this.onScroll);\n    }\n  }\n}\n```\n\n### afterNextRender \u002F afterRender\n\nRun code only in browser after rendering:\n\n```typescript\nimport { afterNextRender, afterRender } from '@angular\u002Fcore';\n\n@Component({...})\nexport class Chart {\n  constructor() {\n    \u002F\u002F Runs once after first render (browser only)\n    afterNextRender(() => {\n      this.initChart();\n    });\n    \n    \u002F\u002F Runs after every render (browser only)\n    afterRender(() => {\n      this.updateChart();\n    });\n  }\n  \n  private initChart() {\n    \u002F\u002F Safe to use DOM APIs here\n    const canvas = document.getElementById('chart');\n    new Chart(canvas, this.config);\n  }\n}\n```\n\n### Inject Browser APIs Safely\n\n```typescript\n\u002F\u002F tokens.ts\nimport { InjectionToken, PLATFORM_ID, inject } from '@angular\u002Fcore';\nimport { isPlatformBrowser } from '@angular\u002Fcommon';\n\nexport const WINDOW = new InjectionToken\u003CWindow | null>('Window', {\n  providedIn: 'root',\n  factory: () => {\n    const platformId = inject(PLATFORM_ID);\n    return isPlatformBrowser(platformId) ? window : null;\n  },\n});\n\nexport const LOCAL_STORAGE = new InjectionToken\u003CStorage | null>('LocalStorage', {\n  providedIn: 'root',\n  factory: () => {\n    const platformId = inject(PLATFORM_ID);\n    return isPlatformBrowser(platformId) ? localStorage : null;\n  },\n});\n\n\u002F\u002F Usage\n@Injectable({ providedIn: 'root' })\nexport class Storage {\n  private storage = inject(LOCAL_STORAGE);\n  \n  get(key: string): string | null {\n    return this.storage?.getItem(key) ?? null;\n  }\n  \n  set(key: string, value: string): void {\n    this.storage?.setItem(key, value);\n  }\n}\n```\n\n## Prerendering\n\n### Static Routes\n\n```typescript\n\u002F\u002F app.routes.server.ts\nexport const serverRoutes: ServerRoute[] = [\n  { path: '', renderMode: RenderMode.Prerender },\n  { path: 'about', renderMode: RenderMode.Prerender },\n  { path: 'contact', renderMode: RenderMode.Prerender },\n  { path: 'blog', renderMode: RenderMode.Prerender },\n];\n```\n\n### Dynamic Routes with getPrerenderParams\n\n```typescript\n\u002F\u002F app.routes.server.ts\nimport { RenderMode, ServerRoute, PrerenderFallback } from '@angular\u002Fssr';\n\nexport const serverRoutes: ServerRoute[] = [\n  {\n    path: 'products\u002F:id',\n    renderMode: RenderMode.Prerender,\n    async getPrerenderParams() {\n      \u002F\u002F Fetch product IDs to prerender\n      const response = await fetch('https:\u002F\u002Fapi.example.com\u002Fproducts');\n      const products = await response.json();\n      return products.map((p: Product) => ({ id: p.id }));\n    },\n    fallback: PrerenderFallback.Server, \u002F\u002F SSR for non-prerendered\n  },\n  {\n    path: 'blog\u002F:slug',\n    renderMode: RenderMode.Prerender,\n    async getPrerenderParams() {\n      const posts = await fetchBlogPosts();\n      return posts.map(post => ({ slug: post.slug }));\n    },\n    fallback: PrerenderFallback.Client, \u002F\u002F SPA for non-prerendered\n  },\n];\n```\n\n### Prerender Fallback Options\n\n| Fallback | Description |\n|----------|-------------|\n| `PrerenderFallback.Server` | SSR for non-prerendered routes |\n| `PrerenderFallback.Client` | Client-side rendering |\n| `PrerenderFallback.None` | 404 for non-prerendered routes |\n\n## HTTP Caching\n\n### TransferState\n\nAutomatically transfer HTTP responses from server to client:\n\n```typescript\nimport { provideClientHydration, withHttpTransferCacheOptions } from '@angular\u002Fplatform-browser';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideClientHydration(\n      withHttpTransferCacheOptions({\n        includePostRequests: true,\n        includeRequestsWithAuthHeaders: false,\n        filter: (req) => !req.url.includes('\u002Fapi\u002Frealtime'),\n      })\n    ),\n  ],\n};\n```\n\n### Manual TransferState\n\n```typescript\nimport { TransferState, makeStateKey } from '@angular\u002Fcore';\n\nconst PRODUCTS_KEY = makeStateKey\u003CProduct[]>('products');\n\n@Injectable({ providedIn: 'root' })\nexport class Product {\n  private http = inject(HttpClient);\n  private transferState = inject(TransferState);\n  private platformId = inject(PLATFORM_ID);\n  \n  getProducts(): Observable\u003CProduct[]> {\n    \u002F\u002F Check if data was transferred from server\n    if (this.transferState.hasKey(PRODUCTS_KEY)) {\n      const products = this.transferState.get(PRODUCTS_KEY, []);\n      this.transferState.remove(PRODUCTS_KEY);\n      return of(products);\n    }\n    \n    return this.http.get\u003CProduct[]>('\u002Fapi\u002Fproducts').pipe(\n      tap(products => {\n        \u002F\u002F Store for transfer on server\n        if (isPlatformServer(this.platformId)) {\n          this.transferState.set(PRODUCTS_KEY, products);\n        }\n      })\n    );\n  }\n}\n```\n\n## Build and Deploy\n\n### Build Commands\n\n```bash\n# Build with SSR\nng build\n\n# Output structure\ndist\u002F\n├── my-app\u002F\n│   ├── browser\u002F      # Client assets\n│   └── server\u002F       # Server bundle\n```\n\n### Run SSR Server\n\n```bash\n# Development\nnpm run serve:ssr:my-app\n\n# Production\nnode dist\u002Fmy-app\u002Fserver\u002Fserver.mjs\n```\n\n### Deploy to Node.js Host\n\n```javascript\n\u002F\u002F server.ts (generated)\nimport { APP_BASE_HREF } from '@angular\u002Fcommon';\nimport { CommonEngine } from '@angular\u002Fssr\u002Fnode';\nimport express from 'express';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport bootstrap from '.\u002Fsrc\u002Fmain.server';\n\nconst serverDistFolder = dirname(fileURLToPath(import.meta.url));\nconst browserDistFolder = resolve(serverDistFolder, '..\u002Fbrowser');\nconst indexHtml = join(serverDistFolder, 'index.server.html');\n\nconst app = express();\nconst commonEngine = new CommonEngine();\n\napp.get('*', express.static(browserDistFolder, { maxAge: '1y', index: false }));\n\napp.get('*', (req, res, next) => {\n  commonEngine\n    .render({\n      bootstrap,\n      documentFilePath: indexHtml,\n      url: req.originalUrl,\n      publicPath: browserDistFolder,\n      providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],\n    })\n    .then((html) => res.send(html))\n    .catch((err) => next(err));\n});\n\napp.listen(4000, () => {\n  console.log('Server listening on http:\u002F\u002Flocalhost:4000');\n});\n```\n\nFor advanced patterns, see [references\u002Fssr-patterns.md](references\u002Fssr-patterns.md).\n\n",{"name":5,"description":7},"main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Fanalogjs\u002Fangular-skills\u002Fangular-ssr",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":55,"prompt":65,"zipAvailable":66},[56,59,62],{"tool":57,"command":58},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill angular-ssr",{"tool":60,"command":61},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill angular-ssr",{"tool":63,"command":64},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill angular-ssr","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fanalogjs\u002Fangular-skills\u002Fangular-ssr\u002FSKILL.md",true,[],[69,83,93,102,108,115],{"skillNo":70,"name":71,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":72,"categories":73,"keywords":79,"publishedAt":40},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[74],{"categoryNo":75,"categoryName":76,"taxonomyType":77,"taxonomyTypeDesc":78},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[80,81,82],"model","model hierarchy","model hierarchy skill",{"skillNo":84,"name":85,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":86,"categories":87,"keywords":89,"publishedAt":40},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[88],{"categoryNo":75,"categoryName":76,"taxonomyType":77,"taxonomyTypeDesc":78},[90,91,92],"MCP","zoom","zoom mcp",{"skillNo":94,"name":95,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":98,"publishedAt":40},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[99,100,101,91],"sdk","zoom apps sdk","apps",{"skillNo":103,"name":104,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":105,"categories":106,"keywords":107,"publishedAt":40},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[104],{"skillNo":109,"name":110,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":111,"categories":112,"keywords":114,"publishedAt":40},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[113],{"categoryNo":75,"categoryName":76,"taxonomyType":77,"taxonomyTypeDesc":78},[110],{"skillNo":116,"name":117,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":120,"publishedAt":40},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[117]]