How to set up Smidge properly in Umbraco

Posted written by Paul Seal on September 12, 2023 Umbraco

In a project at work , our front end developer was complaining that the changes they made to the styles, when developing an Umbraco site locally, would not update unless they did a rebuild of the solution.

I had experienced this issue too and I wasn't happy with it and knew there would be a way to get it to update without needing to rebuild every time.

So I asked for help in the Umbraco Discord Server and the lovely Mike Chambers came to the rescue. He had already had the same issue a while ago and was helped by a former Umbraco HQ employee who told him how to do it.

This post gives you the final solution we got to after going back and forth and reading other threads etc.

AppSettings

First of all I needed to work out which app settings were needed for dev and production. I settled on this:

Development

"RuntimeMinification": {
    "UseInMemoryCache": true,
    "CacheBuster": "Timestamp"
}

Apparently the timestamp cache refreshes after 5 seconds.

Production

"RuntimeMinification": {
    "UseInMemoryCache": true,
    "CacheBuster": "AppDomain"
}

With AppDomain the cache is refreshed every time the app restarts.

I didn't want to use version number because I am sick of forgetting to increment the version number from the Client Dependency days and I'm using Umbraco Cloud without a devops pipeline to automatically increment the version number.

Create your bundles

Ideally you should do this at the end of the Configure method in the Startup.cs file

app.UseSmidge(bundles =>
{
    bundles.CreateCss("home-css", "~/css/homepage.css"
                            ,"~/css/general-styles.css");

    bundles.CreateJs("home-js", "~/js/maps.js",
                                "~/js/homepage.js");
});

IRuntimeMinifier service

The Umbraco backoffice uses an IRuntimeMinifier service that you can inject into your views.

The advantage of using this rather than the SmidgeHelper is that this checks if you are in debug mode or not and renders the correct script accordingly, otherwise you would need to add a debug attribute to your links and scripts and this saves you from having to do that. So the code in the View is the same when you are working locally and when it gets pushed up to the next environment.

Here I am using the IRuntimeMinifier service to generate the CSS link and I am just using an MVC section so it goes in the correct place in the master view.

@using Umbraco.Cms.Core.WebAssets;
@inject IRuntimeMinifier RuntimeMinifier

@section Head {
    @Html.Raw(await RuntimeMinifier.RenderCssHereAsync("home-css"))
}

That will render out this in the markup in development

<link href="/css/homepage.css?d=1" rel="stylesheet">
<link href="/css/general-styles.css?d=1" rel="stylesheet">

We can do the same with the scripts too

@section ScriptsBottom {
    @Html.Raw(await RuntimeMinifier.RenderJsHereAsync("home-js"))
}

That will render out this in the markup in development

<script src="/js/maps.js?d=1"></script>
<script src="/js/homepage.js?d=1"></script>

That all works perfectly for me and solved my problems.

The only thing left was that I might want to add would be attributes such as defer or async, so I created some extension methods to help with that.

public static string Defer(this string value)
{
    return value.AddAttributes(new Dictionary<string, string>() { { "defer", "" } });
}

public static string Async(this string value)
{
    return value.AddAttributes(new Dictionary<string, string>() { { "async", "" } });
}

public static string PreloadJs(this string value)
{
    return value.AddAttributes(new Dictionary<string, string>() { { "rel", "preload" }, { "as", "script" } });
}

public static string PreloadCss(this string value)
{
    return value.AddAttributes(new Dictionary<string, string>() { { "rel", "preload" }, { "as", "style" } });
}

public static string AddAttributes(this string html, Dictionary<string, string> attributes)
{
    if (string.IsNullOrEmpty(html) || attributes == null || attributes.Count == 0)
    {
        return html;
    }

    var regex = new Regex(@"<\w+[^>]*>");
    var matches = regex.Matches(html);

    if (matches.Count == 0)
    {
        return html;
    }

    foreach (Match match in matches)
    {
        var tag = match.Value;
        var content = tag.Substring(1, tag.Length - 2);

        var parts = content.Split(' ').ToList();
        var element = parts[0];
        var existingAttributes = parts.Skip(1).ToList();

        foreach (var attribute in attributes)
        {
            var name = attribute.Key;
            var value = attribute.Value;

            var index = existingAttributes.FindIndex(a => a.StartsWith(name + "="));

            if (index >= 0)
            {
                existingAttributes[index] = $"{name}=\"{value}\"";
            }
            else
            {
                if (value == "")
                {
                    existingAttributes.Add(name);
                }
                else
                {
                    existingAttributes.Add($"{name}=\"{value}\"");
                }
            }
        }

        var newTag = $"<{element} {string.Join(" ", existingAttributes)}>";

        html = html.Replace(tag, newTag);
    }

    // Return the new html string
    return html;
}

Now I can use them like this in my Views:

@Html.Raw(RuntimeMinifier.RenderJsHereAsync("home-js").Result.Defer())

And it will output the script tags like this:

<script src="/js/maps.js?d=1" defer></script>
<script src="/js/homepage.js?d=1" defer></script>

I hope this helps you out at some point too.

If you want to read the discussion I had in discord you can view it here: https://discord-chats.umbraco.com/t/15696918/optimal-settings-for-smidge-runtimeminification-in-local-dev