Last active
May 28, 2021 00:01
-
-
Save Nooshu/121e5bf7f3d5c6528413c57db257894f to your computer and use it in GitHub Desktop.
Webmention code used in the "Implementing Webmentions on this blog" Nooshu blogpost
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
(function(){ | |
// Check see if browser supports the intersection observer | |
if ('IntersectionObserver' in window) { | |
// assume browser supports ES6 | |
var supportsES6 = true; | |
// check see if browser supports ES6 (https://gist.github.com/DaBs/89ccc2ffd1d435efdacff05248514f38) | |
var str = 'class ಠ_ಠ extends Array {constructor(j = "a", ...c) {const q = (({u: e}) => {return { [`s${c}`]: Symbol(j) };})({});super(j, q, ...c);}}' + | |
'new Promise((f) => {const a = function* (){return "\u{20BB7}".match(/./u)[0].length === 2 || true;};for (let vre of a()) {' + | |
'const [uw, as, he, re] = [new Set(), new WeakSet(), new Map(), new WeakMap()];break;}f(new Proxy({}, {get: (han, h) => h in han ? han[h] ' + | |
': "42".repeat(0o10)}));}).then(bi => new ಠ_ಠ(bi.rd));'; | |
// run the ES6 test | |
try { | |
eval(str); | |
} catch(e) { | |
supportsES6 = false; | |
} | |
// abort script loading if ES6 not supported | |
if(!supportsES6){ | |
return; | |
} | |
// Only allow the script loader to fire once | |
let observerFired = false; | |
// Setup observer options | |
let observerOptions = { | |
root: null, | |
rootMargin: '0px', | |
threshold: 1.0 | |
} | |
// append the webmentions script to the head | |
function scriptLoader(){ | |
let script = document.createElement('script'); | |
script.src = '/js/webmentions.js'; | |
document.head.appendChild(script); | |
} | |
function onChange(changes, observer){ | |
changes.forEach(change => { | |
// if the comments wrapper is fully in view | |
if (change.intersectionRatio == 1) { | |
// script already loaded so return | |
if(observerFired){ | |
return; | |
} | |
// script now loaded, so set to true | |
observerFired = true; | |
// load the webmentions script | |
scriptLoader(); | |
// show the loader while this is happening | |
document.querySelector('.content-webmentions__loader').classList.remove('visually-hidden'); | |
} | |
}); | |
} | |
// the element we are looking to move into the viewport | |
let mentionsWrapper = document.querySelector('.content-webmentions'); | |
// check see if the element is on the page, then setup the observer | |
if(mentionsWrapper){ | |
let observer = new IntersectionObserver(onChange, observerOptions); | |
observer.observe(mentionsWrapper); | |
} | |
} | |
})(); |
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
/** @preserve Based heavily on the work by Keith Grant (keithjgrant.com) **/ | |
// IIFE to restrict global namespace | |
(function(){ | |
// link to the anonymous avatar | |
const ANON_AVATAR = '/images/app-shell/mm.png'; | |
// cloudinary app code (remember to restrict to set domains in settings) | |
const CLOUD_CODE = 'dffhrhuy4'; | |
// var to store the built HTML | |
let webmentionHTML = `<div class='content-webmentions__inner'>`; | |
function init(){ | |
// grab the URL of the current page | |
let url = document.location.origin + document.location.pathname; | |
// these are the url permutations we will check against | |
const targets = getUrlPermutations(url); | |
// build each script tag | |
let script = document.createElement('script'); | |
let src = 'https://webmention.io/api/mentions?perPage=500&jsonp=parseWebmentions'; | |
// check the API for each domain | |
targets.forEach(function(targetUrl) { | |
src += `&target[]=${encodeURIComponent(targetUrl)}`; | |
}); | |
// cachebuster | |
src += `&_=${Math.random()}`; | |
script.src = src; | |
script.async = true; | |
document.getElementsByTagName('head')[0].appendChild(script); | |
} | |
// build the permutations array to check the API for | |
function getUrlPermutations(url) { | |
const urls = []; | |
// useful for local testing, populate localhost with live mentions | |
url = url.replace('http://localhost:3000', 'https://nooshu.github.io'); | |
urls.push(url); | |
// check the non-https version too | |
urls.push(url.replace('https://', 'http://')); | |
// check a version of the URL without the trailing slash | |
if (url.substr(-1) === '/') { | |
var noslash = url.substr(0, url.length - 1); | |
urls.push(noslash); | |
urls.push(noslash.replace('https://', 'http://')); | |
} | |
// return array of permutations | |
return urls; | |
} | |
// sort function used for incoming data | |
function webmentionSort(a, b) { | |
const dateA = getWebmentionDate(a); | |
const dateB = getWebmentionDate(b); | |
if (dateA < dateB) { | |
return -1; | |
} else if (dateB < dateA) { | |
return 1; | |
} | |
return 0; | |
} | |
// return a date for the mention from published or verified | |
function getWebmentionDate(webmention) { | |
if (webmention.data.published) { | |
return new Date(webmention.data.published); | |
} | |
return new Date(webmention.verified_date); | |
} | |
function parseWebmentions(data) { | |
// if no mentions, hide the loader | |
if(data.links.length == 0){ | |
document.querySelector('.content-webmentions__loader').classList.add('visually-hidden'); | |
return; | |
} | |
// sort the incoming data | |
let links = data.links.sort(webmentionSort); | |
// create three arrays to store our webmentions | |
let likes = []; | |
let repostsLinks = []; | |
let replies = []; | |
// map over the webmentions and sort the into arrays | |
links.map(function(l) { | |
// if no activity key, unknown type | |
if (!l.activity || !l.activity.type) { | |
console.warning('unknown link type', l); | |
return; | |
} | |
// not verified, then not valid | |
if (!l.verified) { | |
return; | |
} | |
// depending on the type, push into an correct array | |
switch (l.activity.type) { | |
case 'like': | |
likes.push(l); | |
break; | |
case 'repost': | |
case 'link': | |
repostsLinks.push(l); | |
break; | |
default: | |
replies.push(l); | |
break; | |
} | |
}); | |
// render the number of webmentions to the page | |
webmentionHTML += renderWebmentionTotal(data.links.length); | |
// render the form | |
renderForm(); | |
// if we have replies, build the replies HTML | |
if(replies.length){ | |
renderReplies(replies); | |
} | |
// if we have likes, build the like HTML | |
if(likes.length){ | |
renderLikes(likes); | |
} | |
// if we have reposts, build the reposts HTML | |
if(repostsLinks.length){ | |
renderRepostsLinks(repostsLinks); | |
} | |
// close the webmention container | |
webmentionHTML += `</div>`; | |
// now that the HTML is built, add it to the DOM (only a single insertion for optimal performance) | |
document.querySelector('.content-webmentions').innerHTML = webmentionHTML; | |
// setup the send webmention form interactions | |
setupFormInteractions(); | |
} | |
// add form events | |
function setupFormInteractions(){ | |
// store the form element | |
let formEle = document.querySelector('.content-webmentions__form'); | |
// listen for the submit event, execute once heard | |
formEle.addEventListener('submit', formSubmit); | |
} | |
// handle the form submission | |
function formSubmit(evt){ | |
// stop the default form submission | |
evt.preventDefault(); | |
// store the page webmention endpoint | |
let linkHref = document.querySelector('link[rel=webmention]').href; | |
// store the two webmention URLS | |
let target = document.querySelector('#webmention-target').value; | |
let source = document.querySelector('#webmention-source').value; | |
// setup new XMLHttpRequest for the POST | |
let xhr = new XMLHttpRequest(); | |
// do the post | |
xhr.open('POST', `${linkHref}`); | |
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); | |
xhr.send(encodeURI(`source=${source}&target=${target}`)); | |
// wait for the response | |
xhr.onload = function() { | |
let response = JSON.parse(xhr.responseText); | |
if(xhr.status === 200 || xhr.status === 201){ | |
console.info(`XMLHttpRequest success: ${xhr.status}`); | |
} else { | |
console.warn(`XMLHttpRequest failed: ${xhr.status}`); | |
} | |
// feedback response to the user | |
formMessages(response); | |
}; | |
} | |
// populate the feedback message for the user | |
function formMessages(responseData){ | |
// message element | |
let messageEle = document.querySelector('.content-webmentions__form-message'); | |
// messages | |
let thanks = `Thank you for your submission!`; | |
let error = `Looks like something went wrong:`; | |
let message; | |
// adjust content depending on the status | |
switch(responseData.status){ | |
case 'queued': | |
case 'success': | |
message = `${thanks} ${responseData.summary}. <a href="${responseData.location}">View the current status.</a>`; | |
break; | |
default: | |
message = `${error} ${responseData.summary}.`; | |
break; | |
} | |
// set elements HTML to the message | |
messageEle.innerHTML = message; | |
// show the message | |
messageEle.classList.remove('visually-hidden'); | |
} | |
// set to global method for JSONP | |
window.parseWebmentions = parseWebmentions; | |
// human friendly month formats | |
var months = [ | |
'Jan', | |
'Feb', | |
'Mar', | |
'Apr', | |
'May', | |
'Jun', | |
'Jul', | |
'Aug', | |
'Sep', | |
'Oct', | |
'Nov', | |
'Dec' | |
]; | |
// format the date | |
function formatDate(date) { | |
if (!date) { | |
return ''; | |
} | |
// e.g. 22 Oct 2019 | |
return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`; | |
} | |
// run the returned photos through cloudinary to reduce the size | |
// 96 by 96 as resizing to 48 by 48, so 2x for high density devices | |
function cloudifyPhoto(url){ | |
// if no URL, pass back the anonymous images | |
return (url ? `https://res.cloudinary.com/${CLOUD_CODE}/image/fetch/w_96,h_96,q_auto/${url}` : ANON_AVATAR); | |
} | |
// simple function to look a 1 vs many results | |
function renderWebmentionTotal(total){ | |
return `<h2>${total + (total === 1 ? ' Webmention' : ' Webmentions')}</h2>`; | |
} | |
// if we have a url instead of an author, construct the link | |
function getHostName(url) { | |
let a = document.createElement('a'); | |
a.href = url; | |
// remove the www for stylistic reasons | |
return (a.hostname || '').replace('www.', ''); | |
} | |
// render the webmention submit form | |
function renderForm(){ | |
// generate the form HTML | |
let formHTML = `<div class="content-webmentions__form">${formTemplate()}</div>`; | |
// append the generated like HTML to the single set of HTML | |
webmentionHTML += formHTML; | |
} | |
// render the webmention likes from the sorted array | |
function renderLikes(likesArray){ | |
// build our likes HTML | |
let likesHTML = ` | |
<div class="content-webmentions--likes"> | |
<h3>${likesArray.length} ${likesArray.length === 1 ? ' like' : ' likes'}</h3> | |
<ul class='content-webmentions__list'>`; | |
// map over our likes, build the data object | |
likesArray.map(function(l){ | |
let likeObj = { | |
photo: cloudifyPhoto(l.data.author.photo), | |
name: l.data.author.name, | |
authorUrl: l.data.author.url, | |
url: l.data.url, | |
date: new Date(l.data.published || l.verified_date) | |
} | |
// create each like | |
likesHTML += likeRepostLinkTemplate(likeObj); | |
}); | |
// close the like HTML | |
likesHTML += | |
` </ul> | |
</div>`; | |
// append the generated like HTML to the single set of HTML | |
webmentionHTML += likesHTML; | |
} | |
// function to render reposts and links | |
function renderRepostsLinks(repostsArray){ | |
// build our repost / links HTML | |
let repostLinksHTML = ` | |
<div class="content-webmentions--reposts"> | |
<h3>${repostsArray.length} ${repostsArray.length === 1 ? ' repost' : ' reposts'}</h3> | |
<ul class='content-webmentions__list'>`; | |
// map over our reposts / likes, build the data object | |
repostsArray.map(function(l){ | |
let repostLinkObj; | |
// if a repost (has an author) | |
if (l.data.author) { | |
repostLinkObj = { | |
photo: cloudifyPhoto(l.data.author.photo), | |
name: l.data.author.name, | |
authorUrl: l.data.author.url, | |
url: l.data.url, | |
date: new Date(l.data.published || l.verified_date) | |
} | |
} else { | |
// this is for a link | |
repostLinkObj = { | |
photo: ANON_AVATAR, | |
name: getHostName(l.data.url) || 'inbound link', | |
authorUrl: l.data.url, | |
url: l.data.url, | |
date: new Date(l.data.published || l.verified_date), | |
}; | |
} | |
// create each like | |
repostLinksHTML += likeRepostLinkTemplate(repostLinkObj); | |
}); | |
// close the HTML | |
repostLinksHTML += | |
` </ul> | |
</div>`; | |
// append the generated like HTML to the single set of HTML | |
webmentionHTML += repostLinksHTML; | |
} | |
// function to render replies | |
function renderReplies(replyArray){ | |
// build the reply HTML | |
let replyHTML = ` | |
<div class="content-webmentions--replies"> | |
<h3>${replyArray.length} ${replyArray.length === 1 ? ' reply' : ' replies'}</h3> | |
<ul class='content-webmentions__list'>`; | |
// map over each reply, | |
replyArray.map(function(l){ | |
let replyObj = { | |
photo: cloudifyPhoto(l.data.author.photo), | |
name: l.data.author.name, | |
authorUrl: l.data.author.url, | |
url: l.data.url, | |
date: new Date(l.data.published || l.verified_date), | |
content: l.data.content | |
} | |
replyHTML += replyTemplate(replyObj); | |
}); | |
// close the HTML | |
replyHTML += | |
` </ul> | |
</div>`; | |
// append the generated HTML to the total | |
webmentionHTML += replyHTML; | |
} | |
// reply HTML template | |
function replyTemplate(replyObj){ | |
return `<li class="content-webmentions__list-item h-entry"> | |
<div class="comment"> | |
<div class="comment__author reply p-author"> | |
<a class="reply__avatar u-author" href="${replyObj.authorUrl}" title="${replyObj.name}"> | |
<img class="u-photo" src="${replyObj.photo}" alt="${replyObj.name}"> | |
</a> | |
<a class="reply__bar u-url" href="${replyObj.url}"> | |
<p class="reply__author reply__author p-author">${replyObj.name}</p> | |
<p class="reply__date" href="">${formatDate(replyObj.date)}</p> | |
</a> | |
</div> | |
<div class="comment__content e-entry">${replyObj.content}</div> | |
</div> | |
</li>`; | |
} | |
// like, repost, link HTML template | |
function likeRepostLinkTemplate(likeObj){ | |
return `<li class="content-webmentions__list-item h-entry"> | |
<div class="reply h-card p-author"> | |
<a class="reply__bar u-url" href="${likeObj.url}"> | |
<p class="reply__author reply__author p-name">${likeObj.name}</p> | |
<p class="reply__date" href="">${formatDate(likeObj.date)}</p> | |
</a> | |
<a class="reply__avatar u-author" href="${likeObj.authorUrl}" title="${likeObj.name}"> | |
<img class="u-photo" src="${likeObj.photo}" alt="${likeObj.name}"> | |
</a> | |
</div> | |
</li>`; | |
} | |
// template for the JS version of the form | |
function formTemplate(){ | |
return `<form id="webmention-form" class="content-webmentions__form" method="post" action="https://webmention.io/nooshu.github.io/webmention"> | |
<input id="webmention-target" type="hidden" name="target" value="${window.location.href}"> | |
<label class="content-webmentions__form-label" for="webmention-source">Written a response to this post? Let me know the URL using the form below:</label> | |
<input type="text" id="webmention-source" name="source" placeholder="https://example.com/my-post" class="content-webmentions__form-input"> | |
<button type="submit" id="webmention-submit" class="content-webmentions__form-button">Send Webmention</button> | |
<p class="content-webmentions__form-message visually-hidden"></p> | |
</form>`; | |
} | |
// start the dance | |
init(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment