Check URL statuses in Google Docs with Apps Script widget

Mykyta Khmel
4 min readFeb 23, 2023

--

Apps Script logo

Google Docs is a popular document editor for creating and editing text documents, spreadsheets, and presentations. With the use of Google Apps Script, a scripting language for automating tasks in Google Apps, users can enhance their document editing experience by adding custom functionality to Google Docs. One such functionality is the ability to check URLs within a Google Document.

Google Docs allows users to insert hyperlinks into their documents, which can be useful for providing additional resources and information. However, with the increasing amount of malicious websites and broken links, it is essential to verify the validity of these links before sharing the document. A widget for checking URLs within Google Docs can make this process more efficient and convenient.

The following steps will guide you in creating a widget for checking URLs within Google Docs using Google Apps Script.

Apps Script

Open a Google Document in Google Drive and click on the “Extensions” menu. From the drop-down menu, select “Apps Script”

In the Apps Script, copy and paste the following code parts:

  1. Add code part to create menu item
/* Code to add submenu item */
function onOpen(e) {
DocumentApp.getUi().createAddonMenu()
.addItem('Show widget', 'showSidebar')
.addToUi();
}

/* Google script lifecycle */
function onInstall(e) {
onOpen(e);
}

It will result in something like this:

If deployed you can select a name for script

2. Expose function to show html widget in sidebar

/* Code to add submenu item */
function showSidebar() {
const ui = HtmlService.createHtmlOutputFromFile('sidebar')
.setTitle('Link Checker');
DocumentApp.getUi().showSidebar(ui);
}

3. Code to parse links of current open Google Doc

Big thanks to mogsdad for provided gist: https://gist.github.com/mogsdad/6518632

Incorporating it in our logic

function getAllLinks(element) {
var links = [];
element = element || DocumentApp.getActiveDocument().getBody();

if (element.getType() === DocumentApp.ElementType.TEXT) {
var textObj = element.editAsText();
var text = element.getText();
var inUrl = false;
for (var ch = 0; ch < text.length; ch++) {
var url = textObj.getLinkUrl(ch);
if (url != null && ch != text.length - 1) {
if (!inUrl) {
// We are now!
inUrl = true;
var curUrl = {};
curUrl.element = element;
curUrl.url = String(url); // grab a copy
curUrl.startOffset = ch;
}
else {
curUrl.endOffsetInclusive = ch;
}
}
else {
if (inUrl) {
// Not any more, we're not.
inUrl = false;
links.push(curUrl); // add to links
curUrl = {};
}
}
}
}
else {
// Get number of child elements, for elements that can have child elements.
try {
var numChildren = element.getNumChildren();
}
catch (e) {
numChildren = 0;
}
for (var i = 0; i < numChildren; i++) {
links = links.concat(getAllLinks(element.getChild(i)));
}
}

return links;
}

4. Create function to call great gist code

We will call this function from UI to run aggregation of links

function obtainLinks() {
const links = getAllLinks();

return links || [];
}

5. Code to check statuses of links

Also will be called from UI

async function checkLinks(links) {
const requests = UrlFetchApp.fetchAll(links.map(({ url }) => ({ url, method: 'get', followRedirects: false })))
const results = requests.map(r => r.getResponseCode());

return links.map((link, index) => ({url: link.url, code: results[index] || 0}));
}

6. Add some UI

Will not go in details so much:

  • Create sidebar.html in Apps Script project editor
  • add code below to point and execute to exposed functions
<!DOCTYPE html>
<html>

<head>
<base target="_top">
<title></title>
</head>

<body>
<div class="sidebar branding-below">
<form>
<div class="block col-contain">

<table class="links">
<thead>
<tr class="links_row">
<th class="links_heading links_cell--url">URL</th>
<th class="links_heading links_cell--status">Status</th>
</tr>
</thead>
<tbody id="links" class="links_body"></tbody>
</table>
</div>
<div class="block" id="button-bar">
<button class="blue" id="obtain-links">Obtain links</button>
<button class="green" id="check-links" disabled="disabled">Check links</button>
</div>
</form>
</div>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
/**
* On document load, assign click handlers to each button and try to load the
* user's origin and destination language preferences if previously set.
*/
let currentLinks = [];
$(function() {
$('#obtain-links').click(obtainLinks);
$('#check-links').click(checkLinks);
});

/**
* Runs a server-side function to translate the user-selected text and update
* the sidebar UI with the resulting translation.
*/
function obtainLinks() {
setBlueState(true);
$('#error').remove();

google.script.run
.withSuccessHandler(
function(links, element) {
$('#links > *').remove();

currentLinks = links || [];
links.map(function(link) {
$('#links')
.append(`<tr class="links_row"><td class="links_cell links_cell--url">${link.url}</td><td class="links_cell links_cell--status">?</td></tr>`);
});
setGreenState();
setBlueState(false);
})
.withFailureHandler(
function(msg, element) {
showError(msg, $('#button-bar'));
setBlueState(false);
})
.withUserObject(this)
.obtainLinks();
}

function setBlueState(disabled) {
if(!disabled){
$('#obtain-links').removeAttr('disabled');
} else {
$('#obtain-links').attr('disabled', 'disabled');
}
}

function setGreenState() {
if(currentLinks.length > 0){
$('#check-links').removeAttr('disabled');
} else {
$('#check-links').attr('disabled', 'disabled');
}

}

function checkLinks() {
setBlueState(true);
$('#error').remove();

google.script.run
.withSuccessHandler(
function(links, element) {
$('#links > *').remove();

currentLinks = links || [];
links.map(function(link) {
$('#links')
.append(`<tr class="links_row"><td class="links_cell links_cell--url">${link.url}</td><td class="links_cell links_cell--status">${link.code}</td></tr>`);
});
setGreenState();
setBlueState(false);
})
.withFailureHandler(
function(msg, element) {
showError(msg, $('#button-bar'));
setBlueState(false);
})
.withUserObject(this)
.checkLinks(currentLinks);
}

function showError(msg, element) {
const div = $('<div id="error" class="error">' + msg + '</div>');
$(element).after(div);
}
</script>
</body>

</html>

Demo time

And in the magic of some additional CSS you have a widget:

  • Obtains links
  • Fetches statuses
Working demo

--

--