CSS in SDL Tridion - Part 2: Handling image references

This is the second in a series of 3 posts describing how Building Blocks manage storing and publishing CSS files in Tridion. The series details:

  1. Storing and publishing CSS files
  2. Handling image references
  3. Minimising CSS files on publish

In my first post I detailed how we maintain and publish CSS files from Tridion using WebDav and some of the alternative methods. This allows the front end developer to work directly with CSS in Tridion and for publishing to follow the same workflows as other pages.

This just gets the CSS file onto the presentation server in the same state it is saved by the front end developer. But in most cases a CSS file will contain references to images. We want to manage this within Tridion too, so publishing the CSS file also deploys all the image dependencies to the presentation server.

We really don't want to have to make any special changes to the CSS file to get the images to be published (like add TCM ID references) - ideally the CSS file that the front end developer has working on their local machine can be saved straight into Tridion without any changes or "Tridionisation".

This is how we manage to achieve this.

Upload the images into Tridion

The easiest and most efficient way to do this is by using webdav. This simply involves the developer saving all the CSS images into a folder on the mapped drive they've previously setup to update the CSS files. But where should we save the images? The CSS references the images in a folder at the same level as the folder containing the css files (main.css is stored in a folder named "css").

So we save the images in the same relative location in Tridion.

Any sub folders can be created inside the images folder too.

You'll notice that we're saving these images into the "system" folder - a location a content editor may not have access to. In our case the images referenced by the CSS are site furniture type images used for styling the website and not content images so storing them in the "system" folder is ok.

Publishing images

We want any images referenced by the CSS to be deployed when the CSS file is published. A Component Template is the perfect place to do this as the code will be executed only when publishing the CSS file.

In the last post we had an example of a CT that simply gets the contents of the file in the multimedia (mm) component and pushes it into the Output item in the package. We'll modify this code so if the file in the mm component is a CSS file some extra logic is run to resolve and publish the image references.

The code below is a simple example to illustrate the technique. We should extend it remove any hardcoded values reading them from a parameter schema instead, but for simplicity (and space) we'll not.

First we need a function to locate the images folder. The function below takes the CSS multimedia component as an input and looks for a folder at the same level as the files's parent folder for another named "images".

[csharp]
private Folder GetImagesFolder(Component cssComponent)
{
if (cssComponent!=null)
{
// get the parent folder for "images" and "css".
Filter filter = new Filter();
filter.Conditions["ItemType"] = ItemType.Folder;

// loop through all folders at the same level as the folder containing the css file
foreach (Folder f in cssComponent.OrganizationalItem.OrganizationalItem.GetItems(filter))
{
// find the folder named "images"
//TODO: remove hardcoded reference
if (f.Title.ToLowerInvariant()=="images")
{
// found the folder, return the object
return f;
}
}
}else{
Log.Warning("CSS Component is NULL");
}

// nothing found, return null
Log.Warning("CSS Component not found");
return null;
}
[/csharp]

The main work is done in the "Transform" method. What we do is:

  1. Get the contents of the CSS file from the MM component being published
  2. Run a regular expression on the CSS content to find any image references
  3. For each image reference we build a webdav path using the "GetImageComponent" function. This function returns a image mm component from a relative CSS path
  4. If an image component is returned by "GetImageComponent" we publish it (using a very simple publish command). If the image cannot be found we make a note of it
  5. Replace the original image reference in the CSS with the publish path URL returned by publishing the image binary
  6. At the end of the file we write out any unresolved image paths and the date/time the file was published for debugging
  7. Add the updated CSS file into the Output item in the package

(Note that we're using the TemplateBase code found on Tridion World for the helper functions)

[csharp]
public override void Transform(Engine engine, Package package)
{
//initialize
Initialize(engine, package);

// get the component being rendered
Component c = GetComponent();

// get reference to the images folder
Folder imagesFolder = GetImagesFolder(c);
if (imagesFolder==null)
{
Log.Error("Images Folder not found... cannot continue");
return;
}

// read css in mm component into a string
UTF8Encoding encoding = new UTF8Encoding();
byte[] binary = c.BinaryContent.GetByteArray();
string css = (binary != null) ? encoding.GetString(binary, 0, binary.Length) : string.Empty;

// get the image references
Regex pattern = new Regex(@"url((S*))");

// list of paths where images weren't found
string unresolvedImagePaths = string.Empty;

// loop through each image referenced...
foreach (Match m in pattern.Matches(css))
{
string cssImagePath = m.Groups[1].Value.Replace(""", "");
string publishedImagePath = string.Empty;

Component cssComponent = GetImageComponent(imagesFolder.WebDavUrl, cssImagePath, engine);

if (cssComponent!=null)
{
// simple publish
Binary b = engine.PublishingContext.RenderedItem.AddBinary(cssComponent);
publishedImagePath = b.Url;

// update the CSS with the published path
css = css.Replace(cssImagePath, publishedImagePath);
}else{
// couldn't resolve the path
Log.Info("Unresolved path: " + cssImagePath);
unresolvedImagePaths += cssImagePath + Environment.NewLine;
}
}

// write at the bottom of the published file any unresolved image paths and the date/time the file was published
css += string.Format("{0}/*-- Published at {1} --*/", Environment.NewLine, DateTime.Now.ToString());
css += string.Format("{0}/*-- Unresolved Paths --{0}{1}{0}*/", Environment.NewLine, unresolvedImagePaths);

// Push css back into the package's output
package.PushItem("Output", package.CreateStringItem(ContentType.Text, css));
}
[/csharp]

And the "GetImageComponent" function which takes as an input: the webdav path of the images folder in Tridion; the image page referenced in the CSS file; the Tridion engine.

Using the images folder webdav path and the image path, the function creates a webdav for the image and then tries to get a reference to the mm image component using the engine. If an image is found then the mm component is returned. If not then null is returned.

[csharp]
private Component GetImageComponent(string imageFolderWebDav, string cssImagePath, Engine engine)
{
// we're already in the images folder, so get rid of the reference to it from the css path
// TODO: remove hardcoded reference to "../images"
cssImagePath = cssImagePath.Replace("../images", "");
Component img = null;

try
{
// build the full webdav path for the image, using the folder path and the image name (and any subfolders)
img = (Component)engine.GetObject(string.Format("{0}{1}", imageFolderWebDav, cssImagePath));
}catch(Exception ex){
Log.Error(string.Format("Cannot resolve component webdav {0}{1}", imageFolderWebDav, cssImagePath), ex);
}

return img;
}
[/csharp]

Running this TBB in a Component Template should now resolve and publish the images (or create preview URLs). To test, we'll run in the Template Builder.

In the item pane, the relative image paths have now been updated with links to the image preview URLs. We're also alerted of any broken image references in the Output pane.

Conclusion

This method allows the front end developers to work locally on the CSS as usual without having to add to or change the file when adding into Tridion. Publishing the CSS file will now deploy all related images onto the server and update the paths in the published CSS file, without changing the source in the component.

Any questions?

If you need more information or have any questions just get in touch and we'd be happy to answer them for you.