Skip to content

VitePress Video & Image Management

This is a behind-the-scenes story of CmdWise website documentation system, documenting how we automatically upload images and videos from local assets/ to R2, then replace relative paths in documentation with public domain URLs.

✨ Why Do This?

  1. Repository Size Keeps Growing
    VitePress documentation includes many screenshots and demo videos. Long-term accumulation makes both Git repository and Cloudflare Pages deployment packages bloated.

  2. Deployment Has File Count & Size Limits
    Pages free tier has limits on object count and total size. Uploads will fail when exceeding quota.

  3. Want to Fully Utilize R2's Low-Cost Storage + CDN
    Being in the same Cloudflare ecosystem, domain and cache configuration naturally fit together.

🛠️ Solution Overview

Key Points:

  • Original Path & Filename: Cloud uses docs/assets/<local-subdirectory>/<filename>, no hash renaming. Directory structure completely matches repository, beneficial for SEO / debugging.
  • Incremental Upload: Script calculates sha1 for each file and stores in .asset-map.json. Only calls R2 API when file content changes or is new, others directly skip.
  • Content Hash Only for Comparison: Although filename doesn't change, still use hash to detect upload necessity.
  • Low Intrusion: Authors write relative paths as usual, build stage automatically replaces with CDN URLs.

Key Script

Path: packages/docs/scripts/upload-to-r2.js

bash
# View help
node scripts/upload-to-r2.js --help

# Dry run, print plan without execution
pnpm run assets:sync --dry

# Actually sync
pnpm run assets:sync

Script uses @aws-sdk/client-s3 to directly connect R2, compatible with S3 protocol.

🧰 Environment & Configuration

  1. Node ≥18, package manager pnpm / npm.
  2. Cloudflare R2 bucket: cmdwise-assets.
  3. Prepare .env in project packages/docs directory:
ini
R2_ACCOUNT_ID=xxxxxxxxxxxxxxxx
R2_ACCESS_KEY_ID=xxxxxxxxxxxx
R2_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
R2_BUCKET=cmdwise-assets
R2_PUBLIC_BASE_URL=https://assets.cmdwise.app    # Or use native URL first

Please don't commit .env to repository, ensure it's excluded in .gitignore.

🌐 CNAME Configuration Guide

  1. Cloudflare Dashboard → R2 › Custom DomainsAdd custom domain
  2. Enter assets.cmdwise.app, select bucket cmdwise-assets, confirm.
  3. System automatically adds a CNAME record in same account DNS:
    txt
    Type: CNAME
    Name: assets
    Content: <accountid>.r2.cloudflarestorage.com
    Proxy Status: Proxied
  4. Wait for DNS propagation (usually < 1 minute).
  5. Set R2_PUBLIC_BASE_URL to https://assets.cmdwise.app in .env / Secrets.

To switch domains, just change R2_PUBLIC_BASE_URL and re-run assets:sync. Script will batch replace URLs.

🔍 More Technical Details

TopicDescription
S3Client ConfigurationforcePathStyle: true to avoid virtual host style subdomain certificate mismatch causing TLS handshake failure
Runtime Parameters--dry only prints operations, --env .env.prod specifies additional environment file
Directory PrefixUnified upload to docs/assets/. Future website resources can use website/assets/ without affecting existing logic
Future PlansReserved hooks for image compression (WebP) / video transcoding (WebM), process buf before upload

Cloudflare Operations Checklist

  • [x] Obtain R2_ACCOUNT_ID. cf-account-id
  • [x] Create R2 bucket cmdwise-assets.
  • [x] Create access key for R2 bucket cmdwise-assets, obtain R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY. r2-create-access-key

✅ Local Verification Checklist

  • [x] pnpm i succeeds, no dependency errors.
  • [x] pnpm run assets:sync --dry outputs sync plan.
  • [x] pnpm run assets:sync generates/updates .asset-map.json.
  • [x] ./assets/… links in Markdown replaced with https://assets.cmdwise.app/….
  • [x] Run pnpm run dev, images/videos load normally on page.
  • [x] R2 Dashboard shows newly uploaded objects.

🚀 CI Integration Snippet (GitHub Actions)

yaml
- name: Sync docs assets to R2
  run: pnpm --filter @cmdwise/docs run assets:sync
  env:
    R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
    R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
    R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
    R2_BUCKET: cmdwise-assets
    R2_PUBLIC_BASE_URL: https://assets.cmdwise.app

Execute before vitepress build.

🩺 Typical Issue Troubleshooting

SymptomLog/ErrorInvestigation & Solution
EPROTO / sslv3 alert handshake failureCLI errorConfirm script has set forcePathStyle: true, check if endpoint domain matches certificate
Missing credentialsRuntime exitCheck if 4 R2 variables in .env or Secrets are filled correctly
Image 404Browser load failure1) Check if .asset-map.json contains URL 2) Verify R2 Dashboard object key is correct
Very slow uploadMassive HeadObject/PutObject callsUsually incremental invalidation, check if repository cleared .asset-map.json or changed path prefix
CDN not refreshedAfter renaming resource still shows old imageDue to same-name overwrite, Edge may cache; can add version number directory or use hash naming strategy

⚖️ Solution Comparison & Selection

DimensionCurrent Solution (Original Directory+Filename)Hash Filename SolutionWorkers Proxy Solution
SEO Friendly✅ Readable URLs❌ Unreadable✅ Preserves original paths
CDN CachingMedium, filename unchanged needs short cache or version directoriesExcellent, can cache long-termSame as current
Implementation ComplexityLowMedium (need to rewrite filename+Map)High (need to write Workers)
Local Author ExperienceOriginal paths workWrite relative paths but final URLs changeOriginal paths work
Incremental Upload ImplementationAlready supportedNeed to combine with MapStill need Map

Considering CmdWise documentation scale and maintainability, current solution is most balanced. If future large-scale image changes or higher cache hit requirements emerge, we can re-evaluate hash filename solution.