How to fallback to local stylesheet (not script) if CDN fails
Asked Answered
N

12

122

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 ?

Novocaine answered 12/9, 2011 at 3:56 Comment(3)
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.Orthopedist
@Stefan Kendall, i think the right statement is that his site will more than likely to go down than a CDNContinuum
Best way: #26193397Ptosis
F
63

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>
Final answered 17/9, 2011 at 4:13 Comment(6)
good solution, one issue it does not address is if the CDN is way too slow to load... maybe some sort of timeout?Precursor
For 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?Roentgenogram
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.Azeria
Does this work with the protocol-less syntax? I.e. href="//ajax.googleapis.com/ajax/libs/jqueryui/1/themes/start/jquery-ui.css"Diondione
This is also good soloution using onerror event. #30547295Slay
Does it work for CSS loaded from another domain? No, you cannot enumerate .rules/.cssRules for external stylesheets. jsfiddle.net/E6yYN/13Repp
I
88

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.

Ious answered 7/9, 2018 at 13:33 Comment(6)
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.Weakkneed
We don't delete old answers for being "not as elegant", but this is a good answer all the same.Gamekeeper
I searched for so long. This is great! How was this so hard to find. Thank you so much!Jimjams
However, sometimes the CDN just doesn't respond so you don't have any error and just keep waiting....Kilbride
Due to Content Security Policies this one liner will no longer work. :(Potation
It still works with CSP, you just need to use a function rather than inline scriptFlorenceflorencia
F
63

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>
Final answered 17/9, 2011 at 4:13 Comment(6)
good solution, one issue it does not address is if the CDN is way too slow to load... maybe some sort of timeout?Precursor
For 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?Roentgenogram
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.Azeria
Does this work with the protocol-less syntax? I.e. href="//ajax.googleapis.com/ajax/libs/jqueryui/1/themes/start/jquery-ui.css"Diondione
This is also good soloution using onerror event. #30547295Slay
Does it work for CSS loaded from another domain? No, you cannot enumerate .rules/.cssRules for external stylesheets. jsfiddle.net/E6yYN/13Repp
E
30

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>
Eustoliaeutectic answered 19/9, 2011 at 16:4 Comment(3)
May I ask what the issue with using unescaped strings initially, e.g. document.write("<script type='text/javascript' src='path/to/file.js'>")?Dishcloth
@JackTuck: The parser can't differentiate between <script> inside a JS string and one found outside. This is commonly why you also see <\/script> when writing out tags for CDN fallbacks.Hotien
-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.Morales
R
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.

Repp answered 17/9, 2011 at 4:31 Comment(4)
+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 CDNTridimensional
Unfortunately, we can't type in our own classes to CDN files. May be we can try to utilize the one that exists already.Changeful
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 :)Uvulitis
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.Repp
I
8

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>
Imperial answered 10/12, 2014 at 15:25 Comment(1)
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.Liew
S
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.

Subir answered 12/9, 2011 at 4:15 Comment(1)
Can't you check for the name or href? If so, how?Enact
A
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);
Alberta answered 15/10, 2014 at 3:54 Comment(2)
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.Sheffy
with document.styleSheets[i].ownerNode.dataset you can access <link data-* /> attributesBlood
T
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

Thermaesthesia answered 17/9, 2011 at 7:0 Comment(1)
CDN can be reliable, but not the development environment internet connection ;)Joyce
S
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");
Scrapple answered 12/9, 2011 at 4:3 Comment(9)
Yes, at least in modern browser, I am not sure about IE6.Scrapple
Is there a way to just check instead of downloading the whole thing?Continuum
The only possible reason to do the OPs request is to avoid excess network traffic. This creates excess network traffic.Subir
@stefan Kendall: no that is not even the possible reason he wan'ts to make sure that the files get loaded.Scrapple
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.Subir
Sure thing not using a CDN is the safest way but I give the answer assuming that the OP has some bandwith/speed issue.Scrapple
@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.Scrapple
@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.Subir
@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.Scrapple
F
0

Following on from the answer by Jan Martin Keil.

To work with a strong Content Security Policy you need to use a function in the onerror rather than inline code, for example

<link href="https://kendo.cdn.telerik.com/themes/7.0.2/default/default-main.csss" rel="stylesheet" type="text/css"
        onerror="localCss('/lib/bootstrap-main.css')" asp-add-nonce />
<script asp-add-nonce>
    function localCss(cssPath) {
        this.onerror = null;
        var link = document.createElement("link");
        link.href = cssPath;
        link.rel = "stylesheet";
        document.head.appendChild(link);
    }
</script>
Florenceflorencia answered 26/1, 2024 at 15:45 Comment(0)
O
-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.

Oatis answered 17/9, 2011 at 5:15 Comment(1)
@BenSchwarz, that doesn't mean you can paste some irrelevant code which in no way answers the asked question.Marquet
C
-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>
Chaplet answered 5/10, 2011 at 16:45 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.