|
| 1 | +# SSR (Server-Side Rendering) |
| 2 | + |
| 3 | +## Index |
| 4 | + |
| 5 | +- [Overview](#overview) |
| 6 | +- [Production traffic](#production-traffic) |
| 7 | +- [Request flow](#request-flow) |
| 8 | +- [Key files](#key-files) |
| 9 | +- [Render modes](#render-modes) |
| 10 | +- [Configuration](#configuration) |
| 11 | +- [SEO-related app behavior](#seo-related-app-behavior) |
| 12 | +- [Local development](#local-development) |
| 13 | +- [Adding or changing SSR routes](#adding-or-changing-ssr-routes) |
| 14 | +- [Related docs](#related-docs) |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Overview |
| 19 | + |
| 20 | +OSF uses Angular SSR so search-engine crawlers receive fully rendered HTML for public pages (projects, registrations, preprints, discover pages, and similar). |
| 21 | + |
| 22 | +The SSR stack has two layers: |
| 23 | + |
| 24 | +1. **Angular SSR** — renders the app on the server (`app.config.server.ts`, `app.routes.server.ts`). |
| 25 | +2. **Express server** — serves static assets, delegates HTML to Angular, and sends render metrics after the response (`src/server.ts`, `src/server/*`). |
| 26 | + |
| 27 | +In production, **only bot traffic** is routed to the SSR build. Human traffic uses the client-only build. See [Production traffic](#production-traffic). |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## Production traffic |
| 32 | + |
| 33 | +``` |
| 34 | +Crawler → cloud (bot detection) → SSR Node server → HTML + metrics |
| 35 | +Browser → cloud → static/CSR build → SPA shell + client render |
| 36 | +``` |
| 37 | + |
| 38 | +--- |
| 39 | + |
| 40 | +## Request flow |
| 41 | + |
| 42 | +1. Request hits Express (`src/server.ts`). |
| 43 | +2. Static files (`/assets`, hashed JS/CSS, etc.) are served from `browser/` when possible. |
| 44 | +3. HTML navigations go through `createSsrMetricsMiddleware` (`src/server/ssr-metrics.middleware.ts`). |
| 45 | +4. Angular SSR renders the page (`AngularNodeAppEngine.handle`). |
| 46 | +5. HTML is sent to the client immediately. |
| 47 | +6. In the background (`setImmediate`): |
| 48 | + - for bot + HTTP 200: HTML is inspected for completeness and `content_type` |
| 49 | + - a metric payload is POSTed to the OSF API |
| 50 | + |
| 51 | +Bot timing is not blocked by steps 6. See [SSR metrics](./ssr-metrics.md). |
| 52 | + |
| 53 | +--- |
| 54 | + |
| 55 | +## Key files |
| 56 | + |
| 57 | +| File | Role | |
| 58 | +| ---------------------------------------------- | ------------------------------------------------------ | |
| 59 | +| `src/server.ts` | Express entry, static files, wires metrics middleware | |
| 60 | +| `src/main.server.ts` | Angular server bootstrap | |
| 61 | +| `src/app/app.config.server.ts` | Server providers: routes, config, i18n loader | |
| 62 | +| `src/app/app.routes.server.ts` | Per-route render mode (Server / Client / Prerender) | |
| 63 | +| `src/server/ssr-metrics.middleware.ts` | Render timing, HTML inspection trigger, metric queue | |
| 64 | +| `src/server/ssr-html-metrics.ts` | `is_complete` and `content_type` checks | |
| 65 | +| `src/server/ssr-metrics.ts` | POST metric payload to API | |
| 66 | +| `src/server/ssr-server-config.ts` | Load `config.json` + env for metrics | |
| 67 | +| `src/server/static-cache-headers.ts` | Cache headers for static assets | |
| 68 | +| `src/app/shared/services/meta-tags.service.ts` | Dynamic SEO meta tags (`osf:type`, `osf-dynamic-meta`) | |
| 69 | + |
| 70 | +--- |
| 71 | + |
| 72 | +## Render modes |
| 73 | + |
| 74 | +Defined in `src/app/app.routes.server.ts`: |
| 75 | + |
| 76 | +| Mode | When to use | Bot receives | |
| 77 | +| ------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------- | |
| 78 | +| **Server** | Public pages that should be indexed (project overview, preprint detail, discover, …) | Full SSR HTML | |
| 79 | +| **Client** | Auth, forms, dashboards, moderation, editors | CSR shell; metrics may report `is_complete: false` | |
| 80 | +| **Prerender** | Static legal/help pages (`terms-of-use`, `privacy-policy`, …) | Pre-built HTML at build time | |
| 81 | + |
| 82 | +**Rules of thumb** |
| 83 | + |
| 84 | +- Public read-only detail or listing page → **Server** |
| 85 | +- Login, submit, edit, settings, “my …” pages → **Client** |
| 86 | +- Fixed copy that never changes at runtime → **Prerender** |
| 87 | + |
| 88 | +More specific routes must appear **before** broader patterns (e.g. `preprints/:providerId/:id` before `preprints/:providerId`). Unmatched paths fall through to `**` → **Client**. |
| 89 | + |
| 90 | +--- |
| 91 | + |
| 92 | +## Configuration |
| 93 | + |
| 94 | +### `assets/config/config.json` |
| 95 | + |
| 96 | +Copied from `assets/config/template.json` for local dev. Deployed with the build. Used for URLs and third-party keys. |
| 97 | + |
| 98 | +Relevant to SSR / metrics: |
| 99 | + |
| 100 | +| Field | Used by | |
| 101 | +| -------------- | ------------------------------------------------------------------- | |
| 102 | +| `apiDomainUrl` | SSR API calls (via `OSFConfigService`) and metrics POST target host | |
| 103 | +| `webUrl` | Canonical URLs in meta tags; full URL in metric payloads | |
| 104 | + |
| 105 | +### Environment variables (SSR Node process) |
| 106 | + |
| 107 | +| Variable | Used by | |
| 108 | +| ---------------- | ----------------------------------------------------------------------- | |
| 109 | +| `THROTTLE_TOKEN` | `X-Throttle-Token` on SSR API calls (auth interceptor) and metrics POST | |
| 110 | +| `API_DOMAIN_URL` | Fallback if `apiDomainUrl` missing from `config.json` | |
| 111 | +| `WEB_URL` | Fallback if `webUrl` missing from `config.json` | |
| 112 | +| `PORT` | Express listen port (default `4000`) | |
| 113 | + |
| 114 | +**Throttle token:** SSR page-render API calls read `THROTTLE_TOKEN` from the process environment only (`app.config.server.ts`). It is not taken from `config.json` for auth. Metrics use the same env var via `loadSsrServerConfig`. |
| 115 | + |
| 116 | +### Angular SSR config loading |
| 117 | + |
| 118 | +On the server, `OSFConfigService` does not HTTP-fetch `config.json`. It uses `SSR_CONFIG`, populated at startup from disk in `app.config.server.ts`. |
| 119 | + |
| 120 | +Translations on SSR are loaded from `browser/assets/i18n/en.json` (cached at module load). Browser builds use the HTTP loader as usual. |
| 121 | + |
| 122 | +--- |
| 123 | + |
| 124 | +## SEO-related app behavior |
| 125 | + |
| 126 | +### Meta tags |
| 127 | + |
| 128 | +`MetaTagsService` writes dynamic tags with class `osf-dynamic-meta`. Pages also emit `osf:type` (API type: `nodes`, `registrations`, `preprints`, `files`, `users`). Metrics read `osf:type` from the rendered HTML for `content_type`. |
| 129 | + |
| 130 | +Built in `MetaTagsBuilderService` for project, registration, preprint, and file pages. Profile pages call `updateMetaTags` with only `osfType: users` and `mergeDefaults: false`, so they emit a single `osf:type` tag. |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## Local development |
| 135 | + |
| 136 | +| Command | What it does | |
| 137 | +| ----------------------- | --------------------------------------------- | |
| 138 | +| `npm start` | CSR dev server (port 4200) | |
| 139 | +| `npm run start:ssr` | Dev server with SSR (`dev-ssr` configuration) | |
| 140 | +| `npm run build:ssr` | Production SSR build → `dist/osf/` | |
| 141 | +| `npm run serve:ssr:osf` | Run built Express server (port 4000) | |
| 142 | + |
| 143 | +Typical local SSR test: |
| 144 | + |
| 145 | +```bash |
| 146 | +npm run build:ssr |
| 147 | +npm run serve:ssr:osf |
| 148 | +``` |
| 149 | + |
| 150 | +Set `THROTTLE_TOKEN` in the shell if SSR API calls should bypass throttling locally. |
| 151 | + |
| 152 | +Docker `start:docker` runs the **development** configuration (CSR), not the SSR server. Use `build:ssr` + `serve:ssr:osf` or the Dockerfile `ssr` stage for SSR. |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## Adding or changing SSR routes |
| 157 | + |
| 158 | +1. Add the route in `app.routes.ts` (browser routes). |
| 159 | +2. Add a matching entry in `app.routes.server.ts` with the correct `RenderMode`. |
| 160 | +3. If the page should contribute SEO meta, ensure the feature calls `MetaTagsService.updateMetaTags` and sets `osfType` where applicable. |
| 161 | +4. For public indexable pages, prefer **Server**. Do not SSR authenticated workflows unless there is a specific SEO need. |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## Related docs |
| 166 | + |
| 167 | +- [SSR metrics](./ssr-metrics.md) — payload, `is_complete`, dashboards |
| 168 | +- [Architecture](./arch.md) — folder layout |
| 169 | +- [Docker](./docker.md) — container workflows |
0 commit comments