diff --git a/static/skin/viewer.js b/static/skin/viewer.js
index c28cbb18f..085777aab 100644
--- a/static/skin/viewer.js
+++ b/static/skin/viewer.js
@@ -1,3 +1,146 @@
+// Terminology
+//
+// user url: identifier of the page that has to be displayed in the viewer
+// and that is used as the hash component of the viewer URL. For
+// book resources the address url is {book}/{resource} .
+//
+// iframe url: the URL to be loaded in the viewer iframe.
+
+function userUrl2IframeUrl(url) {
+ if ( url == '' ) {
+ return blankPageUrl;
+ }
+
+ if ( url.startsWith('search?') ) {
+ return `${root}/${url}`;
+ }
+
+ return `${root}/content/${url}`;
+}
+
+function getBookFromUserUrl(url) {
+ if ( url == '' ) {
+ return null;
+ }
+
+ if ( url.startsWith('search?') ) {
+ const p = new URLSearchParams(url.slice("search?".length));
+ return p.get('books.name');
+ }
+ return url.split('/')[0];
+}
+
+let currentBook = null;
+let currentBookTitle = null;
+
+const bookUIGroup = document.getElementById('kiwix_serve_taskbar_book_ui_group');
+const homeButton = document.getElementById('kiwix_serve_taskbar_home_button');
+
+function gotoMainPageOfCurrentBook() {
+ location.hash = currentBook + '/';
+}
+
+function gotoUrl(url) {
+ cf.src = url;
+}
+
+function gotoRandomPage() {
+ gotoUrl(`${root}/random?content=${currentBook}`);
+}
+
+function performSearch() {
+ const searchbox = document.getElementById('kiwixsearchbox');
+ const q = encodeURIComponent(searchbox.value);
+ gotoUrl(`${root}/search?books.name=${currentBook}&pattern=${q}`);
+}
+
+function setCurrentBook(book, title) {
+ currentBook = book;
+ currentBookTitle = title;
+ homeButton.title = `Go to the main page of '${title}'`;
+ homeButton.setAttribute("aria-label", homeButton.title);
+ homeButton.innerHTML = ``;
+ const searchbox = document.getElementById('kiwixsearchbox');
+ searchbox.title = `Search '${title}'`;
+ searchbox.setAttribute("aria-label", searchbox.title);
+ bookUIGroup.style.display = 'inline';
+}
+
+function noCurrentBook() {
+ currentBook = null;
+ currentBookTitle = null;
+ bookUIGroup.style.display = 'none';
+}
+
+function updateCurrentBookIfNeeded(userUrl) {
+ const book = getBookFromUserUrl(userUrl);
+ if ( currentBook != book ) {
+ updateCurrentBook(book);
+ }
+}
+
+function updateCurrentBook(book) {
+ if ( book == null ) {
+ noCurrentBook();
+ } else {
+ fetch(`./raw/${book}/meta/Title`).then(async (resp) => {
+ if ( resp.ok ) {
+ setCurrentBook(book, await resp.text());
+ } else {
+ noCurrentBook();
+ }
+ }).catch((err) => {
+ console.log("Error fetching book title: " + err);
+ noCurrentBook();
+ });
+ }
+}
+
+function iframeUrl2UserUrl(url, query) {
+ if ( url == blankPageUrl ) {
+ return '';
+ }
+
+ if ( url == `${root}/search` ) {
+ return `search${query}`;
+ }
+
+ url = url.slice(root.length);
+
+ return url.split('/').slice(2).join('/');
+}
+
+const cf = document.getElementById('content_iframe');
+
+function handle_visual_viewport_change() {
+ cf.height = window.visualViewport.height - cf.offsetTop - 4;
+}
+
+function handle_location_hash_change() {
+ const hash = window.location.hash.slice(1);
+ updateCurrentBookIfNeeded(hash);
+ const iframeContentUrl = userUrl2IframeUrl(hash);
+ console.log("handle_location_hash_change: " + hash);
+ if ( iframeContentUrl != cf.contentWindow.location.pathname ) {
+ cf.contentWindow.location.replace(iframeContentUrl);
+ }
+}
+
+function handle_content_url_change() {
+ document.title = cf.contentDocument.title;
+ const iframeContentUrl = cf.contentWindow.location.pathname;
+ const iframeContentQuery = cf.contentWindow.location.search;
+ console.log('handle_content_url_change: ' + cf.contentWindow.location.href);
+ const newHash = '#' + iframeUrl2UserUrl(iframeContentUrl, iframeContentQuery);
+ const viewerURL = location.origin + location.pathname + location.search;
+ window.location.replace(viewerURL + newHash);
+};
+
+window.onresize = handle_visual_viewport_change;
+window.onhashchange = handle_location_hash_change;
+
+handle_location_hash_change();
+
function htmlDecode(input) {
var doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
diff --git a/static/viewer.html b/static/viewer.html
index 42a785caf..7948cd17c 100644
--- a/static/viewer.html
+++ b/static/viewer.html
@@ -62,149 +62,6 @@