Different remote and local resource URLs, with Service Workers!

I often run into this issue where I want a different URL remotely and a different one locally so I can test my local changes to a library. Sure, relative URLs work a lot of the time, but are often not an option. Developing Mavo is yet another example of this: since Mavo is in a separate repo from mavo.io (its website) as well as test.mavo.io (the testsuite), I can’t just have relative URLs to it that also work remotely. I’ve been encountering this problem way too frequently pretty much since I started in web development. In this post, will describe all solutions and workarounds I’ve used over time for this, including the one I’m currently using for Mavo: Service Workers!

The manual one

Probably the first solution everyone tries is doing it manually: every time you need to test, you just change the URL to a relative, local one and try to remember to change it back before committing. I still use this in some cases, since us developers are a lazy bunch. Usually I have both and use my editor’s (un)commenting shortcut for enabling one or the other:

<script src="https://get.mavo.io/mavo.js"></script>
<!--<script src="../mavo/dist/mavo.js"></script>-->

However, as you might imagine, this approach has several problems, the worst of which is that more than once I forgot and committed with the active script being the local one, which resulted in the remote website totally breaking. Also, it’s clunky, especially when it’s two resources whose URLs you need to change.

The JS one

This idea uses a bit of JS to load the remote URL when the local one fails to load.

<script src="http://localhost:8000/mavo/dist/mavo.js" onerror="this.src='https://get.mavo.io/mavo.js'"></script>

This works, and doesn’t introduce any cognitive overhead for the developer, but the obvious drawback is that it slows things down for the server since a request needs to be sent and fail before the real resource can be loaded. Slowing things down for the local case might be acceptable, even though undesirable, but slowing things down on the remote website for the sake of debugging is completely unacceptable. Furthermore, this exposes the debugging URLs in the HTML source, which gives me a bit of a knee jerk reaction.

A variation of this approach that doesn’t have the performance problem is:

 let host = location.hostname == "localhost"? 'http://localhost:8000/dist' : 'https://get.mavo.io';
 document.write(`<script src="${host}/mavo.js"></scr` + `ipt>`);

This works fine, but it’s very clunky, especially if you have to do this multiple times (e.g. on multiple testing files or demos).

The build tools one

The solution I was following up to a few months ago was to use gulp to copy over the files needed, and then link to my local copies via a relative URL. I would also have a gulp.watch() that monitors changes to the original files and copies them over again:

gulp.task("copy", function() {

gulp.task("watch", function() {
	gulp.watch(["../mavo/dist/*"], ["copy"]);

This worked but I had to remember to run gulp watch every time I started working on each project. Often I forgot, which was a huge source of confusion as to why my changes had no effect. Also, it meant I had copies of Mavo lying around on every repo that uses it and had to manually update them by running gulp, which was suboptimal.

The Service Worker one

In April, after being fed up with having to deal with this problem for over a decade, I posted a tweet:

@MylesBorins replied (though his tweet seems to have disappeared) and suggested that perhaps Service Workers could help. In case you’ve been hiding under a rock for the past couple of years, Service Workers are a new(ish) API that allows you to intercept requests from your website to the network and do whatever you want with them. They are mostly promoted for creating good offline experiences, though they can do a lot more.

I was looking for an excuse to dabble in Service Workers for a while, and this was a great one. Furthermore, browser support doesn’t really matter in this case because the Service Worker is only used locally.

The code I ended up with looks like this in a small script called sitewide.js, which, as you may imagine, is used sitewide:

(function() {

if (location.hostname !== "localhost") {

if (!self.document) {
	// We're in a service worker! Oh man, we’re living in the future! 🌈🦄
	self.addEventListener("fetch", function(evt) {
		var url = evt.request.url;

		if (url.indexOf("get.mavo.io/mavo.") > -1 || url.indexOf("dev.mavo.io/dist/mavo.") > -1) {
			var newURL = url.replace(/.+?(get|dev)\.mavo\.io\/(dist\/)?/, "http://localhost:8000/dist/") + "?" + Date.now();

			var response = fetch(new Request(newURL), evt.request)
				.then(r => r.status < 400? r : Promise.reject()) 
				// if that fails, return original request
				.catch(err => fetch(evt.request));



if ("serviceWorker" in navigator) {
	// Register this script as a service worker
	addEventListener("load", function() {


So far, this has worked more nicely than any of the aforementioned solutions and allows me to just use the normal remote URLs in my HTML. However, it’s not without its own caveats:

  • Service Workers are only activated on a cached pageload, so the first one uses the remote URL. This is almost never a problem locally anyway though, so I’m not concerned about it much.
  • The same origin restriction that service workers have is fairly annoying. So, I have to copy the service worker script on every repo I want to use this on, I cannot just link to it.
  • It needs to be explained to new contributors since most aren’t familiar with Service Workers and how they work at all.

Currently the URLs for both local and remote are baked into the code, but it’s easy to imagine a mini-library that takes care of it as long as you include the local URL as a parameter (e.g. https://get.mavo.io/mavo.js?local=http://localhost:8000/dist/mavo.js).

Other solutions

Solutions I didn’t test (but you may want to) include:

  • .htaccess redirect based on domain, suggested by @codepo8. I don’t use Apache locally, so that’s of no use to me.
  • Symbolic links, suggested by @aleschmidx
  • User scripts (e.g. Greasemonkey), suggested by @WebManWlkg
  • Modifying the hosts file, suggested by @LukeBrowell (that works if you don’t need access to the remote URL at all)

Is there any other solution? What do you do?

  • Andrew Steele

    I have completely different urls for the API I work with locally and remotely, so I have the endpoints stored like constants in an APIEndpoints object.

    For local development, we use FakeRest to simulate API calls and responses (so that I can develop offline without a database), and then I override the endpoints for production to go to the real API.

    The app I’m working on currently is a React app via create-react-app, so I have access to a process.env object to tell me if I’m running in development or production mode.

  • I faced this issue and started using the chrome plugin switcheroo. So, at the time of local development, I simply create a rule to switch to local path. No hassles. Infact, I can even do vice-versa to debug the prod site using my local script..

  • Pingback: Collective #363 | PSD TO WORDPRESS – PSD TO HTML()

  • Before I started to compile my JS and projects with Codekit I’ve used the PHP to load JS files faster on localhost and from CDN when they were live… it worked for me.

    $whitelist = array(‘’, ‘::1’);
    if(!in_array($_SERVER[‘REMOTE_ADDR’], $whitelist)) echo ”;
    else echo ”;

    This can be turned in a function to work with all files the same way and worked pretty well.

  • Lea, 1st you can use relative URLs for your assets. Or if you use Jekyll then before `serve` add `SET JEKYLL_ENV=dev` and use `{% if jekyll.environment == “dev” %}`.
    Any JS workaround will slow down the experience, for development environment it may be fine, but not for production. Good luck!

    • And here ladies and gentlemen we have some textbook ‘splaining. Did you actually read the part where I mentioned that relative URLs don’t work for every case and described a case where they don’t? OBVIOUSLY if you can use relative URLs that’s better. Also, I don’t use Jekyll.

      • I have mentioned also a hack with OS hosts file, so the domain will be linked to your localhost.
        Ah… I just noticed you mentioned that @LukeBrowell already recommended it.

        • Um, did you actually read the article? I did also mention the hosts trick, and how it doesn’t always help because most of the time you want access to the remote URL too. Not to mention that the part about hosts was not even in your original comment, I still have the email notification.

      • error451

        Wow.. I was truly intrigued by your “dilemma” moderna IDE’s , Build solutions and or proper application architecture practices notwithstanding… But your “splaining” sh*t trully exemplifys why those like you should be less myopic when seeking a solution for self imposed problems and more beneficially focused on WTF is wrong with you, and why you feel the need to reply with such vitriol when someone takes the time to reply to your self imposed “purgatory”.

        I look forward to reading you condemnation/blaming of me, everything else that you want to blame, the previous poster the white-knighting due to comments (as since you have the opposing genitalia) others will deem it necessary to interject… But when you’re done with all that… pick up any modern IDE and your issue will be solved.

  • timrnz

    Charles proxy + Map local has been a live saver for me for a few years now. Alternatively if Map local isn’t your cup of tea, map remote might be useful. It really depends on what you’re trying to do honestly but I couldn’t quite glean it from your post.


  • 潜心学习,认真拜读!

  • Pingback: Monthly Web Development Update 11/2017: Browser News, KRACK and Calligraphy With AR – Smashing Magazine()

  • Pingback: Monthly Web Development Update 11/2017: Browser News, KRACK and Calligraphy With AR | CodeBringer()

  • Pingback: Collective #363 - CoalHouse()

  • 一天不来访,浑身上下痒!

  • tomByrer

    Maybe a solution could be `onerror=”this.src=’https://get.mavo.io/mavo.js'”` by default, then have a build-deploy script that optimizes that “ line?

  • Smart solution.

  • 十元钱,只需十元钱:





  • Pingback: Monthly Web Development Update 11/2017: Browser News, KRACK and Vary Header Caching()

  • 一言不发岂能证明我来过了?!

  • 很少能看到这么专注的博客啦!

  • 这样精彩的博客越来越少咯!