Advanced XDV theming

« Return to page index

Tutorial showing how to convert real-life static theme into a Plone theme using collective.xdv

Prerequisites

What do you need in order to work with collective.xdv?

If you are not familiar with XDV or Deliverance please consider reading the wonderful introduction to these technologies by Alexander Limi. If you are going to use XDV to style more than just Plone then this tutorial will of course give you the basics. Though after reading it you might need to read Chapter 12 in Alexander's tutorial, written by Anne Bowtell in more detail.

This tutorial will guide you through applying a static HTML/CSS theme to your Plone (and only Plone) site using collective.xdv.

Static theme

For the practical part I assume you have a static theme done. This means you have created/bought/downloaded a set of static HTML, CSS etc. files that have no relation to Zope/Plone at the moment. You should be able to test your visual theme in a browser without Plone or Zope running.

In this tutorial we use xdvtheme.xdvrocks package as a case study to see the real implementation. You might want to download it and check its static/ folder to get the static theme that we are going to convert to a real Plone theme.

Buildout

As with any Plone project, no matter whether it uses XDV/Deliverance or not, we start with buildout. We will not dive into the details of creating one in this tutorial and assume you have a buildout-driven set up with a Plone site running. There is a wonderful tutorial by Martin Aspeli on taming buildout.

After the buildout is set up you want to add collective.xdv and it’s dependencies to the mix right away. In order to do this add:

[buildout]

extends =

http://good-py.appspot.com/release/collective.xdv/1.0

eggs =

collective.xdv [Zope2.10]

You might want to use 'extends-cache' option in your [buildout] section to make all resources from 'extends=' download to the local folder for further offline usage

If you are trying to install collective.xdv for Plone 4 (Zope 2.12), you can drop the [Zope2.10] part. If you are still on a lower version of Plone with Zope > 2.12 this will make sure you have additional functionality ported from Zope 2.12 that collective.xdv requires.

This should be enough if you’re not on Mac OS. This OS has issues with the lxml binary egg that make installation of collective.xdv on OS X quite problematic. So, if you’re on Mac OS your buildout.cfg should be extended with more information, in addition to the above, like:

[buildout]

parts =
lxml


[versions]
lxml = 2.2.2
zc.buildout =

[lxml]
recipe = z3c.recipe.staticlxml
egg = lxml

zc.buildout = might be necessary on Mac OS because building staticlxml on Mac OS most probably will require a version of zc.buildout newer than the one you get when creating a buildout. This line says that we need the most recent version of zc.buildout.

After re-running ./bin/buildout for your instance and starting it, you should be able to add a Plone site in ZMI with an “XDV Transforms” profile. Doing this will install the “Configuration registry” profile as well to your site. This tells us that collective.xdv is installed and is available for Plone.

At this stage you might also want to add your favorite development tools to the buildout. I always add omelette and plone.reload. You might want to add other tools you like.

Theme package

Any web application needs to know where your static files are. XDV is not an exception. In pure XDV you can tell your web server to fetch your files. In the case of collective.xdv, when you don’t use such server, a standard Plone theme is still the best way of letting a Plone site know that there are files in your theme. Yes, magic doesn’t really happen here - you still need to create at least a basic Plone theme package in order to make your images, stylesheets etc. available for your site. Moreover you really want to register your stylesheets and javascripts with portal_css and portal_javascripts respectively to use all the advantages of these like caching, merging and so on.

There is only one case so far when you can avoid creating a Plone theme using collective.xdv - when all you have is HTML. No images, no stylesheets. But we don’t any longer live in times of pure HTML without styles and images. So you can’t avoid registering your resources.

Generation of an actual theme package is beyond the scope of this tutorial as well. As already mentioned, for this talk’s purpose we will use a ready-generated theme called ‘xdvtheme.xdvrocks’. So, we assume you have managed to either read “Plone 3 Theming” book by Veda Williams or read her “How to Create a Plone 3 Theme Product on the Filesystem” tutorial on plone.org and you have an installable theme package in your buildout.

Theme's structure

Our example theme's structure

At this point we have 3 main parts to work with:

  • static visual theme;
  • Plone theme package;
  • buildout with both collective.xdv and your theme installed.

Put static HTML/CSS into the theme package

How to add your static resources to the Plone theme

After you have a theme’s skeleton you start putting your static resources into it. There are at least 3 options you have for doing this:

  • Add your resources to customs/ folder of your Plone site. Fast, doesn't require Plone theme generation at all. But is very dirty workaround that is not recommended;
  • add your resources as browser resources to the browser/ folder. An advanced method but produces ugly URLs for your resources.
  • add your resources to the skins/ folder of generated theme package. Easy, fast, usually less registration involved compared to the browser/ folder way.

You are free to chose any way you want to add your static resources to your plone site. But in this tutorial we will follow the third option - with skins/ folder as the easiest one since we already have a pre-generated structure within skins/. If you do so, most probably you need to change paths to your resources like images in your style sheets.

Some notes

  • You might want to add a new folder in skins/ for your JavaScripts for example. ZopeSkel doesn’t generate a JavaScripts folder for you. If you are adding such new folder within skins/ add a registration for that folder in skins.zcml and profiles/default/skins.xml
  • If you want to keep the folders’ names as they are in your static theme (like images/ or styles/ etc.) don’t forget to edit profiles/default/skins.xml.
  • If you add any CSS or JS resource don't forget to register it in profiles/default/cssregistry.xml or profiles/default/jsregistry.xml respectively in order to put your resource under the portal_stylesheets and portal_javascripts tools management.

Restart your instance and re-install the theme to make all your recent changes take effect. Once you've made sure your theme is installed and all your resources are available and registered you can start writing the transformation rules.

Enable theme transformation

Take a look at how to make your Plone site work with theme transformations

When you have collective.xdv installed in your site you get a new configuration in the Plone control panel called “Theme transform”. This is where you control your transformations.

To start transformation work you have to specify a path to your theme’s static HTML page (index.html) and rules file (rules.xml). File names don’t really matter but usually it’s a good idea to follow general conventions. The “Theme template” and “Rules file” configuration fields are responsible for specifying these. Both of them accept a relative path to the corresponding files. The paths start at buildout’s top folder not your theme’s top folder.

In the theme's root folder we create rules.xml file with the following code:

<?xml version="1.0" encoding="UTF-8"?>
<rules xmlns="http://openplans.org/deliverance">

</rules>

index.html file has been put into skins/xdvtheme_xdvrocks_custom_templates/ when we copied our static theme to the Plone theme. Thus in the “Theme transform” configuration in the “Theme template” field we put

src/xdvtheme.xdvrocks/xdvtheme/xdvrocks/skins/xdvtheme_xdvrocks_custom_templates/index.html

And in the “Rules file” field we add

src/xdvtheme.xdvrocks/xdvtheme/xdvrocks/rules.xml

After you have added these paths you should explicitly enable theme transformations since collective.xdv is disabled by default when you install it. So select “yes” in “Enabled” field and save the configuration.

Since our rules.xml doesn't contain any rule yet you should now see your site rendering the un-styled HMTL of your static theme - even without your theme’s images. We will fix this in a moment. But to do this we should understand what rules we have available and what those rules do.

XDV rules

What rules XDV has and how they work.

XDV has a basic set of rules that should solve most of the theming needs. Rules are given in order of their application.

  • before - copies specified element(s) from the Plone site right before an element in the theme;
  • drop - drops the whole element(s) either in Plone or in the theme. Has 2 optional parameters:
    • content=“” - specifies element(s) in the Plone site to drop. If this parameter is specified no other parameters in the same rule are taken into account;
    • theme=“” - specifies element(s) in the theme to drop.
    • if-content=“” - condition that only works together with the theme="" parameter. If if-content=“” returns True, the node specified in theme="" is dropped from the output. if-content tests against element(s) in the Plone site.
  • replace - replaces the whole element(s) in the theme with the element(s) from the Plone site;
  • prepend - copies specified element(s) from the Plone site as the very first child of an element in the theme;
  • copy - copies specified element(s) from the Plone site into an element in the theme;
  • append - the same as prepend, but the Plone element(s) is copied as the very last child of an element in the theme;
  • after - copies specified element(s) from the Plone site right after an element in the theme.

Take a look at pages 27-33 of the From design to Plone site. XDV-driven Plone theming. presentation for a visual explanation of each of the rules.

Rules except copy/replace/drop can be applied more than once to each element in the theme. This means that you can’t apply 2 replace or 2 copy rules to the same element in the theme. Though it’s not a big loss - if you need to do this there must be something wrong with your theme.

In general all rules support the if-content condition, not only <drop />. But using it on <drop /> only should solve most of the use cases.

If more than one distinct rule is applied to the same theme element, the order above will be used and the rule that has the higher priority will be enforced. This means that if you have copy and replace rules on the same theme element, replace will be enforced no matter what the order of these rules is in your rules.xml. All rules use XPath version 1.0 for specifying elements.

XPath

How to get XPath

XDV uses XPath 1.0 to specify elements on which the transformation should happen. If you donʼt feel comfortable with this, wait until you find out how easy it is to get XPath from a CSS selector. If after this you will still don't feel comfortable, you might want to check out Deliverance which allows both XPath and basic CSS selectors for specifying elements. Here are some tools you can use.

Firebug

One of the easiest ways of getting XPath is to use the Firebug extension for Firefox.

Pros:

  • works in offline mode;
  • is already your main (I suppose) debugging tool.

Cons:

  • you need to have two different tabs running your Plone site - one for the site without transformation (by default from 127.0.0.1:8080) and another one for the transformed site with the theme applied (by default localhost:8080);
  • the quality and complexity of XPath returned in this case might be not good enough for your needs.
  • Firebug gives XPath for only one particular element that you have clicked. This means it can't give XPath for more than one element and can't make it recursive. So, for instance, that doesn't allow you to get XPath for all list items in unordered lists.

CSS2XPath

Being a CSS person, I prefer getting XPath from CSS selectors. I can construct CSS selectors fully understanding what they are supposed to return. So for me getting XPath from a CSS selector is the most advanced, flexible and clear way. Ian Bicking wrote an online tool that does exactly that - it converts a CSS selector into XPath - http://css2xpath.appspot.com/.

Pros:

  • intuitively clear;
  • you work with what you really know;
  • the resulting XPath is usually much better than the one from Firebug.

Cons:

  • it is online.

Python

You can get the same results as Ianʼs tool gives you but in offline mode. For this you will need to have lxml for Python installed. Sounds scary, right? But the good news is - if you have an instance running with collective.xdv you already have lxml for your python installed. In your buildoutʼs folder you just need to run

./bin/zopepy 

You will get a Python prompt that will make all products installed for your instance including lxml (since it is a dependency for collective.xdv) available for you. So we can do the following:

>>> from lxml.cssselect import css_to_xpath 
>>> css_to_xpath("div + p") 
"descendant-or-self::div/following-sibling::*[name() = 'p' and (position() = 1)]"

Now you can use the returned XPath for specifying elements in your rules.xml.

Writing rules

How to compose and use actual rules when you're working with rules.xml

In your rules.xml add:

<replace content='/html/head/title' theme='/html/head/title' />

<drop theme="/html/head/style" if-content="/html/head/style"/>
<drop theme="/html/head/script" if-content="/html/head/script"/>    
<drop theme="/html/head/link" if-content="/html/head/link"/>

<append  content="/html/head/base" theme="/html/head" />
<append  content="/html/head/meta" theme="/html/head" />
<append  content='/html/head/style' theme='/html/head' />
<append  content='/html/head/script' theme='/html/head' />     
<append  content="/html/head/link" theme="/html/head" />

The first rule replaces <title /> in our static theme with the real title from Plone.

The second set of rules removes <style/>, <link/>, <script/> elements from our theme’s <head/> should Plone provide such elements (note the if-content in those <drop/> rules).

The third set of rules makes sure that we have all necessary bits from Plone’s <head/>.

Now the images. The reason why images are not rendered is because our style sheets are still trying to get the images from the ../images folder. The best way to make images render in our Plone theme is to use the “find & replace” function in our style sheets and replace relative paths for the images with just their IDs so that Plone searches for the images from site’s root.

After doing this your Plone site should look the same as your static index.html.

Now we can actually map content from Plone to our theme.

First, let’s add some useful stuff to our <body />

<prepend theme="/html/body" content="/html/body/@class" />
<prepend theme="/html/body" content="/html/body/@id" />	
<prepend theme="/html/body" content="/html/body/@dir" />

Now let’s make logo have a proper link and some more stuff

<prepend content="//*[@id='portal-logo']/attribute::href" 
        theme="//*[@id='logo']/a" />
<prepend content="//*[@id='portal-logo']/attribute::accesskey" 
        theme="//*[@id='logo']/a" />
<prepend content="//*[@id='portal-logo']/img/attribute::title" 
         theme="//*[@id='logo']/a" />

With these we linked the logo to the site’s root and provided some usability/accessibility helpers.

Now let’s replace our dummy site actions with something more useful

<replace content="//*[@id='portal-siteactions']" 
         theme="//*[@id='portal-siteactions']" />

… and copy the global navigation items into our theme

<copy content='//*[@id="portal-globalnav"]/li' 
      theme='//*[@id="global-navigation"]/ul' />

Now let’s populate the columns with real portlets. In the example theme we have only one column - the right one. But we also have portlets located below the content. So, to keep things simple, we will populate the bottom portlets with portlets from the standard left column:

<drop theme="//*[@id='bottom-portlets']" 
     if-content="not(//*[@id='portal-column-one']/div[@class='visualPadding']/*)"/>
<drop theme="//*[@id='column']" 
     if-content="not(//*[@id='portal-column-two']/div[@class='visualPadding']/*)"/>

<copy content="//*[@id='portal-column-one']/div[@class='visualPadding']/*" 
   theme="//*[@id='bottom-portlets']" />
<copy content="//*[@id='portal-column-two']/div[@class='visualPadding']/*" 
	theme="//*[@id='column']" />

The first set uses very powerful rules - the conditional drop. To remind you of the difference between drops: the regular drop drops the unnecessary element from Plone but the conditional drop removes the element from the theme on output if the ‘condition’ is True.

So, by using conditional drop rules, we want to avoid rendering the columns at all if Plone doesn’t give us any portlets for those columns.

The second set of rules copies portlets from Plone to our theme.

Now, let’s replace the dummy footer with the footer generated by Plone:

<copy content="//*[@id='portal-footer']/*" theme="//*[@id='footer']" />
<append content="//*[@id='kss-spinner']" theme="/html/body" />

We also append the spinner - the animated gif that is actually spinning when you do something in Plone - like save a document - and it takes some time to process the edit form. The spinner is appended to <body/> which means it will be the last child of the <body/> tag.

Finally let’s replace our dummy content with the content coming from Plone:

<copy content="//*[@id='portal-column-content']/*" 
      theme="//*[@id='document']" />

That’s pretty much it. You might want to check the example’s rules.xml though to see more rules that are used for polishing the result.

Tips & Tricks

Dirty and not so dirty tricks that make your life easier when working with collective.xdv

z3c.jbot

While XDV handles the look-and-feel of a page, it leaves the content part of the page to Plone itself. If you want to tweak something that comes from Plone most probably you will still need to customize the template on the Plone level as you do it with a regular Plone theme.

Customizing Plone templates might be not fun. But z3c.jbot (that stands for “Just a Bunch Of Templates”) comes to rescue. We will not dive into details of how to install this package - you can find this out at the product’s page. This package will add a couple of milliseconds to the overall page’s loading, but I think it is worth that.

With the package you can override pretty much any resource or template in Plone no matter whether it is a portlet, view, viewlet or just a regular ZPT.

Change DOCTYPE.

collective.xdv’s output is XHTML 1.0 Transitional by default. You can't "read" the Doctype from your theme's index.html at the moment. Sometimes you might want to change it to another Doctype if your theme requires this. For example you want to have XHTML Strict.

To do so, we need to override standard boilerplate coming from xdv. Of course overriding the whole template is a bad idea because you will need to tune and adjust it every time the boilerplate in XDV is changed. Thus collective.xdv allows you to “extend” the standard XDV. In order to do so you will need to write XSLT, but this time it’s pretty basic and, hey, I have an example for you!

In order to override Doctype (or frankly any output feature of XDV) create a file called extra.xsl in your theme’s root folder with the following content:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output
      doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
      doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"/>
</xsl:stylesheet>

This will result in <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

Once you have extra.xsl in your theme you can let xdv transformations know where to get your extensions. Go to the “Theme transform” configuration and add

    
src/xdvtheme.xdvrocks/xdvtheme/xdvrocks/extra.xsl

in the “XSLT extension file” field. This will give you an output HTML with the specified Doctype.

For XHTML compatible output it is necessary to use with XHTML1.0 transitional or strict doctypes. No other doctype will trigger the compatibility mode in the xml serializer.

For HTML 5 compatible output, use of the XHTML1.0 strict doctype is recommended, as it is an "obsolete permitted doctype string

XDV Plone theme in virtual machine

If you are using any virtual machine for testing your theme in different OS’s and browsers most probably your virtual machine will access your Plone site using a specific IP. In this case you might find that your Plone site doesn’t get theme transformations applied when viewed from your virtual machine. To solve this just add that IP (with the port most probably) to the “Domains” field of “Transform settings” configuration.

ZMI access

It’s always a good idea to keep the ZMI of your site on a separate domain, or access it by IP, to avoid transformations being applied to it.