Skip to content

Commit

Permalink
Add webmentions
Browse files Browse the repository at this point in the history
  • Loading branch information
siakaramalegos committed Nov 22, 2019
1 parent 1b2e967 commit d731856
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 10 deletions.
6 changes: 6 additions & 0 deletions .eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ const { DateTime } = require("luxon");
const fs = require("fs");
const pluginRss = require("@11ty/eleventy-plugin-rss");
const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
const filters = require('./_11ty/filters')

module.exports = function(eleventyConfig) {
// Filters
Object.keys(filters).forEach(filterName => {
eleventyConfig.addFilter(filterName, filters[filterName])
})

eleventyConfig.addPlugin(pluginRss);
eleventyConfig.addPlugin(pluginSyntaxHighlight);
eleventyConfig.setDataDeepMerge(true);
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
_cache/
_site/
node_modules/
package-lock.json
*/.DS_Store
.DS_Store
.env
25 changes: 25 additions & 0 deletions _11ty/filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { DateTime } = require("luxon");

module.exports = {
getWebmentionsForUrl: (webmentions, url) => {
return webmentions.children.filter(entry => entry['wm-target'] === url)
},
isOwnWebmention: (webmention) => {
const urls = [
'https://sia.codes',
'https://twitter.com/thegreengreek'
]
const authorUrl = webmention.author ? webmention.author.url : false
// check if a given URL is part of this site.
return authorUrl && urls.includes(authorUrl)
},
size: (mentions) => {
return !mentions ? 0 : mentions.length
},
webmentionsByType: (mentions, mentionType) => {
return mentions.filter(entry => !!entry[mentionType])
},
readableDateFromISO: (dateStr, formatStr = "dd LLL yyyy 'at' hh:mma") => {
return DateTime.fromISO(dateStr).toFormat(formatStr);
}
}
9 changes: 5 additions & 4 deletions _data/metadata.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
{
"title": "Sia Karamalegos, Freelance Web Performance Engineer",
"url": "https://sia.codes/",
"url": "https://sia.codes",
"description": "Sia is a freelance developer and web performance engineer, speaker, teacher, community organizer, and Google Developers Expert in Web Technologies.",
"feed": {
"subtitle": "Sia is a freelance developer and web performance engineer, speaker, teacher, community organizer, and Google Developers Expert in Web Technologies.",
"filename": "feed.xml",
"path": "/feed/feed.xml",
"url": "https://myurl.com/feed/feed.xml",
"id": "https://myurl.com/"
"url": "https://sia.codes/feed/feed.xml",
"id": "https://sia.codes/"
},
"author": {
"name": "Sia Karamalegos",
"email": "siakaramalegos@gmail.com"
}
},
"domain": "sia.codes"
}
93 changes: 93 additions & 0 deletions _data/webmentions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const fs = require('fs')
const fetch = require('node-fetch')
const unionBy = require('lodash/unionBy')
const domain = require('./metadata.json').domain

// Load .env variables with dotenv
require('dotenv').config()

// Define Cache Location and API Endpoint
const CACHE_FILE_PATH = '_cache/webmentions.json'
const API = 'https://webmention.io/api'
const TOKEN = process.env.WEBMENTION_IO_TOKEN

async function fetchWebmentions(since, perPage = 10000) {
// If we dont have a domain name or token, abort
if (!domain || !TOKEN) {
console.warn('>>> unable to fetch webmentions: missing domain or token')
return false
}

let url = `${API}/mentions.jf2?domain=${domain}&token=${TOKEN}&per-page=${perPage}`
if (since) url += `&since=${since}` // only fetch new mentions

const response = await fetch(url)
if (response.ok) {
const feed = await response.json()
console.log(`>>> ${feed.children.length} new webmentions fetched from ${API}`)
return feed
}

return null
}

// Merge fresh webmentions with cached entries, unique per id
function mergeWebmentions(a, b) {
return unionBy(a.children, b.children, 'wm-id')
}

// save combined webmentions in cache file
function writeToCache(data) {
const dir = '_cache'
const fileContent = JSON.stringify(data, null, 2)
// create cache folder if it doesnt exist already
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
// write data to cache json file
fs.writeFile(CACHE_FILE_PATH, fileContent, err => {
if (err) throw err
console.log(`>>> webmentions cached to ${CACHE_FILE_PATH}`)
})
}

// get cache contents from json file
function readFromCache() {
if (fs.existsSync(CACHE_FILE_PATH)) {
const cacheFile = fs.readFileSync(CACHE_FILE_PATH)
return JSON.parse(cacheFile)
}

// no cache found.
return {
lastFetched: null,
children: []
}
}

module.exports = async function () {
console.log('>>> Reading webmentions from cache...');

const cache = readFromCache()

if (cache.children.length) {
console.log(`>>> ${cache.children.length} webmentions loaded from cache`)
}

// Only fetch new mentions in production
if (process.env.NODE_ENV === 'production') {
console.log('>>> Checking for new webmentions...');
const feed = await fetchWebmentions(cache.lastFetched)
if (feed) {
const webmentions = {
lastFetched: new Date().toISOString(),
children: mergeWebmentions(cache, feed)
}

writeToCache(webmentions)
return webmentions
}
}

return cache
}
9 changes: 8 additions & 1 deletion _includes/layouts/post.njk
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,12 @@ templateClass: tmpl-post

{{ content | safe }}

<p style="margin-top:2rem"><a href="{{ '/' | url }}">← Home</a></p>
<section>
<h2>Webmentions</h3>
{% set webmentionUrl %}{{ page.url | url | absoluteUrl(site.url) }}{% endset %}
{% include 'webmentions.njk' %}
</section>

<p><a href="{{ '/' | url }}">← Home</a></p>

</div>
27 changes: 27 additions & 0 deletions _includes/webmention.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<article class="webmention {% if webmention|isOwnWebmention %}webmention--own{% endif %}" id="webmention-{{ webmention['wm-id'] }}">
<div class="webmention__meta">
{% if webmention.author %}
{% if webmention.author.photo %}
<img src="{{ webmention.author.photo }}" alt="{{ webmention.author.name }}" width="48" height="48" loading="lazy">
{% else %}
<img src="{{ '/img/avatar.svg' | url }}" alt="" width="48" height="48">
{% endif %}
<span>
<a class="h-card u-url" {% if webmention.url %}href="{{ webmention.url }}" {% endif %} target="_blank" rel="noopener noreferrer"><strong class="p-name">{{ webmention.author.name }}</strong></a>
</span>
{% else %}
<span>
<strong>Anonymous</strong>
</span>
{% endif %}

{% if webmention.published %}
<time class="postlist-date" datetime="{{ webmention.published }}">
{{ webmention.published | readableDateFromISO }}
</time>
{% endif %}
</div>
<div>
{{ webmention.content.text }}
</div>
</article>
75 changes: 75 additions & 0 deletions _includes/webmentions.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<div class="webmentions content-grid-sibling" id="webmentions">

{% set mentions = webmentions | getWebmentionsForUrl(metadata.url + webmentionUrl) %}
{% set reposts = mentions | webmentionsByType('repost-of') %}
{% set repostsSize = reposts | size %}
{% set likes = mentions | webmentionsByType('like-of') %}
{% set likesSize = likes | size %}
{% set replies = mentions | webmentionsByType('in-reply-to') %}
{% set repliesSize = replies | size %}


{% if likesSize > 0 %}
<div class="webmentions__facepile">
<h3{% if repostsSize > 11 or likesSize > 11 %} class="webmentions__facepile__squish" {% endif %}>{{ likesSize }}
Like{% if likesSize != 1 %}s{% endif %}</h3>

{% for webmention in likes %}

{% if webmention.url != "" %}
<a class="h-card u-url link-u-exempt" href="{{ webmention.url }}" target="_blank" rel="noopener noreferrer">
{% endif %}

{% if webmention.author.photo %}
<img src="{{ webmention.author.photo }}" alt="{{ webmention.author.name }}" width="48" height="48" loading="lazy">
{% else %}
<img src="{{ '/img/avatar.svg' | url }}" alt="" width="48" height="48">
{% endif %}

{% if webmention.url != "" %}
</a>
{% endif %}
{% endfor %}
</div>
{% endif %}

{% if repostsSize > 0 %}
<div class="webmentions__facepile">
<h3{% if repostsSize > 11 or likesSize > 11 %} class="webmentions__facepile__squish" {% endif %}>{{ repostsSize }} Retweet{% if repostsSize != 1 %}s{% endif %}</h3>

{% for webmention in reposts %}
{% if webmention.url != "" %}
<a class="h-card u-url link-u-exempt" href="{{ webmention.url }}" target="_blank" rel="noopener noreferrer">
{% endif %}

{% if webmention.author.photo %}
<img src="{{ webmention.author.photo }}" alt="{{ webmention.author.name }}" width="48" height="48" loading="lazy">
{% else %}
<img src="{{ '/img/avatar.svg' | url }}" alt="" width="48" height="48">
{% endif %}
{% if webmention.url != "" %}
</a>
{% endif %}
{% endfor %}
</div>
{% endif %}

{% if repliesSize > 0 %}
<div class="webmention-replies">
<h3>{{ repliesSize }} {% if repliesSize == "1" %}Reply{% else %}Replies{% endif %}</h3>

{% for webmention in replies %}
{% include 'webmention.njk' %}
{% endfor %}
</div>
{% endif %}

<p>These are <a href="https://indieweb.org/Webmention">webmentions</a> via the <a href="https://indieweb.org/">IndieWeb</a> and <a href="https://webmention.io/">webmention.io</a>. Mention this post from your site:</p>

<form action="https://webmention.io/sia.codes/webmention" method="post" class="form-webmention">
<label for="form-webmention-source">URL</label><br>
<input id="form-webmention-source" type="url" name="source" placeholder="https://example.com" required>
<input type="hidden" name="target" value="https://sia.codes/{{ page.url }}">
<input type="submit" class="button button-small" value="Send Webmention">
</form>
</div>
41 changes: 39 additions & 2 deletions css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ a:hover {
border: none;
box-shadow: none;}
.a-reset a:hover { background: none; }
a.button {
a.button, input.button {
/* Permalink - use to edit and share this gradient: https://colorzilla.com/gradient-editor/#cd0084+30,b000f5+100 */
background: rgb(205,0,132); /* Old browsers var(--pink-50); and purple-50*/
background: -moz-linear-gradient(-45deg, rgba(205,0,132,1) 30%, rgba(176,0,245,1) 100%);
Expand All @@ -177,14 +177,16 @@ a.button {
border: none;
box-shadow: 0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.12);
color: var(--white);
cursor: pointer;
font-weight: 700;
text-decoration: none;
text-transform: uppercase;}
a.button-default {
display: inline-block;
font-size: 20px;
padding: 16px 32px;}
a.button:hover { background: var(--pink-40);}
input.button-small {padding: 4px 8px;}
a.button:hover, input.button:hover { background: var(--pink-40);}
a.button-circle {
align-items: center;
border-radius: 100%;
Expand Down Expand Up @@ -405,9 +407,35 @@ footer p { font-size: 1em;}
.social a svg {width: 24px;}
footer a svg {width: 24px;}

/* Webmentions */
.webmentions img {border-radius: 50%;}
.webmentions h3 {margin-top: 40px;}
.webmentions__facepile a {
border: none;
box-shadow: none;}
.webmentions__facepile a:hover {background: none;}
.webmentions a:hover img { filter: drop-shadow(3px 5px 10px var(--pink-20));}
.form-webmention {margin: 16px 0; width: 100%;}
.webmention-replies img {
height: 24px;
width: 24px;}
.webmention-replies article {margin-bottom: 32px;}
.webmention-replies .webmention__meta {margin-bottom: 8px;}
.webmention-replies .webmention__meta time {display: block; margin-top: 8px;}
form input[type="url"] {
display: block;
height: 2rem;
margin: 4px 0;
width: 100%;}
form .button {height: 2rem; width: 150px;}
form label {font-weight: 700;}

@media screen and (min-width: 380px) {
.social a svg {width: 32px;}
footer a svg {width: 32px;}
form input[type="url"] {
display: inline-block;
width: calc(100% - 160px);}
}
@media screen and (min-width: 440px) {.home {display: inline-block;}}
@media screen and (min-width: 500px) {
Expand All @@ -416,6 +444,15 @@ footer a svg {width: 24px;}
grid-template-columns: 2fr 1fr;}
.event-location {margin: 0;}
.event-location img {margin: 0 8px;}
.webmention-replies .webmention__meta {
display: grid;
grid-template-columns: 30px 1fr 200px;
}
.webmention-replies .webmention__meta time {
display: inline;
margin-top: 0;
text-align: right;
}
}
@media screen and (min-width: 665px) {
header {
Expand Down
1 change: 1 addition & 0 deletions img/avatar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions javascript/create-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const fs = require('fs')
fs.writeFileSync('../.env', `WEBMENTION_IO_TOKEN=${process.env.WEBMENTION_IO_TOKEN}\n`)
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "5.0.0",
"description": "A starter repository for a blog web site using the Eleventy static site generator.",
"scripts": {
"build": "npx eleventy",
"build": "NODE_ENV=production npx eleventy",
"start": "npx eleventy --serve",
"watch": "npx eleventy --watch",
"debug": "DEBUG=* npx eleventy"
Expand All @@ -28,7 +28,11 @@
"@11ty/eleventy-plugin-syntaxhighlight": "^2.0.3",
"luxon": "^1.12.0",
"markdown-it": "^8.4.2",
"markdown-it-anchor": "^5.0.2"
"markdown-it-anchor": "^5.0.2",
"node-fetch": "^2.6.0"
},
"dependencies": {}
"dependencies": {
"dotenv": "^8.2.0",
"lodash": "^4.17.15"
}
}

0 comments on commit d731856

Please sign in to comment.