294

My question is similar to this:

ASP.NET MVC 4 Minification & Background Images

Except that I want to stick with MVC's own bundling if I can. I'm having a brain crash trying to figure out what the correct pattern is for specifying style bundles such that standalone css and image sets such as jQuery UI work.

I have a typical MVC site structure with /Content/css/ which contains my base CSS such as styles.css. Within that css folder I also have subfolders such as /jquery-ui which contains its CSS file plus an /images folder. Image paths in the jQuery UI CSS are relative to that folder and I don't want to mess with them.

As I understand it, when I specify a StyleBundle I need to specify a virtual path which does not also match a real content path, because (assuming I'm ignoring routes to Content) IIS would then try to resolve that path as a physical file. So I'm specifying:

bundles.Add(new StyleBundle("~/Content/styles/jquery-ui")
       .Include("~/Content/css/jquery-ui/*.css"));

rendered using:

@Styles.Render("~/Content/styles/jquery-ui")

I can see the request going out to:

http://localhost/MySite/Content/styles/jquery-ui?v=nL_6HPFtzoqrts9nwrtjq0VQFYnhMjY5EopXsK8cxmg1

This is returning the correct, minified CSS response. But then the browser sends a request for a relatively linked image as:

http://localhost/MySite/Content/styles/images/ui-bg_highlight-soft_100_eeeeee_1x100.png

Which is a 404.

I understand that the last part of my URL jquery-ui is an extensionless URL, a handler for my bundle, so I can see why the relative request for the image is simply /styles/images/.

So my question is what is the correct way of handling this situation?

Community
  • 1
  • 1
Tom W Hall
  • 5,153
  • 4
  • 27
  • 34
  • 9
    after been frustrated **over and over again** with the new Bundling and Minification part, I moved on to [**Cassete**](http://getcassette.net/) witch is now free and works way better! – balexandre Jul 06 '12 at 04:48
  • 3
    Thanks for the link, Cassette looks nice and I'll definitely check it out. But I want to stick with the provided approach if possible, surely this must be possible without messing with image paths in 3rd party CSS files every time a new version is released. for now I've kept my ScriptBundles (which work nicely) but reverted to plain CSS links until I get a resolution. Cheers. – Tom W Hall Jul 06 '12 at 05:05
  • Adding the likely error for SEO reasons: The controller for path '/bundles/images/blah.jpg' was not found or does not implement IController. – Luke Puplett Jun 05 '13 at 17:55

16 Answers16

363

According to this thread on MVC4 css bundling and image references, if you define your bundle as:

bundles.Add(new StyleBundle("~/Content/css/jquery-ui/bundle")
                   .Include("~/Content/css/jquery-ui/*.css"));

Where you define the bundle on the same path as the source files that made up the bundle, the relative image paths will still work. The last part of the bundle path is really the file name for that specific bundle (i.e., /bundle can be any name you like).

This will only work if you are bundling together CSS from the same folder (which I think makes sense from a bundling perspective).

Update

As per the comment below by @Hao Kung, alternatively this may now be achieved by applying a CssRewriteUrlTransformation (Change relative URL references to CSS files when bundled).

NOTE: I have not confirmed comments regarding issues with rewriting to absolute paths within a virtual directory, so this may not work for everyone (?).

bundles.Add(new StyleBundle("~/Content/css/jquery-ui/bundle")
                   .Include("~/Content/css/jquery-ui/*.css",
                    new CssRewriteUrlTransform()));
KyleMit
  • 45,382
  • 53
  • 367
  • 544
Chris Baxter
  • 15,423
  • 9
  • 49
  • 70
  • 1
    Legend! Yep, that works perfectly. I have CSS at different levels but they each have their own images folders, e.g. my main site CSS is in the root CSS folder and then jquery-ui is inside that with its own images folder, so I just specify 2 bundles, one for my base CSS and one for jQuery UI - which is maybe not uber-optimal in terms of requests, but life is short. Cheers! – Tom W Hall Jul 08 '12 at 22:41
  • 3
    Yeah unfortunately until bundling has support for rewriting embedded urls inside of the css itself, you need the virtual directory of the css bundle to match the css files before bundling. This is why the default template bundles don't have urls like ~/bundles/themes, and instead look like the directory structure: ~/content/theemes/base/css – Hao Kung Jul 13 '12 at 22:56
  • 1
    @HaoKung - I understand what you are saying but that doesn't explain why new StyleBundle("~/Content/css") doesn't match "~/Content/site.css", yet it still works. And if I want to add my custom CSS to that same Include method, it never resolves. Any ideas why this is? – Kahanu Jan 22 '13 at 17:48
  • 1
    @Kahanu - new StyleBundle( **"~/Content/** css") does match **"~/Content/** site.css". I've bolded the folder names for you, and left the filename unbolded. As you notice, the bolded parts do indeed match, which is why it works. – Robert McKee Feb 14 '13 at 18:49
  • 1
    @RobertMcKee - I actually finally buckled down and figured it out once and for all. I posted my results on MVCCentral.net. http://www.mvccentral.net/s/70 – Kahanu Feb 15 '13 at 22:26
  • 29
    This is now supported via ItemTransforms, .Include("~/Content/css/jquery-ui/*.css", new CssRewriteUrlTransform())); in the 1.1Beta1 should fix this issue – Hao Kung Mar 13 '13 at 16:58
  • 2
    Is this fixed in Microsoft ASP.NET Web Optimization Framework 1.1.3 ? I havend found any information about what is changed in this ? – Andrus Feb 25 '14 at 06:44
  • 14
    new CssRewriteUrlTransform() is fine if you have a website in IIS. but if its an application or sub application this will not work, and you have to resort to defining your bundle in the same location as you CSS. – avidenic Jul 15 '14 at 14:57
  • 1
    @avidenic - And it is exactly what I wanted to avoid with [this code](http://stackoverflow.com/a/27890912/551322) :) – nrodic Jan 11 '15 at 19:08
  • @nrodic - Nice, will try it out. – avidenic Jan 12 '15 at 07:24
  • This solution worked brilliantly for me. I was able to implement it without adding /* to the file paths. – Leonardo Wildt May 28 '15 at 14:21
  • @Chris Baxter - It works fine with the Visual Studio 2013 but I want to use "CssRewriteUrlTransform" with Visual Studio 2010 Bundling but fails to fix it. It shows me an error that namespace is missing for "CssRewriteUrlTransform", will you please let me know how to fix this issue in visual studio 2010? – Keyur Mistry May 18 '16 at 06:32
  • @KinjalMistry I do not have VS2010 installed to test and I no longer use ASP.NET MVC bundling; I would suggest asking a new question for help. – Chris Baxter May 18 '16 at 14:48
  • Oh wow, must have spent a few hours bashing my head against my badly named Bundles... I always wondered why I had been (automatically) naming my bundles with a convention and didn't realise till now. Also it's probably better not to rely on CssRewruiteUrlTransform() if you can do without. – JARRRRG Jan 12 '17 at 09:04
  • This is amazing. I've been struggling for several days to get this figured out. Finally. I owe you! – Paramone Jul 12 '17 at 07:54
34

Grinn / ThePirat solution works well.

I did not like that it new'd the Include method on bundle, and that it created temporary files in the content directory. (they ended up getting checked in, deployed, then the service wouldn't start!)

So to follow the design of Bundling, I elected to perform essentially the same code, but in an IBundleTransform implementation::

class StyleRelativePathTransform
    : IBundleTransform
{
    public StyleRelativePathTransform()
    {
    }

    public void Process(BundleContext context, BundleResponse response)
    {
        response.Content = String.Empty;

        Regex pattern = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase);
        // open each of the files
        foreach (FileInfo cssFileInfo in response.Files)
        {
            if (cssFileInfo.Exists)
            {
                // apply the RegEx to the file (to change relative paths)
                string contents = File.ReadAllText(cssFileInfo.FullName);
                MatchCollection matches = pattern.Matches(contents);
                // Ignore the file if no match 
                if (matches.Count > 0)
                {
                    string cssFilePath = cssFileInfo.DirectoryName;
                    string cssVirtualPath = context.HttpContext.RelativeFromAbsolutePath(cssFilePath);
                    foreach (Match match in matches)
                    {
                        // this is a path that is relative to the CSS file
                        string relativeToCSS = match.Groups[2].Value;
                        // combine the relative path to the cssAbsolute
                        string absoluteToUrl = Path.GetFullPath(Path.Combine(cssFilePath, relativeToCSS));

                        // make this server relative
                        string serverRelativeUrl = context.HttpContext.RelativeFromAbsolutePath(absoluteToUrl);

                        string quote = match.Groups[1].Value;
                        string replace = String.Format("url({0}{1}{0})", quote, serverRelativeUrl);
                        contents = contents.Replace(match.Groups[0].Value, replace);
                    }
                }
                // copy the result into the response.
                response.Content = String.Format("{0}\r\n{1}", response.Content, contents);
            }
        }
    }
}

And then wrapped this up in a Bundle Implemetation:

public class StyleImagePathBundle 
    : Bundle
{
    public StyleImagePathBundle(string virtualPath)
        : base(virtualPath)
    {
        base.Transforms.Add(new StyleRelativePathTransform());
        base.Transforms.Add(new CssMinify());
    }

    public StyleImagePathBundle(string virtualPath, string cdnPath)
        : base(virtualPath, cdnPath)
    {
        base.Transforms.Add(new StyleRelativePathTransform());
        base.Transforms.Add(new CssMinify());
    }
}

Sample Usage:

static void RegisterBundles(BundleCollection bundles)
{
...
    bundles.Add(new StyleImagePathBundle("~/bundles/Bootstrap")
            .Include(
                "~/Content/css/bootstrap.css",
                "~/Content/css/bootstrap-responsive.css",
                "~/Content/css/jquery.fancybox.css",
                "~/Content/css/style.css",
                "~/Content/css/error.css",
                "~/Content/validation.css"
            ));

Here is my extension method for RelativeFromAbsolutePath:

   public static string RelativeFromAbsolutePath(this HttpContextBase context, string path)
    {
        var request = context.Request;
        var applicationPath = request.PhysicalApplicationPath;
        var virtualDir = request.ApplicationPath;
        virtualDir = virtualDir == "/" ? virtualDir : (virtualDir + "/");
        return path.Replace(applicationPath, virtualDir).Replace(@"\", "/");
    }
AcidPAT
  • 679
  • 6
  • 11
  • This seems cleanest to me, too. Thanks. I'm voting all three of you up because it looked to be a team effort. :) – Josh Mouch Nov 08 '12 at 18:18
  • The code as you have it now isn't working for me. I'm trying to fix it, but thought I'd let you know. The context.HttpContext.RelativeFromAbsolutePath method doesn't exist. Also, if the url path starts with a "/" (making it absolute), your path combining logic is off. – Josh Mouch Nov 08 '12 at 18:27
  • @Josh - Just added the declaration for RelativeFromAbsolutePath to the answer. I have been always using "~/... for all the CSS File names so there is no ambiguity – AcidPAT Nov 16 '12 at 19:05
  • Thanks @AcidPAT. I ran into the similar issues with my solution when deploying. If I wanted to make a change to a single CSS file without re-publishing I'd have to restart the whole app so it would regenerate the .bundle files. – Grinn Dec 04 '12 at 21:40
  • 2
    @AcidPAT great work. The logic failed if the url had a querystring (some 3rd party libraries add it, like FontAwesome for its .woff reference.) It's an easy fix though. One can adjust the Regex or fix `relativeToCSS` before calling `Path.GetFullPath()`. – sergiopereira Feb 24 '13 at 21:37
  • @sergiopereira if only stackoverflow hadn't cut off your comment. ;-) – ms007 Apr 18 '13 at 16:07
  • I did a partial rewrite of the transformer that among other things accounts for the querystring issue @sergiopereira mentioned: https://gist.github.com/dotnetchris/3d1e4fe9b0fa77eefc82 I also spent effort improving variable names and reducing nesting/duplication. – Chris Marisic Aug 20 '14 at 18:29
  • 2
    @ChrisMarisic your code doesn't seem to work - response.Files is an array of BundleFiles, these object don't have properties such as "Exists", "DirectoryName" etc. – Nick Coad Dec 08 '14 at 05:54
  • @gnack are you not using .NET 4.5 or .NET4? Those are all built in types. – Chris Marisic Dec 08 '14 at 22:24
  • @ChrisMarisic are they properties of the BundleFile class? According to this page they're not: http://msdn.microsoft.com/en-us/library/system.web.optimization.bundlefile(v=vs.110).aspx – Nick Coad Dec 08 '14 at 22:43
  • 2
    @ChrisMarisic is there perhaps a namespace I should be importing that provides extension methods for the BundleFile class? – Nick Coad Dec 08 '14 at 22:53
  • @NickCoad It looks like an issue with MVC 5 vs MVC 4. Has anybody found a solution that works with BundleFiles? – shanabus Jul 11 '17 at 13:13
20

Better yet (IMHO) implement a custom Bundle that fixes the image paths. I wrote one for my app.

using System;
using System.Collections.Generic;
using IO = System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Optimization;

...

public class StyleImagePathBundle : Bundle
{
    public StyleImagePathBundle(string virtualPath)
        : base(virtualPath, new IBundleTransform[1]
      {
        (IBundleTransform) new CssMinify()
      })
    {
    }

    public StyleImagePathBundle(string virtualPath, string cdnPath)
        : base(virtualPath, cdnPath, new IBundleTransform[1]
      {
        (IBundleTransform) new CssMinify()
      })
    {
    }

    public new Bundle Include(params string[] virtualPaths)
    {
        if (HttpContext.Current.IsDebuggingEnabled)
        {
            // Debugging. Bundling will not occur so act normal and no one gets hurt.
            base.Include(virtualPaths.ToArray());
            return this;
        }

        // In production mode so CSS will be bundled. Correct image paths.
        var bundlePaths = new List<string>();
        var svr = HttpContext.Current.Server;
        foreach (var path in virtualPaths)
        {
            var pattern = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase);
            var contents = IO.File.ReadAllText(svr.MapPath(path));
            if(!pattern.IsMatch(contents))
            {
                bundlePaths.Add(path);
                continue;
            }


            var bundlePath = (IO.Path.GetDirectoryName(path) ?? string.Empty).Replace(@"\", "/") + "/";
            var bundleUrlPath = VirtualPathUtility.ToAbsolute(bundlePath);
            var bundleFilePath = String.Format("{0}{1}.bundle{2}",
                                               bundlePath,
                                               IO.Path.GetFileNameWithoutExtension(path),
                                               IO.Path.GetExtension(path));
            contents = pattern.Replace(contents, "url($1" + bundleUrlPath + "$2$1)");
            IO.File.WriteAllText(svr.MapPath(bundleFilePath), contents);
            bundlePaths.Add(bundleFilePath);
        }
        base.Include(bundlePaths.ToArray());
        return this;
    }

}

To use it, do:

bundles.Add(new StyleImagePathBundle("~/bundles/css").Include(
  "~/This/Is/Some/Folder/Path/layout.css"));

...instead of...

bundles.Add(new StyleBundle("~/bundles/css").Include(
  "~/This/Is/Some/Folder/Path/layout.css"));

What it does is (when not in debug mode) looks for url(<something>) and replaces it with url(<absolute\path\to\something>). I wrote the thing about 10 seconds ago so it might need a little tweaking. I've taken into account fully-qualified URLs and base64 DataURIs by making sure there's no colons (:) in the URL path. In our environment, images normally reside in the same folder as their css files, but I've tested it with both parent folders (url(../someFile.png)) and child folders (url(someFolder/someFile.png).

George Stocker
  • 55,025
  • 29
  • 167
  • 231
Grinn
  • 5,083
  • 33
  • 48
  • This is a great solution. I modified your Regex slightly so that it would also work with LESS files, but the original concept was exactly what I needed. Thanks. – Tim Coulter Oct 17 '12 at 09:28
  • 1
    You might put regex initialization outside the loop as well. Perhaps as a static readonly property. – Miha Markic Nov 22 '12 at 16:04
12

It is not necessary to specify a transform or have crazy subdirectory paths. After much troubleshooting I isolated it to this "simple" rule (is it a bug?)...

If your bundle path does not start with relative root of the items being included, then the web application root will not be taken into account.

Sounds like more of a bug to me, but anyway that's how you fix it with the current .NET 4.51 version. Perhaps the other answers were necessary on older ASP.NET builds, can't say don't have time to retrospectively test all that.

To clarify, here is an example:

I have these files...

~/Content/Images/Backgrounds/Some_Background_Tile.gif
~/Content/Site.css  - references the background image relatively, i.e. background: url('Images/...')

Then setup the bundle like...

BundleTable.Add(new StyleBundle("~/Bundles/Styles").Include("~/Content/Site.css"));

And render it like...

@Styles.Render("~/Bundles/Styles")

And get the "behaviour" (bug), the CSS files themselves have the application root (e.g. "http://localhost:1234/MySite/Content/Site.css") but the CSS image within all start "/Content/Images/..." or "/Images/..." depending on whether I add the transform or not.

Even tried creating the "Bundles" folder to see if it was to do with the path existing or not, but that didn't change anything. The solution to the problem is really the requirement that the name of the bundle must start with the path root.

Meaning this example is fixed by registering and rendering the bundle path like..

BundleTable.Add(new StyleBundle("~/Content/StylesBundle").Include("~/Content/Site.css"));
...
@Styles.Render("~/Content/StylesBundle")

So of course you could say this is RTFM, but I am quite sure me and others picked-up this "~/Bundles/..." path from the default template or somewhere in documentation at MSDN or ASP.NET web site, or just stumbled upon it because actually it's a quite logical name for a virtual path and makes sense to choose such virtual paths which do not conflict with real directories.

Anyway, that's the way it is. Microsoft see no bug. I don't agree with this, either it should work as expected or some exception should be thrown, or an additional override to adding the bundle path which opts to include the application root or not. I can't imagine why anyone would not want the application root included when there was one (normally unless you installed your web site with a DNS alias/default web site root). So actually that should be the default anyway.

Tony Wall
  • 1,352
  • 20
  • 18
  • Seems to me the simplest "solution". The others may have side effects, like with image:data. – Fabrice Oct 21 '14 at 21:55
  • @MohamedEmaish it does work, you probably got something wrong. Learn how to trace the requests, e.g. use Fiddler Tool to see what URLs are being requested by the browser. The goal is not to hard-code the whole relative path so your web site can be installed in different locations (root paths) on the same server or your product can change the default URL without having to re-write a lot of the web site (the point of having and application root variable). – Tony Wall Feb 04 '15 at 07:07
  • Went with this option and it worked great. Had to make sure each bundle only had items from a single folder (cannot include items from other folders or subfolders), which is slightly annoying but as long as it works I'm happy! Thanks for the post. – hvaughan3 Feb 09 '16 at 18:38
  • 1
    Thanks. Sigh. Some day I'd like to spend more time actually writing code than browsing Stack. – Bruce Pierson Mar 15 '16 at 20:23
  • I had similar issue where a custom jquery-ui which had nested folders. as soon as I leveled things up as above, it worked. It does not like nested folders. – Andrei Bazanov Mar 30 '16 at 08:49
11

I found that CssRewriteUrlTransform fails to run if you're referencing a *.css file and you have the associated *.min.css file in the same folder.

To fix this, either delete the *.min.css file or reference it directly in your bundle:

bundles.Add(new Bundle("~/bundles/bootstrap")
    .Include("~/Libs/bootstrap3/css/bootstrap.min.css", new CssRewriteUrlTransform()));

After that you do that, your URLs will be transformed correctly and your images should be correctly resolved.

ajbeaven
  • 8,509
  • 11
  • 74
  • 109
  • 1
    Thank you! After two days of searching online, this is the first mention I've seen anywhere of CssRewriteUrlTransform working with *.css files, but not with the associated *.min.css file that are pulled in when you're not running in a debug environment. Definitely seems like a bug to me. Will have to manually check the environment type to define a bundle with the unminified version for debugging, but at least I have a workaround now! – Sean Mar 17 '16 at 16:31
  • 1
    This fixed the problem for me. This certainly seems like a bug. It makes no sense that it should ignore CssRewriteUrlTransform if it finds a pre-existing .min.css file. – user1751825 Mar 19 '18 at 23:59
11

Maybe I am biased, but I quite like my solution as it doesn't do any transforming, regex's etc and it's has the least amount of code :)

This works for a site hosted as a Virtual Directory in a IIS Web Site and as a root website on IIS

So I created an Implentation of IItemTransform encapsulated the CssRewriteUrlTransform and used VirtualPathUtility to fix the path and call the existing code:

/// <summary>
/// Is a wrapper class over CssRewriteUrlTransform to fix url's in css files for sites on IIS within Virutal Directories
/// and sites at the Root level
/// </summary>
public class CssUrlTransformWrapper : IItemTransform
{
    private readonly CssRewriteUrlTransform _cssRewriteUrlTransform;

    public CssUrlTransformWrapper()
    {
        _cssRewriteUrlTransform = new CssRewriteUrlTransform();
    }

    public string Process(string includedVirtualPath, string input)
    {
        return _cssRewriteUrlTransform.Process("~" + VirtualPathUtility.ToAbsolute(includedVirtualPath), input);
    }
}


//App_Start.cs
public static void Start()
{
      BundleTable.Bundles.Add(new StyleBundle("~/bundles/fontawesome")
                         .Include("~/content/font-awesome.css", new CssUrlTransformWrapper()));
}

Seems to work fine for me?

SimonGates
  • 5,623
  • 3
  • 35
  • 50
  • 1
    This is perfectly suite for me. excellent solution. my vote is +1 – imdadhusen Jan 08 '16 at 09:28
  • 1
    This is the correct answer. The CssUrlTransformWrapper class provided by the framework addresses the problem, except it doesn't work only when the application is not at the web site root. This wrapper succinctly addresses that shortcoming. – Nine Tails Apr 19 '17 at 11:16
6

As of v1.1.0-alpha1 (pre release package) the framework uses the VirtualPathProvider to access files rather than touching the physical file system.

The updated transformer can be seen below:

public class StyleRelativePathTransform
    : IBundleTransform
{
    public void Process(BundleContext context, BundleResponse response)
    {
        Regex pattern = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase);

        response.Content = string.Empty;

        // open each of the files
        foreach (var file in response.Files)
        {
            using (var reader = new StreamReader(file.Open()))
            {
                var contents = reader.ReadToEnd();

                // apply the RegEx to the file (to change relative paths)
                var matches = pattern.Matches(contents);

                if (matches.Count > 0)
                {
                    var directoryPath = VirtualPathUtility.GetDirectory(file.VirtualPath);

                    foreach (Match match in matches)
                    {
                        // this is a path that is relative to the CSS file
                        var imageRelativePath = match.Groups[2].Value;

                        // get the image virtual path
                        var imageVirtualPath = VirtualPathUtility.Combine(directoryPath, imageRelativePath);

                        // convert the image virtual path to absolute
                        var quote = match.Groups[1].Value;
                        var replace = String.Format("url({0}{1}{0})", quote, VirtualPathUtility.ToAbsolute(imageVirtualPath));
                        contents = contents.Replace(match.Groups[0].Value, replace);
                    }

                }
                // copy the result into the response.
                response.Content = String.Format("{0}\r\n{1}", response.Content, contents);
            }
        }
    }
}
Ben Foster
  • 32,767
  • 35
  • 157
  • 274
6

Although Chris Baxter's answer helps with original problem, it doesn't work in my case when application is hosted in virtual directory. After investigating the options, I finished with DIY solution.

ProperStyleBundle class includes code borrowed from original CssRewriteUrlTransform to properly transform relative paths within virtual directory. It also throws if file doesn't exist and prevents reordering of files in the bundle (code taken from BetterStyleBundle).

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Optimization;
using System.Linq;

namespace MyNamespace
{
    public class ProperStyleBundle : StyleBundle
    {
        public override IBundleOrderer Orderer
        {
            get { return new NonOrderingBundleOrderer(); }
            set { throw new Exception( "Unable to override Non-Ordered bundler" ); }
        }

        public ProperStyleBundle( string virtualPath ) : base( virtualPath ) {}

        public ProperStyleBundle( string virtualPath, string cdnPath ) : base( virtualPath, cdnPath ) {}

        public override Bundle Include( params string[] virtualPaths )
        {
            foreach ( var virtualPath in virtualPaths ) {
                this.Include( virtualPath );
            }
            return this;
        }

        public override Bundle Include( string virtualPath, params IItemTransform[] transforms )
        {
            var realPath = System.Web.Hosting.HostingEnvironment.MapPath( virtualPath );
            if( !File.Exists( realPath ) )
            {
                throw new FileNotFoundException( "Virtual path not found: " + virtualPath );
            }
            var trans = new List<IItemTransform>( transforms ).Union( new[] { new ProperCssRewriteUrlTransform( virtualPath ) } ).ToArray();
            return base.Include( virtualPath, trans );
        }

        // This provides files in the same order as they have been added. 
        private class NonOrderingBundleOrderer : IBundleOrderer
        {
            public IEnumerable<BundleFile> OrderFiles( BundleContext context, IEnumerable<BundleFile> files )
            {
                return files;
            }
        }

        private class ProperCssRewriteUrlTransform : IItemTransform
        {
            private readonly string _basePath;

            public ProperCssRewriteUrlTransform( string basePath )
            {
                _basePath = basePath.EndsWith( "/" ) ? basePath : VirtualPathUtility.GetDirectory( basePath );
            }

            public string Process( string includedVirtualPath, string input )
            {
                if ( includedVirtualPath == null ) {
                    throw new ArgumentNullException( "includedVirtualPath" );
                }
                return ConvertUrlsToAbsolute( _basePath, input );
            }

            private static string RebaseUrlToAbsolute( string baseUrl, string url )
            {
                if ( string.IsNullOrWhiteSpace( url )
                     || string.IsNullOrWhiteSpace( baseUrl )
                     || url.StartsWith( "/", StringComparison.OrdinalIgnoreCase )
                     || url.StartsWith( "data:", StringComparison.OrdinalIgnoreCase )
                    ) {
                    return url;
                }
                if ( !baseUrl.EndsWith( "/", StringComparison.OrdinalIgnoreCase ) ) {
                    baseUrl = baseUrl + "/";
                }
                return VirtualPathUtility.ToAbsolute( baseUrl + url );
            }

            private static string ConvertUrlsToAbsolute( string baseUrl, string content )
            {
                if ( string.IsNullOrWhiteSpace( content ) ) {
                    return content;
                }
                return new Regex( "url\\(['\"]?(?<url>[^)]+?)['\"]?\\)" )
                    .Replace( content, ( match =>
                                         "url(" + RebaseUrlToAbsolute( baseUrl, match.Groups["url"].Value ) + ")" ) );
            }
        }
    }
}

Use it like StyleBundle:

bundles.Add( new ProperStyleBundle( "~/styles/ui" )
    .Include( "~/Content/Themes/cm_default/style.css" )
    .Include( "~/Content/themes/custom-theme/jquery-ui-1.8.23.custom.css" )
    .Include( "~/Content/DataTables-1.9.4/media/css/jquery.dataTables.css" )
    .Include( "~/Content/DataTables-1.9.4/extras/TableTools/media/css/TableTools.css" ) );
nrodic
  • 2,953
  • 3
  • 30
  • 35
  • 2
    Nice solution, but still fails (just like CssRewriteUrlTransform) if you have a data URI in your CSS (e.g. "data:image/png;base64,..."). You shouldn't change url's starting with "data:" in RebaseUrlToAbsolute(). – miles82 Jan 27 '15 at 14:36
  • 1
    @miles82 Of course! Thanks for pointing this out. I have changed RebaseUrlToAbsolute(). – nrodic Jan 27 '15 at 15:43
5

Here is a Bundle Transform that will replace css urls with urls relative to that css file. Just add it to your bundle and it should fix the issue.

public class CssUrlTransform: IBundleTransform
{
    public void Process(BundleContext context, BundleResponse response) {
        Regex exp = new Regex(@"url\([^\)]+\)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
        foreach (FileInfo css in response.Files) {
            string cssAppRelativePath = css.FullName.Replace(context.HttpContext.Request.PhysicalApplicationPath, context.HttpContext.Request.ApplicationPath).Replace(Path.DirectorySeparatorChar, '/');
            string cssDir = cssAppRelativePath.Substring(0, cssAppRelativePath.LastIndexOf('/'));
            response.Content = exp.Replace(response.Content, m => TransformUrl(m, cssDir));
        }
    }


    private string TransformUrl(Match match, string cssDir) {
        string url = match.Value.Substring(4, match.Length - 5).Trim('\'', '"');

        if (url.StartsWith("http://") || url.StartsWith("data:image")) return match.Value;

        if (!url.StartsWith("/"))
            url = string.Format("{0}/{1}", cssDir, url);

        return string.Format("url({0})", url);
    }

}
  • How to use it?, It's show me an exception: `cannot convert type from BundleFile to FileInfo` – Stiger Apr 04 '14 at 04:48
  • @Stiger change css.FullName.Replace( to css.VirtualFile.VirtualPath.Replace( – lkurylo Nov 17 '14 at 08:45
  • I might be using this wrong, but does that foreach rewrite all the urls on every iteration and leave them relative to the last css file it saw? – Andyrooger Jan 16 '15 at 18:15
4

Another option would be to use the IIS URL Rewrite module to map the virtual bundle image folder to the physical image folder. Below is an example of a rewrite rule from that you could use for a bundle called "~/bundles/yourpage/styles" - note the regex matches on alphanumeric characters as well as hyphens, underscores and periods, which are common in image file names.

<rewrite>
  <rules>
    <rule name="Bundle Images">
      <match url="^bundles/yourpage/images/([a-zA-Z0-9\-_.]+)" />
      <action type="Rewrite" url="Content/css/jquery-ui/images/{R:1}" />
    </rule>
  </rules>
</rewrite>

This approach creates a little extra overhead, but allows you to have more control over your bundle names, and also reduces the number of bundles you may have to reference on one page. Of course, if you have to reference multiple 3rd party css files that contain relative image path references, you still can't get around creating multiple bundles.

DanO
  • 832
  • 1
  • 7
  • 15
4

Grinn solution is great.

However it doesn't work for me when there are parent folder relative references in the url. i.e. url('../../images/car.png')

So, I slightly changed the Include method in order to resolve the paths for each regex match, allowing relative paths and also to optionally embed the images in the css.

I also changed the IF DEBUG to check BundleTable.EnableOptimizations instead of HttpContext.Current.IsDebuggingEnabled.

    public new Bundle Include(params string[] virtualPaths)
    {
        if (!BundleTable.EnableOptimizations)
        {
            // Debugging. Bundling will not occur so act normal and no one gets hurt. 
            base.Include(virtualPaths.ToArray());
            return this;
        }
        var bundlePaths = new List<string>();
        var server = HttpContext.Current.Server;
        var pattern = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase);
        foreach (var path in virtualPaths)
        {
            var contents = File.ReadAllText(server.MapPath(path));
            var matches = pattern.Matches(contents);
            // Ignore the file if no matches
            if (matches.Count == 0)
            {
                bundlePaths.Add(path);
                continue;
            }
            var bundlePath = (System.IO.Path.GetDirectoryName(path) ?? string.Empty).Replace(@"\", "/") + "/";
            var bundleUrlPath = VirtualPathUtility.ToAbsolute(bundlePath);
            var bundleFilePath = string.Format("{0}{1}.bundle{2}",
                                               bundlePath,
                                               System.IO.Path.GetFileNameWithoutExtension(path),
                                               System.IO.Path.GetExtension(path));
            // Transform the url (works with relative path to parent folder "../")
            contents = pattern.Replace(contents, m =>
            {
                var relativeUrl = m.Groups[2].Value;
                var urlReplace = GetUrlReplace(bundleUrlPath, relativeUrl, server);
                return string.Format("url({0}{1}{0})", m.Groups[1].Value, urlReplace);
            });
            File.WriteAllText(server.MapPath(bundleFilePath), contents);
            bundlePaths.Add(bundleFilePath);
        }
        base.Include(bundlePaths.ToArray());
        return this;
    }


    private string GetUrlReplace(string bundleUrlPath, string relativeUrl, HttpServerUtility server)
    {
        // Return the absolute uri
        Uri baseUri = new Uri("http://dummy.org");
        var absoluteUrl = new Uri(new Uri(baseUri, bundleUrlPath), relativeUrl).AbsolutePath;
        var localPath = server.MapPath(absoluteUrl);
        if (IsEmbedEnabled && File.Exists(localPath))
        {
            var fi = new FileInfo(localPath);
            if (fi.Length < 0x4000)
            {
                // Embed the image in uri
                string contentType = GetContentType(fi.Extension);
                if (null != contentType)
                {
                    var base64 = Convert.ToBase64String(File.ReadAllBytes(localPath));
                    // Return the serialized image
                    return string.Format("data:{0};base64,{1}", contentType, base64);
                }
            }
        }
        // Return the absolute uri 
        return absoluteUrl;
    }

Hope it helps, regards.

thepirat000
  • 10,774
  • 4
  • 38
  • 63
2

You can simply add another level of depth to your virtual bundle path

    //Two levels deep bundle path so that paths are maintained after minification
    bundles.Add(new StyleBundle("~/Content/css/css").Include("~/Content/bootstrap/bootstrap.css", "~/Content/site.css"));

This is a super low-tech answer and kind of a hack but it works and won't require any pre-processing. Given the length and complexity of some of these answers I prefer doing it this way.

Brian Rosamilia
  • 1,348
  • 11
  • 21
  • This does not help when you have your web app as virtual application in IIS. I mean it can work but you must name your IIS virtual app as in your code, which is not what you want, right? – psulek Apr 23 '14 at 17:59
  • I've the same problem when app is virtual application in IIS. This answer helps me. – BILL Jun 02 '14 at 09:45
2

I had this problem with bundles having incorrect path's to images and CssRewriteUrlTransform not resolving relative parent paths .. correctly (there was also problem with external resources like webfonts). That's why I wrote this custom transform (appears to do all of the above correctly):

public class CssRewriteUrlTransform2 : IItemTransform
{
    public string Process(string includedVirtualPath, string input)
    {
        var pathParts = includedVirtualPath.Replace("~/", "/").Split('/');
        pathParts = pathParts.Take(pathParts.Count() - 1).ToArray();
        return Regex.Replace
        (
            input,
            @"(url\(['""]?)((?:\/??\.\.)*)(.*?)(['""]?\))",
            m => 
            {
                // Somehow assigning this to a variable is faster than directly returning the output
                var output =
                (
                    // Check if it's an aboslute url or base64
                    m.Groups[3].Value.IndexOf(':') == -1 ?
                    (
                        m.Groups[1].Value +
                        (
                            (
                                (
                                    m.Groups[2].Value.Length > 0 ||
                                    !m.Groups[3].Value.StartsWith('/')
                                )
                            ) ?
                            string.Join("/", pathParts.Take(pathParts.Count() - m.Groups[2].Value.Count(".."))) :
                            ""
                        ) +
                        (!m.Groups[3].Value.StartsWith('/') ? "/" + m.Groups[3].Value : m.Groups[3].Value) +
                        m.Groups[4].Value
                    ) :
                    m.Groups[0].Value
                );
                return output;
            }
        );
    }
}

Edit: I didn't realize it, but I used some custom extension methods in the code. The source code of those is:

/// <summary>
/// Based on: http://stackoverflow.com/a/11773674
/// </summary>
public static int Count(this string source, string substring)
{
    int count = 0, n = 0;

    while ((n = source.IndexOf(substring, n, StringComparison.InvariantCulture)) != -1)
    {
        n += substring.Length;
        ++count;
    }
    return count;
}

public static bool StartsWith(this string source, char value)
{
    if (source.Length == 0)
    {
        return false;
    }
    return source[0] == value;
}

Of course it should be possible to replace String.StartsWith(char) with String.StartsWith(string).

jahu
  • 4,899
  • 2
  • 35
  • 55
  • I don't have a String.Count() overload that accepts a string (`m.Groups[2].Value.Count("..")` doesn't work.) And `Value.StartsWith('/')` doesn't work either because StartsWith expects a string instead of a char. – jao Nov 13 '14 at 09:18
  • @jao my bad I included my own extension methods in the code without realizing it. – jahu Nov 13 '14 at 10:24
  • 1
    @jao added the source code of those extension methods to the answer. – jahu Nov 13 '14 at 10:30
1

After little investigation I concluded the followings: You have 2 options:

  1. go with transformations. Very usefull package for this: https://bundletransformer.codeplex.com/ you need following transformation for every problematic bundle:

    BundleResolver.Current = new CustomBundleResolver();
    var cssTransformer = new StyleTransformer();
    standardCssBundle.Transforms.Add(cssTransformer);
    bundles.Add(standardCssBundle);
    

Advantages: of this solution, you can name your bundle whatever you want => you can combine css files into one bundle from different directories. Disadvantages: You need to transform every problematic bundle

  1. Use the same relative root for the name of the bundle like where the css file is located. Advantages: there is no need for transformation. Disadvantages: You have limitation on combining css sheets from different directories into one bundle.
Kovács Ede
  • 128
  • 7
0

CssRewriteUrlTransform fixed my problem.
If your code still not loading images after using CssRewriteUrlTransform, then change your css filename's from:

.Include("~/Content/jquery/jquery-ui-1.10.3.custom.css", new CssRewriteUrlTransform())

To:

.Include("~/Content/jquery/jquery-ui.css", new CssRewriteUrlTransform())

Someway .(dots) are not recognizing in url.

Hakan Fıstık
  • 11,376
  • 8
  • 74
  • 105
Nalan Madheswaran
  • 8,156
  • 1
  • 48
  • 37
0

Just remember to fix multiple CSS inclusions in a bundle such as:

bundles.Add(new StyleBundle("~/Content/styles/jquery-ui")
    .Include("~/Content/css/path1/somestyle1.css", "~/Content/css/path2/somestyle2.css"));

You cannot just add new CssRewriteUrlTransform() to the end as you can with one CSS file as the method does not support it, so you have to use Include multiple times:

bundles.Add(new StyleBundle("~/Content/styles/jquery-ui")
    .Include("~/Content/css/path1/somestyle1.css", new CssRewriteUrlTransform())
    .Include("~/Content/css/path2/somestyle2.css", new CssRewriteUrlTransform()));
SharpC
  • 5,368
  • 3
  • 37
  • 36