November 7th, 2011

One great feature of Plone is the resource registries which speed up the performance of your pages by merging, compressing and improving caching of your CSS and JavaScript resources. plone.app.theming offers many advantages over the traditional way to theme a Plone site but doesn’t yet support integration with the resource registries.

There are plans to add support from Plone 4.3 onwards but here’s an interim solution I came up with so that I could merge the CSS and JavaScript of my theme right now.  The approach I took is to register my theme’s CSS and JavaScript in the registry with conditional logic based on attributes of the request, use one of the existing custom views for an object to set that attribute on the request and then call templates within Products.ResourceRegistries.

Background to the site

The site I’m theming has two URLs: one for anonymous, public access; one for the editors.  The editors get the standard Plone sunburst theme with a preview pane whilst the public URL doesn’t include any CSS or JavaScript and little HTML from the default Plone UI.

The public view is built up of a number of custom views for an object in Plone all pulled together using rules.xml.

Create the custom view

Register a new view in configure.zcml

    <browser:page
        for="*"
        name="standard-page-elements"
        template="standard_page_elements.pt"
        permission="zope2.View"
        />

Create the custom view class

from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.ResourceRegistries import browser
from os.path import dirname
from sys import modules

class StandardPageElementsView(BaseThemeView):
    styles_viewlet = ViewPageTemplateFile('styles.pt',
                                  _prefix = dirname(modules['Products.ResourceRegistries.browser'].__file__))
    scripts_viewlet = ViewPageTemplateFile('scripts.pt',
                                  _prefix = dirname(modules['Products.ResourceRegistries.browser'].__file__))
    def mark_request(self):
        self.request.other['plone.app.theming.name'] = 'my.theme'

    def styles(self):
        self.mark_request()
        return self.styles_viewlet(self)

    def scripts(self):
        self.mark_request()
        return self.scripts_viewlet(self)

Next create the template for the view

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:tal="http://xml.zope.org/namespaces/tal"
<head>
    <tal:styles tal:replace="structure view/styles" />
    <tal:styles tal:replace="structure view/scripts" />
</head>
<body></body>
</html>

Register the theme’s resources

In cssregistry.xml

<stylesheet title="" applyPrefix="True" authenticated="False"
    cacheable="True" compression="safe" conditionalcomment="" cookable="True"
    enabled="on"
    expression="python:portal.REQUEST.other.has_key('plone.app.theming.name')"
    id="++theme++my.them/css/main.css" media="screen" rel="stylesheet"
    rendering="link"/>
<stylesheet title="" applyPrefix="True" authenticated="False"
    cacheable="True" compression="safe" conditionalcomment="" cookable="True"
    enabled="on"
    expression="python:portal.REQUEST.other.has_key('plone.app.theming.name')"
    id="++theme++my.theme/css/other.css" media="screen" rel="stylesheet"
    rendering="link"
    insert-after="++theme++my.theme/css/main.css"/>

And do something similar for your scripts in jsregistry.xml

Switch off default Plone CSS and JavaScript

This is a complete pain (roll on Plone 4.3!). You need to disable each resource by hand. I wrote a test for this:


    def test_css_registry(self):
        css_registry = getToolByName(self.portal, 'portal_css')

        theme_css_ids = ('++theme++my.theme/css/main.css',
                         '++theme++my.theme/css/other.css')

        for css in (css for css in css_registry.getResourcesDict().values() if css.getId() not in theme_css_ids):
            self.assertTrue(css.getExpression().startswith(
                               "python:(not portal.REQUEST.other.has_key('plone.app.theming.name'))"),
                            '%s - %s' % (css.getId(), css.getExpression()))

And then in cssregistry.xml you need a line for each Plone css file like the following:

<stylesheet expression="python:(not portal.REQUEST.other.has_key('plone.app.theming.name'))" id="member.css"/>

And then do something similar for jsregistery.xml

Include via rules.xml

You need to block stylesheets (including ones in conditional comments) and scripts from the main view and then pull in ones from your custom view:

    <drop theme="/html/head/link[@rel = 'stylesheet']" />
    <drop theme="/html/head/comment()" />
    <drop theme="/html/head/script" />
    <append theme="/html/head" content="/html/head/*" href="@@standard-page-elements" />

Different resources for different pages

I have the requirement to serve different CSS and scripts for different pages.  I’ve extended the approach above to register different “standard-page-elements” view classes for different objects which set additional attributes on the request.

Roll on Plone 4.3

Work is in progress for Products.ResourceRegistries to look up resources based on an adapter and plone.app.theming to register adapters that provide such resources from link and script tags in the html head element of the theme.  If this can be achieved it would obviously be a big win although it sounds tricky for some aspects of existing resource registry functionality like conditional css for IE and specifying which scripts can be compressed safely.

Comments are closed.