110

I am linking to the jQuery Mobile stylesheet on a CDN and would like to fall back to my local version of the stylesheet if the CDN fails. For scripts the solution is well known:

<!-- Load jQuery and jQuery mobile with fall back to local server -->
<script src="http://code.jquery.com/jquery-1.6.3.min.js"></script>
<script type="text/javascript">
  if (typeof jQuery == 'undefined') {
    document.write(unescape("%3Cscript src='jquery-1.6.3.min.js'%3E"));
  }
</script>

I would like to do something similar for a style sheet:

<link rel="stylesheet" href="http://code.jquery.com/mobile/1.0b3/jquery.mobile-1.0b3.min.css" />

I am not sure if a similar approach can be achieved because I am not sure whether the browser blocks in the same way when linking a script as it does when loading a script (maybe it is possible to load a stylesheet in a script tag and then inject it into the page) ?

So my question is: How do I ensure a stylesheet is loaded locally if a CDN fails ?

Salman A
  • 229,425
  • 77
  • 398
  • 489
ssn
  • 2,195
  • 4
  • 18
  • 27
  • 2
    I'd like to know if this is possible as well... If I really fretted about the CDN being down, I would just use local hosting. – Fosco Sep 12 '11 at 04:02
  • 2
    @Stefan Kendall, i think the right statement is that his site will more than likely to go down than a CDN – Shawn Mclean Sep 12 '11 at 23:34
  • Best way: http://stackoverflow.com/questions/26192897/should-i-use-bootstrap-from-cdn-or-make-a-copy-on-my-server/26198380 – nmit026 Mar 13 '17 at 20:23

11 Answers11

62

Not cross-browser tested but I think this will work. Will have to be after you load jquery though, or you'll have to rewrite it in plain Javascript.

<script type="text/javascript">
$.each(document.styleSheets, function(i,sheet){
  if(sheet.href=='http://code.jquery.com/mobile/1.0b3/jquery.mobile-1.0b3.min.css') {
    var rules = sheet.rules ? sheet.rules : sheet.cssRules;
    if (rules.length == 0) {
      $('<link rel="stylesheet" type="text/css" href="path/to/local/jquery.mobile-1.0b3.min.css" />').appendTo('head');
    }
 }
})
</script>
Yahel
  • 35,856
  • 22
  • 98
  • 150
katy lavallee
  • 2,571
  • 1
  • 27
  • 26
  • good solution, one issue it does not address is if the CDN is way too slow to load... maybe some sort of timeout? – GeorgeU Sep 22 '11 at 15:04
  • 2
    For http://code.jquery.com/ui/1.10.2/themes/smoothness/jquery-ui.css, I get rules = null, even though it's been loaded properly. I am using Chrome 26 and I think it's because the script is cross domain? – simplfuzz Apr 05 '13 at 09:09
  • 8
    The solution doesn't really work for all CDNs/Stylesheets, for example `CSSStyleSheet` js objects that come from bootstrapcdn.com all have empty `rules` and `cssRules` fields in my browser (Chrome 31). **UPD**: it actually might be a crossdomain issue, css file in the answer also doesn't work for me. – Maksim Vi. Dec 27 '13 at 21:16
  • Does this work with the protocol-less syntax? I.e. href="//ajax.googleapis.com/ajax/libs/jqueryui/1/themes/start/jquery-ui.css" – Hoppe Oct 11 '14 at 18:51
  • 3
    This is also good soloution using `onerror` event. http://stackoverflow.com/questions/30546795/how-to-detect-when-one-or-more-js-css-library-fail-to-load-e-g-cdn/#answer-30547720 – Chemical Programmer Aug 07 '15 at 04:21
  • 2
    Does it work for CSS loaded from another domain? No, you cannot enumerate `.rules`/`.cssRules` for external stylesheets. http://jsfiddle.net/E6yYN/13/ – Salman A Oct 13 '16 at 07:09
31

One could use onerror for that:

<link rel="stylesheet" href="cdn.css" onerror="this.onerror=null;this.href='local.css';" />

The this.onerror=null; is to avoid endless loops in case the fallback it self is not available. But it could also be used to have multiple fallbacks.

However, this currently only works in Firefox and Chrome.

Update: Meanwhile, this seems to be supported by all common browsers.

Jan Martin Keil
  • 609
  • 5
  • 8
  • 8
    CONGRATULATIONS to all of you who, like me, couldn't believe that the complicated schemes proposed in earlier answers could still be the best way, and made it all the way down to here. This half-of-a-one-liner works perfectly. Let us see how fast we can up-vote it to the top. Or maybe someone with moderator access can delete the old answers. – Jerry Krinock Sep 22 '20 at 23:18
29

I guess the question is to detect whether a stylesheet is loaded or not. One possible approach is as follows:

1) Add a special rule to the end of your CSS file, like:

#foo { display: none !important; }

2) Add the corresponding div in your HTML:

<div id="foo"></div>

3) On document ready, check whether #foo is visible or not. If the stylesheet was loaded, it will not be visible.

Demo here -- loads jquery-ui smoothness theme; no rule is added to stylesheet.

Salman A
  • 229,425
  • 77
  • 398
  • 489
  • 44
    +1 for a clever solution. Only problem is, one normally can't go and add a line to the end of a style sheet that is hosted on someone's CDN – Jannie Theunissen Aug 10 '12 at 12:24
  • 2
    Unfortunately, we can't type in our own classes to CDN files. May be we can try to utilize the one that exists already. – Ahamed May 08 '14 at 19:28
  • 1
    I like this one A LOT, thank you. It's quite powerful really. Obviously I cannot manipulate the CDN stylesheet but I know what classes are being used so I amended the code to show check if they are visible - very clever indeed :) – Nosnibor Jan 11 '15 at 14:18
  • 3
    NB: you do not _really_ have to add a new rule to the external CSS. Just use an existing rule whose behavior is known. In my demo I use ui-helper-hidden class which is supposed to hide the element, i then check if the element gets hidden on page load. – Salman A Oct 13 '16 at 07:16
29

Assuming you are using the same CDN for css and jQuery, why not just do one test and catch it all??

<link href="//ajax.googleapis.com/ajax/libs/jqueryui/1/themes/start/jquery-ui.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js"></script>
<script type="text/javascript">
    if (typeof jQuery == 'undefined') {
        document.write(unescape('%3Clink rel="stylesheet" type="text/css" href="../../Content/jquery-ui-1.8.16.custom.css" /%3E'));
        document.write(unescape('%3Cscript type="text/javascript" src="/jQuery/jquery-1.6.4.min.js" %3E%3C/script%3E'));
        document.write(unescape('%3Cscript type="text/javascript" src="/jQuery/jquery-ui-1.8.16.custom.min.js" %3E%3C/script%3E'));
    }
</script>
Mike Wills
  • 19,825
  • 26
  • 87
  • 143
  • 1
    May I ask what the issue with using unescaped strings initially, e.g. `document.write(" – Jack Jan 22 '15 at 20:38
  • 2
    @JackTuck: The parser can't differentiate between ` – Brad Christie Mar 15 '15 at 18:00
  • 7
    -1 `why not just do one test and catch it all?` -- Because there are a million reasons that one might fail, and the others succeed. – Flimzy Jun 30 '15 at 02:48
7

this article suggests some solutions for the bootstrap css http://eddmann.com/posts/providing-local-js-and-css-resources-for-cdn-fallbacks/

alternatively this works for fontawesome

<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
<script>
    (function($){
        var $span = $('<span class="fa" style="display:none"></span>').appendTo('body');
        if ($span.css('fontFamily') !== 'FontAwesome' ) {
            // Fallback Link
            $('head').append('<link href="/css/font-awesome.min.css" rel="stylesheet">');
        }
        $span.remove();
    })(jQuery);
</script>
dc2009
  • 822
  • 1
  • 9
  • 7
  • For those looking to use this with Font Awesome 5, you'll want to change 'FontAwesome' (in the if clause) to 'Font Awesome 5 Free' (if you're using the free fonts). Otherwise, it should work fine. – Jason Clark May 16 '18 at 16:04
5

You might be able to test for the existence of the stylesheet in document.styleSheets.

var rules = [];
if (document.styleSheets[1].cssRules)
    rules = document.styleSheets[i].cssRules
else if (document.styleSheets[i].rules)
    rule= document.styleSheets[i].rules

Test for something specific to the CSS file you're using.

Stefan Kendall
  • 61,898
  • 63
  • 233
  • 391
3

Here's an extension to katy lavallee's answer. I've wrapped everything in self-executing function syntax to prevent variable collisions. I've also made the script non-specific to a single link. I.E., now any stylesheet link with a "data-fallback" url attribute will automatically be parsed. You don't have to hard-code the urls into this script like before. Note that this should be run at the end of the <head> element rather than at the end of the <body> element, otherwise it could cause FOUC.

http://jsfiddle.net/skibulk/jnfgyrLt/

<link rel="stylesheet" type="text/css" href="broken-link.css" data-fallback="broken-link2.css">

.

(function($){
    var links = {};

    $( "link[data-fallback]" ).each( function( index, link ) {
        links[link.href] = link;
    });

    $.each( document.styleSheets, function(index, sheet) {
        if(links[sheet.href]) {
            var rules = sheet.rules ? sheet.rules : sheet.cssRules;
            if (rules.length == 0) {
                link = $(links[sheet.href]);
                link.attr( 'href', link.attr("data-fallback") );
            }
        }
    });
})(jQuery);
skibulk
  • 2,768
  • 30
  • 39
  • 1
    I like the encapsulation, but in general you can't inspect sheet.rules for a cross-domain stylesheet. You can still use this general idea but need to do a different check. – John Vinopal Sep 18 '15 at 22:13
  • with `document.styleSheets[i].ownerNode.dataset` you can access `` attributes – Alwin Kesler Oct 09 '18 at 14:57
2

Do you really want to go down this javascript route to load CSS in case a CDN fails?

I haven't thought all the performance implications through but you're going to lose control of when the CSS is loaded and in general for page load performance, CSS is the first thing you want to download after the HTML.

Why not handle this at the infrastructure level - map your own domain name to the CDN, give it a short TTL, monitor the files on the CDN (e.g. using Watchmouse or something else), if CDN fails, change the DNS to backup site.

Other options that might help are "cache forever" on static content but there's no guarantee the browser will keep them of course or using the app-cache.

In reality as someone said at the top, if your CDN is unreliable get a new one

Andy

Andy Davies
  • 5,697
  • 2
  • 24
  • 21
1

Look at these functions:

$.ajax({
    url:'CSS URL HERE',
    type:'HEAD',
    error: function()
    {
        AddLocalCss();
    },
    success: function()
    {
        //file exists
    }
});

And here is vanilla JavaScript version:

function UrlExists(url)
{
    var http = new XMLHttpRequest();
    http.open('HEAD', url, false);
    http.send();
    return http.status!=404;
}
if (!UrlExists('CSS URL HERE') {
AddLocalCss();
}

Now the actual function:

function AddLocalCss(){
document.write('<link rel="stylesheet" type="text/css" href=" LOCAL CSS URL HERE">')
}

Just make sure AddLocalCss is called in the head.

You might also consider using one of the following ways explained in this answer:

Load using AJAX

$.get(myStylesLocation, function(css)
{
   $('<style type="text/css"></style>')
      .html(css)
      .appendTo("head");
});

Load using dynamically-created

$('<link rel="stylesheet" type="text/css" href="'+myStylesLocation+'" >')
   .appendTo("head");
Load using dynamically-created <style>

$('<style type="text/css"></style>')
    .html('@import url("' + myStylesLocation + '")')
    .appendTo("head");

or

$('<style type="text/css">@import url("' + myStylesLocation + '")</style>')
    .appendTo("head");
Community
  • 1
  • 1
  • Yes, at least in modern browser, I am not sure about IE6. –  Sep 12 '11 at 04:09
  • Is there a way to just check instead of downloading the whole thing? – Shawn Mclean Sep 12 '11 at 04:09
  • The only possible reason to do the OPs request is to avoid excess network traffic. This creates excess network traffic. – Stefan Kendall Sep 12 '11 at 04:12
  • @stefan Kendall: no that is not even the possible reason he wan'ts to make sure that the files get loaded. –  Sep 12 '11 at 04:16
  • If that was the only concern, you would just not use a CDN. Testing just the header is better, but I'm pretty sure most CDNs and your browser aren't going to allow XSS. – Stefan Kendall Sep 12 '11 at 04:38
  • Sure thing not using a CDN is the safest way but I give the answer assuming that the OP has some bandwith/speed issue. –  Sep 12 '11 at 04:56
  • @stefan Kendall: I am wondering if you down-voted because the answer is not the best for your assumption of OP situation but not the actual question. –  Sep 12 '11 at 05:10
  • @Omeid: I explained why this probably won't work. Go try and create a jsfiddle testing this. Also, this doesn't account for when the CDN returns 503s, or when it returns 200s with empty content. There are many "potential edge cases" this doesn't cover, and it's almost certainly better handled at the CDN level anyway. – Stefan Kendall Sep 12 '11 at 17:27
  • @stefan: the OP asked to check for the CSS if its loaded or not and then to decide to download it from its server and it actually works. –  Sep 13 '11 at 00:33
-1

I'd probably use something like yepnope.js

yepnope([{
  load: 'http:/­/ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js',
  complete: function () {
    if (!window.jQuery) {
      yepnope('local/jquery.min.js');
    }
  }
}]);

Taken from the readme.

  • 10
    @BenSchwarz, that doesn't mean you can paste some irrelevant code which in no way answers the asked question. – AlicanC Nov 25 '13 at 18:58
-8
//(load your cdn lib here first)

<script>window.jQuery || document.write("<script src='//me.com/path/jquery-1.x.min.js'>\x3C/script>")</script>
Barry Kaye
  • 7,346
  • 6
  • 38
  • 58
crazy4groovy
  • 115
  • 1
  • 5