Play-CMS supports powerful caching mechanisms at the block level. Caching is disabled by default, it can be enabled globally via configuration and refined per block class.

Caching uses Play's Ehcache plugin, but also accesses Ehcache's API via ch.insign.commons.CacheManager. The block's render() output (Html) is stored in-memory (currently no serialisation and disk storage).

How it works

Every block has a .render() method which renders its template, and a .cached() method which tries to return a cached version first, and falls back to render() if no cache is available.

Steps

  • Template or FrontendController calls block.cached()
  • .cached() checks if caching should be used for this request
    • if yes: Return cached content if available, otherwise call .render(), cache and return the content
    • if no: return .render()

When are blocks flushed from cache?

  1. If blocks are modified, play-cms calls block.markModified()*
  2. When a block reaches its scheduled display from or display to date*
  3. If the cache entry is older than the specified global time to life (cms.blockcache.ttl)
  4. If BlockCache.flush() is called (global cache flush)
  5. By manually calling the block's .cache().invalidate()

* This will mark the cache of this and all ancestor blocks (ie. the containing collection and/or page) as invalid.

However, if you created collections or pages with custom dynamic block queries, the cache system cannot know where blocks get included - so you need to handle this case manually.

Configuring the cache

The most crucial part in caching is creating a proper cache key which is unique for the different content variants while trying to keep the cache entries to a minimum. BlockCache offers various settings, and in addition the block class can overwrite the .getKey() method to further control the key generation.

Setting
Meaning
Default
setEnabled turn caching on/off false
handlePOST Cache responses to POST requests false
handleGET Cache responses to requests that contain GET parameters false
ignoreUser Do not create per- (authenticated) user cache entries false
ignoreGroups TODO: Do not create per set of user-group entries
(Warning: Do not ignore users and groups or everybody will see e.g. frontend edit controls if they happen to get cached) 
false
ignoreLanguage Do not create per-language cache entries false

 Global configuration

## Block cache settings
cms.blockcache.enabled = true
cms.blockcache.size = 512 # MB (LRU strategy will evict least recently used entries when the size limit is reached)
cms.blockcache.ttl = 604800 # sec. (1 week)
cms.blockcache.handleGET = false
cms.blockcache.handlePOST = false
cms.blockcache.ignoreUser = false
cms.blockcache.ignoreGroups = false
cms.blockcache.ignoreLanguage = false

Block class

The block.cache() method is used to access the BlockCache instance for this block. Use it to configure per-class settings or extend the default behaviour.

Example: Configure the BlockCache instance

MyBlock.java

/**
 * Configure a default BlockCache instance
 */
@Override
public BlockCache cache() {
    if (cache == null) {
        cache = new BlockCache(this);
 
        cache
            .setEnabled(true)
            .handlePOST(false)
            .handleGET(true)
            .ignoreUser(true)
            .ignoreGroups(true)
            .ignoreLanguage(false);    
    }
    return cache;
}

 

Example: Override the key generation to add custom behaviour

/**
 * Customize the cache key generation.
 */
@Override
public BlockCache cache() {
    if (cache == null) {
 
        cache = new BlockCache(this) {
            @Override
            public String getKey() {
                 
                // Create morning and afternoon cache entries depending on "am" and "pm" time
                String amPm = new SimpleDateFormat("aa").format(new Date());
                return super.getKey() + "-" + amPm;
            }
        };
 
    }
    return cache;
}

Usage

In general, caching needs proper configuration, but not much implementation.

  • Controller level: The cms uses page.cached() from its FrontendController when a url is requested
  • Templates: Use @block.cached (and not @block.render) to ensure cached content can be used.
  • Test the caching behaviour with different users / roles! This is crucial - you want to be sure non-admins cannot see cached admin-content etc.
  • Hint: add <logger name="ch.insign.cms" level="DEBUG" /> to logger.xml to analyze the caching behaviour. Check entries like
    "cached: Using cached block-10802-1-de-page:mine"
  • Use the ?cache=false GET parameter to disable caching for this request
  • Use BlockCache.flush() to flush the entire cache (the cache is also flushed upon a play restart)

Live partials in otherwise cached pages

Often your main site layout contains dynamic per-user content, such as username, login state, cart content, feedback messages from flash scope etc. For these cases, you can define uncached live partials, which are not directly rendered when the page is rendered. Instead, during rendering, a live partial placeholder is placed in the template (Example: [uncached:miniCart]). Before output, these placeholders are replaced with the rendered content of these live partials, no matter whether the page's template was just rendered or comes from cache.

Example:

1. Define your uncached live partials in your CMSApiLifecycle implementation:

MyCMSApiLifecycle.java

@Override
public void registerUncachedPartials(CMSApi cmsApi) {
    cmsApi.getUncachedManager()
        .register("miniCart", () -> views.html.shared.miniCart.render())
        .register("myAccountBox", () -> views.html.shared.myAccountBox.render())
}

 

2. In your template (often the main layout):

@Template.addUncached("miniCart")

Note: If you use your main layout file for both cms and non-cms pages, then use:

@Template.addUncached("miniCart", page)

 

Where page is the instance of the Page interface. addUncached will then determine if the current page is a cms page (instace of PageBlock) and use the live partial mechanism. If it's not a cms page, then it will render and add the partial directly.

 

Footers and other global blocks

Global blocks such as footers that were added by their key and appear on  all pages need special treatment. If they change, all cache entries become invalid. Just flush the entire cache when such blocks get modified:

MyFooterBlock.java

@Override
public void markModified() {
 
    // The footer appears on all pages, so changes will invalidate all cache entries.
    BlockCache.flush();
 
    super.markModified();
}