Cloudflare R2 Setup

Copy Markdown View Source

StaticBlog publishes the built site to a Cloudflare R2 bucket using AWS-compatible S3 API calls signed with Signature V4.

Creating an R2 bucket

  1. Log in to the Cloudflare dashboard.
  2. Navigate to R2 Object Storage in the sidebar.
  3. Click Create bucket.
  4. Choose a bucket name (e.g. content).
  5. Select a location hint if desired (R2 automatically distributes globally).

Creating API credentials

  1. In the R2 section, go to Manage R2 API Tokens.
  2. Click Create API Token.
  3. Give the token a descriptive name (e.g. blog-publish).
  4. Under Permissions, select Object Read & Write.
  5. Optionally scope the token to a specific bucket.
  6. Click Create API Token.
  7. Copy the Access Key ID and Secret Access Key. These are shown only once.

Your Account ID is visible at the top of the R2 dashboard or in the Cloudflare dashboard URL.

Configuration

Add the R2 config to your config/config.exs:

config :static_blog, :r2,
  bucket: "content",
  prefix: "blog",
  region: "auto"
  • :bucket -- the R2 bucket name.
  • :prefix -- a key prefix for all uploaded files. This lets multiple sites share a single bucket (e.g. blog/index.html, docs/index.html).
  • :region -- always "auto" for R2.

Environment variables

Set three environment variables before publishing:

export R2_ACCOUNT_ID=your-cloudflare-account-id
export R2_ACCESS_KEY_ID=your-r2-access-key
export R2_SECRET_ACCESS_KEY=your-r2-secret-key

Custom env var names

If you need different env var names (e.g. to avoid conflicts with other tools), configure them:

config :static_blog, :r2_env_vars, %{
  account_id: "MY_R2_ACCOUNT",
  access_key_id: "MY_R2_KEY",
  secret_access_key: "MY_R2_SECRET"
}

Publishing

mix blog.publish

This builds the site and syncs it to R2. The publisher:

  1. Walks the local _site/ tree and computes MD5 hashes.
  2. Lists remote objects under the configured prefix.
  3. Uploads files that are new or changed (MD5 differs from ETag).
  4. Deletes remote files that no longer exist locally, but only those tracked in the .blog-manifest file.
  5. Writes an updated .blog-manifest listing all current keys.

The manifest ensures the publisher never deletes content it didn't upload. This is safe for shared buckets.

Serving the site

R2 does not serve websites directly. You need a Cloudflare Worker or R2 public bucket with a custom domain to serve the content.

The benefit of using an R2 public bucket is that there are no egress fees and therefore costs are essentially zero. However you are unable to set headers which may limit SEO ranking.

The benefit of the worker approach is that you can host the blog in a subdirectory fo the bucket, implement routing rules and apply repsonse headers which helps SEO and Lighthouse Metrics rankings. Up to 100_000 requests per month are free, additional requests incur charges (correct as of April 2026).

A typical setup:

  1. Enable R2 Public Access on the bucket, or create a Cloudflare Worker that reads from the bucket and serves responses.
  2. Attach a custom domain to the Worker or public bucket.
  3. The Worker should handle directory-index rewriting (e.g. /posts/my-post/ serves posts/my-post/index.html) and serve 404.html for missing paths.

A minimal Worker example is typically included in the consumer project's worker/ directory.

Verifying a publish

After publishing, verify by checking the output:

Published 42 files from /path/to/_site
 Published uploaded: 3, skipped: 39, deleted: 0
  • uploaded -- files that were new or changed.
  • skipped -- files that matched the remote ETag (no upload needed).
  • deleted -- files removed from the bucket because they no longer exist locally.