# UNDRR Asset Library

Versioned, shared web assets for UNDRR properties, served from `assets.undrr.org` and distributed via Cloudflare's CDN.

This repository (`shared-web-assets`) is the source of truth: commits here publish to the asset library.

## Overview

- Reusable assets are published at <https://assets.undrr.org/> for Drupal and non-Drupal consumers.
- Git repo: <https://gitlab.com/undrr/common/shared-web-assets/-/pipelines>
- Additional context is available in the ticket at <https://gitlab.com/undrr/web-backlog/-/issues/2115>
- URLs: <https://assets.undrr.org/>
  - Example: <https://assets.undrr.org/analytics/v1.0.0/google_analytics_enhancements.js>
  - On testing: <https://test.undrr-asset-library.pages.dev/analytics/v1.0.0/google_analytics_enhancements.js>

> `https://assets.undrr.org/testing/static/` is a compatibility alias on the
> production host. It serves production content and must not be used to verify
> a test deployment.

## Asset types

The following asset types are available:

- Logos
- Google Analytics tracker
- PreventionWeb widget embed code
- Fonts
- GHS Icons (<https://assets.undrr.org/images/GHS_icons/v1/ghs04.svg>)
- [Documentation](docs/) (writing guides for news, events, publications and blogs; Gutenberg content guide and markup samples)
- [llms.txt](llms.txt) -- machine-readable index of documentation and data for AI tools
- ... Additional assets to be added

## Version Structure

Assets are organized in versioned subfolders following semantic versioning; e.g:

`/analytics/v1.0.0/analytics.js`

Published version directories are immutable: add changes in a new version
directory and keep existing versions available and accessible.

## Infrastructure

The asset library has two layers:

- **CI/CD**: GitLab pipeline (`.gitlab-ci.yml`) builds artifacts from this repo and publishes them directly to Cloudflare Pages via `wrangler`
- **Cloudflare Pages**: hosts and serves the files, with Cloudflare's CDN/HTTP-3/caching in front

Previously a third layer existed — nginx on `tools.undrr.org` (UNOG-hosted) plus a separate Node.js auto-deployment app that pulled build artifacts from GitLab and wrote them to disk. Both are retired as of undrr/web-backlog#2849; this repo's pipeline pushes straight to Cloudflare Pages now.

### Deployment architecture

```
┌─────────────────────────────────────────────────────────────────────┐
│ 1. GitLab CI (this repo)                                            │
│    .gitlab-ci.yml → build-job assembles artifacts                   │
│                   → deploy-test-job runs `wrangler pages deploy`    │
│                   → deploy-production-job runs `wrangler pages      │
│                     deploy` (manual trigger, tags only)             │
│                                                                     │
│ 2. Cloudflare Pages (undrr-asset-library project)                   │
│    Production branch (main):  tags, manually triggered              │
│    Test branch:               MRs to main, main commits, tags       │
│    Cloudflare CDN caches responses in front of both                 │
└─────────────────────────────────────────────────────────────────────┘
```

#### Step by step

1. An MR targeting `main`, a push to `main` or a git tag triggers the [GitLab CI pipeline](https://gitlab.com/undrr/common/shared-web-assets/-/pipelines)
2. **build-job** (stage 1): installs Mangrove assets, generates sitemap and index files, rsyncs everything into a `build/` directory, validates the file count, and uploads `build/` as a CI artifact
3. **deploy-test-job** (stage 2): runs automatically — `npx wrangler pages deploy build/ --project-name=undrr-asset-library --branch=test`, authenticated via `CLOUDFLARE_API_TOKEN`
4. **deploy-production-job** (stage 2, tags only): requires a manual trigger in the GitLab UI — same command with `--branch=main`
5. Cloudflare Pages publishes each deploy and serves it, with the CDN caching in front

#### Deployment targets

- **MRs to `main` and commits on `main`** → auto-deploy to Cloudflare Pages `test` branch: <https://test.undrr-asset-library.pages.dev/>.
- **Git tags** → also auto-deploy to `test`, and unlock manual production deploy to the `main` branch via `deploy-production-job`.
- **Production verification**: use <https://assets.undrr.org/>. The project domain <https://undrr-asset-library.pages.dev/> always serves production and helps isolate Pages-vs-DNS issues.

Before creating a production tag, get peer review, verify the tag pipeline's test deploy, and confirm it matches your commit. Mangrove is fetched per build, so separately built artifacts can differ.

#### Path compatibility: the legacy `/static/` prefix

The canonical public URL is the naked path: `https://assets.undrr.org/<path>` (e.g. `/mangrove/1.5.0/components/MegaMenu.js`). That matches this repo's own directory structure, and it is what new links and documentation should use.

Historically the contract was `https://assets.undrr.org/static/<path>`, but `/static/` never existed in this repo or its build output. It came from the *old* auto-deployment app's target path (`.../assets.unisdr.org/testing/static`, see [`undrr/common/auto-deployment`](https://gitlab.com/undrr/common/auto-deployment)'s README) — the extraction destination added that prefix, not this repo's structure. That app no longer runs; Cloudflare Pages serves `build/` directly, at the naked path.

[`_redirects`](_redirects) keeps `/static/*` (and the older `/testing/static/*`) resolving as an internal rewrite: `/static/*  /:splat  200`. Status 200 means *rewrite* — serve this content, keep the requested URL — not a redirect, so it costs no extra request and returns no `Location` header. A large body of already-published links depends on it, so it must not be removed; it is simply no longer the prefix we propagate. Both forms apply to every branch's deployment (`main` and `test`) and have been verified against live Cloudflare Pages deployments.

#### Cross-origin headers

Assets here are consumed cross-origin by Drupal sites and other UNDRR properties, so the CORS/Private-Network-Access header set is required on every response, not optional (see [incident #2643](https://gitlab.com/undrr/web-backlog/-/issues/2643)). Previously served by the UNOG nginx vhost config; Cloudflare Pages has no equivalent vhost, so the [`_headers`](_headers) file at the repo root declares them instead — Cloudflare Pages reads this file from the deployed directory and applies it to every response:

- `Access-Control-Allow-Origin: *`
- `Cross-Origin-Resource-Policy: cross-origin`
- `Access-Control-Allow-Methods: GET, OPTIONS`
- `Access-Control-Allow-Headers: Content-Type`
- `Access-Control-Allow-Private-Network: true`

`Access-Control-Allow-Private-Network` (PNA) exists only because UNOG-internal users currently resolve `assets.undrr.org` to a private IP via split-horizon DNS, which triggers Chrome's PNA preflight. Once DNS cuts over to a Cloudflare Pages CNAME (a public network, not a private IP), the hope is PNA handling won't be needed for this domain at all — confirm once cutover has actually happened rather than assuming this header stays required indefinitely. OPTIONS on the `test` branch currently returns 405 (Cloudflare Pages doesn't run a PNA preflight handler for static assets), which this note captures rather than treating as an outstanding bug to fix immediately.

`tests/smoke/CorsHeadersTest.php` in the drupal-platform repo asserts this exact header set against `assets.undrr.org` for both GET and OPTIONS requests, and additionally expects OPTIONS to return HTTP 204. **Unverified:** a `_headers` file can only add response headers, not change the status code Cloudflare Pages' edge already returns for an OPTIONS request against a static asset — whether that's 204 by default hasn't been confirmed against a real Cloudflare Pages deployment yet. Check this once `deploy-test-job` has run, before assuming `testAssetsUndrrOrgOptionsLive` will pass unmodified.

#### Credentials and tokens

| Token | Where it lives | What it does | How to rotate |
|-------|---------------|--------------|---------------|
| `CLOUDFLARE_API_TOKEN` | [GitLab CI/CD variables](https://gitlab.com/undrr/common/shared-web-assets/-/settings/ci_cd) for this project | Authenticates `wrangler pages deploy`. Account-level, scoped to `Pages:Edit` — deliberately not a personal/expiring token, per incident #2740 below | Generate a new account-level token in the Cloudflare dashboard, update the CI/CD variable |
| `CLOUDFLARE_ACCOUNT_ID` | [GitLab CI/CD variables](https://gitlab.com/undrr/common/shared-web-assets/-/settings/ci_cd) for this project | Identifies which Cloudflare account `wrangler` deploys to | Rarely changes; check the Cloudflare dashboard URL if unsure |

### Deployment troubleshooting

If assets aren't appearing after a pipeline run, work through these in order:

1. **Is the pipeline green?** Check the [pipelines page](https://gitlab.com/undrr/common/shared-web-assets/-/pipelines). Open the deploy-test-job/deploy-production-job log — `wrangler` prints its own error output directly. Remember `deploy-production-job` only runs on tags and needs a manual trigger — a tag pipeline sitting "stuck" on it isn't a failure.
2. **Auth/permission errors from `wrangler`** — `CLOUDFLARE_API_TOKEN` is missing, expired, or no longer scoped to `Pages:Edit` on this account. Check [GitLab CI/CD settings](https://gitlab.com/undrr/common/shared-web-assets/-/settings/ci_cd) and the token's scope in the Cloudflare dashboard.
3. **Wrong project/account** — confirm `CLOUDFLARE_ACCOUNT_ID` matches the account the `undrr-asset-library` Pages project lives in.
4. **Pipeline is green but responses look stale** — check Cloudflare's cache; see Caching Strategy below.
5. **Pipeline is green but files are missing/wrong** — check the deployment directly in the [Cloudflare Pages dashboard](https://dash.cloudflare.com/0684bcc589f557a6f3ae5a5f31c060c8/pages/view/undrr-asset-library) for the deployment matching that pipeline's commit/tag.

For the incident that drove the account-level-token requirement, see [incident #2740](https://gitlab.com/undrr/web-backlog/-/issues/2740).

### Caching Strategy

The old setup used a path-based split (`/static/` vs `/testing/static/`) on a
single host. Cloudflare Pages instead separates production and testing by
branch and hostname:

- **Production Environment** (`https://assets.undrr.org/`): 2-week cache duration for optimal performance
    - https://dash.cloudflare.com/0684bcc589f557a6f3ae5a5f31c060c8/undrr.org/caching/cache-rules/4e99e4bd24d94bfca8ac507e81e13665
- **Testing Environment** (`https://test.undrr-asset-library.pages.dev/`): served directly by the Cloudflare Pages `test` branch; responses currently require revalidation (`Cache-Control: public, max-age=0, must-revalidate`)
- **Legacy compatibility alias** (`https://assets.undrr.org/testing/static/`): serves the production branch and is not a testing environment
- **Query String Handling**: Cache rules are configured to ignore query strings, ensuring consistent caching behavior

For detailed information about caching implementation and troubleshooting, see the [CDN caching rules ticket](https://gitlab.com/undrr/web-backlog/-/issues/2296).

## Local Testing

For local development and testing of library assets, a local server with hot refresh is provided that mimics the production environment with proper CORS headers.

### Setup

1. Install dependencies:

   ```bash
   npm install
   ```

2. Fetch the generated Mangrove assets used by the homepage, documentation
   viewer, sitemap and 404 page. Repeat when their pinned version changes:

   ```bash
   npm run deploy-mangrove
   ```

   `--latest` alone does not download the pinned versions these pages use.

3. Start the local server:

   ```bash
   npm run serve
   ```

   This will:

   - Start a server on `http://localhost:8000`
   - Enable CORS headers for cross-origin requests
   - **Automatically refresh the browser when files change (hot refresh)**
   - Automatically open your browser
   - Watch for changes in HTML, CSS, JS, and JSON files

### Testing Assets

Once the server is running, you can test assets locally by replacing the production URL with your local server URL:

**Production:**

```
https://assets.undrr.org/cookie-banner/v1/config.json
```

**Local Testing:**

```
http://localhost:8000/cookie-banner/v1/config.json
```

### Example Asset URLs for Testing

- **Cookie Banner Config**: `http://localhost:8000/cookie-banner/v1/config.json`
- **Cookie Banner Example**: `http://localhost:8000/cookie-banner/v1/example.html`
- **Cookie Banner CSS**: `http://localhost:8000/cookie-banner/v1/cookieconsent.css`
- **Cookie Banner JS**: `http://localhost:8000/cookie-banner/v1/cookieconsent.umd.js`
- **Analytics Script**: `http://localhost:8000/analytics/v1.0.0/google_analytics_enhancements.js`
- **Roboto Font**: `http://localhost:8000/fonts/roboto/v1.0.0/Regular/`
- **GHS Icons**: `http://localhost:8000/images/GHS_icons/v1/`

### Testing locally

Assets can be tested with the local server:

1. Start `npm run serve`
2. Open `http://localhost:8000/cookie-banner/v1/example.html`
3. Edit files and verify changes via hot refresh

### Development Server with Auto-Regeneration

For the ultimate development experience, use the enhanced development server that automatically regenerates documentation and sitemaps when files change:

```bash
# Start development server with auto-regeneration
npm run dev

# Start with verbose logging to see all operations
npm run dev -- --verbose
```

**What it does:**

- Starts live-server on `http://localhost:8000` with CORS and hot refresh
- Watches files for changes (excluding node_modules, .git, etc.)
- Regenerates `sitemap.html` on file add/change/remove
- Deploys `index.html` to directories with README.md
- Uses a 1-second debounce and respects `.gitignore`

## Documentation Web Interface

This repository includes a lightweight web interface for viewing documentation in any directory containing a README.md file.

### Automated creation of index.html files

The root `index.html` is a standalone landing page. Edit it directly to update the introductory content. Developer documentation stays in this README.

Use the included deployment script to copy `scripts/readme-template.html` to asset directories with README.md files. The homepage and app-owned pages are preserved:

```bash
# Update generated documentation pages in README directories
npm run deploy-index

# Preview what would be deployed (without making changes)
npm run deploy-index:preview
```

### Example Usage

After deployment, you can view documentation for any component:

- **Asset library homepage**: `http://localhost:8000/`
- **Developer documentation**: `README.md` in the repository
- **Cookie Banner docs**: `http://localhost:8000/cookie-banner/v1/`
- **Analytics docs**: `http://localhost:8000/analytics/v1.0.0/`
- **Font documentation**: `http://localhost:8000/fonts/roboto/v1.0.0/`
- **Other Markdown files beside a README**: `http://localhost:8000/docs/editorial-guides/?page=news.md`

The viewer renders another Markdown file in the same directory when given `?page=name.md` (lowercase letters, digits and hyphens only), and links between those files open in the viewer. The raw `.md` URLs keep working for tools that fetch Markdown directly.

### Deployment Script Details

The `scripts/deploy-index.js` script:

- **Recursively scans** all directories for README.md files
- **Copies `scripts/readme-template.html`** to documentation directories, updating generated indexes
- **Preserves** the root homepage and app-owned index files
- **Excludes** system directories (node_modules, .git, etc.)
- **Provides detailed logging** of all operations
- **Supports dry-run mode** for testing before deployment
- **Handles errors gracefully** with comprehensive error reporting

**Available options:**

- `--verbose` / `-v`: Show detailed logging
- `--dry-run` / `-d`: Preview changes without writing files
- `--help` / `-h`: Show help information

## Sitemap Generation

Generate `sitemap.html`, a table-of-contents style navigation of accessible project files.

### Usage

```bash
# Generate sitemap (asset URLs use the current page origin)
npm run generate-sitemap

# Include downloaded Mangrove versions, matching CI
npm run generate-sitemap -- --no-gitignore

# Preview sitemap without writing files
npm run generate-sitemap:preview
```

### Features

The sitemap generator creates an interactive table-of-contents:

- **Organized by directory structure** with collapsible sections
- **File type icons and badges** for easy identification
- **Live search functionality** to quickly find specific files
- **Clickable links** to all assets and documentation
- **File statistics** showing total files, directories, and documentation
- **Responsive design** that works on all devices
- **Respects .gitignore patterns** for proper file exclusions
- **Excludes hidden files** (starting with `.`) and .yml/.yaml files

### Output

The generated `sitemap.html` includes:

- **Beautiful visual interface** with modern styling
- **Directory-based organization** showing project structure
- **Interactive search box** for quick file discovery
- **File type categorization** with color-coded badges
- **Direct links** to all accessible files
- **Usage statistics** and generation metadata

### Script Details

The `scripts/generate-sitemap.js` script:

- **Parses .gitignore** to respect existing exclusion rules
- **Recursively scans** all directories and files
- **Excludes system directories** (node_modules, .git, etc.)
- **Provides detailed logging** of included/excluded files
- **Supports dry-run mode** for testing before generation
- **Handles errors gracefully** with comprehensive error reporting

**Available options:**

- `--no-gitignore`: Include generated assets such as downloaded Mangrove versions
- `--verbose` / `-v`: Show detailed logging of all file operations
- `--dry-run` / `-d`: Preview sitemap content without writing files
- `--help` / `-h`: Show help information

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for branch naming, asset conventions,
validation and the release workflow. MRs targeting `main`, commits to `main`
and tags deploy to testing automatically. Production requires a reviewed tag
and an explicit manual trigger of `deploy-production-job` in that tag's pipeline.

## Support

For issues or questions, please create a ticket in the [UNDRR Web Backlog](https://gitlab.com/undrr/web-backlog) project.
