StaticBlog publishes the built site to a Cloudflare R2 bucket using AWS-compatible S3 API calls signed with Signature V4.
Creating an R2 bucket
- Log in to the Cloudflare dashboard.
- Navigate to R2 Object Storage in the sidebar.
- Click Create bucket.
- Choose a bucket name (e.g.
content). - Select a location hint if desired (R2 automatically distributes globally).
Creating API credentials
- In the R2 section, go to Manage R2 API Tokens.
- Click Create API Token.
- Give the token a descriptive name (e.g.
blog-publish). - Under Permissions, select Object Read & Write.
- Optionally scope the token to a specific bucket.
- Click Create API Token.
- 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:
- Walks the local
_site/tree and computes MD5 hashes. - Lists remote objects under the configured prefix.
- Uploads files that are new or changed (MD5 differs from ETag).
- Deletes remote files that no longer exist locally, but only those tracked in the
.blog-manifestfile. - Writes an updated
.blog-manifestlisting 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:
- Enable R2 Public Access on the bucket, or create a Cloudflare Worker that reads from the bucket and serves responses.
- Attach a custom domain to the Worker or public bucket.
- The Worker should handle directory-index rewriting (e.g.
/posts/my-post/servesposts/my-post/index.html) and serve404.htmlfor 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.