diff --git a/README.md b/README.md
index a28bc7160b3c3a3242f41e0c87d1704626b3c09a..61d2cc5554b5e941f124fd9324aa359784022f89 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@ Meshviewer is an online visualization app to represent nodes and links on a map
 #### Main differences to https://github.com/ffnord/meshviewer
 _Some similar features might have been implemented/merged_
 
+- Replaced router - including language, mode, node, link, location
 - Leaflet upgraded to v1 - faster on mobile
 - Forcegraph rewrite with d3.js v4
 - Map layer modes (Allow to set a default layer based on time combined with a stylesheet)
diff --git a/app.js b/app.js
index 5bc3e5ae14860e0ea7492eefcc94b45331bd57d7..1901493f94648a47c9ab43d25d719fa6adcd3543 100644
--- a/app.js
+++ b/app.js
@@ -57,6 +57,7 @@ require.config({
   baseUrl: 'lib',
   paths: {
     'polyglot': '../node_modules/node-polyglot/build/polyglot',
+    'Navigo': '../node_modules/navigo/lib/navigo',
     'leaflet': '../node_modules/leaflet/dist/leaflet',
     'moment': '../node_modules/moment/moment',
     // d3 modules indirect dependencies
@@ -78,8 +79,7 @@ require.config({
     'd3-drag': '../node_modules/d3-drag/build/d3-drag',
     'virtual-dom': '../node_modules/virtual-dom/dist/virtual-dom',
     'rbush': '../node_modules/rbush/rbush',
-    'helper': 'utils/helper',
-    'language': 'utils/language'
+    'helper': 'utils/helper'
   },
   shim: {
     'd3-drag': ['d3-selection'],
diff --git a/lib/forcegraph.js b/lib/forcegraph.js
index 43592de99494c7fdc2bbcb8fd84369c5987a0987..4b282457aa6eccddddc4540da829ef645c01535c 100644
--- a/lib/forcegraph.js
+++ b/lib/forcegraph.js
@@ -46,13 +46,12 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr
         var n = force.find(e[0], e[1], NODE_RADIUS_SELECT);
 
         if (n !== undefined) {
-          router.node(n.o.node)();
+          router.fullUrl({ node: n.o.node.nodeinfo.node_id });
           return;
         }
 
         e = { x: e[0], y: e[1] };
 
-
         var closedLink;
         var radius = LINK_RADIUS_SELECT;
         intLinks
@@ -65,7 +64,7 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr
           });
 
         if (closedLink !== undefined) {
-          router.link(closedLink.o)();
+          router.fullUrl({ link: closedLink.o.id });
         }
       }
 
@@ -226,6 +225,11 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr
         redraw();
       };
 
+
+      self.gotoLocation = function gotoLocation() {
+        // ignore
+      };
+
       self.destroy = function destroy() {
         force.stop();
         canvas.remove();
diff --git a/lib/gui.js b/lib/gui.js
index a4f4b64647ad100fc049df9f8d7dffc0c23fdf70..d317d66553b66ee00a56967c6d4e421ce817fca2 100644
--- a/lib/gui.js
+++ b/lib/gui.js
@@ -66,11 +66,13 @@ define(['d3-interpolate', 'map', 'sidebar', 'tabs', 'container', 'legend',
       buttonToggle.classList.add('ion-eye', 'shadow');
       buttonToggle.setAttribute('data-tooltip', _.t('button.switchView'));
       buttonToggle.onclick = function onclick() {
+        var data;
         if (content.constructor === Map) {
-          router.view('g');
+          data = { view: 'graph', lat: undefined, lng: undefined, zoom: undefined };
         } else {
-          router.view('m');
+          data = { view: 'map' };
         }
+        router.fullUrl(data, false, true);
       };
 
       buttons.appendChild(buttonToggle);
@@ -119,10 +121,8 @@ define(['d3-interpolate', 'map', 'sidebar', 'tabs', 'container', 'legend',
       router.addTarget(title);
       router.addTarget(infobox);
 
-      router.addView('m', mkView(Map));
-      router.addView('g', mkView(ForceGraph));
-
-      router.view('m');
+      router.addView('map', mkView(Map));
+      router.addView('graph', mkView(ForceGraph));
 
       self.setData = fanoutUnfiltered.setData;
 
diff --git a/lib/infobox/link.js b/lib/infobox/link.js
index fa653cbfa0144373fcf7815595b02432f6474cd0..2e4e36eb0f99b964f6e9d25d2982f16cde25591f 100644
--- a/lib/infobox/link.js
+++ b/lib/infobox/link.js
@@ -18,8 +18,7 @@ define(['helper'], function (helper) {
     var a1;
     if (!unknown) {
       a1 = document.createElement('a');
-      a1.href = router.getUrl({ n: d.source.node_id });
-      a1.onclick = router.node(d.source.node);
+      a1.href = router.generateLink({ node: d.source.node_id });
     } else {
       a1 = document.createElement('span');
     }
@@ -31,8 +30,7 @@ define(['helper'], function (helper) {
     h2.appendChild(arrow);
 
     var a2 = document.createElement('a');
-    a2.href = router.getUrl({ n: d.target.node_id });
-    a2.onclick = router.node(d.target.node);
+    a2.href = router.generateLink({ node: d.target.node_id });
     a2.textContent = d.target.node.nodeinfo.hostname;
     h2.appendChild(a2);
     el.appendChild(h2);
diff --git a/lib/infobox/main.js b/lib/infobox/main.js
index 679f3cfe1e5c546666a08b31d44f8bf06c778875..2822ed38a00c5eaab755b08b525aab0013b52b8e 100644
--- a/lib/infobox/main.js
+++ b/lib/infobox/main.js
@@ -28,35 +28,21 @@ define(['infobox/link', 'infobox/node', 'infobox/location'], function (link, nod
       var closeButton = document.createElement('button');
       closeButton.classList.add('close');
       closeButton.classList.add('ion-close');
-      closeButton.onclick = router.reset;
-      el.appendChild(closeButton);
-    }
-
-    function clear() {
-      var closeButton = el.firstChild;
-      while (el.firstChild) {
-        el.removeChild(el.firstChild);
-      }
+      closeButton.onclick = function () {
+        router.fullUrl();
+      };
       el.appendChild(closeButton);
     }
 
     self.resetView = destroy;
 
-    self.gotoNode = function gotoNode(d, update) {
-      if (update !== true) {
-        create();
-      } else {
-        clear();
-      }
+    self.gotoNode = function gotoNode(d) {
+      create();
       node(config, el, router, d);
     };
 
-    self.gotoLink = function gotoLink(d, update) {
-      if (update !== true) {
-        create();
-      } else {
-        clear();
-      }
+    self.gotoLink = function gotoLink(d) {
+      create();
       link(config, el, router, d);
     };
 
diff --git a/lib/infobox/node.js b/lib/infobox/node.js
index 6bd94c41c56b76141045c32d7d8407f92f6195f9..aa599c960372d31a16371338d11bc49d0bebfc1e 100644
--- a/lib/infobox/node.js
+++ b/lib/infobox/node.js
@@ -191,7 +191,12 @@ define(['sorttable', 'virtual-dom', 'd3-interpolate', 'moment', 'helper'],
         }
 
         if (!unknown) {
-          name.push(V.h('a', { href: router.getUrl({ n: n.node.nodeinfo.node_id }), onclick: router.node(n.node), className: 'online' }, n.node.nodeinfo.hostname));
+          name.push(V.h('a', {
+            href: router.generateLink({ node: n.node.nodeinfo.node_id }),
+            onclick: function (e) {
+              router.fullUrl({ node: n.node.nodeinfo.node_id }, e);
+            }, className: 'online'
+          }, n.node.nodeinfo.hostname));
         } else {
           name.push(n.link.id);
         }
diff --git a/lib/linklist.js b/lib/linklist.js
index 4f9797423aa7903b8ff2887d09e43216a489e9dd..5391a452a5ce255b103611f0973abfa11fc0fe21 100644
--- a/lib/linklist.js
+++ b/lib/linklist.js
@@ -33,7 +33,12 @@ define(['sorttable', 'virtual-dom', 'helper'], function (SortTable, V, helper) {
     table.el.classList.add('link-list');
 
     function renderRow(d) {
-      var td1Content = [V.h('a', { href: router.getUrl({ l: d.id }), onclick: router.link(d) }, linkName(d))];
+      var td1Content = [V.h('a', {
+        href: router.generateLink({ link: d.id }),
+        onclick: function (e) {
+          router.fullUrl({ link: d.id }, e);
+        }
+      }, linkName(d))];
 
       var td1 = V.h('td', td1Content);
       var td2 = V.h('td', { style: { color: linkScale(1 / d.tq) } }, helper.showTq(d));
diff --git a/lib/main.js b/lib/main.js
index b1f68c9cb878790acded15c8c5dd8b9b9a404b4f..4b863bc0b1dcb0f1f5a1a1ace982cb35d7c884cf 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -1,5 +1,5 @@
-define(['polyglot', 'moment', 'router', 'leaflet', 'gui', 'helper', 'language'],
-  function (Polyglot, moment, Router, L, GUI, helper, Language) {
+define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'],
+  function (moment, Router, L, GUI, helper, Language) {
     'use strict';
 
     return function (config) {
@@ -117,7 +117,6 @@ define(['polyglot', 'moment', 'router', 'leaflet', 'gui', 'helper', 'language'],
           }
         });
 
-
         links.sort(function (a, b) {
           return b.tq - a.tq;
         });
@@ -138,8 +137,7 @@ define(['polyglot', 'moment', 'router', 'leaflet', 'gui', 'helper', 'language'],
       }
 
       var language = new Language(config);
-
-      var router = new Router();
+      var router = new Router(language);
 
       var urls = [];
 
@@ -153,6 +151,7 @@ define(['polyglot', 'moment', 'router', 'leaflet', 'gui', 'helper', 'language'],
       }
 
       function update() {
+        language.init(router);
         return Promise.all(urls.map(helper.getJSON))
           .then(handleData);
       }
@@ -162,13 +161,12 @@ define(['polyglot', 'moment', 'router', 'leaflet', 'gui', 'helper', 'language'],
           var gui = new GUI(config, router, language);
           gui.setData(d);
           router.setData(d);
-          router.start();
+          router.resolve();
 
           window.setInterval(function () {
             update().then(function (n) {
               gui.setData(n);
               router.setData(n);
-              router.update();
             });
           }, 60000);
         })
diff --git a/lib/map.js b/lib/map.js
index 121083817aece22f57b43a3c67875588182559fb..f569e49e038f02623a0440e85abc4cea65e36d9b 100644
--- a/lib/map.js
+++ b/lib/map.js
@@ -76,7 +76,9 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm
           m.setStyle(iconFunc(d));
         };
 
-        m.on('click', router.node(d));
+        m.on('click', function () {
+          router.fullUrl({ node: d.nodeinfo.node_id });
+        });
         m.bindTooltip(d.nodeinfo.hostname);
 
         dict[d.nodeinfo.node_id] = m;
@@ -105,7 +107,9 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm
         };
 
         line.bindTooltip(d.source.node.nodeinfo.hostname + ' – ' + d.target.node.nodeinfo.hostname + '<br><strong>' + helper.showDistance(d) + ' / ' + helper.showTq(d) + '</strong>');
-        line.on('click', router.link(d));
+        line.on('click', function () {
+          router.fullUrl({ link: d.id });
+        });
 
         dict[d.id] = line;
 
@@ -230,7 +234,7 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm
       }
 
       function showCoordinates(e) {
-        router.gotoLocation(e.latlng);
+        router.fullUrl({ zoom: map.getZoom(), lat: e.latlng.lat, lng: e.latlng.lng });
         disableCoords();
       }
 
@@ -345,8 +349,8 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm
         });
       }
 
-      function setView(bounds) {
-        map.fitBounds(bounds, { paddingTopLeft: [sidebar(), 0], maxZoom: config.nodeZoom });
+      function setView(bounds, zoom) {
+        map.fitBounds(bounds, { paddingTopLeft: [sidebar(), 0], maxZoom: (zoom ? zoom : config.nodeZoom) });
       }
 
       function goto(m) {
@@ -479,20 +483,21 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm
         updateView();
       };
 
-      self.gotoNode = function gotoNode(d, update) {
+      self.gotoNode = function gotoNode(d) {
         disableTracking();
         highlight = { type: 'node', o: d };
-        updateView(update);
+        updateView();
       };
 
-      self.gotoLink = function gotoLink(d, update) {
+      self.gotoLink = function gotoLink(d) {
         disableTracking();
         highlight = { type: 'link', o: d };
-        updateView(update);
+        updateView();
       };
 
-      self.gotoLocation = function gotoLocation() {
-        // ignore
+      self.gotoLocation = function gotoLocation(d) {
+        disableTracking();
+        map.setView([d.lat, d.lng], d.zoom);
       };
 
       self.destroy = function destroy() {
diff --git a/lib/nodelist.js b/lib/nodelist.js
index 2afc0831a71919a6fd0230b650741f6edaada828..7432b95a78fb1a61b725fdd59a48c4dc855e6fc4 100644
--- a/lib/nodelist.js
+++ b/lib/nodelist.js
@@ -65,8 +65,10 @@ define(['sorttable', 'virtual-dom', 'helper'], function (SortTable, V, helper) {
 
       td1Content.push(V.h('a', {
         className: aClass.join(' '),
-        onclick: router.node(d),
-        href: router.getUrl({ n: d.nodeinfo.node_id })
+        href: router.generateLink({ node: d.nodeinfo.node_id }),
+        onclick: function (e) {
+          router.fullUrl({ node: d.nodeinfo.node_id }, e);
+        }
       }, d.nodeinfo.hostname));
       if (helper.hasLocation(d)) {
         td0Content.push(V.h('span', { className: 'icon ion-location' }));
diff --git a/lib/router.js b/lib/router.js
deleted file mode 100644
index 70fd75bd653ade4f21dacbe05eaa1340561cfd94..0000000000000000000000000000000000000000
--- a/lib/router.js
+++ /dev/null
@@ -1,242 +0,0 @@
-define(['helper'], function (helper) {
-  'use strict';
-
-  return function () {
-    var self = this;
-    var objects = { nodes: {}, links: {} };
-    var targets = [];
-    var views = {};
-    var currentView;
-    var currentObject;
-    var running = false;
-
-    function saveState() {
-      var e = '#!';
-
-      e += 'v:' + currentView;
-
-      if (currentObject) {
-        if ('node' in currentObject) {
-          e += ';n:' + encodeURIComponent(currentObject.node.nodeinfo.node_id);
-        } else if ('link' in currentObject) {
-          e += ';l:' + encodeURIComponent(currentObject.link.id);
-        }
-      }
-
-      window.history.pushState(e, undefined, e);
-    }
-
-    function resetView(push) {
-      push = helper.trueDefault(push);
-
-      targets.forEach(function (t) {
-        t.resetView();
-      });
-
-      if (push) {
-        currentObject = undefined;
-        saveState();
-      }
-    }
-
-    function gotoNode(d, update) {
-      if (!d) {
-        return false;
-      }
-
-      targets.forEach(function (t) {
-        t.gotoNode(d, update);
-      });
-
-      return true;
-    }
-
-    function gotoLink(d, update) {
-      if (!d) {
-        return false;
-      }
-
-      targets.forEach(function (t) {
-        t.gotoLink(d, update);
-      });
-
-      return true;
-    }
-
-    function loadState(s, update) {
-      if (!s) {
-        return false;
-      }
-
-      s = decodeURIComponent(s);
-
-      if (!s.startsWith('#!')) {
-        return false;
-      }
-
-      var targetSet = false;
-
-      s.slice(2).split(';').forEach(function (d) {
-        var args = d.split(':');
-
-        if (update !== true && args[0] === 'v' && args[1] in views) {
-          currentView = args[1];
-          views[args[1]]();
-        }
-
-        var id;
-
-        if (args[0] === 'n') {
-          id = args[1];
-          if (id in objects.nodes) {
-            currentObject = { node: objects.nodes[id] };
-            gotoNode(objects.nodes[id], update);
-            targetSet = true;
-          }
-        }
-
-        if (args[0] === 'l') {
-          id = args[1];
-          if (id in objects.links) {
-            currentObject = { link: objects.links[id] };
-            gotoLink(objects.links[id], update);
-            targetSet = true;
-          }
-        }
-      });
-
-      return targetSet;
-    }
-
-    self.getUrl = function getUrl(data) {
-      var e = '#!';
-
-      if (data.n) {
-        e += 'n:' + encodeURIComponent(data.n);
-      }
-
-      if (data.l) {
-        e += 'l:' + encodeURIComponent(data.l);
-      }
-
-      return e;
-    };
-
-    self.start = function start() {
-      running = true;
-
-      if (!loadState(window.location.hash)) {
-        resetView(false);
-      }
-
-      window.onpopstate = function onpopstate(d) {
-        if (!loadState(d.state)) {
-          resetView(false);
-        }
-      };
-    };
-
-    self.view = function view(d) {
-      if (d in views) {
-        views[d]();
-
-        if (!currentView || running) {
-          currentView = d;
-        }
-
-        if (!running) {
-          return;
-        }
-
-        saveState();
-
-        if (!currentObject) {
-          resetView(false);
-          return;
-        }
-
-        if ('node' in currentObject) {
-          gotoNode(currentObject.node);
-        }
-
-        if ('link' in currentObject) {
-          gotoLink(currentObject.link);
-        }
-      }
-    };
-
-    self.node = function node(d) {
-      return function () {
-        if (gotoNode(d)) {
-          currentObject = { node: d };
-          saveState();
-        }
-
-        return false;
-      };
-    };
-
-    self.link = function link(d) {
-      return function () {
-        if (gotoLink(d)) {
-          currentObject = { link: d };
-          saveState();
-        }
-
-        return false;
-      };
-    };
-
-    self.gotoLocation = function gotoLocation(d) {
-      if (!d) {
-        return false;
-      }
-
-      targets.forEach(function (t) {
-        if (!t.gotoLocation) {
-          console.warn('has no gotoLocation', t);
-        }
-        t.gotoLocation(d);
-      });
-
-      return true;
-    };
-
-    self.reset = function reset() {
-      resetView();
-    };
-
-    self.addTarget = function addTarget(d) {
-      targets.push(d);
-    };
-
-    self.removeTarget = function removeTarget(d) {
-      targets = targets.filter(function (e) {
-        return d !== e;
-      });
-    };
-
-    self.addView = function addView(k, d) {
-      views[k] = d;
-    };
-
-    self.setData = function setData(data) {
-      objects.nodes = {};
-      objects.links = {};
-
-      data.nodes.all.forEach(function (d) {
-        objects.nodes[d.nodeinfo.node_id] = d;
-      });
-
-      data.graph.links.forEach(function (d) {
-        objects.links[d.id] = d;
-      });
-    };
-
-    self.update = function update() {
-      loadState(window.location.hash, true);
-    };
-
-    return self;
-  };
-});
diff --git a/lib/simplenodelist.js b/lib/simplenodelist.js
index 81123c789ce0102132b39ac842950ff60b1848de..a4e9e3f179582a67b1acf92acbff7a59e11a07bf 100644
--- a/lib/simplenodelist.js
+++ b/lib/simplenodelist.js
@@ -41,8 +41,10 @@ define(['moment', 'virtual-dom', 'helper'], function (moment, V, helper) {
 
         td1Content.push(V.h('a', {
           className: aClass.join(' '),
-          onclick: router.node(d),
-          href: router.getUrl({ n: d.nodeinfo.node_id })
+          href: router.generateLink({ node: d.nodeinfo.node_id }),
+          onclick: function (e) {
+            router.fullUrl({ node: d.nodeinfo.node_id }, e);
+          }
         }, d.nodeinfo.hostname));
 
         if (helper.hasLocation(d)) {
diff --git a/lib/title.js b/lib/title.js
index c1ee5425cf29e34970e31231f17fedd711e1a76c..be92f8d56e1effbf06f8fe18f46611dcd41343cd 100644
--- a/lib/title.js
+++ b/lib/title.js
@@ -17,15 +17,11 @@ define(function () {
     };
 
     this.gotoNode = function gotoNode(d) {
-      if (d) {
-        setTitle(d.nodeinfo.hostname);
-      }
+      setTitle(d.nodeinfo.hostname);
     };
 
     this.gotoLink = function gotoLink(d) {
-      if (d) {
-        setTitle((d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + ' – ' + d.target.node.nodeinfo.hostname);
-      }
+      setTitle((d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + ' – ' + d.target.node.nodeinfo.hostname);
     };
 
     this.gotoLocation = function gotoLocation() {
diff --git a/lib/utils/language.js b/lib/utils/language.js
index 501fdd2eb5dfa14ce82813dbe115f96a08228424..5364d22898d5a8ab5bc9d75f43d508a19bdedfcc 100644
--- a/lib/utils/language.js
+++ b/lib/utils/language.js
@@ -1,10 +1,12 @@
 define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
   'use strict';
   return function (config) {
+    var router;
+
     function languageSelect(el) {
       var select = document.createElement('select');
       select.className = 'language-switch';
-      select.addEventListener('change', setLocale);
+      select.addEventListener('change', setSelectLocale);
       el.appendChild(select);
 
       // Keep english
@@ -14,8 +16,12 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
       }
     }
 
-    function setLocale(event) {
-      localStorage.setItem('language', getLocale(event.target.value));
+    function setSelectLocale(event) {
+      router.fullUrl({ lang: event.target.value }, false, true);
+    }
+
+    function setLocale(lang) {
+      localStorage.setItem('language', getLocale(lang));
       location.reload();
     }
 
@@ -51,10 +57,16 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
       }
     }
 
-    window._ = new Polyglot({ locale: getLocale(), allowMissing: true });
-    helper.getJSON('locale/' + _.locale() + '.json?' + config.cacheBreaker).then(setTranslation);
+    function init(r) {
+      router = r;
+      window._ = new Polyglot({ locale: getLocale(router.getLang()), allowMissing: true });
+      helper.getJSON('locale/' + _.locale() + '.json?' + config.cacheBreaker).then(setTranslation);
+    }
 
     return {
+      init: init,
+      getLocale: getLocale,
+      setLocale: setLocale,
       languageSelect: languageSelect
     };
   };
diff --git a/lib/utils/router.js b/lib/utils/router.js
new file mode 100644
index 0000000000000000000000000000000000000000..2fb19889491670c218eaa5d89b92fe4fac2afee6
--- /dev/null
+++ b/lib/utils/router.js
@@ -0,0 +1,156 @@
+define(['Navigo'], function (Navigo) {
+  'use strict';
+
+  return function (language) {
+    var init = false;
+    var objects = { nodes: {}, links: {} };
+    var targets = [];
+    var views = {};
+    var current = {};
+    var state = { lang: language.getLocale(), view: 'map' };
+
+    function resetView() {
+      targets.forEach(function (t) {
+        t.resetView();
+      });
+    }
+
+    function gotoNode(d) {
+      if (d.nodeId in objects.nodes) {
+        targets.forEach(function (t) {
+          t.gotoNode(objects.nodes[d.nodeId]);
+        });
+      }
+    }
+
+    function gotoLink(d) {
+      if (d.linkId in objects.links) {
+        targets.forEach(function (t) {
+          t.gotoLink(objects.links[d.linkId]);
+        });
+      }
+    }
+
+    function view(d) {
+      if (d.view in views) {
+        views[d.view]();
+        state.view = d.view;
+        resetView();
+      }
+    }
+
+    function customRoute(lang, viewValue, node, link, zoom, lat, lng) {
+      current = {
+        lang: lang,
+        view: viewValue,
+        node: node,
+        link: link,
+        zoom: zoom,
+        lat: lat,
+        lng: lng
+      };
+
+      if (lang && lang !== state.lang && lang === language.getLocale(lang)) {
+        language.setLocale(lang);
+      }
+
+      if (!init || viewValue && viewValue !== state.view) {
+        if (!viewValue) {
+          viewValue = state.view;
+        }
+        view({ view: viewValue });
+        init = true;
+      }
+
+      if (node) {
+        gotoNode({ nodeId: node });
+      } else if (link) {
+        gotoLink({ linkId: link });
+      } else if (lat) {
+        targets.forEach(function (t) {
+          t.gotoLocation({
+            zoom: parseInt(zoom, 10),
+            lat: parseFloat(lat),
+            lng: parseFloat(lng)
+          });
+        });
+      } else {
+        resetView();
+      }
+    }
+
+    var router = new Navigo(null, true);
+
+    router
+      .on(/^\/?#?\/([\w]{2})?\/?(map|graph)?\/?([a-f\d]{12})?([a-f\d\-]{25})?\/?(?:(\d+)\/([\d.]+)\/([\d.]+))?$/, customRoute)
+      .on({
+        '*': function () {
+          router.fullUrl();
+        }
+      });
+
+    router.generateLink = function generateLink(data, full, deep) {
+      var result = '#';
+
+      if (full) {
+        data = Object.assign({}, state, data);
+      } else if (deep) {
+        data = Object.assign({}, current, data);
+      }
+
+      for (var key in data) {
+        if (!data.hasOwnProperty(key) || data[key] === undefined) {
+          continue;
+        }
+        result += '/' + data[key];
+      }
+
+      return result;
+    };
+
+    router.fullUrl = function fullUrl(data, e, deep) {
+      if (e) {
+        e.preventDefault();
+      }
+      router.navigate(router.generateLink(data, !deep, deep));
+    };
+
+    router.getLang = function getLang() {
+      var lang = location.hash.match(/^\/?#\/([\w]{2})\//);
+      if (lang) {
+        state.lang = language.getLocale(lang[1]);
+        return lang[1];
+      }
+      return null;
+    };
+
+    router.addTarget = function addTarget(d) {
+      targets.push(d);
+    };
+
+    router.removeTarget = function removeTarget(d) {
+      targets = targets.filter(function (e) {
+        return d !== e;
+      });
+    };
+
+    router.addView = function addView(k, d) {
+      views[k] = d;
+    };
+
+    router.setData = function setData(data) {
+      objects.nodes = {};
+      objects.links = {};
+
+      data.nodes.all.forEach(function (d) {
+        objects.nodes[d.nodeinfo.node_id] = d;
+      });
+
+      data.graph.links.forEach(function (d) {
+        objects.links[d.id] = d;
+      });
+    };
+
+    return router;
+  };
+});
diff --git a/package.json b/package.json
index 6ed775c743214c05df698f7a92e3e7486e4af12f..701f3c94e8f2c41f69b6743fb5a07377e203647b 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
     "d3-zoom": "^1.1.3",
     "leaflet": "^1.0.3",
     "moment": "^2.17.1",
+    "navigo": "^4.6.0",
     "node-polyglot": "^2.2.2",
     "promise-polyfill": "^6.0.2",
     "rbush": "^2.0.1",
diff --git a/yarn.lock b/yarn.lock
index 4740663113354f028ff4f76d31c6920d30fbf7dc..1250b6141ee81550d062af45fdd1c4b214e47a67 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3259,6 +3259,10 @@ natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
 
+navigo@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/navigo/-/navigo-4.6.0.tgz#212cf08e1658874243a4acaea041aee42c30480a"
+
 ncname@1.0.x:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/ncname/-/ncname-1.0.0.tgz#5b57ad18b1ca092864ef62b0b1ed8194f383b71c"