Generate Skin Extension Content with Velocity

Last modified by Eleni Cojocariu on 2026/09/30 18:26

Steps

Generate a skin extension's code with Velocity instead of writing it by hand, so one extension can serve different CSS or JavaScript depending on the reader, on the request and on the pages around it.

  1. Set the object's "Parse content" field to "Yes". What the browser receives is still plain CSS or JavaScript: the Velocity runs on the server, and none of it is left in the served file.
  2. Three global Velocity variables are available in parsed content: $doc, $request and $xcontext. $doc is the page holding the extension object, not the page being read, which is what makes the attachments of the extension page addressable from its own code.
  3. Keep in mind that Velocity knows nothing about CSS. Everything shaped like a directive or a reference is evaluated, and anything undefined is printed unchanged, which is the only reason a CSS id survives at all. Define a macro of that name in the same content and the selector is eaten, with nothing to say so:
    #macro (xwikicontent)/* eaten */#end
    #xwikicontent { background-color: lightBlue; }

    That is served as:

    /* eaten */ { background-color: lightBlue; }
  4. Serve different content per reader with the ordinary directives. In a JavaScript extension:
    #if (!$xcontext.userReference)
    alert("Hello guest!");
    #else
    alert("Hello user!");
    #end

    and in a style sheet extension:

    #xwikicontent {
    #if (!$xcontext.userReference)
      background-color: #f5f5f5;
    #else
      background-color: #ffffff;
    #end
    }

    An extension that varies like this must not be cached: its address is the same for every reader.

  5. Escape every value that reaches the JavaScript, because Velocity substitutes it as code and not as text. alert(false); works, false being a literal in both languages, but alert($xcontext.user); arrives as alert(XWiki.Admin);, which the browser reads as a property of the global XWiki object it already defines: it alerts undefined instead of the user name. Quoting alone is not enough either, since a value may hold characters that are illegal inside a JavaScript string. The rule is:
    alert('$escapetool.javascript($xcontext.user)');
  6. Build the address of a file rather than hard-coding it. This is the most common reason a background image never appears: with "Parse content" left at "No" the code building the address is never evaluated. A file on disk is looked up in the current skin, then the base skin, the default skin and the templates directory, which Skins describes:
    background-image: url($xwiki.getSkinFile("path/to/the/image.png"));

    An attachment of the extension page itself, and an attachment of any other page:

    background-image: url($doc.getAttachmentURL("image.png"));
    background-image: url($xwiki.getDocument("Some.Document").getAttachmentURL("image.png"));

    Naming the attachments after what they decorate then lets one loop write a rule per case instead of one rule per file:

    #foreach ($ext in ['odt', 'ods', 'pdf', 'zip'])
    #xwikicontent a.$ext {
      background: transparent url($doc.getAttachmentURL("${ext}.png")) no-repeat scroll right center;
    }
    #end
  7. Pass the extension something to work with. There are three ways, and the first is the one to use:
    #set ($discard = $xwiki.jsx.use('My.JavaScript', {'myParameter': 'value'}))
    • The parameter map of the pull call. Its entries reach the extension as request parameters, read in the content as $request.myParameter; Skin Extension Script API documents the map and the four names that already mean something.
    • Reading the data out of the HTML page, from the JavaScript itself.
    • Setting an attribute in the user session and reading it back in the parsed content. This one costs on both counts: the value is cached by the server unless caching is turned off, and the session grows.

    All three serve a style sheet extension too, except the second: CSS cannot read anything out of the page.

  8. Read the generated file back to see what the browser gets. Its address is the one written into the head element of any page that pulls it, /xwiki/bin/ssx/My/CSS for a style sheet and /xwiki/bin/jsx/My/JavaScript for a script; add minify=false to it and the Velocity is already gone:
    #xwikicontent {
      background-color: #ffffff;
    }
    #xwikicontent a.pdf {
      background: transparent url(/xwiki/bin/download/Sandbox/ParsedStyleSheet/WebHome/pdf.png) no-repeat scroll right center;
    }

FAQ

The served file contains a literal $request.myParameter. Why?

Velocity prints a reference it cannot resolve unchanged rather than replacing it with nothing, so a parameter the pull call did not pass ends up in the file as its own name. Write $!request.myParameter for an empty value instead, or test the parameter with #if before using it.

Related

Get Connected