How to Change the Font in your Google Documents with Apps Script

An organization recently migrated their Word Documents from Microsoft Office to Google Drive. The migration has been smooth but the Word documents imported as Google Docs are using Calibri, the default font family of Microsoft Word.

The company is looking to replace the fonts in multiple Google Documents such that the document headings using Georgia while the body paragraphs are rendered in Droid Sans at 12 pt.

Replace Font Styles in Google Docs

This example show how to replace the font family of your Google Documents of specific sections - the heading titles are rendered in a different font while the tables, list items, body and table of contents are formatted with a separate font.

const updateFontFamily = () => {
  const document = DocumentApp.getActiveDocument();

  const headingStyles = {
    [DocumentApp.Attribute.FONT_FAMILY]: "Georgia",
    [DocumentApp.Attribute.FONT_SIZE]: 14,

  const normalParagraphStyles = {
    [DocumentApp.Attribute.FONT_FAMILY]: "Droid Sans",
    [DocumentApp.Attribute.FONT_SIZE]: 12,

  const body = document.getBody();

  [...Array(body.getNumChildren())].map((_, index) => {
    const child = body.getChild(index);
    const childType = child.getType();
    if (childType === DocumentApp.ElementType.PARAGRAPH) {
      if (
        child.asParagraph().getHeading() === DocumentApp.ParagraphHeading.NORMAL
      ) {
      } else {
    } else if (childType === DocumentApp.ElementType.TABLE) {
    } else if (childType === DocumentApp.ElementType.TABLE_OF_CONTENTS) {
    } else if (childType === DocumentApp.ElementType.LIST_ITEM) {


How to Replace Text and Hyperlinks in Google Documents with Apps Script

The company’s handbook is written in Google Docs. The document spans several pages and now the writer has been asked to create links such that all mentions of the company name in the document are linking to the company’s official website.

It can be a time consuming task but with Google Apps Script, specific words in a document can be hyperlinked in bulk in one click.

This example show how to search and replace all occurrences of a text phrase, the company name in this case, and add links to a specific website.

const addLinks = () => {
  const searchPhrase = "Digital Inspiration";
  const hyperlink = "";

  const document = DocumentApp.getActiveDocument();
  const body = document.getBody();
  let search = null;

  while ((search = body.findText(searchPhrase, search))) {
    const searchElement = search.getElement();
    const startIndex = search.getStartOffset();
    const endIndex = search.getEndOffsetInclusive();
    searchElement.asText().setLinkUrl(startIndex, endIndex, hyperlink);


For the next iteration of the handbook, the company’s name has changed but the website domain is the same. The writer is required to change every instance of the company’s name in the document but the underlying hyperlink should not be modified..

const changeText = () => {
  const searchText = "Blue Widgets Inc.";
  const replaceText = "Orange Inc.";

  const document = DocumentApp.getActiveDocument();
  const body = document.getBody();
  let search = null;

  while ((search = body.findText(searchText, search))) {
    const searchElement = search.getElement();
    const startIndex = search.getStartOffset();
    const endIndex = search.getEndOffsetInclusive();

    const textElement = searchElement.asText();
    const existingLink = textElement.getLinkUrl(startIndex);
    textElement.deleteText(startIndex, endIndex);
    textElement.insertText(startIndex, replaceText);
      startIndex + replaceText.length - 1,


The next Apps Script snippets shows how to change all instance of the company name and also replace the site URL with another domain name.

const changeTextWithUrl = () => {
  const searchText = "Blue Widgets Inc.";
  const replaceText = "Orange Inc.";
  const replaceUrl = "";

  const document = DocumentApp.getActiveDocument();
  const body = document.getBody();
  let search = null;

  while ((search = body.findText(searchText, search))) {
    const searchElement = search.getElement();
    const startIndex = search.getStartOffset();
    const endIndex = search.getEndOffsetInclusive();

    const textElement = searchElement.asText();
    textElement.deleteText(startIndex, endIndex);
    textElement.insertText(startIndex, replaceText);
      startIndex + replaceText.length - 1,


How to Convert Column Numbers (e.g. 28) to A1 Notation (e.g. AB) in Google Sheets

Google Sheets includes built-in functions for converting cell references in A1 notation to row and column numbers and another function for converting column alphabets (like AA) into the column index (26 in this case).

=ADDRESS(23, 28, 4) - Returns the A1 style notation of the cell whose row number is 23 and column number is 28.

=COLUMN(C9) - Returns the column number of a specified cell C9 where column A corresponds to 1 and column AA corresponds to 27.

Column Numbers in A1 Notation

Get A1 Notation with JavaScript

If you are working with the Google Sheets API, you may sometimes needs to calculate the A1 notation style reference of a cell whose row and column numbers are known in the JSON data of the sheet.

For container bound Google Sheets, the getA1Notation() method can return the range address in A1 Notation.

const sheet = SpreadsheetApp.getActiveSheet();
const range = sheet.getRange(1, 2);

If you are not using the Spreadsheet service, you can also compute the A1 notation reference of a cell using simple JavaScript.

 * @param {number} row - The row number of the cell reference. Row 1 is row number 0.
 * @param {number} column - The column number of the cell reference. A is column number 0.
 * @returns {string} Returns a cell reference as a string using A1 Notation
 * @example
 *   getA1Notation(2, 4) returns "E3"
 *   getA1Notation(2, 4) returns "E3"
const getA1Notation = (row, column) => {
  const a1Notation = [`${row + 1}`];
  const totalAlphabets = "Z".charCodeAt() - "A".charCodeAt() + 1;
  let block = column;
  while (block >= 0) {
      String.fromCharCode((block % totalAlphabets) + "A".charCodeAt())
    block = Math.floor(block / totalAlphabets) - 1;
  return a1Notation.join("");

This is equivalent to =ADDRESS() function of Google Sheets.

Get Column Number from A1 Notation

The next function takes the cell reference in A1 notation and returns the column number and row number of any cell in the spreadsheet.

 * @param {string} cell -  The cell address in A1 notation
 * @returns {object} The row number and column number of the cell (0-based)
 * @example
 *   fromA1Notation("A2") returns {row: 1, column: 3}

const fromA1Notation = (cell) => {
  const [, columnName, row] = cell.toUpperCase().match(/([A-Z]+)([0-9]+)/);
  const characters = "Z".charCodeAt() - "A".charCodeAt() + 1;

  let column = 0;
  columnName.split("").forEach((char) => {
    column *= characters;
    column += char.charCodeAt() - "A".charCodeAt() + 1;

  return { row, column };

This is equivalent to the =ROW() and =COLUMN() functions available in Google Sheets.

Google Documents – How to Replace Text in Header and Footer

The upcoming release of Document Studio includes support for adding markers in the header, footer and the footnotes section of your Microsoft Word template. The add-on will automatically replace this placeholder text with actual values sourced from Google Sheets or Google Forms.

Header and footer in Google Docs

This Apps Script snippet uses the Google Docs API to find and replace multiple blocks of text in the header and footer section of your Google Document. The header and footer sections are children of the parent DOCUMENT section.

const replaceHeaderFooter = () => {
  // Returns the document with the specified ID
  const doc = DocumentApp.openById("DOCUMENT ID");

  // Retrieves the headers's container element which is DOCUMENT
  const parent = doc.getHeader().getParent();

  for (let i = 0; i < parent.getNumChildren(); i += 1) {
    // Retrieves the child element at the specified child index
    const child = parent.getChild(i);

    // Determine the exact type of a given child element
    const childType = child.getType();

    if (childType === DocumentApp.ElementType.HEADER_SECTION) {
      // Replaces all occurrences of a given text in regex pattern
      child.asHeaderSection().replaceText("{{Company}}", "Digital Inspiration");
    } else if (childType === DocumentApp.ElementType.FOOTER_SECTION) {
      // Replaces all occurrences of a given text in regex pattern
      child.asFooterSection().replaceText("{{Copyright}}", "© Amit Agarwal");

  // Saves the current Document.
  // Causes pending updates to be flushed and applied.

If the current document doesn’t include an header section, the getHeader() function will return null so you may wish to include additional checks to determine whether a document has an header or footer.

How to Schedule a Google Meeting with Google Calendar and Apps Script

This Apps Script sample shows how you can programmatically schedule video meetings inside Google Meet with one or more participants using the Google Calendar API. It can be useful for teachers who wish to schedule regular meetings with their students but instead of manually creating meeting invites, they can easily automate the whole process for the entire class.

Schedule Google Meeting

Setup Google Meeting with Apps Script

Give your meeting a title, the start date, the meeting duration, the list of attendees and how often you wanted to be reminded of the upcoming Google meeting. A new meeting event will be added to your Google Calendar and you’ll also be provided with a Google Meet link that you share with your students and colleagues through mail merge.

const createGoogleMeeting = () => {
  // The default calendar where this meeting should be created
  const calendarId = "primary";

  // Schedule a meeting for May 30, 2021 at 1:45 PM
  // January = 0, February = 1, March = 2, and so on
  const eventStartDate = new Date(2021, 4, 30, 13, 45);

  // Set the meeting duration to 45 minutes
  const eventEndDate = new Date(eventStartDate.getTime());
  eventEndDate.setMinutes(eventEndDate.getMinutes() + 45);

  const getEventDate = (eventDate) => {
    // Dates are computed as per the script's default timezone
    const timeZone = Session.getScriptTimeZone();

    // Format the datetime in `full-date T full-time` format
    return {
      dateTime: Utilities.formatDate(

  // Email addresses and names (optional) of meeting attendees
  const meetingAttendees = [
      displayName: "Amit Agarwal",
      email: "",
      responseStatus: "accepted",
    { email: "", responseStatus: "needsAction" },
    { email: "", responseStatus: "needsAction" },
      displayName: "Angus McDonald",
      email: "",
      responseStatus: "tentative",

  // Generate a random id
  const meetingRequestId = Utilities.getUuid();

  // Send an email reminder a day prior to the meeting and also
  // browser notifications15 minutes before the event start time
  const meetingReminders = [
      method: "email",
      minutes: 24 * 60,
      method: "popup",
      minutes: 15,

  const { hangoutLink, htmlLink } = Calendar.Events.insert(
      summary: "Maths 101: Trigonometry Lecture",
      description: "Analyzing the graphs of Trigonometric Functions",
      location: "10 Hanover Square, NY 10005",
      attendees: meetingAttendees,
      conferenceData: {
        createRequest: {
          requestId: meetingRequestId,
          conferenceSolutionKey: {
            type: "hangoutsMeet",
      start: getEventDate(eventStartDate),
      end: getEventDate(eventEndDate),
      guestsCanInviteOthers: false,
      guestsCanModify: false,
      status: "confirmed",
      reminders: {
        useDefault: false,
        overrides: meetingReminders,
    { conferenceDataVersion: 1 }

  Logger.log("Launch meeting in Google Meet: %s", hangoutLink);
  Logger.log("Open event inside Google Calendar: %s", htmlLink);

Also see: Generate Add to Calendar Links

Google Meeting with Recurring Schedule

The above code can be extended to create meetings that occur on a recurring schedule.

You need to simply add a recurrence attribute to the meeting event resource that specifies the recurring event in RRULE notation. For instance, the following rule will schedule a recurring video meeting for your Maths lecture every week on Monday, Thursday for 8 times.


Here are some other useful RRULE examples:

  • FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR - Occurs every week except on weekends
  • FREQ=MONTHLY;INTERVAL=2;BYDAY=TU - Occurs every Tuesday, every other month
  • INTERVAL=2;FREQ=WEEKLY - Occurs every other week
  • FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,TH;BYMONTH=12 - Occurs every other week in December on Tuesday and Thursday
  • FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU,-1SU - Occurs every other month on the first and last Sunday of the month

How to Add a Watermark in Google Documents

Microsoft Word includes a useful “Insert Watermark” feature to help you easily add your brand’s logo image or a text stamp that fades behind the content of every page in the document. A company’s policy may require employees to add watermarks to indicate if any document is in draft stage or if the document is confidential and not meant for external distribution.

Microsoft Word Watermark

Insert Watermarks in Google Docs

Unlike Microsoft Word, there’s no built-in support for Watermarks in Google Docs but there’s a simple workaround - create a faded image with the text of your watermark and place that image behind the text of your document pages. Here’s how:

1. Create the watermark stamp

Launch MS Paint on your computer and create a simple watermark image in landscape mode with dark gray text. Please use a bold font like Impact with large font size as the large image can always be resized inside Google Docs but not vice-versa.

I’ve also added some ready-to-use image stamps on Canva and Imgur.

Upload Watermark Image

2. Upload Watermark to Google Docs

Inside Google Docs, go to the Insert menu, choose the Image submenu and select Upload from Computer. Upload the watermark image that you saved in the previous step to Google Docs.

3. Open Image Options

Right-click the uploaded image inside Google Docs and choose Image Options from the contextual menu.

Watermark Diagonal Image

4. Change Rotation

Expand the Image Options sidebar and, under the Size & Rotation section, set the angle to around 320° to make the watermark diagonal.

5. Send the Image Behind Text

  • Under the text wrapping section, choose Behind Text to send the watermark image behind the content of your document.
  • Under Position, choose the Fixed position option with the layout set as Center. This will position your watermark image right in the center of the page.
  • Under the Adjustments section, set the transparency level to around 80% to fade the watermark image in the background.

The Watermark Effect in Documents

Here’s how the final watermark effect will look like in your Google Document.

Watermark in Document

Tip: You can use Document Studio to generate PDF files from Google Forms and the watermarks would be also show up in your PDF document.

How to Use PayPal Subscriptions API with Node.js

Our Google add-on store uses PayPal Subscriptions with Digital Goods to process recurring payments and the invoices are sent to customers through Document Studio.

There are two steps.

  1. Customers makes the payment and completes the order on our website.
  2. PayPal sends a BILLING.SUBSCRIPTION.ACTIVATED webhook to a serverless function.
  3. The function (running on Firebase, Google Cloud) verifies the subscription and checks if the status is active.
  4. It invokes the Apps Script API to complete the order.

The cloud function was previously using the official PayPal SDK for Node.js but it has been recently deprecated and no longer supports the new PayPal subscriptions API endpoints. Migrating from the PayPal Node SDK to your own solution is relatively simple and involves two steps:

1. Get the PayPal Access Token

const { default: axios } = require("axios");

const getPayPalAccessToken = async () => {
  const client_id = "PayPal Client ID goes here";
  const client_secret = "PayPal Client Secret goes here";
  const options = {
    url: "",
    method: "POST",
    headers: {
      Accept: "application/json",
      "Accept-Language": "en_US",
      "Content-Type": "application/x-www-form-urlencoded",
    auth: {
      username: client_id,
      password: client_secret,
    params: {
      grant_type: "client_credentials",
  const { status, data } = await axios(options);
  return data.access_token;

If you are planning to test your integration with your PayPal sandbox account instead of the production version, replace in the requests with and use the sandbox client secret credentials.

2. Verify the Subscription

A successful request returns the HTTP 200 OK status code and a JSON response body.

const { default: axios } = require("axios");

const verifyPayPalSubscription = async (subscription_id) => {
  const token = await getPayPalAccessToken();
  const options = {
    method: "GET",
    url: `${subscription_id}`,
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: "application/json",
  const { status, data = {} } = await axios(options);
  if (status === 200) {
    const { subscriber: { email_address } = {}, status } = data;
    return status === "ACTIVE";
  return false;

Once the PayPal Subscription is found to be active, an HTTP request is made to the Google Apps Script API that sends the invoice and license to the customer. Learn more.

The Best Extensions for Google Chrome

Best Google Chrome extensions

Here’s a comprehensive list of the best extensions for Google Chrome that will help you do more and also enhance your web browsing experience.

The Best Google Chrome extensions

  • Vimium C — Browse the web, interact with web pages, navigate browser history using keyboard shortcuts. Inspired by Vim commands.
  • Fika - Read pages in a Kindle-style clean layout with beautiful typography while hiding the distracting parts.
  • PushBullet — Easily transfer web page links, text notes, or push photos and files from the computer to your phone and vice-versa.
  • OneTab — Save all your open tabs in a group and duplicate your session anytime later with a click.
  • Gmail Sender Icons - Quickly identify the sending domain of an email in Gmail without opening the message.
  • Bubble Cursor - The add-on makes it easy for you to click on links that are too small to select with your mouse or for typing inside input boxes.
  • Picture in Picture - Watch YouTube videos in an always-on-top floating window while you work on other tasks.
  • Zoho Annotator - A simple ad-free screenshot capture tool with a built-in editor for annotating images. Also integrates with Google Drive, OneDrive and Dropbox.
  • Loom - Record a screencast video with voice narration, include your webcam video and share instantly. Also see Screenity.
  • URL Render - Instead of clicking individual links on the Google search page, hover your mouse and instantly view the underlying page in a floating window.
  • Clipt - A universal clipboard for Android and desktop computers. Copy any text on your phone and instantly paste it on the desktop and vice-versa.
  • Hover Zoom - Hover your mouse over any thumbnail image on sites like Facebook and Amazon and the add-on will enlarge the image to its full size.
  • Quick Source Viewer - A better alternative to the native View Source option that displays both JS and CSS files in addition to HTML content.
  • Save To Drive - Right-click and save the current web page, or images on a page, to your Google Drive.
  • Google Input - Type text in any language of your choice using virtual keyboards, your handwriting and transliteration.
  • Google Dictionary - Use this add-on to view definitions of words and learn their correct pronunciation.
  • uBlacklist - Blocks specific sites and entire domains from appearing in your Google search results.
  • Gmail Notes - Attach sticky notes to your email message that are persistent and will show up the next time you open that same email inside Gmail.
  • Ugly Email - Some emails contain a tracking pixel that notifies the sender when their emails are read. This add-on blocks the tracking attempts.
  • Stylebot - Change the appearance of any web page with CSS selectors, permanently hide elements on a page and the changes persist across browser sessions.
  • Webtime Tracker — Keep track of how you spend your time on individual websites and visualize your Internet usage through graphs.
  • Skip Search - An address bar shortcut for the I'm lucky command that directly takes you to the most relevant website for your search query.
  • Single File - Download the entire web page into a single HTML file. The associated CSS files, fonts and images are also saved in the file.
  • Go Incognito - Open the current tab of your browser in a new incognito tab, useful for browsing news websites are behind paywalls.
  • Feeder - An excellent RSS feed reader that is accessible from the Chrome toolbar and supports notifications for your favorite feeds.
  • Scribe - Create step-by-step guides and tutorials by recording steps and publish them as PDF guides.
  • Visbug - A must-have design tool for web developers that brings powerful editing capabilities to the browser.
  • TabNotes - A minimalistic start page for Chrome that opens a simple notepad in each new tab.
  • Sitemod - Modify any website on the Internet through Chrome Dev Tools and create a permanent copy of the modified website with a unique, shareable URL.
  • iCloud Passwords - Access your Safari passwords that are saved inside iOS and Mac device from Chrome on Windows PCs.
  • Twitter Screenshots - Take beautiful, uncluttered screenshots of tweets with a click.
  • StayFocusd - It helps you limit the time you spend on distracting websites Facebook or Reddit by temporarily blocking the site when the allotted time has passed.
  • Black Menu - Create shortcuts to your favorite Google services and access your emails, calendar, videos and more in an easily accessible popup.

How to Use Notion with Gmail and Google Sheets using Apps Script

Notion, my absolute favorite tool for storing all sorts of things from web pages to code snippets to recipes, just got better. They’ve released a public API and thus it will be a lot easier for developers to read and write to their Notion workspace from external apps.

For instance, you can create a document in Google Docs and export it to Notion while staying inside Docs. Google Sheets users can pull pages from Notion database into their spreadsheet. Any new submissions in Google Forms can be directly saved to Notion and so on!

Save Gmail Messages in Notion

I have put together a Gmail add-on that makes it easy for you to save email messages, or any other text content, from Gmail to your Notion workspace with a click. Here’s how the app works.

Step 1: Connect Gmail to Notion


Step 2: Allow Access to Notion pages - if you have multiple databases in your Notion workspace, you have an option to grant access to select databases and the rest will be inaccessible to the external app.

Authorize Notion

Step 3: Choose Email - open any email message in Gmail and you’ll have an option to edit the content of the email subject and body before sending the content to your Notion page. Please note that the app only supports plain text format at this time.

Send Email to Notion

Step 4: Open Notion - As soon as you hit the Send to Notion button, the content of the currently selected email message is added to your Notion database. You can click the All updates link in your Notion sidebar to view to recently added page.

Notion page

If you would like to try this Gmail to Notion app, please get in touch.

How to Use Notion with Google Apps Script

If you would to integrate your own Google add-on with Notion API, here’s a brief outline of the steps involved.

  1. Go to and click the Create New Integration button. You’ll be provided with a Client ID and Client Secret that you’ll need in a later step.

  2. Include the OAuth2 library in your Apps Script project and invoke the getRedirectUri method to get the OAuth2 redirect URL for the previous step.

const getNotionService = () => {
  return OAuth2.createService("Notion")
      Authorization: `Basic ${Utilities.base64Encode(

const authCallback = (request) => {
  const isAuthorized = getNotionService().handleCallback(request);
  return HtmlService.createHtmlOutput(
    isAuthorized ? "Success!" : "Access Denied!"

const getRedirectUri = () => {
  1. Connect to Notion API - Make a Get HTTP request to the /vi/databases to fetch a list of all databases that the user has explicitly shared with authorized app.
function getDatabasesList() {
  var service = getNotionService();
  if (service.hasAccess()) {
    const url = "";
    const response = UrlFetchApp.fetch(url, {
      headers: {
        Authorization: `Bearer ${service.getAccessToken()}`,
        "Notion-Version": "2021-05-13",
    const { results = [] } = JSON.parse(response.getContentText());
    const databases = results
      .filter(({ object }) => object === "database")
      .map(({ id, title: [{ plain_text: title }] }) => ({ id, title }));
    console.log({ databases });
  } else {
    console.log("Please authorize access to Notion");

Gmail to Notion - Try the App

The Gmail to Notion app is in private beta. If you would like to use it with your Gmail or Google Workspace account, please get in touch for an invite.

Create React App with Multiple Entry Points

The Create React App frameworks lets you easily build single page applications but it doesn’t support multiple entry points. To give you an example, if a website outputs separate home pages for mobile and desktop clients, the pages could be sharing some common React components between them, and it may thus not be practical to build two completely separate React applications.

Also see: Bundle React App with Gulp

CRA doesn’t support multiple entry points but there are couple of ways to solve this problem.

Option 1 Eject from the Create React App using the npm run eject command and update the entry inside webpack.config.js file to include multiple entry points.

Option 2 Use an alternate build tool like Vite.js that includes support for multiple entry points out of the box.

Option 3 Use the rewired app - it lets you easily make changes and small tweaks to the default Webpack configuration without ejecting the app.

Option 4 Use REACT_APP environment variables to specify the target component and then use ES5 dynamic imports to load the corresponding app component as shown in this example.

React Multiple Entry Points

Multiple Entry Points for Create React App

If you intend to use the Create React App configuration without ejecting it, here’s a simple workaround that will help you define multiple entry points and the output will be bundle in separate folders.

Inside the src folder, create two components.

// ./src/Desktop.js
import React from "react";

const Desktop = () => {
  return <h1>For Desktop</h1>;

export default Desktop;
// ./src/Mobile.js
import React from "react";

const Mobile = () => {
  return <h1>For Mobile</h1>;

export default Mobile;

The default entry file index.js looks something like this:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

    <App />

Next, edit your package.json file and add commands, one per build target.

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "build:mobile": "cp src/Mobile.js src/App.js && npm run build && mv build build-mobile",
    "build:desktop": "cp src/Desktop.js src/App.js && npm run build && mv build build-desktop"

Run npm run build:mobile when the build target is mobile or npm run build:desktop for the desktop entry point.