I write this site in Obsidian. Quartz 5 turns all of the markdown files into a website, and Cloudflare Pages deploys it online.

I used to serve full size images directly from Cloudflare Pages (which has a 25MB limit for static asset files). I ran into issues, however, trying to load all of the full resolution images on Raves at once.

That inspired me to build the open-source quartz-cloudflare-media helper to move photos and videos to Cloudflare R2 at build time, and serve the photos via Cloudflare Images, which resizes the images and caches them at the edge.

The pipeline

The Pages build runs install-plugins → media:sync → Quartz build → media:finalize → media:check → verify:site. It finds media used by pages I’m publishing, uploads anything new, builds the site, then checks the result.

flowchart LR
  A[Obsidian notes and media] --> B[quartz-cloudflare-media]
  B -->|site pages| C[Cloudflare Pages]
  B -->|original files| D[Cloudflare R2]
  C --> E[Your browser]
  E -->|photos| F[Cloudflare Images]
  F -->|original photo| D
  E -->|videos| D

1. Find the media I’m publishing

The helper only scans notes Quartz is set to publish. Draft and ignored notes stay out of the published site, so their attachments don’t get uploaded by accident. If it can’t find a file, or more than one file matches, it stops and throws an error.

2. Keep the originals in R2

Each file gets a content hash. If that exact file is already in R2, it won’t be uploaded again. If I change the file, and it gets a new URL. Note paths and the local manifest stay on my local device.

3. Serve the right thing to the browser

Once Quartz has sorted out the Obsidian embeds, the helper replaces local media links with Cloudflare URLs. Cloudflare Images makes smaller photo versions for different screens and formats. Videos come straight from R2.

4. Check the finished build

After the build, the helper removes extra media copies and checks that each media link points where it should. If something’s off, the build stops here.

Why this site uses R2 directly

I already had an R2 bucket and a media domain, so I kept using that setup here. A new install from the open source repo defaults to a Cloudflare Worker in front of a private R2 bucket. The package sets it up for you, which is a bit easier than configuring the bucket and domain yourself.

Privacy and cleanup

Only media used by published notes gets uploaded. Drafts and note paths stay local. Old files aren’t deleted automatically from R2; prune --dry-run shows me what could be removed so I can review it first.