Linking to another HTML page in Google Apps Script

While the HtmlService allows you to serve HTML, it is not "hosting" pages, and you cannot access the various html files in your Apps Script project by URL directly. Instead, your Web App will have a URL when it is published, and that is the only URL you have.

Here's a way that you can serve separate pages from your script, and have them behave similarly to html file links.

The doGet() function is passed an event when called, and we can take advantage of that to indicate which page we want served. If our Web App ID is <SCRIPTURL>, here is what a URL plus a querystring requesting a specific page will look like:

https://script.google.com/macros/s/<SCRIPTURL>/dev?page=my1

Using templated HTML, we can generate the necessary URL + querystring on the fly. In our doGet(), we just need to parse the querystring to determine which page to serve.

Here's the script, with two sample pages containing buttons to flip between them.

Code.gs

/**
 * Get the URL for the Google Apps Script running as a WebApp.
 */
function getScriptUrl() {
 var url = ScriptApp.getService().getUrl();
 return url;
}

/**
 * Get "home page", or a requested page.
 * Expects a 'page' parameter in querystring.
 *
 * @param {event} e Event passed to doGet, with querystring
 * @returns {String/html} Html to be served
 */
function doGet(e) {
  Logger.log( Utilities.jsonStringify(e) );
  if (!e.parameter.page) {
    // When no specific page requested, return "home page"
    return HtmlService.createTemplateFromFile('my1').evaluate();
  }
  // else, use page parameter to pick an html file from the script
  return HtmlService.createTemplateFromFile(e.parameter['page']).evaluate();
}

my1.html

<html>
  <body>
    <h1>Source = my1.html</h1>
    <?var url = getScriptUrl();?><a href='<?=url?>?page=my2'> <input type='button' name='button' value='my2.html'></a>
  </body>
</html>

my2.html

<html>
  <body>
    <h1>Source = my2.html</h1>
    <?var url = getScriptUrl();?><a href='<?=url?>?page=my1'> <input type='button' name='button' value='my1.html'></a>
  </body>
</html>

Multi Page Web App

This is working basic four page webapp. It only displays the page title and four buttons on the left sidebar for navigation.

Code.gs:

function getScriptURL(qs) {
  var url = ScriptApp.getService().getUrl();
  //Logger.log(url + qs);
  return url + qs ;
}

function doGet(e) 
{
  //Logger.log('query params: ' + Utilities.jsonStringify(e));
  if(e.queryString !=='')
  {  
    switch(e.parameter.mode)
    {
      case 'page4':
        setPage('Page4')      
        return HtmlService
        .createTemplateFromFile('Page4')
        .evaluate()
        .addMetaTag('viewport', 'width=device-width, initial-scale=1')
        .setTitle("Page4"); 
        break;  
      case 'page3':
        setPage('Page3');        
        return HtmlService
        .createTemplateFromFile('Page3')
        .evaluate()
        .addMetaTag('viewport', 'width=device-width, initial-scale=1')
        .setTitle("Page3");
        break;
      case 'page2':
        setPage('Page2');        
        return HtmlService
        .createTemplateFromFile('Page2')
        .evaluate()
        .addMetaTag('viewport', 'width=device-width, initial-scale=1')
        .setTitle("Page2");
        break;  
      case 'page1':
         setPage('Page1');
         return HtmlService
        .createTemplateFromFile('Page1')
        .evaluate()
        .addMetaTag('viewport', 'width=device-width, initial-scale=1')
        .setTitle("Page1");
        break;
      default:
        setPage('Page1');
        return HtmlService
        .createTemplateFromFile('Page1')
        .evaluate()
        .addMetaTag('viewport', 'width=device-width, initial-scale=1')
        .setTitle("Page1");
        break;
    }
  }
  else
  {
    setPage('Page1');
    return HtmlService
    .createTemplateFromFile('Page1')
    .evaluate()
    .addMetaTag('viewport', 'width=device-width, initial-scale=1')
    .setTitle("Page1");
  }
}

function getPageData()
{
  var s='';
  s+='<input type="button" value="Page1" onClick="getUrl(\'?mode=page1\');" />';
  s+='<br /><input type="button" value="Page2" onClick="getUrl(\'?mode=page2\');" />';
  s+='<br /><input type="button" value="Page3" onClick="getUrl(\'?mode=page3\');" />';
  s+='<br /><input type="button" value="Page4" onClick="getUrl(\'?mode=page4\');" />';
  var rObj={menu:s,title:getPage()};
  Logger.log(rObj);
  return rObj;
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

Pages.gs

I decided to switch to user scripts properties service since you may want to have more that one user. I often develop scripts that only I use.

function setPage(page) {
  var ps=PropertiesService.getUserProperties();
  ps.setProperty('PageTitle', page);
  return ps.getProperty('PageTitle');
}

function initPage() {
  var ps=PropertiesService.getUserProperties();
  ps.setProperty('PageTitle','');
  return ps.getProperty('PageTitle');
}

function getPage() {
  var ps=PropertiesService.getUserProperties();
  var pt=ps.getProperty('PageTitle');
  return pt;
}

globals.gs:

I was using this for the page title but decided that some of you may wish to have multiple users and so user properties service is a more logical selection. I'll probably use this for something else so I left it in. But your call.

function getGlobals(){
  var ss=SpreadsheetApp.getActive();
  var sh=ss.getSheetByName('Globals');
  var rg=sh.getRange(1,1,getGlobalHeight(),2);
  var vA=rg.getValues();
  var g={};
  for(var i=0;i<vA.length;i++){
    g[vA[i][0]]=vA[i][1];
  }
  return g;
}

function setGlobals(dfltObj){
  if(dfltObj){
    var ss=SpreadsheetApp.getActive();
    var sh=ss.getSheetByName('Globals');
    var rg=sh.getRange(1,1,getGlobalHeight(),2);
    var vA=rg.getValues();
    for(var i=0;i<vA.length;i++){
      vA[i][1]=dfltObj[vA[i][0]];
    }
    rg.setValues(vA);
  }
}

function getGlobal(key) {
  var rObj=getGlobals();
  if(rObj.hasOwnProperty(key)){
    return rObj[key];
  }else{
    throw(Utilities.formatString('JJE-SimpleUtilitiesScripts-Error: Globals does not contain a key named %s.',key));
  }  
}

function setGlobal(key,value){
  var curObj=getGlobals();
  curObj[key]=value;
  setGlobals(curObj);
}

function getGlobalHeight(){
  var ss=SpreadsheetApp.getActive();
  var sh=ss.getSheetByName('Globals');
  var rg=sh.getRange(1,1,sh.getMaxRows(),1);
  var vA=rg.getValues();
  for(var i=0;i<vA.length;i++){
    if(!vA[i][0]){
      break;
    }
  }
  return i;
}

Page1.html:

1 of 4. They start out the same. I make them this way and if I need to customize them I'll usually just do it right inside the html page. But it's also possible to add additional javascript or css pages to support variations from page to page.

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <?!= include('res') ?>
    <?!= include('css') ?>
  </head>
  <body>
    <?!= include('content') ?>
    <?!= include('script') ?>
  </body>
</html>
<html><head>

content.html:

    <div class="sidenav"></div>
    <div class="header">
      <h1 id="ttl"></h1>
    </div>
    <div class="main"></div>

script.html:

<script>
  $(function(){
    google.script.run
    .withSuccessHandler(updatePageData)
    .getPageData();
  });
    function getUrl(qs){
      google.script.run
      .withSuccessHandler(loadNewPage)
      .getScriptURL(qs);
    }
     function updatePageData(dObj){
      $('.sidenav').html(dObj.menu);
      $('.header #ttl').html(dObj.title);
    }
    function loadNewPage(url){
      window.open(url,"_top");
    }
    console.log('script.html');
</script>

css.html:

A very simple styles page.

<style>
input[type="button"],input[type="text"],label{margin:2px 2px 5px 5px;}
body {
  background-color:#fbd393;
  font-family: "Lato", sans-serif;
}
.sidenav {
  height: 100%;
  width: 75px;
  position: fixed;
  z-index: 1;
  top: 0;
  left: 0;
  background-color: #EEE;
  overflow-x: hidden;
  padding-top: 5px;
}

.sidenav a {
  padding: 6px 8px 6px 16px;
  text-decoration: none;
  font-size: 16px;
  color: #818181;
  display: block;
}
.sidenav a:hover,label:hover {
  color: #770000;
}
.header{
  margin-left: 75px; /* Same as the width of the sidenav */
  font-size: 16px; 
  padding: 0px 5px;
  background-color:#fbd393;
  height:60px;
}

.main {
  margin-left: 75px; /* Same as the width of the sidenav */
  font-size: 16px; /* Increased text to enable scrolling */
  padding: 0px 5px;
  background-color:#e9e8de;
  height:450px;
}

@media screen and (max-height: 450px) {
  .sidenav {padding-top: 5px;}
  .sidenav a {font-size: 16px;}
}

</style>

res.html:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>

Animation:

enter image description here