Docs Content Negotiation

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

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

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