Aditya Sharma

Turn a Google Sheet Into a Website: 3 Routes and Their Limits

On this page, 8 sections

You can turn a Google Sheet into a website in three ways: publish the sheet to the web and read it from a page, run an Apps Script web app that serves the rows, or generate static HTML files from an export. They differ in who can see the data, how stale it can get, and what breaks first.

The right pick depends on one question: is the sheet a database behind a page, or is it the page’s content? A price list the owner edits weekly suits a static build. A directory that several people update all day suits a web app.

This post compares the three, gives a script for the web app route and a generator for the static route, and says what I ran. It builds on getting data from a website into Google Sheets, which goes the opposite way.

The three routes at a glance

RouteWhat serves the pageFreshnessWhat to watch
Publish to web, page reads itGoogle, published copyUpdates in a few minutes (Google Docs Help)Anyone with the link can read what you published
Apps Script web appApps Script, runs your code per visitLive, reads the sheet each requestScript quotas and run time
Static generatorAny web host, plain filesAs fresh as your last buildYou rebuild and upload on every change

Route 1: publish the sheet and read it

In Sheets you open File, then Share, then Publish to web. The Google Docs Help page says you can publish the entire spreadsheet or individual sheets and choose a publishing format.

It also says changes you make are updated in the published version and that the automatic update might take a few minutes. A checkbox labelled Automatically republish when changes are made controls that, and removing a file from the web means stopping the publishing.

The help page warns you to be careful when publishing content with private or sensitive information. That is the main limit of this route. It is not a login-protected view of your data. If a column should not be public, do not publish that sheet.

Route 2: an Apps Script web app

The Apps Script web apps guide says a script needs a doGet(e) or doPost(e) function that returns an HtmlOutput or TextOutput object to be published as a web app, and that request parameters arrive in the event parameter. The same guide shows JSON being returned through ContentService with a JSON mime type.

This function reads a sheet named Pages and returns every row as a JSON object keyed by the header row:

function doGet(e) {
  const sheet = SpreadsheetApp.getActive().getSheetByName("Pages");
  const values = sheet.getDataRange().getValues();
  const header = values.shift();
  const rows = values.map(r => {
    const o = {};
    header.forEach((h, i) => { o[h] = r[i]; });
    return o;
  });
  return ContentService
    .createTextOutput(JSON.stringify(rows))
    .setMimeType(ContentService.MimeType.JSON);
}

I ran node –check on it and then executed it with mocked SpreadsheetApp and ContentService, with a header row and two data rows. It returned the JSON mime type and the body [{“title”:”Home”,”slug”:”home”},{“title”:”About”,”slug”:”about”}]. I did not deploy it as a web app, so the first real request to your own deployment is the test.

A web app is the only one of the three where the page can change the moment a cell changes, because the script reads the sheet on each request. It is also the one that spends script quota.

The Apps Script quotas page lists a 6 minute limit for a script run and 20,000 URL Fetch calls a day on a consumer account (100,000 on Workspace). A web app that only reads a sheet does not make URL Fetch calls, so the run-time limit is the one to keep in mind.

If you want HTML rather than JSON, the HTML Service guide covers building pages from templates. I did not test a template here.

Route 3: generate static files

Export the sheet as CSV, then turn each row into a page. The script below reads pages.csv, escapes every value with html.escape so a stray angle bracket in a cell cannot break the page, and writes one HTML file per row with a nav bar.

import csv, html, pathlib
out = pathlib.Path("site")
out.mkdir(exist_ok=True)
with open("pages.csv", newline="", encoding="utf-8") as f:
    rows = list(csv.DictReader(f))
nav = " ".join(f'<a href="{html.escape(r["slug"])}.html">{html.escape(r["title"])}</a>' for r in rows)
for r in rows:
    page = (f'<!doctype html><meta charset="utf-8"><title>{html.escape(r["title"])}</title>'
            f'<nav>{nav}</nav><h1>{html.escape(r["title"])}</h1><p>{html.escape(r["body"])}</p>')
    (out / f'{r["slug"]}.html').write_text(page, encoding="utf-8")
print(len(rows), "pages written")

I ran it on a 3-row CSV (Home, About, Contact). It printed 3 pages written and produced about.html, contact.html and home.html. The About page came out as a title, a nav with three links, an h1 and the body text. The output is plain files, so it works on any host that serves a folder.

The size question

Sheets has a ceiling. The Drive help page says a spreadsheet can hold up to 20 million cells. A site with 1,000 rows and 10 columns is 10,000 cells, which is 0.05 percent of that (computed in Python). The cell cap is not the constraint for a small site. Rebuild effort is.

Which route fits

  • A handful of pages that change monthly: static, upload the folder after each build.
  • A directory many people edit: web app returning JSON, with a static page that reads it.
  • A quick public table you control entirely: publish to web, and publish only a sheet that holds public data.
  • Anything with customer data in it: none of the three is a login system.

Two things apply to all of them. First, design the sheet with one header row and no merged cells, because every route turns rows into objects. Second, keep a draft tab for edits and a clean tab for publishing, so a half-finished cell never goes live.

After you publish: check every page is findable

Whichever route you choose, a generated site is only useful if its pages can be found. Make a list of the slugs in your sheet and compare it to what the host actually serves. The method in how to find all URLs on a website works here too: list what you expect, list what the site exposes, and look at the difference.

Keep slugs unique and lowercase, and add a check column in the sheet that flags a duplicate slug before you build. In the static route, two rows with the same slug write to the same file, and the second silently replaces the first. The generator above does not guard against that, so add the check in the sheet or in the script.

Treat the sheet as the source of truth and the site as a build product. If you edit an HTML file by hand, the next build overwrites it. Make the change in the sheet, or the edit is gone.

Do this today

  1. List what the page needs: how many rows, how often they change, who edits them.
  2. Check every column for anything you would not put on a public page.
  3. If it is public and small, build static files with the script above.
  4. If many people edit it, deploy the web app and check the URL in a private window.

Resources

Tell me where I am wrong

Your email is not published and I do not add it to any list. Corrections with a source are the ones I act on fastest.