Skip to content
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ take up to 60 seconds once the docker build finishes.
### Recommended

- [Docker Commands](docs/docker.md).
- [Architecture](docs/arch.md).
- [ESLint Strategy](docs/eslint.md).
- [Git Conventions](docs/git-convention.md).
- [i18n](docs/i18n.md).
- [Models conventions](docs/models.md).
- [NGXS Conventions](docs/ngxs.md).
- [SSR](docs/ssr.md).
- [SSR metrics](docs/ssr-metrics.md).
- [Testing Strategy](docs/testing.md).
- [Sentry error filtering](docs/sentry.md).

Expand Down Expand Up @@ -60,6 +64,6 @@ Install Volta from [volta](https://volta.sh/) and it will automatically pin Node

## Configuration

OSF uses an `assets/config/config.json` file for any 3rd-party tokens. This file is not committed to the repo.
OSF uses `src/assets/config/config.json` for third-party tokens and environment URLs. This file is not committed to the repo.

There is a `assets/config/template.json` file that can be copied to `assets/config/config.json` to store any 3rd-party tokens locally.
Copy `src/assets/config/template.json` to `src/assets/config/config.json` for local development. At runtime the app loads it from `/assets/config/config.json`.
6 changes: 6 additions & 0 deletions docs/arch.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ See [NGXS State Management](./ngxs.md).

---

## SSR

Server-side rendering, route render modes, config, and bot traffic are documented in [SSR](./ssr.md). Render metrics are in [SSR metrics](./ssr-metrics.md).

---

## 🚀 Dynamic File Generation (Schematics)

Use Angular CLI for scaffolding:
Expand Down
146 changes: 146 additions & 0 deletions docs/ssr-metrics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# SSR metrics

## Index

- [Overview](#overview)
- [What we track](#what-we-track)
- [When a metric is sent](#when-a-metric-is-sent)
- [When HTML is inspected](#when-html-is-inspected)
- [is_complete rules](#is_complete-rules)
- [content_type](#content_type)
- [Payload and API](#payload-and-api)
- [Related docs](#related-docs)

---

## Overview

After the SSR server sends HTML to a crawler, it POSTs a JSON:API metric to the OSF API. Collection runs in the background and does not delay the response.

Implementation: `src/server/ssr-metrics.middleware.ts`, `src/server/ssr-metrics.ts`, `src/server/ssr-html-metrics.ts`.

In production, the SSR Node process receives **bot traffic only**. Metrics are emitted for HTML navigations on that server.

---

## What we track

| Goal | Field | Notes |
| ------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Render success rate | `status` | Derive success as HTTP 2xx. Failures use `0` (no Angular response) or `500` (render exception). |
| Render speed | `ttfb` | Milliseconds from request start until Angular returns a response. Server-side render time, not browser network TTFB. |
| Page completeness | `is_complete` | Checked from rendered HTML. See [is_complete rules](#is_complete-rules). |
| Content type | `content_type` | API resource type from `osf:type` meta (`nodes`, `registrations`, `preprints`, `files`, `users`). Null when not set or not inspected. |
| Bot vs other | `is_bot` | From User-Agent regex in middleware. |
| Crawler identity | `user_agent` | Truncated to 512 characters. |
| Page | `url` | Full public URL (`webUrl` + path). **Query string is stripped** (no `view_only` or other params). |

We do **not** track Search Console index status. “Pages by content type” in SSR means **successful bot responses grouped by `content_type`**, not confirmed Google indexing.

---

## When a metric is sent

Sent for **HTML navigations** handled by the metrics middleware.

**Skipped** (no metric):

- `/assets/*`, `/static/*`
- `/.well-known/*`
- Static file extensions (`.js`, `.css`, `.ico`, `.json`, images, fonts, …)

**Included**:

- Successful renders (any HTTP status Angular returns)
- Missing Angular response → `status: 0`
- Render exception → `status: 500`

---

## When HTML is inspected

HTML is read only when:

- User-Agent matches the bot regex (`SEARCH_BOT` in middleware)
- HTTP status is **200**

If not inspected, `is_complete` stays `false` and `content_type` stays `null` even when the page rendered successfully.

On a bot-only SSR host, every 200 could be inspected; today the code also requires a bot regex match.

Inspection runs **after** the response is sent. It clones the response body only when inspection will run.

---

## is_complete rules

Implemented in `inspectSsrHtml` (`src/server/ssr-html-metrics.ts`).

All must pass:

1. **SSR marker** — `<osf-root>` has `ng-server-context` (page was server-rendered, not a bare CSR shell).
2. **Non-empty root** — inner HTML of `<osf-root>` is not empty after whitespace is removed.
3. **Meta tags or allowlisted path** — either:
- HTML contains `osf-dynamic-meta`, or
- path is on the meta-optional allowlist (search, discover, terms, user, institutions, meetings, collections, provider landing pages, etc.)

---

## content_type

Read from rendered HTML:

```html
<meta name="osf:type" content="nodes" />
```

Set by `MetaTagsService` (`osfType`). Project, registration, preprint, and file pages go through `MetaTagsBuilderService`. Profile pages set `osfType: users` only.

**Populated for:** projects, registrations, preprints, files, users (`/user/:id` and `/profile`).

**Often null for:** institutions, meetings, collections, search, discover, and any page without `osf:type`. That is expected if those types are out of scope for the metric.

---

## Payload and API

**Endpoint**

```
POST {apiDomainUrl}/_/metrics/events/ssr_metrics/
```

**Headers**

- `Accept: application/vnd.api+json;version=2.20`
- `Content-Type: application/vnd.api+json`
- `X-Throttle-Token: {THROTTLE_TOKEN}` when env is set

**Example body**

```json
{
"data": {
"attributes": {
"url": "https://osf.io/abc12/overview",
"ttfb": 842,
"is_bot": true,
"is_complete": true,
"content_type": "nodes",
"status": 200,
"user_agent": "Mozilla/5.0 (compatible; Googlebot/2.1; ...)"
}
}
}
```

If `apiDomainUrl` is missing, the POST is skipped silently.

Failed POSTs are logged with `console.error` in the middleware catch block.

---

## Related docs

- [SSR overview](./ssr.md) — architecture, routes, config
- [Architecture](./arch.md) — file layout
169 changes: 169 additions & 0 deletions docs/ssr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# SSR (Server-Side Rendering)

## Index

- [Overview](#overview)
- [Production traffic](#production-traffic)
- [Request flow](#request-flow)
- [Key files](#key-files)
- [Render modes](#render-modes)
- [Configuration](#configuration)
- [SEO-related app behavior](#seo-related-app-behavior)
- [Local development](#local-development)
- [Adding or changing SSR routes](#adding-or-changing-ssr-routes)
- [Related docs](#related-docs)

---

## Overview

OSF uses Angular SSR so search-engine crawlers receive fully rendered HTML for public pages (projects, registrations, preprints, discover pages, and similar).

The SSR stack has two layers:

1. **Angular SSR** — renders the app on the server (`app.config.server.ts`, `app.routes.server.ts`).
2. **Express server** — serves static assets, delegates HTML to Angular, and sends render metrics after the response (`src/server.ts`, `src/server/*`).

In production, **only bot traffic** is routed to the SSR build. Human traffic uses the client-only build. See [Production traffic](#production-traffic).

---

## Production traffic

```
Crawler → cloud (bot detection) → SSR Node server → HTML + metrics
Browser → cloud → static/CSR build → SPA shell + client render
```

---

## Request flow

1. Request hits Express (`src/server.ts`).
2. Static files (`/assets`, hashed JS/CSS, etc.) are served from `browser/` when possible.
3. HTML navigations go through `createSsrMetricsMiddleware` (`src/server/ssr-metrics.middleware.ts`).
4. Angular SSR renders the page (`AngularNodeAppEngine.handle`).
5. HTML is sent to the client immediately.
6. In the background (`setImmediate`):
- for bot + HTTP 200: HTML is inspected for completeness and `content_type`
- a metric payload is POSTed to the OSF API

Bot timing is not blocked by steps 6. See [SSR metrics](./ssr-metrics.md).

---

## Key files

| File | Role |
| ---------------------------------------------- | ------------------------------------------------------ |
| `src/server.ts` | Express entry, static files, wires metrics middleware |
| `src/main.server.ts` | Angular server bootstrap |
| `src/app/app.config.server.ts` | Server providers: routes, config, i18n loader |
| `src/app/app.routes.server.ts` | Per-route render mode (Server / Client / Prerender) |
| `src/server/ssr-metrics.middleware.ts` | Render timing, HTML inspection trigger, metric queue |
| `src/server/ssr-html-metrics.ts` | `is_complete` and `content_type` checks |
| `src/server/ssr-metrics.ts` | POST metric payload to API |
| `src/server/ssr-server-config.ts` | Load `config.json` + env for metrics |
| `src/server/static-cache-headers.ts` | Cache headers for static assets |
| `src/app/shared/services/meta-tags.service.ts` | Dynamic SEO meta tags (`osf:type`, `osf-dynamic-meta`) |

---

## Render modes

Defined in `src/app/app.routes.server.ts`:

| Mode | When to use | Bot receives |
| ------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------- |
| **Server** | Public pages that should be indexed (project overview, preprint detail, discover, …) | Full SSR HTML |
| **Client** | Auth, forms, dashboards, moderation, editors | CSR shell; metrics may report `is_complete: false` |
| **Prerender** | Static legal/help pages (`terms-of-use`, `privacy-policy`, …) | Pre-built HTML at build time |

**Rules of thumb**

- Public read-only detail or listing page → **Server**
- Login, submit, edit, settings, “my …” pages → **Client**
- Fixed copy that never changes at runtime → **Prerender**

More specific routes must appear **before** broader patterns (e.g. `preprints/:providerId/:id` before `preprints/:providerId`). Unmatched paths fall through to `**` → **Client**.

---

## Configuration

### `assets/config/config.json`

Copied from `assets/config/template.json` for local dev. Deployed with the build. Used for URLs and third-party keys.

Relevant to SSR / metrics:

| Field | Used by |
| -------------- | ------------------------------------------------------------------- |
| `apiDomainUrl` | SSR API calls (via `OSFConfigService`) and metrics POST target host |
| `webUrl` | Canonical URLs in meta tags; full URL in metric payloads |

### Environment variables (SSR Node process)

| Variable | Used by |
| ---------------- | ----------------------------------------------------------------------- |
| `THROTTLE_TOKEN` | `X-Throttle-Token` on SSR API calls (auth interceptor) and metrics POST |
| `API_DOMAIN_URL` | Fallback if `apiDomainUrl` missing from `config.json` |
| `WEB_URL` | Fallback if `webUrl` missing from `config.json` |
| `PORT` | Express listen port (default `4000`) |

**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`.

### Angular SSR config loading

On the server, `OSFConfigService` does not HTTP-fetch `config.json`. It uses `SSR_CONFIG`, populated at startup from disk in `app.config.server.ts`.

Translations on SSR are loaded from `browser/assets/i18n/en.json` (cached at module load). Browser builds use the HTTP loader as usual.

---

## SEO-related app behavior

### Meta tags

`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`.

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.

---

## Local development

| Command | What it does |
| ----------------------- | --------------------------------------------- |
| `npm start` | CSR dev server (port 4200) |
| `npm run start:ssr` | Dev server with SSR (`dev-ssr` configuration) |
| `npm run build:ssr` | Production SSR build → `dist/osf/` |
| `npm run serve:ssr:osf` | Run built Express server (port 4000) |

Typical local SSR test:

```bash
npm run build:ssr
npm run serve:ssr:osf
```

Set `THROTTLE_TOKEN` in the shell if SSR API calls should bypass throttling locally.

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.

---

## Adding or changing SSR routes

1. Add the route in `app.routes.ts` (browser routes).
2. Add a matching entry in `app.routes.server.ts` with the correct `RenderMode`.
3. If the page should contribute SEO meta, ensure the feature calls `MetaTagsService.updateMetaTags` and sets `osfType` where applicable.
4. For public indexable pages, prefer **Server**. Do not SSR authenticated workflows unless there is a specific SEO need.

---

## Related docs

- [SSR metrics](./ssr-metrics.md) — payload, `is_complete`, dashboards
- [Architecture](./arch.md) — folder layout
- [Docker](./docker.md) — container workflows
Loading
Loading