Last active
June 23, 2021 21:36
-
-
Save Cornally/219cb6bce5fa7c0fe8570c0c3f15fff8 to your computer and use it in GitHub Desktop.
OpenGraph Lambda Processor — Return custom opengraph responses for your SPA with this lambda function that fetches data for a specific record.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
const https = require("https"); | |
// These UAs receive the special page for OpenGraph | |
const opengraphUAs = [ | |
'facebookexternalhit/1.1', | |
'Twitterbot', | |
'Googlebot', | |
'Slack', | |
'LinkedInBot/1.0', | |
'Pinterest' | |
]; | |
// This will work on Node 8.10 environment | |
function requestArticle(guid) { | |
return new Promise((resolve) => { | |
const options = { | |
host : "api.audify.fm", | |
port : "443", | |
path : `/stream/${guid}?language=en`, | |
headers : { | |
"Content-Type" : "application/json" | |
} | |
}; | |
https.get(options, (res) => { | |
res.setEncoding("utf8"); | |
let body = ""; | |
res.on("data", data => { | |
body += data; | |
}); | |
res.on("end", () => { | |
body = JSON.parse(body); | |
resolve(body.items[0]); | |
}); | |
}); | |
}); | |
} | |
async function fetchArticle(guid) { | |
try { | |
let response = await requestArticle(guid); | |
return response; | |
} | |
catch(error) { | |
return error; | |
} | |
} | |
// Return full URL, 'https://app.audify.fm/article/something-title/:guid' | |
function getUrl(article, guid) { | |
const readableTitle = article.title.replace(/[\W_]\s*$/, '').replace(/[\W_]+/g, '-').toLowerCase(); | |
return `https://app.audify.fm/article/${readableTitle}/${guid}` | |
} | |
// Generate resulting markup for clients being served OG. Replace double-quotes with html entities to ensure | |
// tag values are not cut short if a double-quote exists in the field's value. Add a prefix to summary and truncate | |
// to 240 words so we're not spilling the beans. | |
function getBodyHtml(data, ua, url, uri) { | |
const title = data.title.replace(/"/g, """); | |
const summary = 'TL;DR — ' + data.summary.replace(/^(.{240}[^\s]*).*/, "$1").replace(/"/g, """) + '…'; | |
return `<!DOCTYPE html> | |
<html> | |
<head> | |
<!-- COMMON TAGS --> | |
<meta charset="utf-8"> | |
<title>${data.title} - Audify.FM</title> | |
<!-- Search Engine --> | |
<meta name="description" content="${summary}"> | |
<meta name="image" content="${data.image_url}"> | |
<!-- Schema.org for Google --> | |
<meta itemprop="name" content="${title}"> | |
<meta itemprop="description" content="${summary}"> | |
<meta itemprop="image" content="${data.image_url}"> | |
<!-- Open Graph general (Facebook, Pinterest & Google+) --> | |
<meta prefix="og: http://ogp.me/ns#" name="og:type" content="website"> | |
<meta prefix="og: http://ogp.me/ns#" name="og:audio" content="${data.audio_url}" /> | |
<meta prefix="og: http://ogp.me/ns#" name="og:title" content="${title}"> | |
<meta prefix="og: http://ogp.me/ns#" name="og:description" content="${summary}"> | |
<meta prefix="og: http://ogp.me/ns#" name="og:image" content="${data.image_url}"> | |
<meta prefix="og: http://ogp.me/ns#" name="og:url" content="${url}"> | |
<meta prefix="og: http://ogp.me/ns#" name="og:site_name" content="${title}"> | |
</head> | |
<body> | |
<p>uri: ${uri}</p> | |
<p>agent: ${ua}</p> | |
<p>name: ${title}</p> | |
<p>description: ${summary} </p> | |
</body> | |
</html>`; | |
} | |
exports.handler = async (event, context, callback) => { | |
const request = event.Records[0].cf.request; | |
// Store user-agent of viewer request | |
const requestUA = request.headers['user-agent'][0].value; | |
const serveOpenGraphContent = !!opengraphUAs.find(ua => { | |
if (requestUA.indexOf(ua) !== -1) return true; | |
}); | |
// Uncomment for debugging | |
console.log('serve opengraph content: ' + serveOpenGraphContent + ' to ' + requestUA); | |
// If the request is not for an article, return early. | |
const uri = request.uri; | |
const splitUri = uri.split('/'); | |
if (!serveOpenGraphContent) { | |
callback(null, request); | |
} | |
// Get article guid. When split out, the guid is the third param in the URI, '/article/fake-title-here/:guid' | |
const idx = splitUri.indexOf('article') + 2; | |
const guid = splitUri[idx]; | |
// Fetch a single article | |
let article = await fetchArticle(guid); | |
// Get URL ... even though we have it in the original request | |
const url = getUrl(article); | |
// Generate resulting markup for OG-enabled UAs | |
const body = getBodyHtml(article, requestUA, url, uri); | |
const response = { | |
status: '200', | |
statusDescription: 'HTTP OK', | |
body, | |
headers: { | |
'cache-control': [{ | |
key: 'Cache-Control', | |
value: 'max-age=100' | |
}], | |
'content-type': [{ | |
key: 'Content-Type', | |
value: 'text/html' | |
}] | |
} | |
}; | |
return response; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add the above function to lambda, create a CloudFront behavior for your route (e.g.
/article/*
) for handling viewer requests. This will render your respective og/meta tag data to round out that serverless sauce.