Takazudo Modular Docs

Type to search...

to open search from anywhere

CDN Architecture

CDN Architecture

Technical reference for the CDN infrastructure. For daily image workflow, see Image Workflow.

Architecture

graph TB
    subgraph "Cloudflare"
        R2[R2 Bucket: zmodmedia<br/>pub-8bb5199db1d947eb8e50e01c018498c8.r2.dev]
    end

    subgraph "Netlify"
        N1[Main Site<br/>takazudomodular.com]
    end

    U[User Request] -->|/images/p/*| N1
    N1 -->|Proxy Redirect 200| R2

All product images are stored in a single R2 bucket and served through Netlify proxy redirects. The browser sees the main domain — R2 is invisible to users.

Components

Main Site (takazudomodular.com)

  • Build: Pre-built Astro site deployed from CI/CD
  • Content: Application code, HTML, CSS, JavaScript
  • Images: None (proxied to R2)
  • Configuration: netlify.toml and static/_redirects with proxy rules

Cloudflare R2 Bucket (zmodmedia)

  • Content: All processed product images
  • Formats: WebP (5 sizes), JPG (OGP, only for __og / __ogonly files), PNG (mercari), JSON (metadata)
  • Cache: Cache-Control headers set at upload time
  • Public URL: https://pub-8bb5199db1d947eb8e50e01c018498c8.r2.dev

Configuration Files

netlify.toml

Located at: /netlify.toml

# Production CDN Redirects - Cloudflare R2
[[redirects]]
  from = "/images/p/*"
  to = "https://pub-8bb5199db1d947eb8e50e01c018498c8.r2.dev/images/p/:splat"
  status = 200
  force = true

[[redirects]]
  from = "/static/images/p/*"
  to = "https://pub-8bb5199db1d947eb8e50e01c018498c8.r2.dev/images/p/:splat"
  status = 200
  force = true

[[redirects]]
  from = "/i/*"
  to = "https://pub-8bb5199db1d947eb8e50e01c018498c8.r2.dev/images/p/:splat"
  status = 200
  force = true
  • Redirects use status = 200 (proxy) so the browser sees the main domain
  • force = true ensures redirects override any local files
  • /i/* is a legacy short-path alias

static/_redirects

Located at: /static/_redirects

# Proxy all product images to Cloudflare R2
/images/p/*  https://pub-8bb5199db1d947eb8e50e01c018498c8.r2.dev/images/p/:splat  200!
/static/images/p/*  https://pub-8bb5199db1d947eb8e50e01c018498c8.r2.dev/images/p/:splat  200!

static/_headers (Cache Configuration)

Located at: /static/_headers

# 1 week caching for processed images
/images/p/*
  Cache-Control: public, max-age=604800

# 1 week caching for font files
/fonts/*
  Cache-Control: public, max-age=604800

# Standard caching for other static assets
/static/*
  Cache-Control: public, max-age=86400

# No caching for HTML pages
/*.html
  Cache-Control: no-cache, no-store, must-revalidate

URL Patterns

Request PathProxied ToContent Type
/images/p/oxi-one-mk2/600w.webpR2: images/p/oxi-one-mk2/600w.webpProduct image
/images/p/oxi-one-mk2/mercari.pngR2: images/p/oxi-one-mk2/mercari.pngMarketplace image
/images/p/oxi-one-mk2/ogp.jpgR2: images/p/oxi-one-mk2/ogp.jpgOGP image (only for __og / __ogonly files)
/images/p/oxi-one-mk2/metadata.jsonR2: images/p/oxi-one-mk2/metadata.jsonImage metadata

Caching Strategy

R2 Upload Headers

Set at upload time by scripts/upload-images-to-r2.mjs:

  • Images (webp, jpg, png, gif): public, max-age=31536000, immutable (1 year)
  • Metadata JSON: public, max-age=3600 (1 hour)

Netlify Cache Headers

Configured in static/_headers:

  • Images: public, max-age=604800 (1 week, Netlify edge)
  • HTML Pages: no-cache, no-store, must-revalidate

Deployment

Main Site

  1. Build Phase (GitHub Actions):

    pnpm build  # Builds site to dist/
  2. Deploy Phase (CI/CD):

    pnpm deploy:prod  # Uses scripts/deploy-production.sh
    netlify deploy --prod --no-build --dir=dist

The --no-build flag prevents Netlify from attempting to build, as we deploy pre-built files.

Image Deployment to R2

Image uploads are independent of main site deployments. The upload script uses incremental sync (MD5/ETag comparison), so re-running is safe and fast when nothing has changed.

Configuration Map

ConfigurationLocationPurpose
Netlify redirects/netlify.toml, /static/_redirectsR2 proxy rules
Cache headers/static/_headersBrowser caching policies
R2 credentials.env (local), GitHub Secrets (CI)S3-compatible access
Upload script/scripts/upload-images-to-r2.mjsPush images to R2
Download script/scripts/download-images-from-r2.mjsPull images from R2
Orphan check/scripts/check-r2-orphans.mjsDetect orphaned R2 images
Sync verifier/scripts/verify-r2-sync.mjsPre-push sync check
CDN URL config/lib/images/metadata-loader.tsRuntime URL generation

CI E2E Testing

In the production deploy pipeline, E2E tests run against the static build served by the serve package, which does not support Netlify redirects. The CI image interceptor (tests/e2e/helpers/ci-image-interceptor.js) handles this:

How It Works

  1. Playwright route interception: When CI=true, setupCiImageInterception() intercepts all **/images/p/** requests at the browser level
  2. Placeholder responses: Returns tiny 1x1 pixel placeholders (WebP, JPEG, PNG, GIF) based on URL extension. Extensionless URLs (e.g., /images/p/addac401-level) default to WebP
  3. Pass-through for non-images: Only metadata.json and other non-image files pass through to the server

Defense in Depth

The E2E spec files also skip /images/p/ URLs in their 404 checkers. This provides a safety net if any image request slips past the interceptor:

// In full-check-production.spec.js and full-check-production-ci.spec.js
!requestUrl.includes('/images/p/')

Image Availability Verification

Actual image availability on R2 is verified separately by pnpm r2:verify-sync (scripts/verify-r2-sync.mjs), not by E2E tests. E2E tests focus on page rendering and JavaScript errors.

Troubleshooting

Images Not Loading (404)

# Check if redirect is working
curl -IL https://takazudomodular.com/images/p/oxi-one-mk2/600w.webp

# Check R2 directly
curl -I https://pub-8bb5199db1d947eb8e50e01c018498c8.r2.dev/images/p/oxi-one-mk2/600w.webp

Verify: image exists in R2, redirect rules in netlify.toml are correct, R2 bucket public access is enabled.

Cache Not Working

# Check cache headers — should include:
# Cache-Control: public, max-age=31536000, immutable
curl -I https://takazudomodular.com/images/p/oxi-one-mk2/600w.webp

If missing, re-upload the image (pnpm r2:upload --slug <name>).

Local Dev Missing Images

# Download all from R2
pnpm r2:download

# Or process from source
pnpm convimgs && pnpm build:metadata

Benefits

MetricBefore CDNAfter CDNImprovement
Build Size4.93GB~500MB-90%
Deploy Success Rate~60%~99%+65%
Build Time12-15 min3-4 min-75%

Migration History

Phase 1: Dual Netlify CDN (Legacy)

The original CDN architecture used two separate Netlify sites:

  • cdn-media-takazudomodular.netlify.app - General product images
  • cdn-mercari-takazudomodular.netlify.app - Mercari marketplace images

Phase 2: Cloudflare R2 (Current)

Migrated to a single Cloudflare R2 bucket (zmodmedia) to:

  • Eliminate dual-CDN complexity
  • Remove egress fees
  • Leverage Cloudflare’s global edge network
  • Enable easy download for fresh clones

Status: Complete and operational

Phase 3: Media Branch Retirement (Complete)

R2 has been verified in production. The legacy media-branch infrastructure has been retired:

  1. media branch deleted (no longer needed for image hosting)
  2. media-deploy.yml GitHub Actions workflow removed
  3. ✅ LFS tracking rules removed from .gitattributes (file deleted in commit aadefc46b)
  4. cache-maintenance.yml removed
  5. ⚠️ Dual Netlify CDN site configurations: cleanup in repo Secrets / Netlify dashboard (manual)

References