CDN Integration

Overview

CDN integration lets you front Accent CMS with a content delivery network (Cloudflare, Fastly, CloudFront, Bunny, …) and adds three capabilities on top of the standard HTTP cache headers:

  1. Asset URL rewriting — point asset references at your CDN origin from templates (cdn_url()) and from rendered markdown images (automatic).
  2. Font privacy proxy — serve Google Fonts from your own origin so visitor browsers never contact Google, avoiding the IP-address leak that European courts have ruled a GDPR violation.
  3. Cache purge coordination — notify the CDN to drop stale pages when content changes (outbound), and let the CDN invalidate the local page cache (inbound).

CDN integration is a Pro feature. The helper functions are always available so themes stay portable, but enabling cdn.enabled for accent serve --production requires a Pro license. Static accent build is always free.

Configuration

Add a cdn: section to config.yaml:

cdn:
  enabled: true
  base_url: "https://cdn.example.com"

  font_proxy:
    enabled: true
    families:
      - "Inter:wght@400;700"
    cache_dir: ./.font-cache

  purge:
    webhook_secret: "${CDN_PURGE_SECRET}"
    on_content_change: true
    cloudflare:
      zone_id: "${CF_ZONE_ID}"
      api_token: "${CF_API_TOKEN}"
    # Or, for any other CDN, a generic webhook:
    # webhook:
    #   url: "https://api.fastly.com/purge/..."
    #   method: POST
    #   headers:
    #     Fastly-Key: "${FASTLY_API_TOKEN}"

Secret-bearing values (webhook_secret, the Cloudflare token, generic webhook headers) support ${VAR} and ${VAR:-default} environment-variable placeholders, expanded at config load — keep secrets out of version control by injecting them from the environment.

Asset URL rewriting

In templates

Use cdn_url() for any asset you want served from the CDN:

<link rel="stylesheet" href="{{ cdn_url('/assets/css/main.css') }}">
<script src="{{ cdn_url('/assets/js/app.js') }}"></script>

With cdn.enabled: true and base_url: "https://cdn.example.com", this emits https://cdn.example.com/assets/css/main.css. With CDN disabled it returns the path unchanged, so the same templates work on every edition. The rewrite happens identically in accent serve and accent build, so static output already points at the CDN.

In markdown content

Image URLs in markdown are rewritten automatically when CDN is enabled. A local image like ![hero](/media/hero.webp) becomes https://cdn.example.com/media/hero.webp in the rendered HTML — no template changes needed. Only paths beginning with one of the configured rewrite_prefixes (by default /media/, /content-media/, /assets/, /theme/assets/) are rewritten; external and unrelated URLs pass through untouched.

Font privacy proxy

A direct Google Fonts <link> makes every visitor’s browser request fonts.googleapis.com and fonts.gstatic.com, sending their IP, User-Agent, and Referer to Google. The font proxy eliminates this: Accent CMS fetches the CSS and font files once, caches them locally, rewrites the @font-face url()s to same-origin /_fonts/ paths, and serves everything from your own domain.

Link fonts through font_css_url():

<link rel="stylesheet" href="{{ font_css_url('Inter:wght@400;700') }}">

With the proxy enabled this renders a same-origin path (/_fonts/css/inter-wght-400-700.css); with it disabled it falls back to a direct Google Fonts URL. List every family you use under cdn.font_proxy.families so the static accent build can pre-download them into the output directory.

Cache purge

Outbound (Accent CMS → CDN)

With purge.on_content_change: true, a content change in accent serve triggers a best-effort purge so the CDN drops its stale copies. Configure either the first-class cloudflare integration (zone purge) or a webhook for any other provider.

Inbound (CDN → Accent CMS)

A CDN (or any trusted orchestrator) can invalidate Accent CMS’s local page cache by POSTing to /_webhooks/cdn/purge with an X-Accent-Signature HMAC-SHA256 header computed over the request body using purge.webhook_secret:

{ "urls": ["/blog/my-post/"] }

Send { "purge_all": true } to clear the entire cache. The endpoint is only mounted when cdn.enabled is set and webhook_secret resolves to a real value (an empty or unexpanded ${VAR} placeholder is refused, matching the git push webhook).

Reference