I moved portfolio media to a separate asset host, assets.montek.dev, backed by Cloudflare R2. The website stores each asset URL in BaseHub and renders it in Next.js. Cloudflare provides the storage and delivery network; my work is the upload workflow, domain integration and website rendering.
Separating media from page content gives me stable URLs for images and recordings. I can edit an article without rebuilding its video file. This is an asset-delivery setup, not a CDN network that I implemented myself.
How an asset reaches the reader
Upload media → R2 object storage → assets.montek.dev
BaseHub record → Next.js page → browser requests the asset URLBaseHub stores the title, text, captions and asset references. The server reads those fields. The browser requests images and videos directly from their R2 URLs. Next.js is configured with images.unoptimized: true, so it does not route these requests through Vercel's image optimizer. The media bytes do not travel inside the article's rich-text JSON.
The project gallery uses ordinary image and video elements. Images load lazily. Muted preview videos use preload="none"; the reader can open the full-size media. These settings limit eager requests, but they do not prove a specific bandwidth saving.
Object names and URLs
images/showcase/k9/k9_2.jpeg
videos/showcase/k9/k9_3.mp4These are existing object paths used by the site. The directory structure makes it clear which project owns a file. A public object can be read by anyone who knows its URL. Private drafts, keys and raw personal recordings should not be uploaded to this public asset namespace.
For a revised file, use a new name or a content hash. Replacing bytes at the same URL can leave readers with an older cached copy. The media manifest maps verified assets to their R2 URLs and lists any explicitly generated responsive variants. getMediaImageProps emits those variants as a native srcset; it does not generate WebP or AVIF at request time. Images without listed variants use the original asset URL. Cache lifetimes and cache hits depend on the asset host's HTTP headers and Cloudflare rules.
A minimal upload workflow
R2 offers an S3-compatible API. This command is a template for uploading one reviewed public file. Use a bucket-scoped credential in your normal local credential configuration. Replace the endpoint and bucket with your own values.
aws s3 cp ./demo.mp4 s3://YOUR_BUCKET/videos/project/demo-v1.mp4 \
--endpoint-url "$R2_ENDPOINT" \
--content-type video/mp4After an upload, inspect the public URL before adding it to BaseHub. Check the content type, file size and playback. For a recording, play from the beginning, seek into the middle, and play the final seconds. An HTTP 200 response verifies reachability only.
Custom-domain delivery
Cloudflare supports attaching an R2 bucket to a custom domain. Its managed r2.dev URL is intended for development. A custom domain enables Cloudflare cache features, but caching still depends on the configured rules and file type. I do not assume that every request is a cache hit.
Cloudflare: public buckets and custom domains
Cloudflare: S3 API compatibility
What the current site demonstrates
The current CMS points to images and recordings on assets.montek.dev. The website handles image galleries, native video playback and media inside articles. That establishes the integration: stored asset, CMS reference and rendered page.
I have not published a controlled comparison of latency, cache-hit rate, geographic performance or total delivery cost. Those would require a defined workload and measurements. The useful result here is a media workflow that the portfolio already uses.