Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Statamic Cloudflare Images

A {{ cfimage }} Statamic tag, shaped to feel like {{ glide }}, that serves and transforms images through a Cloudflare Worker (R2 binding + Workers Images binding) instead of Glide/GD on the app server. No image processing or image bytes ever pass through your Statamic site.

Why not Glide

Glide (and Laravel's own Image facade) both do the resize/encode work on your own server, on request. This addon moves that entirely to Cloudflare's edge: the Worker reads the source straight out of R2 and transforms it there, so origin CPU and bandwidth are never in the path for image variants after the first request.

Architecture assumptions

This addon is built around two things being true of your infrastructure:

  • Your zone (domain) is on Cloudflare — the Worker is deployed as a route on that zone, so Cloudflare has to be managing DNS for it.
  • Your asset originals live in Cloudflare R2 — the Worker reads the source file directly out of R2 itself. Assets stored elsewhere (S3, local/public disk, another CDN) aren't supported today.

How the pieces fit together

If you haven't used Cloudflare before, three separate things need to exist before {{ cfimage }} will serve anything:

  1. R2 — Cloudflare's object storage. Your asset files themselves (originals) need to actually live here. This addon doesn't upload anything for you — you point a normal Laravel filesystem disk at R2 and Statamic uploads to it exactly as it would to local disk or S3.
  2. A Worker — a small script Cloudflare runs at the edge, published by this package into your app under workers/cfimage/. It receives the signed URLs this addon builds, reads the source file out of R2, resizes it via the Workers Images binding, and returns the result.
  3. This package — builds signed {{ cfimage }} URLs pointing at your Worker, and gives you php artisan cfimage:publish to keep the Worker's own config (wrangler.toml, preset table) in sync with config/cfimage.php so nothing is maintained in two places.

Prerequisites

  • A Cloudflare account, with the domain you'll serve images from added to Cloudflare (i.e. Cloudflare is managing its DNS).
  • R2 enabled on the account — the R2 dashboard tab will prompt you to enable it the first time you visit, no separate paid plan required for R2 itself.
  • Cloudflare Images / Workers Images enabled on the account — this is the part that actually resizes images at the edge, and it's a separate product from R2. Check the "Images" tab in the dashboard before your first deploy; on some accounts this requires a paid plan.

Installation

composer require thoughtco/statamic-cloudflare-images
php artisan vendor:publish --tag=cfimage-config  #this publishes config/cfimage.php
php artisan vendor:publish --tag=cfimage-worker  #this publishes workers/cfimage/

1. Create an R2 bucket and point a disk at it

  1. In the Cloudflare dashboard, go to R2 → Create bucket and give it a name (e.g. my-app-assets).
  2. Create an API token scoped to R2: R2 → Manage API tokens → Create API token, with read/write access to that bucket. This gives you an Access Key ID and Secret Access Key — save both, the secret is only shown once.
  3. Your Cloudflare Account ID is shown in the right-hand sidebar of almost every page in the dashboard — you'll need it for the R2 endpoint below and again later for the Worker.
  4. Add an R2 disk in config/filesystems.php, using Laravel's S3 driver (R2 is S3-compatible):
    'r2' => [
        'driver' => 's3',
        'key' => env('R2_ACCESS_KEY_ID'),
        'secret' => env('R2_SECRET_ACCESS_KEY'),
        'region' => 'auto',
        'bucket' => env('R2_BUCKET'),
        'endpoint' => env('R2_ENDPOINT'), // https://<account_id>.r2.cloudflarestorage.com
        'use_path_style_endpoint' => true,
    ],
  5. Point a Statamic asset container (or your Spatie Media disk, etc.) at this r2 disk. This is the disk config('cfimage.disks') will reference in step 2 — the addon reads existing assets from it, it doesn't migrate anything for you.

2. Configure the package

Open config/cfimage.php and set, at minimum:

  • disks — every disk {{ cfimage }} may serve from, mapped to an arbitrary R2 binding name the Worker will use, e.g. ['assets' => 'ASSETS']. Each key must be a disk that exists in config/filesystems.php and has a bucket value set (the r2 disk from step 1, for example).
  • presets — named shortcuts in the same shape as Statamic's Glide presets, e.g. 'thumbnail' => ['w' => 300, 'h' => 300, 'q' => 80, 'fm' => 'webp']. You can literally copy/paste them from the assets config file.

See the comments in that file for the rest of the options.

3. Set up the Worker

From your app root, the published Worker source lives in workers/cfimage/.

  1. Install dependencies (this installs wrangler locally — every command below runs it via npx, no global install needed):
    cd workers/cfimage
    npm install
    
  2. Authenticate wrangler so it's allowed to deploy to your account:
    npx wrangler login
    
    This opens a browser to authorize against your Cloudflare account. For CI or other non-interactive environments, set a CLOUDFLARE_API_TOKEN environment variable instead (dashboard → My Profile → API Tokens → Create Token, the "Edit Cloudflare Workers" template covers it) — wrangler picks this up automatically and skips the browser flow.
  3. Point your chosen hostname at Cloudflare. Decide on the hostname {{ cfimage }} URLs will use (e.g. images.example.com) and add a DNS record for it in the Cloudflare dashboard, proxied (orange cloud on). The record's target doesn't matter — the Worker's route intercepts requests to this hostname before they'd reach any actual origin. Set this hostname as CFIMAGE_URL in your app's .env (https://images.example.com).
  4. Fill in the Worker deploy settings in config/cfimage.php's worker array:
    • zone_name — your domain as it appears in Cloudflare (e.g. example.com), set via CFIMAGE_ZONE_NAME.
    • account_id — only needed if the account authenticated in step 2 has access to more than one Cloudflare account; otherwise wrangler infers it and this can stay blank. It's the Account ID from step 1.3, set via CFIMAGE_ACCOUNT_ID.
    • bucket_name values are generated automatically from config('cfimage.disks') and config/filesystems.php — nothing to fill in by hand here, but the R2 bucket(s) themselves must already exist (step 1).
  5. Generate a signing secret — any random string works, e.g.:
    openssl rand -hex 32
    
    Set this value in both places, exactly matching, or every URL will 404:
    npx wrangler secret put CFIMAGE_SIGNING_KEY
    
    (run from workers/cfimage/) and as CFIMAGE_SIGNING_KEY in your app's .env.
  6. Generate and deploy the Worker. From your app root:
    php artisan cfimage:publish --deploy
    
    This regenerates wrangler.toml and the preset table from config/cfimage.php, then runs wrangler deploy. (Run without --deploy to just regenerate the files without deploying.)

Regenerating wrangler.toml and the preset table

config/cfimage.php is the single source of truth for the URL path prefix, presets, disks, and Worker deploy settings — bucket names come from config/filesystems.php, so nothing bucket-related needs duplicating by hand. Never hand-edit wrangler.toml or src/presets.generated.js in the published workers/cfimage/ directory — both are fully overwritten any time you change config/cfimage.php and re-run:

php artisan cfimage:publish            # regenerate only
php artisan cfimage:publish --deploy   # regenerate + wrangler deploy

URL shape

The signature, preset/modifiers, and source key all live in the URL path, not the query string:

<CFIMAGE_URL>/<path>/<disk>/<sig>/<preset-or-modifiers>/<key...>

e.g. https://images.example.com/cfimage/assets/<sig>/p_thumbnail/photo.jpg or, without a preset, .../w_300,h_200,fit_crop/photo.jpg. Keeping these in the path (rather than query params) means a CDN cache rule or proxy that strips/ignores query strings can't silently drop the signature or collapse distinct variants onto one cache entry.

Every failure path (bad signature, unknown disk, unresolvable modifiers, missing source file) returns a bare 404 to the client — the specific reason is only ever logged server-side (console.error, visible via wrangler tail or Cloudflare Logs), so a prober can't use the response itself to learn which part of a forged or malformed URL is wrong.

Usage in templates

Mirrors {{ glide }}'s shape:

{{ cfimage:image preset="thumbnail" }}{{ url }}{{ /cfimage:image }}

<img src="{{ cfimage:image preset="thumbnail" }}" alt="...">

{{ cfimage src="{ mobile_image ?? image }" preset="thumbnail" }}

Accepts a Statamic Asset, or any object exposing a public $disk property and a getPathRelativeToRoot() method (e.g. a Spatie Media model), or a plain path string (resolved against cfimage.default_disk).

Notes

  • dpr is applied by multiplying the requested width/height before the resize — an approximation, not a pixel-for-pixel match of Glide's own dpr handling.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages