Skip to content

Instantly share code, notes, and snippets.

@martymcguire
Created June 20, 2017 18:39
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save martymcguire/f240610b61e882b2301c681661f45b16 to your computer and use it in GitHub Desktop.
Save martymcguire/f240610b61e882b2301c681661f45b16 to your computer and use it in GitHub Desktop.
Plugin and templates to make Webmention.io data available to a Jekyll site
{% unless include.faces == empty %}
<div class="row" style="margin-bottom: 1em"><div class="col-xs-12">
<h4>{{ include.name }}</h4>
<div class="facepile">
{% for face in include.faces %}
{% assign author = face.data.author %}
{% if author.photo %}
{% assign photo = author.photo | imageproxy: 60 %}
{% endif %}
{% case include.mftype %}
{% when 'reply' %}
{% assign stripped_content = face.data.content | strip_html %}
{% assign is_emoji = stripped_content | is_emoji? %}
{% if is_emoji %}
{% assign emoji = stripped_content %}
{% endif %}
{% else %}
{% assign icon = "" %}
{% endcase %}
{% if emoji %}
{% capture icon %}<span class='activity-icon'>{{ emoji }}</span>{% endcapture %}
{% else %}{% assign icon = "" %}
{% endif %}
<div class="face p-{{ include.mftype }} h-cite">
<a class="u-url" href="{{ face.data.url }}"><span class="p-author h-card"><img class="u-photo" src="{{ photo }}" alt="{{ author.name }}" title="{{ author.name }}"/></span></a>{{ icon }}
</div>
{% endfor %}
</div>
</div></div>
{% endunless %}
{% assign reply = include.reply %}
{% assign photo = "" %}
{% if reply.data.author.photo %}
{% capture photo %}
<img class="u-photo" src="{{ reply.data.author.photo | imageproxy: 30 }}" alt="{{ reply.data.author.name }}"/>
{% endcapture %}
{% endif %}
<div class="u-comment h-cite" style="background-color: rgba(255,255,255, {% cycle "0.25", "0.5" %});">
<a class="u-author h-card" href="{{ reply.data.author.url }}">{{ photo }}<span class="p-name">{{ reply.data.author.name }}</span></a>
at
<a class="u-url" href="{{ reply.data.url }}">
<time class="dt-published" datetime="{{ reply.data.published | date_to_xmlschema }}">{{ reply.data.published }}</time>
</a>
said:
{% if reply.activity.type == "link" %}
{% if reply.data.name %}<p class="p-name">{{ reply.data.name }}</p>{% endif %}
{% if reply.data.content %}<p class="p-content">{{ reply.data.content | strip_html | truncate: 500 }}</p>{% endif %}
{% else %}
<p class="p-content p-name">{{ reply.data.content | strip_html | truncate: 500 }}</p>
{% endif %}
</div>
{% assign reply = include.reply %}
{% if reply.data.author.photo %}
{% capture photo %}
<img class="u-photo" src="{{ reply.data.author.photo | imageproxy: 30 }}" alt="{{ reply.data.author.name }}"/>
{% endcapture %}
{% endif %}
<div class="u-repost h-cite" style="background-color: rgba(255,255,255, {% cycle "0.25", "0.5" %});">
<a class="u-author h-card" href="{{ reply.data.author.url }}">{{ photo }}<span class="p-name">{{ reply.data.author.name }}</span></a>
at
<a class="u-url" href="{{ reply.data.url }}">
<time class="dt-published" datetime="{{ reply.verified_date | date_to_xmlschema }}">{{ reply.verified_date }}</time>
</a>
<p class="p-content p-name"><i class="fa fa-retweet"></i> {{ m.activity.sentence_html }}</p>
</div>
{% assign mentions = include.mentions | filter_mentions: "single_post" %}
{% include facepile.html faces=mentions.reposts name="Reposts" mftype="repost" %}
{% include facepile.html faces=mentions.likes name="Likes" mftype="like" %}
{% include facepile.html faces=mentions.reactions name="Reactions" mftype="reply" %}
{% include facepile.html faces=mentions.going name="Attending" mftype="attendee" %}
{% include facepile.html faces=mentions.maybes name="Maybe" mftype="maybe" %}
{% unless mentions.replies == empty %}
<h4>Mentions</h4>
<div class="mentions">
{% for reply in mentions.replies %}
{% include mentions_reply.html reply=reply %}
{% endfor %}
</div>
{% endunless %}
{% assign reply = include.reply %}
{% if reply.data.author.photo %}
{% capture photo %}
<img class="u-photo" src="{{ reply.data.author.photo | imageproxy: 30 }}" alt="{{ reply.data.author.name }}"/>
{% endcapture %}
{% endif %}
<div class="u-like h-cite" style="background-color: rgba(255,255,255, {% cycle "0.25", "0.5" %});">
<a class="u-author h-card" href="{{ reply.data.author.url }}">{{ photo }}<span class="p-name">{{ reply.data.author.name }}</span></a>
at
<a class="u-url" href="{{ reply.data.url }}">
<time class="dt-published" datetime="{{ reply.verified_date | date_to_xmlschema }}">{{ reply.verified_date }}</time>
</a>
<p class="p-content p-name"><i class="fa fa-star-o"></i> {{ m.activity.sentence_html }}</p>
</div>
... other page elements ...
{% assign webmentions = page | webmention_data %}
{% include mentions.html mentions=webmentions %}
... other page elements ...
# Generator that queries http://webmention.io for Webmentions
# related to your site's posts and pages.
# Configuration should be in _config.yml, for example:
#
# webmention_io:
# api_key: XXXXXXXXXXXX
#
# A cache is used to reduce traffic to webmention.io at compile time.
# Raw webmention data is cached as Jekyll data in:
# _data/webmention_io/cache/
require 'net/http'
require 'json'
require 'twemoji'
require 'digest/sha1'
module Jekyll
class WebmentionIoGenerator < Generator
safe true
@@data_dir_path = "_data"
def generate(site)
@site = site
@cfg = verify_config()
return if @cfg.nil?
update_mentions()
end
def verify_config()
if not @site.config.include?('webmention_io')
STDERR.puts ("WARN: `webmention_io` is not defined in _config.yml. Not fetching webmentions.")
return nil
end
cfg = @site.config['webmention_io']
if not cfg.include? 'api_key'
STDERR.puts ("WARN: `webmention_io.api_key` is not defined in _config.yml. Not fetching webmentions.")
return nil
end
cfg
end
def update_mentions()
@cfg['domains'].each do |domain|
cache_key = domain.gsub('.','_')
cache = _get_site_data("webmention_io|cache|#{cache_key}|by_key", {})
index = _get_site_data("webmention_io|cache|#{cache_key}|by_target", {})
# puts "Cache contains #{cache.length} mentions for #{domain}"
done = false
page = 0
new_count = 0
while not done
# puts "Fetching page #{page} of mentions."
mentions = fetch_mentions_page(domain, page)
if mentions.nil? || (mentions.length == 0)
done = true
else
mentions.each do |m|
if mention_is_new?(m, cache)
add_to_cache(m, cache)
add_to_index(m, index)
new_count += 1
else
done = true
break
end
end
page += 1
end
end
if new_count > 0
data = { "by_key": cache, "by_target": index }
# update site.data for this domain
_set_site_data("webmention_io|cache|#{cache_key}", data)
# write mentions cache for this domain to disk
_write_site_data("webmention_io|cache|#{cache_key}", data)
end
end
end
def id_for_mention(mention)
Digest::SHA1.hexdigest (mention['source'] + mention['target'])
end
def mention_is_new?(mention, cache)
m = get_mention_from_cache(mention, cache)
not mentions_match?(m, mention)
end
def get_mention_from_cache(mention, cache)
k = id_for_mention(mention)
cache[k]
end
def add_to_cache(mention, cache)
m = get_mention_from_cache(mention, cache)
if not m.nil?
puts "Updated mention! Old: #{m.inspect} New: #{mention.inspect}"
end
k = id_for_mention(mention)
cache[k] = mention
end
def add_to_index(mention, index)
t = mention['target']
path = URI(t).path
k = id_for_mention(mention)
index[path] ||= []
index[path] << k unless index[path].include?(k)
end
def mentions_match? (a, b)
(not a.nil?) and
(not b.nil?) and
(a['source'] == b['source']) and
(a['target'] == b['target']) and
(a['verified_date'] == b['verified_date'])
end
def fetch_mentions_page(domain, page)
params = {
'token': @cfg['api_key'],
'domain': domain,
'page': page
}
path = "https://webmention.io/api/mentions?" + URI.encode_www_form(params)
uri = URI(path)
response = Net::HTTP.get(uri)
json = JSON.parse(response)
return json['links']
end
def _get_site_data(key, default = {})
parts = key.split('|')
tree = @site.data
while (part = parts.shift) != nil
if not tree.include? part
return default
end
tree = tree[part]
end
return tree
end
def _set_site_data(key, data)
parts = key.split('|')
tree = @site.data
while (parts.length > 1)
part = parts.shift
tree[part] = tree.include?(part) ? tree[part] : {}
tree = tree[part]
end
tree[parts.shift] = data
end
def _write_site_data(key, data)
path = File.join(@@data_dir_path, key.split('|')) + '.json'
if not File.exists? File.dirname(path)
FileUtils.mkdir_p File.dirname(path)
end
File.open(path, "w") do |f|
f.write(data.to_json)
end
end
end
end
# Liquid Filter for returning the mentions of the given page
module Jekyll
module WebmentionIoFilter
def webmention_data(page)
return webmention_data_for_url(page['url'])
end
def webmention_data_for_url(url, prefix=nil)
path = URI(url).path
site = @context.registers[:site]
mentions = []
for domain in site.config["webmention_io"]["domains"]
index_key = domain.gsub('.','_')
cache_data = site.data["webmention_io"]["cache"][index_key] || {}
wmindex = cache_data["by_target"] || {}
wmcache = cache_data["by_key"] || {}
if wmindex.include?(path)
mentions += wmindex[path].map{ |k| wmcache[k] }
end
end
return mentions
end
def sort_mentions(mentions, byorder = "verified_date,asc")
(by, order) = byorder.split(',').map{ |v| v.strip }
return mentions.sort do |a,b|
if('asc' == order)
a[by] <=> b[by]
elsif('desc' == order)
b[by] <=> a[by]
else
STDERR.puts ("WARN: sort_mentions: unknown value for direction `#{order}`.")
a <=> b
end
end
end
def filter_mentions(mentions, filter_name = "default")
site = @context.registers[:site]
if (
(not site.config.include?('webmention_io')) or
(not site.config['webmention_io'].include?('filters')) or
(not site.config['webmention_io']['filters'].include?(filter_name))
)
STDERR.puts ("WARN: `webmention_io.filters.#{filter_name}` is not defined in _config.yml.")
return { "replies": mentions }
end
groups = site.config['webmention_io']['filters'][filter_name]
# we'll return this
filtered_mentions = Hash[groups.map{ |k,v| [k, []] } ]
# map from activity type to the array the mention belongs in
# saves multi-step lookups
type_group_map = {}
groups.each do |k, types|
types.each do |t|
type_group_map[t] = filtered_mentions[k]
end
end
mentions.each do |m|
m_type = m['activity']['type']
# TODO: make emoji conversion a config option
m_is_emoji = ((m_type == 'reply') and
(is_emoji?(m['data']['content'].to_s.gsub(/<.*?>/, ''))))
m_type = m_is_emoji ? 'emoji' : m_type
# TODO: make filtering out reply and link wms with no content a cfg opt
m_is_blank_link_reply = (
((m_type == 'reply') or (m_type == 'link')) and
(
(m['data']['content'].nil? or m['data']['content'] == "") and
(m['data']['name'].nil? or m['data']['name'] == "")
)
)
m_type = m_is_blank_link_reply ? nil : m_type
if type_group_map.include? m_type
type_group_map[m_type] << m
end
end
return filtered_mentions
end
def is_emoji?(content)
my_emoji = ["❤️" ]
return my_emoji.include?(content) || (Twemoji.find_by(unicode: content) != nil)
end
end
end
Liquid::Template.register_filter(Jekyll::WebmentionIoFilter)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment