appscript.dev
Automation Advanced Slides Drive

Auto-publish a deck as an embeddable web page

Push a Northwind deck live after approval — set it to publish-to-web automatically.

Published Jan 11, 2026

Northwind builds pitch decks in Google Slides and embeds the approved ones on client microsites. The slow step is the handoff: a deck gets signed off in a review thread, then someone has to remember to open it, change the sharing, copy the embed URL, and paste it into the site. When that step slips, the microsite either shows nothing or shows a deck that is still marked private.

This script closes the gap. It reads a publishing queue, finds every deck that is approved but not yet published, makes it publicly viewable, and records the embed URL and a timestamp back in the sheet. The site build then just reads those URLs — no manual sharing changes, no stale links.

What you’ll need

  • A Publishing queue Google Sheet with a header row and these columns: deckId, approved (a checkbox or TRUE/FALSE), publishedAt, and embedUrl.
  • The Slides files themselves, each owned by or shared with the account that runs the script — it needs edit rights to change sharing.
  • The Drive API enabled under Services in the Apps Script editor, so sharing changes apply cleanly.

The script

// The sheet that lists decks waiting to go live.
const PUBLISH_QUEUE_ID = '1abcPublishQueueId';

// Slides embed URLs follow a fixed shape — only the deck ID changes.
const EMBED_URL = (deckId) =>
  `https://docs.google.com/presentation/d/${deckId}/embed`;

/**
 * Makes a single deck publicly viewable and returns its embed URL.
 * Anyone with the link can view; nobody can edit.
 */
function publishDeck(deckId) {
  const file = DriveApp.getFileById(deckId);

  // Flip sharing to "anyone can view" so the embed renders for the public.
  file.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW);

  return EMBED_URL(deckId);
}

/**
 * Walks the publishing queue and publishes every deck that is approved
 * but has not been published yet, writing the result back to the sheet.
 */
function publishApprovedDecks() {
  const sheet = SpreadsheetApp.openById(PUBLISH_QUEUE_ID).getSheets()[0];
  const values = sheet.getDataRange().getValues();

  // Split the header off and map column names to their indexes.
  const [header, ...rows] = values;
  if (!rows.length) {
    Logger.log('Publishing queue is empty — nothing to do.');
    return;
  }
  const col = Object.fromEntries(header.map((k, i) => [k, i]));

  let published = 0;
  rows.forEach((row, i) => {
    // Skip rows that are not approved, or are already published.
    if (!row[col.approved] || row[col.publishedAt]) return;

    // Publish the deck and stamp the result back into the row.
    const embed = publishDeck(row[col.deckId]);
    values[i + 1][col.embedUrl] = embed;
    values[i + 1][col.publishedAt] = new Date();
    published++;
    Logger.log(`Published ${row[col.deckId]}: ${embed}`);
  });

  // One write covers every change — far faster than per-cell writes.
  sheet.getDataRange().setValues(values);
  Logger.log(`Done — published ${published} deck(s).`);
}

How it works

  1. publishApprovedDecks opens the publishing queue and reads every row in one getDataRange() call.
  2. It splits off the header row and builds a col lookup, so the rest of the code refers to columns by name rather than by fragile numeric indexes.
  3. If there are no data rows it logs a message and stops.
  4. For each row, it skips anything that is not approved or already has a publishedAt timestamp — that timestamp is what stops a deck being republished on every run.
  5. For an eligible row it calls publishDeck, which uses setSharing to make the file viewable by anyone with the link and returns the embed URL.
  6. It writes the embed URL and the current time back into the in-memory copy of the data, then commits everything with a single setValues call at the end.

Example run

The Publishing queue sheet before a run:

deckIdapprovedpublishedAtembedUrl
1deckAcmeTRUE
1deckWagnerFALSE
1deckBalticTRUE2026-01-09 14:02https://…embed

After a run, only the first row changes — 1deckWagner is not approved, and 1deckBaltic already has a timestamp:

deckIdapprovedpublishedAtembedUrl
1deckAcmeTRUE2026-01-11 09:00https://docs.google.com/presentation/d/1deckAcme/embed
1deckWagnerFALSE
1deckBalticTRUE2026-01-09 14:02https://…embed

The site build reads the embedUrl column and drops each value into an <iframe>.

Trigger it

Run this on a schedule so approvals go live without anyone watching the queue:

  1. In the Apps Script editor open Triggers (the clock icon).
  2. Add a trigger for publishApprovedDecks, Time-driven, Hour timer, every hour. A deck then goes public within an hour of being ticked.

You can also run it by hand straight after a review meeting if you want a deck live immediately.

Watch out for

  • DriveApp.Access.ANYONE means truly public — the deck is indexable and visible to anyone who finds the URL. If the deck must stay link-only, use ANYONE_WITH_LINK instead.
  • The embed shows the deck’s current state, not a frozen snapshot. Edits made after publishing appear live, so lock the deck or work on a copy once it is approved.
  • The script never un-publishes. If a deck is pulled, you must reset its sharing by hand — consider adding a retired column and a matching pass.
  • Sharing changes count against Drive quotas. A handful of decks per run is fine; do not point this at hundreds of files in one go.
  • The publishedAt timestamp is the only guard against duplicates. If someone clears that cell, the deck is treated as new and republished on the next run.

Related