# UNDRR Asset Library

The UNDRR asset library: a versioned, shared collection of web assets for UNDRR web 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.

> **Mid-migration (undrr/web-backlog#2849):** this repo's CI now deploys straight to Cloudflare Pages instead of the old UNOG VM — the "Deployment architecture" section below is current and accurate. Everything else in this README, plus the generated `sitemap.html`/`index.html` and several project READMEs, still describes the pre-migration setup (e.g. `/testing/static/` paths, the Caching Strategy numbers below) and hasn't been swept yet — that cleanup is deliberately deferred to a follow-up ticket until this is live in PROD and UNOG is consuming it, to keep the migration itself low-risk. Don't treat anything outside "Deployment architecture" as current until then.

## Overview

- The asset library contains reusable web assets available at <https://assets.undrr.org/static/>. It supports both Drupal and non-Drupal consumers while improving performance and decoupling deployments.
- 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/static/>
  - Example: <https://assets.undrr.org/static/analytics/v1.0.0/google_analytics_enhancements.js>
  - On testing: <https://assets.undrr.org/testing/static/analytics/v1.0.0/google_analytics_enhancements.js>

## Asset types

The following asset types are available:

- Logos
- Google Analytics tracker
- PreventionWeb widget embed code
- Fonts
- GHS Icons (<https://assets.undrr.org/static/images/GHS_icons/v1/ghs04.svg>)
- [Documentation](docs/) (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`

All versions are to remain 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:               commits to main AND tags, automatic   │
│    Cloudflare CDN caches responses in front of both                 │
└─────────────────────────────────────────────────────────────────────┘
```

#### Step by step

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

- **Commits to `main`** → automatically deployed to Cloudflare Pages' `test` branch (<https://test.undrr-asset-library.pages.dev/>) — same intent as the old `DRUPAL_SHARED_WEB_ASSETS_TESTING` action. Named `test`, not `testing`, to match this platform's `test-{domain}` hostname convention.
- **Git tags** → automatically deployed to the `test` branch too (so a tagged build is verified on the test tier before going live), **and** made available for a manual production deploy to Cloudflare Pages' `main` branch, the project's designated production branch — same intent as the old `DRUPAL_SHARED_WEB_ASSETS_PRODUCTION` action. The manual gate is deliberate: nothing reaches the live asset library without someone explicitly triggering `deploy-production-job` in the GitLab UI.

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

The public URL contract is `https://assets.undrr.org/static/<path>` (e.g. `/static/mangrove/1.5.0/components/MegaMenu.js`) — but `/static/` doesn't exist anywhere 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. Cloudflare Pages serves `build/` directly with no such rewrite available at the extraction step, so [`_redirects`](_redirects) adds it back as an internal rewrite (`/static/* /:splat 200` — 200 means "serve this content", not a redirect; the requested URL stays `/static/...`). This applies to every branch's deployment (`main` and `test`), so both `assets.undrr.org/static/...` and `test.undrr-asset-library.pages.dev/static/...` resolve consistently. **Unverified against a real deployment** — confirm a `/static/...` path actually resolves once `deploy-test-job` has run, before relying on it.

#### 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

**TBC as part of undrr/web-backlog#2849 — this section describes the pre-migration UNOG/nginx setup and has not yet been re-verified against Cloudflare Pages.** The old setup used a *path*-based split (`/static/` vs `/testing/static/`) on a single host; Cloudflare Pages uses a *branch/domain*-based split instead (a custom domain on the production branch vs a `*.pages.dev`/preview URL for other branches) — the URL shape for testing assets is changing, not just the backend. Confirm the equivalent cache-rule configuration (and update any hardcoded `/testing/static/` references, in this repo or consumers) before relying on the numbers below:

- **Production Environment** (`https://assets.undrr.org/static/`): 2-week cache duration for optimal performance
    - https://dash.cloudflare.com/0684bcc589f557a6f3ae5a5f31c060c8/undrr.org/caching/cache-rules/4e99e4bd24d94bfca8ac507e81e13665
- **Testing Environment** (`https://assets.undrr.org/testing/static/`): 5-minute cache duration for rapid iteration during development
    - https://dash.cloudflare.com/0684bcc589f557a6f3ae5a5f31c060c8/undrr.org/caching/cache-rules/5bb70c3bc79442869a7a1602c10b30fe
- **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. 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/static/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 locally by using the simple html server:

1. Start the local server: `npm run serve`
2. Open `http://localhost:8000/cookie-banner/v1/example.html` in your browser
3. **Make changes to any files - the browser will automatically refresh!**
4. Or include the config in your own test page:

   ```html
   <script>
     fetch("http://localhost:8000/cookie-banner/v1/config.json")
       .then((response) => response.json())
       .then((config) => {
         // Use config for local testing
         console.log("Cookie banner config:", config);
       });
   </script>
   ```

### Hot Refresh Benefits

The live-server setup provides:

- **Instant feedback**: See changes immediately without manual browser refresh
- **Faster development**: No need to manually reload when editing HTML, CSS, JS, or JSON files
- **Better testing workflow**: Quickly iterate on cookie banner configurations, analytics scripts, or other assets

### 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 all files** for changes (excluding node_modules, .git, etc.)
- **Automatically regenerates sitemap.html** when files are added, modified, or removed
- **Automatically deploys index.html** to directories with README.md files
- **Debounced regeneration** (1-second delay) to avoid excessive rebuilds during rapid changes
- **Smart file exclusions** respects .gitignore patterns and excludes build artifacts

**Perfect for:**

- Adding new assets and seeing them immediately appear in the sitemap
- Creating new documentation and having it automatically deployed
- Reorganizing files and maintaining up-to-date navigation
- Iterating on projects with real-time documentation updates

**Example workflow:**

1. Run `npm run dev` to start the development server
2. Add a new asset file or README.md in any directory
3. Watch as the sitemap automatically updates and index.html files are deployed
4. Your browser refreshes to show the latest changes
5. No manual commands needed - everything stays in sync!

## 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

Use the included deployment script to automatically copy the documentation interface to all directories with README.md files:

```bash
# Deploy index.html to all README directories (overwrites existing files)
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:

- **Root documentation**: `http://localhost:8000/`
- **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/`

### Deployment Script Details

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

- **Recursively scans** all directories for README.md files
- **Copies index.html** to each directory (always overwrites existing 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

Automatically generate a `sitemap.html` file containing a beautiful table-of-contents style navigation of all accessible files in the project.

### Usage

```bash
# Generate sitemap with production URLs
npm run generate-sitemap

# Generate sitemap with local URLs for testing
npm run generate-sitemap:local

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

### Features

The sitemap generator creates a beautiful, 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` file 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:**

- `--local` / `-l`: Use local base URL (<http://localhost:8000>)
- `--verbose` / `-v`: Show detailed logging of all file operations
- `--dry-run` / `-d`: Preview sitemap content without writing files
- `--help` / `-h`: Show help information

## Contributing

1. Create a branch for your changes
2. Make updates following the established folder structure
3. Submit merge request
4. CI pipeline will handle deployment upon merge
   - All commits to `main` will be deployed to testing
   - Tags will be deployed to prod — candidate commit for production tag must be reviewed and approved by someone other than the code author before it is created
   - The pipeline validates build artifacts and the auto-deployment callback — if either fails, the pipeline goes red
   - View the deployment status at <https://gitlab.com/undrr/common/shared-web-assets/-/pipelines>

## Support

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