Caching & Cloudflare Invalidation

7inOne serves anonymous traffic through a Cloudflare CDN in front of the Plone backend. To keep the CDN cache consistent with content changes, the caching feature tags every cacheable response and purges the matching cache entries on Cloudflare whenever the underlying content changes. A Redis job queue decouples the (potentially slow) Cloudflare API calls from the request that triggered them and handles time-based (scheduled) purges.

Architecture Overview

The feature combines three moving parts:

  1. Cache-Tag headers – every anonymous GET response is tagged with the context’s UID and portal_type, so Cloudflare stores those tags alongside the cached object.

  2. CloudFlareClient – a Zope utility that talks to the Cloudflare purge_cache API. It can purge by tag, by URL prefix, or purge everything for a zone.

  3. Redis (RQ) job queue – content-change events enqueue a background job that POSTs to the @invalidate REST service, which collects the UIDs to purge and calls the client. Future-dated changes (effective / expiration / event end) are scheduled on a separate queue.

┌───────────────┐  content change   ┌──────────────┐   enqueue   ┌──────────┐
│  Plone event  │──────────────────▶│ invalidate   │────────────▶│  Redis   │
│  subscribers  │                   │   hook()     │             │  (RQ)    │
└───────────────┘                   └──────────────┘             └────┬─────┘
                                                                      │ POST @invalidate
                                                                      ▼
                                          ┌──────────────┐    purge    ┌──────────────┐
                                          │ UID collector│────────────▶│  Cloudflare  │
                                          │  (per type)  │   by tag    │   purge API  │
                                          └──────────────┘             └──────────────┘

Note

Only anonymous GET responses are cached and tagged. Authenticated backend traffic is never tagged and never cached at the CDN, so the whole invalidation machinery only ever has to worry about the anonymous view of the site.

Cache-Tag Header Model

For every successful anonymous GET request to a content object or the site root, a publication subscriber sets two equivalent response headers:

  • Cache-Tag – consumed by Cloudflare for tag-based purging.

  • X-Cache-Tag – the same value, exposed for debugging/inspection.

The default tag set for a content response is:

  • the context UID

  • the normalized portal_type (e.g. contentpage, file)

Individual endpoints may add their own tags on top of these. For example the @banner, @responsible and @responsible-backreferences endpoints append generic tags (banner, responsible, responsible-backreferences) so that a single change can invalidate every cached response that embedded that data, regardless of which page it appeared on. When multiple tags apply to one response they are merged into a single comma-separated Cache-Tag header.

Tag-Based vs. URL/Prefix Purging

The CloudFlareClient supports three purge strategies. Which one is used depends on what changed.

Tag-based purge (purge_cache_by_tags / purge_cache_by_objs) : The default mechanism. Given a set of UIDs (and generic tags), Cloudflare drops every cached object carrying one of those tags. This is how normal content edits are invalidated. Tags are purged in batches of at most 100 per Cloudflare request.

URL/prefix purge (purge_cache_by_prefixes) : Used where there is no Cache-Tag to match – most importantly for redirector (alternative URL) entries and the manual control-panel buttons. A prefix is the host + path portion of a URL.

Purge everything (purge_everything) : A full zone flush, triggered only by the Full Cache Purge button in the control panel.

Important

Tag-based purging is zone-wide. A Cache-Tag purge clears the matching objects across all subdomains of the zone in a single call – you never have to enumerate subdomains for tag purges.

URL/prefix purging is not. A prefix purge only clears the exact host you name, so every subdomain that may have served the content has to be listed explicitly. This is why redirector invalidation expands each path into one URL per configured subdomain (see Subdomain Configuration).

404 responses have no Cache-Tag. Cloudflare cannot tag a not-found response, so a cached 404 can only be cleared via URL/prefix purging, never by tag.

Subdomain Configuration

Cloudflare credentials and per-domain settings live in a single JSON registry record, wcs.backend.caching.mapping. Each top-level key is a registrable domain mapped to its Cloudflare zone and the subdomains served from it:

{
    "webcloud7.ch": {
        "zoneid": "5d6d31b39f784ef1e6920e9fbfeaa9ab",
        "apikey": "SHcw9lp0BjMv_R_-4IS26x2oNiqHZfwha6RtCAEm",
        "subdomains": ["www"]
    }
}
  • zoneid – the Cloudflare zone ID used in the purge API URL.

  • apikey – the Cloudflare API token (sent as a Bearer token).

  • subdomains – the subdomains served from this zone. Defaults to ["www"] when omitted.

The subdomains list only matters for URL/prefix purges, where each path is expanded into https://<subdomain>.<domain>/<path> for every entry. For tag-based purges the list is irrelevant, because a tag purge already covers the whole zone.

The Caching Control Panel

A dedicated control panel, 7inOne Caching (@@caching-controlpanel, restricted to Manage portal), exposes both the configuration and a set of manual purge actions.

Configuration section

Field

Registry record

Purpose

Enable cache invalidation

wcs.backend.caching.enabled

Master switch. While off, all automatic invalidation is skipped.

API base URL

wcs.backend.caching.api_base_url

The public API domain (used for redirector and API Cache purges).

Cloudflare configuration

wcs.backend.caching.mapping

The domain → zone/apikey/subdomains JSON shown above.

The client only considers itself correctly configured when the domain extracted from the API base URL is present as a key in the mapping; otherwise automatic invalidation is treated as disabled and logged as a warning.

Manual invalidation section

  • API Cache – prefix-purges the configured API base URL.

  • Backend Cache – prefix-purges the backend site URL.

  • Full Cache Purge – purges everything for the configured preview (frontend) domain. The button is only shown when the preview domain is part of the mapping.

These buttons are an operational escape hatch; day-to-day invalidation is fully automatic via the collector model below.

The Collector Model

When something has to be invalidated, the system does not simply purge the one object that changed. Each content type knows best which other cached responses depend on it, so an IUIDPurgeCollector adapter is looked up for the changed object and asked for the complete set of UIDs (and generic tags) to purge. The @invalidate service feeds that set straight into a tag-based Cloudflare purge.

Base collector

The default collector (DxUIDContentCollector, registered for all Dexterity content) always returns:

  • the object’s own UID, plus

  • the UID of its parent, plus

  • the UIDs of all back-references to it (filtered by view permission and active/effective dates), plus

  • for Simplelayout pages, the UIDs of all blocks on the page.

A block collector additionally always pulls in its parent page, and the site root collector additionally pulls in the site’s default page. This recursive walk ensures that listing pages, referencing pages and embedding blocks are refreshed alongside the edited object.

Type-specific collectors

Several content types extend the base collector to cover the places their data is surfaced:

Collector

Triggered for

Adds to the purge set

BannerUIDContentCollector

Banner

The generic banner tag when the banner is published or scheduled, so every cached @banner response is refreshed. On retract/delete only the banner’s own UID is purged.

NewsUIDContentCollector

News items

The UIDs of every News-listing block whose query would include this news item.

MediaFolderUIDContentCollector

Media folders (and the files/images inside)

The media folder’s back-references and parent, so listing blocks and pages embedding the folder are refreshed when a file/image is added or changed.

ResponsibleUIDContentCollector

Pages with the Responsible Unit behavior

The generic responsible and responsible-backreferences tags.

TopicFilterUIDContentCollector

Content with flat-topic support

The UIDs of every (anonymous-visible) TopicFilter block whose first result page actually contains this content, verified against Elasticsearch.

What triggers a collection

Content-lifecycle event subscribers decide when a collection/purge runs:

  • Workflow transitions on published content, and working-copy apply (staging) – purge immediately.

  • Modification of content that has no workflow state – only schedules a future purge if the content has a future effective/expiration date.

  • Deletion / trashing of published content – purge immediately (trashing an already-trashed object is ignored).

  • Media folder file/image add or modify, and image crop changes – purge immediately.

  • Redirector (alternative URL) additions – purge synchronously by URL prefix, expanded across all configured subdomains.

Time-based purges (future effective date, expiration date, or event end) are enqueued on the important Redis queue and fire at the scheduled moment, so a page becomes visible or disappears from the cache exactly when it should.

The @invalidate REST Service

@invalidate is the single entry point that performs a tag-based purge for a context. It runs the collector for the context, logs the resolved objects, and asks the Cloudflare client to purge the collected UIDs. The background Redis jobs described above call this service; you can also call it directly to force an invalidation of a specific object and everything that depends on it.

The service is a POST to the object’s URL and returns the UIDs that were purged together with the raw Cloudflare API responses.

JavaScript

async function invalidate(objectUrl, auth) {
    const response = await fetch(`${objectUrl}/@invalidate`, {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Authorization': `Basic ${auth}`
        }
    });
    return response.json();
}

// Usage
const result = await invalidate(
    'https://backend.example.ch/Plone/topics/my-page',
    btoa('admin:secret')
);
console.log(result.uids_to_purch);       // ["abc123...", "def456...", "banner"]
console.log(result.cloudflare_response); // raw Cloudflare purge responses

Python

import requests

response = requests.post(
    'https://backend.example.ch/Plone/topics/my-page/@invalidate',
    headers={'Accept': 'application/json'},
    auth=('admin', 'secret'),
)
data = response.json()
print(data['uids_to_purch'])

Example response:

{
    "uids_to_purch": [
        "8f1d2c3b4a5e6f7081920a1b2c3d4e5f",
        "1a2b3c4d5e6f70819203a4b5c6d7e8f9",
        "banner"
    ],
    "cloudflare_response": [
        {
            "result": {"id": "5d6d31b39f784ef1e6920e9fbfeaa9ab"},
            "success": true,
            "errors": [],
            "messages": []
        }
    ]
}

uids_to_purch is the full set the collector resolved for the context (own UID, parent, back-references, blocks, and any type-specific tags). cloudflare_response is a list with one entry per Cloudflare purge request – more than one when the tag count exceeds the 100-tags-per-request limit and the purge is split into batches.