JavaScript Compression in SDL Tridion (minification and obfuscation. CSS too)

I’ve been building up my jot notes throughout the year and finally got the chance to make them into publishable form, which lead me to this sudden series of articles. Thanks, John, for letting me post on your site.

To achieve maximum compression on a JavaScript file, minification is not enough.  Obfuscation is where it’s at.  This means replacing variable names to the shortest amount of characters (or bytes) that the language syntax allows.  For example: var myReallyLongVariableName=”foobar” would become var a=”foobar”; but only within its scope, and so on.  You basically have to implement a decent chunk of code that usually comes as part of any language interpreter or compiler.  It’s quite a bit more involved than minification alone.  My point is, use a proven package that does this and just hook it up to the best CMS known to man.

My preferred way is actually to not store JavaScript or CSS in Tridion.  Put it with the rest of your website’s code into the repository like TFS or Subversion.  Basically, bundle it into your build process which should go through Change Management with the likes of JAR/DLL files and DB packages.  JS is code after all.  This way you can just add a step to your build process.  Ask the build master to crunch the JS/CSS through a compressor (possibly as a manual step) before or automate within an ANT, NANT or MSBuild script.  If, however, you are a content author or Support person that hates dealing with your org’s Change Management process or have some other reason to store js and/or CSS code as easily publishable as content, then this is for you.

A requirement I often get requests for is to minify and obfuscate JS files which are stored in Tridion, either in Multimedia Components or pasted into a text field of a normal component.    Minification is trivial.  Simply remove the extra whitespace and comments.  Obfuscation, however, is a bit more involved as I mentioned above.  So let’s find a good JS compressor package that’s available today.

YUI Compressor is the best minification and obfuscation library that is out there.  It was created by engineers at Yahoo!.  Since the original package is in Java, but Tridion templating is done in C#, we’ll turn to the YUI Compressor .NET port (link is below).

There are two classes with methods that do what we need to do:

  1. CSSCompressor.Compress(string)
  2. JavaScriptCompressor.Compress(string)

Both of the above return string.  So all we need to do is extract the CSS or JavaScript content from our component as a string, call the YUI Compressor methods and publish the CSS or JS file.

There are a few ways that a CSS/JS file can be published:

1) Static publishing model – the component holding the JS is added to a Page and published (using a simple page template).  Our fellow Tridion community member, Jonathan Whiteside, has an excellent article series that dives into these details in his discussion on Tridion CSS Minification (http://blog.building-blocks.com/2011/01/tridion-tbb-automatically-minify-css-javascript-on-publish).

2) Dynamic publishing model – the component is published via a dynamic component template that relies on a TBB which extracts the JS content from the component and calls the AddBinary method with a Structure Group URI as a parameter.  No Page is necessary here, but you do need at least the file system Broker running.

My aim in this post is not to get into the details of #1 or #2 above.  I will show an example that extracts the JS or CSS as a string from the MM Component (using Jonathan’s method), call on the YUI Compressor to obfuscate and minify, and place the compress string as a variable onto the Package.  You can do whatever you wish with it at that point – put the TBB into a static or dynamic CT.

Here we go:

  1. Download the .NET port of the YUI Compressor from here: http://yuicompressor.codeplex.com/
  2. Take these DLLs and put them in the GAC or the Tridion Bin folder on your Content Manager server(s).  Make sure these DLLs are on all the CM servers that the TBB will need to execute on (all load balanced CM nodes, Dev, QA, Prod, etc).  Make sure to submit a Change Request to have these DLLs installed on anything past the Development environment, but if you have a good, no BS rep with your server admins (like I always do ;), this shouldn’t be a problem.
  3. Make sure the Template Base class (which is part of Will’s Get Linked Components extension) is added into your Visual Studion project.
  4. Create a new Template Building Block class in Visual Studio with this code:
    using System;
    using System.Text;
    using System.Text.RegularExpressions;
    using Tridion.ContentManager;
    using Tridion.ContentManager.ContentManagement;
    using Tridion.ContentManager.Templating;
    using Tridion.ContentManager.Templating.Assembly;
    using ContentBloom.Tridion.B2C.Templates.Base;
    using System.IO;
    using Yahoo.Yui.Compressor;
    using System.Globalization;
    
    namespace ContentBloom.Tridion.B2C.Templates
    {
        [TcmTemplateTitle("Compress JS and CSS using YUI Compressor")]
        public class CompressYUI : TemplateBase
        {
            enum BinaryType
            {
                JS, CSS, Other
            }
    
            public override void Transform(Engine engine, Package package)
            {
                // Initialize
                Initialize(engine, package);
                string codeContent;
    
                // Get CSS Content
                Logger.Debug("Retrieving code from Component.");
                Component comp = this.GetComponent();
                string filename = Utilities.GetFilename(comp.BinaryContent.Filename);
                BinaryType bt = GetFileType(filename);
                if(bt == BinaryType.JS || bt == BinaryType.CSS)
                {
                    string needsCompression = package.EvaluateExpression("Component.Metadata.compress");
                    // if the "Compress" field is not specified, then default it to True.
                    if (string.IsNullOrEmpty(needsCompression) || needsCompression == "True")
                    {
                        codeContent = GetCodeContent(comp);
    
                        try
                        {
                            Logger.Debug("Before minification:\n" + codeContent);
                            switch (bt)
                            {
                                case BinaryType.CSS:
                                    codeContent = CssCompressor.Compress(codeContent);
                                    break;
                                case BinaryType.JS:
                                    codeContent = JavaScriptCompressor.Compress(codeContent, true, true, false, false, -1, Encoding.UTF8, new CultureInfo("en-US", false));
                                    break;
                            }
                            Logger.Debug("After minification:\n" + codeContent);
                        }
                        catch (Exception e)
                        {
                            string message = e.Message;
                            if (e.Message.Contains("syntax error"))
                            {
                                message = "There is a problem with the encoding of the file. Please change the encoding of the file to UTF-8 WITH NO BYTE ORDER MARK/UTF-8 without signature.";
                            }
                            throw new InvalidDataException(message, e);
                        }
    
                        //push to Output.
                        package.PushItem("Output", package.CreateStringItem(ContentType.Text, componentCount.ToString(codeContent)));
                    }
                    else
                    {
                        Logger.Info("Component metadata \"Compress\" field specifies to avoid compression. Therefore, exiting.");
                    }
                }
                else
                {
                    Logger.Info("No CSS or JS file found, so nothing to do.");
                }
            }
    
            private BinaryType GetFileType(string filename)
            {
                Logger.Debug("Entering method GetFileType");
                BinaryType bt = BinaryType.Other;
                filename = filename.ToLower();
                Logger.Debug("filename=" + filename);
                if (filename.EndsWith(".css"))
                {
                    bt = BinaryType.CSS;
                }
                else if (filename.EndsWith(".js"))
                {
                    bt = BinaryType.JS;
                }
                Logger.Debug("Exiting method GetFileType");
                return bt;
            }
    
            ///
            /// Get the CSS content in plain text from the current component
            ///
            /// CSS content in plain text
            private string GetCodeContent(Component c)
            {
                // code path
                string codeContent = string.Empty;
    
                // read the css into a string
                UTF8Encoding encoding = new UTF8Encoding();
                byte[] binary = c.BinaryContent.GetByteArray();
                if (binary != null) codeContent = encoding.GetString(binary, 0, binary.Length);
    
                return codeContent;
            }
        }
    }
    
  5. Finally, add the TBB to your Component Template and enjoy the sexiest minified and obfuscated JavaScript.

Minify/Obfuscate Only on Demand

You can go one step further and add a bit of logic to minify and obfuscate only for certain scripts, e.g. obfuscate one, but not the other.  Here’s what you can do…

In your JavaScript Multimedia Component schema add a couple of Text metadata fields:

  1. Field name: “Minify”.  Value: Select from Dropdown “Yes/No”.
  2. Field name: “Obfuscate”.  Value: Select from Dropdown “Yes/No”.

Modify the TBB above to read these fields from the Component and minify/obfuscate accordingly.

3 thoughts on “JavaScript Compression in SDL Tridion (minification and obfuscation. CSS too)

  1. Hey Nickoli,

    I am trying to follow along this example but my template builder keeps complaining that it can’t load the Yahoo compressor dll. I’ve copied all the the downloaded dll’s to the Tridion bin folder but still it can’t find them. I don’t want to deploy them to the GAC it’s less obvious that way what is being used. Any idea’s?

    Kind regards,
    Ryan.

  2. Hi Ryan, I’m stoked that you’re using this extension! I see that you’ve raised the question on Stack Exchange and looks like figured out where Template Builder is loading the assembly from. I’ve added an answer, though it’s probably not what you wanted to hear, nevertheless, here it is: http://tridion.stackexchange.com/a/5616/159

    Though this relates to a more generic question that’s been raised about loading 3rd party DLLs. Here is that thread:
    http://tridion.stackexchange.com/questions/2160/do-3rd-party-dlls-in-the-tridion-2011-event-system-need-to-be-registered-in-the/2166#2166

  3. Thank you for your reply. I am also planning to integrate it with sass.net so our css can have all that sassy goodness in it 😉

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>