# Docs Content Negotiation

LLMS index: [llms.txt](/llms.txt) | Full content: [llms-full.txt](/llms-full.txt)

---

CloudFront distribution with HTTP content negotiation for the Hummingbird
public documentation site (`hummingbird-project.io`). When a client sends
`Accept: text/markdown`, a CloudFront Function rewrites the request URI to the
pre-built `.md` equivalent so GitLab Pages serves Hugo's markdown output
instead of HTML.

## Overview

Hugo generates `.md` files alongside `.html` for every page. GitLab Pages is a
static file server — it cannot inspect `Accept` headers and select a response
type. This stack interposes a CloudFront distribution between DNS and GitLab
Pages, adding the header-based routing that GitLab Pages cannot provide.

The stack is deployed by the infrastructure repository. After each deploy,
Route53 A and AAAA alias records for `hummingbird-project.io` are updated to
point at the CloudFront distribution.

## Architecture

```mermaid
flowchart LR
    client["Client\n(browser / agent)"]
    dns["Route53\nhummingbird-project.io"]
    cf["CloudFront\nDistribution"]
    fn["CloudFront Function\n(viewer-request)"]
    pages["GitLab Pages\nredhat.gitlab.io"]

    client -->|"HTTPS"| dns
    dns -->|"A/AAAA alias"| cf
    cf --> fn
    fn -->|"rewrite URI if\nAccept: text/markdown"| cf
    cf -->|"forwards viewer Host header"| pages
```

### Content Negotiation Logic

The `cloudfront-js-2.0` viewer-request function runs at every edge location
before the cache is consulted:

1. If `Accept` does not include `text/markdown` → pass through unchanged (HTML
   response served as before).
2. Rewrite only known HTML-like patterns:
   - `/docs/` (trailing slash) → `/docs/index.md`
   - `/docs/page.html` → `/docs/page.md`
   - `/docs/page` (no extension) → `/docs/page/index.md`
3. Everything else (`.css`, `.js`, `.png`, `.md`, `.txt`, etc.) → pass through
   unchanged.

Because the rewrite happens before the cache key is computed (viewer-request
event), HTML and markdown responses land in separate cache entries. No custom
cache policy is needed — the standard `CachingOptimized` managed policy is
used.

### Origin

The origin is `redhat.gitlab.io` (the GitLab Pages default domain). GitLab
Pages routes requests based on the `Host` header. The `AllViewer` origin
request policy forwards the viewer's `Host: hummingbird-project.io` header
to the origin, so the correct project is served regardless of what DNS
`hummingbird-project.io` resolves to.

## Components

| Resource                     | Type                    | Description                                               |
|------------------------------|-------------------------|-----------------------------------------------------------|
| `ContentNegotiationFunction` | CloudFront Function     | viewer-request; rewrites URI for `Accept: text/markdown`  |
| `DocsCertificate`            | ACM Certificate         | TLS for the custom domain; DNS-validated via Route53      |
| `DocsDistribution`           | CloudFront Distribution | CDN proxy in front of GitLab Pages                        |

## Parameters

| Parameter        | Description                                                                  |
|------------------|------------------------------------------------------------------------------|
| `ResourcePrefix` | Prefix for resource names (e.g., `arr-hummingbird-prod-docs-cdn`)            |
| `DocsDomainName` | Custom domain for the documentation site (e.g., `hummingbird-project.io`)    |
| `HostedZoneId`   | Route53 hosted zone ID — used for ACM DNS validation CNAME auto-creation     |

## Outputs

| Output                   | Description                                       |
|--------------------------|---------------------------------------------------|
| `DistributionDomainName` | CloudFront domain name                            |
| `DistributionId`         | CloudFront distribution ID                        |

## Deployment

Deployed by the infrastructure repository. Stack name and region are
configured in `samconfig.toml.j2` in the infrastructure repo.

On the first deploy, CloudFormation requests an ACM certificate and
auto-creates the DNS validation CNAME in Route53 (via `HostedZoneId`). Once
validated (~2–5 minutes), the distribution is created and Route53 alias records
are updated to point at it.

## Verification

```bash
# Content negotiation: should return text/markdown
curl -H "Accept: text/markdown" https://hummingbird-project.io/docs/ -sI

# Normal browsing: should return text/html (unchanged)
curl https://hummingbird-project.io/docs/ -sI

# Static assets are excluded from rewriting
curl -H "Accept: text/markdown" https://hummingbird-project.io/favicon.ico -sI

# Alternate domains still redirect to primary (unchanged)
curl https://project-hummingbird.io/ -sI   # expect 308
curl https://hummingbird-project.org/ -sI  # expect 308

# DNS resolves to CloudFront, not the GitLab Pages IP
dig hummingbird-project.io A +short
```

## Scope

Only the primary domain (`hummingbird-project.io`) is proxied through
CloudFront. The alternate domains (`project-hummingbird.io`,
`hummingbird-project.org`) continue pointing directly to GitLab Pages, which
issues 308 redirects to the primary domain.
