Skip to content

Caching

Give your module's output the cache metadata that keeps one site's version off the other sites, and drops it when the site changes.

What Sites adds for you

  • site is a required cache context. SitesServiceProvider adds it to renderer.config's required_cache_contexts, as Workspaces does for workspace. Every render array, and so every HTML page, varies by the active site.
  • Configuration varies per site. A configuration object read through the config factory carries the site context and the site's cache tags, so addCacheableDependency($config) covers per-site configuration overrides.
  • The anonymous page cache keys by URL. Every hostname and every sub-site path prefix has its own entries.

Add site where the renderer does not

The renderer adds site only to what it renders. Add it yourself to:

  • access results whose answer differs per site: route, entity, block and field access;
  • cacheable responses that are no render array, like a CacheableJsonResponse, a plain-text CacheableResponse or a CacheableNotFoundHttpException: Dynamic Page Cache adds only the route and the request format to them;
  • your own cache entries: put the site ID in the cache ID.

The site object is a cacheable dependency (SiteInterface extends CacheableDependencyInterface): its contexts include site, and its tag is site:<site_id>, which Sites invalidates when the site or its site type is saved. Add it as a dependency wherever you read a setting:

#[Hook('block_access')]
public function blockAccess(BlockInterface $block, string $operation, AccountInterface $account): AccessResultInterface {
  if ($block->getPluginId() !== 'my_module_banner') {
    return AccessResult::neutral();
  }
  $site = $this->currentSite->getSite();
  return AccessResult::allowedIf((bool) $site->getSetting('my_module_banner'))
    ->addCacheableDependency($site);
}
$site = $this->currentSite->getSite();
$response = new CacheableJsonResponse(['title' => $site->label()]);
$response->addCacheableDependency($site);

$cid = 'my_module:summary:' . $site->id();

Drupal\sites_robotstxt\Controller\SitesRobotsTxtController adds site to a response that is no render array.

Which tag to carry

Sites invalidates these tags; your output carries the narrowest one that describes what it read. Drupal\sites\Cache\MembershipCacheTags and Drupal\sites\Cache\SiteAccessCacheability spell them.

Your output read Tag Helper
A site's settings site:<site_id> MembershipCacheTags::sitePlugin()
A site's hostname, aliases or prefix, like a link to it site_host:<site_id> MembershipCacheTags::siteHost()
Which sites one entity belongs to site_content_membership:<type>:<id> MembershipCacheTags::entity()
Which content belongs to one site site_content_membership_site:<site_id> MembershipCacheTags::site()
A list of one entity type on one site site_content_list:<type>:<site_id> MembershipCacheTags::siteList()
One account's per-site roles site_role_assignment:user:<uid> SiteAccessCacheability::assignmentTag()
Which sites exist sites_list

The list tags site_content_relationship_list and site_role_assignment_list change with any membership or role assignment on any site. They belong on hub reports; on a site's page they drop the cache for writes on every other site. Cache contexts and tags lists all tags and when they are invalidated. For Views, use the Tag based, per site cache plugin (Queries and Views).

Vary by what the viewer may do: user.sites

The user.sites context varies by the viewer's memberships and per-site roles. Accounts with the same ones share an entry; the user context would store one entry per account, and Dynamic Page Cache refuses to store a page that varies by user. Use it with user.permissions: SiteAccessCacheability::grantContexts() returns user.permissions, user.roles and user.sites, and SiteAccessCacheability::grant() and ::entityCapability() return the full metadata of a per-site access decision.

A module that grants per-site access from data of its own adds that data to the context: a service tagged sites.user_sites_provider that implements Drupal\sites\Cache\Context\UserSitesProviderInterface. Return a cheap, stable string; do not evaluate access in it. Sites ships two providers, one for role assignments and one (in Sites content) for memberships. This is the first, SiteRoleAssignmentProvider:

public function value(AccountInterface $account): string {
  $parts = [];
  foreach ($this->siteRoleManager->getRoleMapForUser($account) as $site_id => $role_ids) {
    $parts[] = $site_id . '=' . implode(',', $role_ids);
  }
  return 'roles:' . implode(';', $parts);
}
services:
  my_module.user_sites_provider:
    class: Drupal\my_module\Cache\MyUserSitesProvider
    autowire: true
    tags:
      - { name: sites.user_sites_provider }

Two more site contexts

site follows the active site, which a site switch or a preview changes.

  • sites_negotiated_site varies by the site the request arrived on. Add it where you branch on $current_site->negotiated()->isHub(); add site where you branch on $current_site->isHub().
  • sites_context_site varies, on the hub, by the site a request acts in (its ?site_id, or the canonical site of the route's entity). The access policy adds it; you rarely do.

Page cache and reverse proxies

The anonymous page cache answers before Sites picks the request's site. A site is therefore picked from the hostname and path alone; see Hooks and events. What a reverse proxy or CDN must do is on Caching and performance.

To check a page, set http.response.debug_cacheability_headers: true in services.yml and read X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags. Testing and debugging shows a functional test of the page cache per hostname.