Smart Life US

← All posts · 2026-08-13 · Excel & Automation

Google Sheets web app multiple screens: Apps Script doGet page routing — Receiving appointment system 4

Google Sheets web app multiple screens: Apps Script doGet page routing — Receiving appointment system 4

Introduction: When you want to manage multiple Google Sheets web app screens in one place

When you look up how to create multiple screens in a Google Sheets web app, you’ll often see examples that create a separate Apps Script project for each screen. But if you run separate check‑in, admin dashboard, and on‑site lookup screens for a receiving appointment system, the number of projects explodes, and URLs, permissions, and shared functions all drift apart, making real‑world maintenance difficult.

doGet 페이지 라우팅 구조

This post walks through how to select among multiple HTML screens within a single Apps Script project using only the page parameter of doGet. You’ll append something like ?page=checkin to the URL to choose which screen to show, and structure the code so that it’s easy to use both a sidebar and a browser web app from the same codebase later.

We’ll assume you’ve already set up the basic sheet structure from the previous post, Create the sheet structure for a Google Sheets receiving appointment system — Basics 1. The goal of this Part 4 is to build the skeleton of the actual user‑facing web screens on top of that, and to finish Apps Script doGet page routing and Google Sheets web app deployment in one go.


Recap of the APPT series structure and today’s goal

A receiving appointment system is often used simultaneously by dock drivers, door allocators, and office staff. In practice, you usually need three kinds of screens:

  • A browser web app where outside carriers/vendors submit appointments
  • An admin screen where operators view and edit appointment status
  • An on‑site screen where receiving staff check in arriving trucks

If you build each one as a separate Apps Script project, every time you change sheet structure or business rules you have to update all the projects, and it’s easy to get confused about which project is the latest. That’s why this series is designed from the start so that all code from every part can live in a single Apps Script project without conflicts.

To do that, all constants and function names related to appointments use the APPT_ prefix. For example, sheet/web app settings become APPT_CONFIG, APPT_WEBAPP_CONFIG, menu‑related constants become APPT_MENU_CONFIG, and so on. Even if values are the same, duplicate names cause const redeclaration errors. Shared helpers from Part 1 such as getOrCreateSheet_() stay as they are and are not re-listed here. The date/time helpers (APPT_ymd_, APPT_hm_) are covered separately in Part 5.

In this part we’ll complete three items:

  1. Build a routing function in doGet(e) that selects different HTML screens based on e.parameter.page
  2. Use APPT_addWebAppMenu_() in conjunction with onOpen() to open each web app screen from the sheet’s top menu
  3. Pass page and titles into HtmlService templates so that each screen shows its own browser tab title

To confirm everything works, deploy the web app once. If visiting the base URL and the URL with ?page=checkin shows different screens, and if you can open both the home and check‑in web apps in new tabs from the sheet menu, you’ve hit this part’s goal.


Splitting screens by page parameter in doGet(e)

First we’ll create a router function inside Apps Script’s doGet(e) that reads the URL’s page parameter and selects which HTML file to display. Once this structure is in place, adding screens later is as simple as adding to VALID_PAGES and adding a template file, which makes maintenance far easier.

Paste this code at the top of Code.gs for this project. If you already created a doGet in another tutorial, replace that function body with this routing structure. There can only be one doGet per project.

Apps Script (JavaScript)
// → Only change this section to match your environment
const APPT_WEBAPP_CONFIG = {                         // → Web app config bundle
  DEFAULT_PAGE: 'home',                              // → Default screen key
  VALID_PAGES: ['home', 'checkin'],                  // → Allowed page list
  TITLE_MAP: {                                       // → Title per screen
    home: 'Receiving appointment main screen',       // → Default screen title
    checkin: 'Receiving check-in screen'             // → Check-in screen title
  }
};                                                   // → End of config

function doGet(e) {                                  // → Web app GET entrypoint
  var page = (e && e.parameter && e.parameter.page)  // → Read page parameter
    ? String(e.parameter.page)                       // → Convert to string
    : APPT_WEBAPP_CONFIG.DEFAULT_PAGE;               // → Fallback to default

  if (APPT_WEBAPP_CONFIG.VALID_PAGES                // → Check allowed list exists
      && APPT_WEBAPP_CONFIG
      && APPT_WEBAPP_CONFIG.VALID_PAGES.indexOf(page) === -1) {  // → Validate value
    page = APPT_WEBAPP_CONFIG.DEFAULT_PAGE;          // → Invalid → use default
  }

  var template = HtmlService                         // → Create template object
    .createTemplateFromFile('APPT_' + page);         // → File name rule: APPT_home, etc.

  template.page = page;                              // → Pass page variable into template
  template.title = APPT_WEBAPP_CONFIG.TITLE_MAP[page] || '';  // → Pass title

  var html = template.evaluate();                    // → Evaluate HTML
  html.setTitle(template.title);                     // → Browser tab title

  return html;                                       // → Return to user
}

How to check it: in the Apps Script editor, create two HTML files APPT_home and APPT_checkin via File → New → HTML, even with simple dummy content, then deploy the web app once. If opening the deployment URL (like .../exec) and .../exec?page=checkin shows different titles/content, routing is working.


Creating HtmlService template files: home and checkin screens

With the router ready, we’ll create the HtmlService templates that will actually display in the browser. The structure is standard HTML, and you can use <?= ... ?> to insert template variables (title, page).

For this part, we’ll focus on routing and link behavior, so both screens will have minimal UI:

  • APPT_home: main receiving appointment info + a button to go to the check‑in screen
  • APPT_checkin: input fields for truck and carrier + a link back to the main screen

Create both files from Apps Script editor → File → New → HTML. Use the exact file names APPT_home and APPT_checkin (no extension; Apps Script adds .html automatically).

Step 1 — APPT_home basic screen template

HTML
<!-- APPT_home.html → Receiving appointment main screen -->
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta charset="UTF-8">
    <title><?= title ?></title>
    <style>
      body { font-family: sans-serif; padding: 16px; }
      a.button {
        display: inline-block;
        padding: 8px 16px;
        margin-top: 8px;
        border-radius: 4px;
        background: #1976d2;
        color: #fff;
        text-decoration: none;
      }
    </style>
  </head>
  <body>
    <h1><?= title ?></h1>
    <p>This is the main screen for the receiving appointment.</p>
    <p>Click the button below to start truck check-in.</p>
    <a class="button" href="?page=checkin">Open check-in screen</a>
  </body>
</html>

How to confirm: when you open just the deployment URL (like .../exec), you should see the text “This is the main screen for the receiving appointment.” and a button labeled “Open check-in screen”.

Step 2 — APPT_checkin check‑in screen template

HTML
<!-- APPT_checkin.html → Receiving check-in screen -->
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta charset="UTF-8">
    <title><?= title ?></title>
    <style>
      body { font-family: sans-serif; padding: 16px; }
      label { display: block; margin-top: 8px; }
      input { padding: 4px 8px; }
      .back { margin-top: 16px; }
    </style>
  </head>
  <body>
    <h1><?= title ?></h1>
    <p>This screen is for entering information when a receiving truck arrives.</p>

    <label>Truck number
      <input type="text" id="truckNo">
    </label>
    <label>Carrier name
      <input type="text" id="carrier">
    </label>

    <div class="back">
      <a href="?page=home">Back to main screen</a>
    </div>
  </body>
</html>

How to confirm: open the deployment URL with ?page=checkin. You should see the check‑in description, input fields, and a “Back to main screen” link. Clicking that link should take you back to the main screen.


How to merge onOpen so series code doesn’t clash

Other parts in this series (Parts 1, 2, 3) also define onOpen() to build top menus. When a project holds several functions with the same name, only the last declaration survives. If you paste this part's onOpen on top of an earlier one, the earlier menus (sheet setup, seeding, settings) disappear silently, without a single error.

So this series keeps one onOpen for the whole project and each part only adds an add○○Menu_(menu) helper. Here is the complete onOpen through Part 4. The typeof checks let it work even if you haven't added the earlier parts yet.

Apps Script (JavaScript)
function onOpen() {                                  // → Runs once when sheet opens
  var ui = SpreadsheetApp.getUi();                   // → UI object
  var menu = ui.createMenu('Reservation Tool');      // → Shared series menu (same name everywhere)

  // If earlier parts are already in the project, their menus are attached too.
  // The names must match what those parts actually created, character for character.
  if (typeof addApptBaseMenu_ === 'function') {      // → Part 1 base menu
    addApptBaseMenu_(menu);                          // → Sheet setup items
  }
  if (typeof APPT_addSeedMenu_ === 'function') {     // → Part 2 door/yard seed
    APPT_addSeedMenu_(menu);                         // → Seed items
  }
  if (typeof APPT_addSettingsMenu_ === 'function') { // → Part 3 settings
    APPT_addSettingsMenu_(menu);                     // → Settings items
  }

  APPT_addWebAppMenu_(menu);                         // → This part: open web app screens

  menu.addToUi();                                    // → Attach the menu to the sheet
}

If you already created onOpen() in Parts 1–3, don't create a second one. Either replace the existing onOpen with the code above, or add just one line inside it. Calling createMenu again creates a second menu with the same name.

Apps Script (JavaScript)
APPT_addWebAppMenu_(menu);   // one line inside your existing onOpen, right before menu.addToUi()

To summarize:

  • Keep only one onOpen function in the entire project,
  • call createMenu('Reservation Tool') only once,
  • in each post add only a menu helper such as addApptBaseMenu_(), APPT_addSeedMenu_(), APPT_addSettingsMenu_(), APPT_addWebAppMenu_(), and
  • call every one of them inside that single onOpen. Miss one and that part's menu quietly vanishes.

Connecting the Google Sheets menu to web app URLs

In real operations, if you only manage web app URLs via bookmarks, every time staff change or deployment settings change you get confusion. If you expose the web app directly from the sheet menu, training and handover become easier and mistakes decrease.

Here we’ll use APPT_getWebAppBaseUrl_() to read the currently deployed web app URL, and APPT_openHomeWebApp() / APPT_openCheckinWebApp() to open that URL in a new tab. Add this code just below the doGet(e) function in Code.gs.

Apps Script (JavaScript)
// → Menu setup added in this part
const APPT_MENU_CONFIG = {                           // → Menu config object
  ITEM_OPEN_HOME: 'Open web app main screen',        // → Home screen menu
  ITEM_OPEN_CHECKIN: 'Open web app check-in screen'  // → Check-in screen menu
};                                                   // → End of config
// The menu name ('Reservation Tool') stays exactly as Part 1 created it and is used
// once, inside onOpen. Keeping a second copy here invites the two to drift apart —
// and when they drift, the sheet grows two menus with the same name.

function APPT_addWebAppMenu_(menu) {                 // → Add this part's items to the shared menu
  menu.addItem(
    APPT_MENU_CONFIG.ITEM_OPEN_HOME,
    'APPT_openHomeWebApp'
  );
  menu.addItem(
    APPT_MENU_CONFIG.ITEM_OPEN_CHECKIN,
    'APPT_openCheckinWebApp'
  );
}

function APPT_getWebAppBaseUrl_() {                  // → Get current web app URL
  var url = ScriptApp
    .getService()
    .getUrl();
  return url;
}

function APPT_openHomeWebApp() {                     // → Open home screen in browser
  var base = APPT_getWebAppBaseUrl_();
  if (!base || base === '') {
    SpreadsheetApp.getUi().alert(
      'The web app has not been deployed.\n' +
      'In the Apps Script editor, go to [Deploy] → [New deployment], choose Web app, then deploy.\n' +
      'After deploying, refresh the Google Sheet and try the menu again.'
    );
    return;
  }
  var url = base + '?page=' + encodeURIComponent('home');
  var html = HtmlService.createHtmlOutput(
      '<script>window.open("' + url + '","_blank");' +
      'google.script.host.close();</script>'
    );
  SpreadsheetApp.getUi().showModalDialog(
    html,
    'Open web app main screen'
  );
}

function APPT_openCheckinWebApp() {                  // → Open check-in screen in browser
  var base = APPT_getWebAppBaseUrl_();
  if (!base || base === '') {
    SpreadsheetApp.getUi().alert(
      'The web app has not been deployed.\n' +
      'In the Apps Script editor, go to [Deploy] → [New deployment], choose Web app, then deploy.\n' +
      'After deploying, refresh the Google Sheet and try the menu again.'
    );
    return;
  }
  var url = base + '?page=' + encodeURIComponent('checkin');
  var html = HtmlService.createHtmlOutput(
      '<script>window.open("' + url + '","_blank");' +
      'google.script.host.close();</script>'
    );
  SpreadsheetApp.getUi().showModalDialog(
    html,
    'Open web app check-in screen'
  );
}

To actually use this code, wire it to the onOpen above. There must be exactly one onOpen in the project, so use the code from “Merging onOpen across the series” and do not create another one here. If you already have an onOpen, add the single line APPT_addWebAppMenu_(menu); inside it.

Important: You must deploy the web app first before testing the menu.

Recommended test steps:

  1. In the Apps Script editor, click [Deploy] → [New deployment], choose “Web app” as the type, and deploy for the first time.
  2. Refresh the Google Sheet.
  3. When you see “Appointment tools” in the top menu, click “Open web app main screen” and “Open web app check-in screen” one by one.
  4. Each menu should open a new browser tab, with URLs ending in ?page=home and ?page=checkin respectively. If the correct screens appear, it’s working.

Common real‑world issues and Apps Script tips

When you run a receiving appointment system in production and manage multiple Google Sheets web app screens in a single project, these issues and fixes often come up:

  1. Broken links when web app URL changes

If you refactor scripts or create a brand‑new deployment, the /exec URL can change. Any absolute URLs hard‑coded in HTML templates will instantly become stale, breaking old links and bookmarks. To prevent this, leave template links as relative routes like ?page=checkin, and for sheet menus or external announcements, always use the latest URL from APPT_getWebAppBaseUrl_().

  1. Handling invalid page parameters

In real usage, URLs like ?page=aaa with typos are common. With a whitelist like APPT_WEBAPP_CONFIG.VALID_PAGES, you can treat anything not on the list as invalid and redirect to the default screen. That way, even if page is missing or unexpected, the system doesn’t break.

  1. Code conflicts across series parts

To keep everything working even when you put the entire series into a single Apps Script project, follow these rules:

  • Prefix all appointment‑specific constants and functions with APPT_.
  • For single‑entry functions like doGet and onOpen, don’t create duplicates—merge everything into one router‑style definition.
  • Define shared helpers only once and avoid redefining them in later parts.

If you need more robustness for production—error handling, concurrency locks, or backups—see the separate post How to handle errors and back up in Google Sheets Apps Script | LockService, try/catch, DriveApp backup and add defensive logic from there.


Wrap‑up: A structure that lets one deployment serve many screens

This post covered creating multiple screens in a Google Sheets web app by routing HtmlService templates via the page parameter in Apps Script’s doGet(e), and connecting those screens to the sheet menu. With APPT_WEBAPP_CONFIG controlling VALID_PAGES and TITLE_MAP, and templates like APPT_home and APPT_checkin split by role, it becomes much easier to grow the system without losing structure.

A concrete next step: open the Apps Script editor for any receiving/appointment‑related Google Sheet you use today, paste in the doGet(e) from this post, add the APPT_home and APPT_checkin templates, and then add APPT_MENU_CONFIG plus APPT_addWebAppMenu_(). Deploy the web app once, visit it with ?page=checkin, and confirm that different screens show from a single URL. Once you’ve verified that, you can extend the same pattern step by step for list views, admin screens, and more.