Created
June 20, 2017 18:39
-
-
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
This file contains hidden or 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
{% 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 %} |
This file contains hidden or 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
{% 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> |
This file contains hidden or 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
{% 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> |
This file contains hidden or 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
{% 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 %} |
This file contains hidden or 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
{% 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> |
This file contains hidden or 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
... other page elements ... | |
{% assign webmentions = page | webmention_data %} | |
{% include mentions.html mentions=webmentions %} | |
... other page elements ... |
This file contains hidden or 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
# 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