Send HTML email from a Google Doc template
Use a styled Doc as the source for branded, on-brand HTML email — no design tool needed.
Published May 5, 2026
Designing an HTML email properly usually means a separate tool, a separate login, and a separate person who knows how to use it. For a small team that is a lot of friction for what is, in the end, a page of formatted text with a logo on top.
Northwind writes its monthly newsletter where it writes everything else — in
Google Docs. The team styles the Doc with headings, bold text, and links,
drops in a couple of {{placeholder}} tokens for the bits that change each
month, and this script does the rest. It exports the Doc as HTML, swaps the
tokens for real values, and sends the result through Gmail. The Doc is the
design tool, and everyone already knows how to use it.
What you’ll need
- A Google Doc to act as the template, formatted however you want the email to
look. Use
{{token}}markers anywhere a value should be filled in at send time — for example{{month}}or{{headline}}. - The Doc’s ID, taken from its URL (the long string between
/d/and/edit). - A Gmail account to send from — the script sends as whoever runs it.
- No add-ons or libraries. The export uses Google’s built-in HTML export of Docs.
The script
// The Google Doc used as the newsletter template.
const TEMPLATE_DOC_ID = '1abcNewsletterTemplateId';
/**
* Renders the template Doc to HTML, substitutes the given tokens, and
* sends the result as a single HTML email.
*
* @param {string} recipient Address (or comma-separated addresses).
* @param {string} subject The email subject line.
* @param {Object} replacements Map of {{token}} name to its value.
*/
function sendDocAsHtmlEmail(recipient, subject, replacements) {
// 1. Turn the Doc into ready-to-send HTML with tokens replaced.
const html = renderDocToHtml(TEMPLATE_DOC_ID, replacements);
// 2. Send it. The plain-text body is left empty; htmlBody wins
// in clients that render HTML.
GmailApp.sendEmail(recipient, subject, '', { htmlBody: html });
Logger.log(`Newsletter sent to ${recipient}.`);
}
/**
* Exports a Google Doc as HTML and replaces every {{token}} with its
* value from the replacements map.
*
* @param {string} docId The ID of the Doc to export.
* @param {Object} replacements Map of token name to replacement value.
* @returns {string} The finished HTML.
*/
function renderDocToHtml(docId, replacements = {}) {
// 1. Ask Google's export endpoint for the Doc in HTML format.
const url =
'https://docs.google.com/feeds/download/documents/export/Export' +
`?id=${docId}&exportFormat=html`;
// 2. Authorise the request with the script's own OAuth token.
const response = UrlFetchApp.fetch(url, {
headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` },
});
// 3. Substitute every {{token}} with its value.
let html = response.getContentText();
for (const [token, value] of Object.entries(replacements)) {
html = html.replace(new RegExp(`{{${token}}}`, 'g'), value);
}
return html;
}
How it works
sendDocAsHtmlEmailis the function you call. It hands the template ID and the replacement values torenderDocToHtml, then sends what comes back.renderDocToHtmlbuilds a URL pointing at Google’s Docs export endpoint withexportFormat=html. This is the same export Docs uses internally, so the HTML reflects the styling in the Doc.- The request carries
ScriptApp.getOAuthToken()as a bearer token, which is the script’s own permission to read the Doc — no API key, no public sharing. - The exported HTML is scanned for every
{{token}}named inreplacements, and each occurrence is swapped for its value with a global regex. - Back in
sendDocAsHtmlEmail,GmailApp.sendEmailsends the finished HTML via thehtmlBodyoption. The plain-text argument is left empty.
Run it
Write a small wrapper that fills in this month’s values and call it when the newsletter is ready:
function sendMayNewsletter() {
sendDocAsHtmlEmail('[email protected]', 'Northwind — May edition', {
month: 'May 2026',
headline: 'A new way to brief us',
});
}
To send it: open the Apps Script editor, select sendMayNewsletter, and click
Run. Approve the authorisation prompt the first time. If you would rather
the newsletter went out on a schedule, add a time-driven trigger for the
wrapper function instead.
Example run
Suppose the template Doc contains a heading that reads {{headline}} and a
line Your {{month}} update from Northwind. Calling sendMayNewsletter with
the values above produces an email whose heading reads A new way to brief
us and whose intro line reads Your May 2026 update from Northwind — with
all the Doc’s bold text, lists, and links intact.
Watch out for
- Email clients strip a lot of CSS. Gmail in particular drops most of what Docs exports. Keep the design simple — headings, bold, bullet lists, and links survive; multi-column layouts and fancy spacing usually do not. Send yourself a test before sending it wide.
- Inline images placed in the Doc are exported as base64 data inside the HTML. That keeps them self-contained but bloats the email; a few small images are fine, a gallery is not.
- Token names are case-sensitive and must match the Doc exactly. A
{{Month}}in the Doc will not be touched by amonthreplacement. - The export endpoint is an undocumented Google URL. It is stable and widely
used, but it is not an official API — if it ever changes, switch to building
the HTML from
DocumentAppinstead. - One
sendEmailcall to many addresses puts every recipient in the sameTofield. For a real subscriber list, loop and send individually, and respect Gmail’s daily sending quota.
Related
Send meeting follow-ups with the notes attached
After a Calendar event ends, email attendees the linked notes Doc automatically.
Updated May 19, 2026
Embed inline charts in a status email
Render a Sheets chart as an image inside the email body, not as an attachment.
Updated May 12, 2026
Parse bank-alert emails into an expense ledger
Convert transaction alerts from Northwind's bank into categorised spend rows automatically.
Updated Apr 28, 2026
Generate a printable address book from contacts
Export Northwind's Google Contacts to a formatted Doc you can actually print.
Updated Apr 21, 2026
Email a weekly "what changed" report from a Sheet
Diff the Projects sheet week over week and email the team the rows that changed.
Updated Apr 14, 2026