Webmention code used in the "Implementing Webmentions on this blog" Nooshu blogpost
(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); | |
} | |
} | |
})(); |
/** @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