Wednesday, August 22, 2018

Citation.js: Showing Blogger Posts on a Different Site

Citation.js: Showing Blogger Posts on a Different Site

I made a small client for Blogger that takes a tag and transforms it into its own little blog: citation.js.org/blog/?post=542…. No metadata though, as it’s all client-side.
— Lars Willighagen (@larswillighagen) August 6, 2018

I made a Material-themed page showing Citation.js blog posts from Blogger. It supports pagination, tags, search and linking individual posts. Since it’s a single, static page I can’t support meta and link tags for metadata, that would require JavaScript which indexers don’t run.

The great thing about the Blogger API is that you can generate feeds for single tags, like Citation.js for example, and search for tags and general queries within that tag. That’s what makes all this possible. The URL scheme is very simple:

# Tag feed
https://$BLOG.blogspot.com/feeds/posts/default/-/$TAG

# Tag-in-tag feed
https://$BLOG.blogspot.com/feeds/posts/default/-/$TAG/$OTHER_TAG

# Search-in-tag feed
# Note: don't copy this, there's a ZWS before ?q= for syntax highlighting
https://$BLOG.blogspot.com/feeds/posts/default/-/$TAG​?q=$QUERY

# Post
https://$BLOG.blogspot.com/feeds/posts/default/$POST

Pagination and response formats complicate things a little, and are dealt with in the code below.

Apart from the Material theme, it only uses vanilla JavaScript to generate the pages. The search bar doesn’t even use JavaScript at all, just good ol’ form semantics. The JavaScript it does use is fairly simple. First, the query is parsed and an API URL is generated.

window.onload = function () {
  var params = {}
  
  location.search.slice(1).split('&').map(function (pair) {
    pair = pair.split('=')
    params[pair[0]] = pair[1]
  })

  var url

  if (params.post) {
    url = 'https://larsgw.blogspot.com/feeds/posts/default/' + params.post + '?alt=json-in-script&callback=cb'
  } else if (params.tag) {
    url = 'https://larsgw.blogspot.com/feeds/posts/default/-/Citation.js/' + params.tag + '?alt=json-in-script&callback=cb'
  } else if (params.query) {
    url = 'https://larsgw.blogspot.com/feeds/posts/default/-/Citation.js/?q=' + params.query + '&alt=json-in-script&callback=cb'
  } else {
    url = 'https://larsgw.blogspot.com/feeds/posts/default/-/Citation.js?alt=json-in-script&callback=cb'
  }

  var startIndex = location.href.match(/start-index=(\d+)/)
  if (startIndex) {
    url += '&' + startIndex[0]
  }

  load(url)
}

Since the only JSON API for Blogger is JSON-in-script, we append a script element loading the resource. This then calls the callback, cb.

function cb (data) {
  content.innerHTML = data.feed ? templates.feed(data.feed.entry) : templates.feedItem(data.entry)

  // pagination
  if (data.feed) {
    var href = location.href
    var hasIndex = href.indexOf('start-index') > -1
    var hasParams = href.indexOf('?') > -1
    var indexPattern = /start-index=(\d+)/

    var prev = find(data.feed.link, function (link) { return link.rel === 'previous' })
    if (prev) {
      prev = 'start-index=' + prev.href.match(indexPattern)[1]
      var url = hasIndex ? href.replace(indexPattern, prev) : href + (hasParams ? '?' : '') + prev
      paginatePrev.setAttribute('href', url)
    }

    var next = find(data.feed.link, function (link) { return link.rel === 'next' })
    if (next) {
      next = 'start-index=' + next.href.match(indexPattern)[1]
      var url = hasIndex ? href.replace(indexPattern, next) : href + (hasParams ? '&' : '?') + next
      paginateNext.setAttribute('href', url)
    }
  }
}

function load (url) {
  loader.setAttribute('src', url)
}

The callback then uses simple templates, which are just JS functions taking in the API response and outputting HTML to show the results on the page. Then, it figures out the pagination. Below is an example template. It extracts the post id to make links and does some preprocessing, removing stackedit metadata and styling and lowering each heading two levels. Then, it puts together the HTML with some additional util functions and subtemplates.

  feedItem: function (item) {
    var id = item.id.$t.replace(/^.*\.post-(\d+)$/, '$1')
    var content = item.content.$t
      .replace(/^[\s\S]*<div class="stackedit__html">([\s\S]*)<\/div>[\s\S]*$/, '$1')
      .replace(/<(\/?)h([1-6])/g, function (match, slash, level) {
        if (+level > 4) {
          return '<' + slash + 'b'
        } else {
          return '<' + slash + 'h' + (+level + 2)
        }
      })

    return '<div class="mdl-card mdl-shadow--2dp mdl-cell mdl-cell--12-col">' +
      '<div class="mdl-card__title">' +
        '<h2 class="mdl-card__title-text">' +
          '<a href="?post=' + id + '">' + item.title.$t + '</a>' +
        '</h2>' +
      '</div>' +
      '<div class="mdl-card__supporting-text mdl-card--border">' +
        '<p>' +
          '<span><i class="material-icons">edit</i> ' +
            templates.author(item.author[0]) +
          '</span>' +
          '<span><i class="material-icons">access_time</i> ' +
            formatDate(item.updated.$t) +
          '</span>' +
          '<span><i class="material-icons">link</i> <a href="' +
            canonical(item.link) +
          '">Original post</a></span>' +
        '</p>' +
        '<p>' +
          '<span><i class="material-icons">bookmark</i> ' +
            map(item.category, templates.tag).join(' ') +
          '</span>' +
        '</p>' +
      '</div>' +
      '<div class="mdl-card__supporting-text">' +
        content +
      '</div>' +
    '</div>'
  },

The full source is available at here, and the page can be viewed here.

Blog screenshot
Blog screenshot

Saturday, August 11, 2018

Modern Altmetric badges

Modern Altmetric badges

I recently found myself working with Altmetric badges again, and I realized how cumbersome it can be to work with scripts. The Altmetric badges can only be added by using their JavaScript library, while it would be a lot more user friendly to have a simple URL that embeds the badge, preferably even an image. I may be a bit spoiled by the badge ecosystem of the open source community, including Shields.io. There, badges are dynamically generated on the server side.

Badges in open source JavaScript projects
Badges in open source JavaScript projects

Unfortunately, Shields doesn’t support Altmetric. It does, however, support dynamic badges, and Altmetric does have an API. The endpoint is api.altmetric.com/v1/doi/ for DOI-based access (which is what we want in this case). So the parameters needed for the badge are:

  • Data type: JSON (the output format of the API)
  • label: Altmetric
  • url: https://api.altmetric.com/v1/doi/<DOI>
  • query: $.score
  • style: social

The logo would be https://www.altmetric.com/wp-content/themes/altmetric/favicon.ico, but I can’t get that to work. The resulting URL is

https://img.shields.io/badge/dynamic/json.svg?url=https://api.altmetric.com/v1/doi/DOI&label=Altmetric&query=$.score&style=social

Which looks like this: Altmetric badge

Note, however, that the use of the Altmetric API is limited:

Free, rate-limited API

  • No key required.
  • Includes research object metadata and metrics only.
  • Available only for one-time, limited term research projects.
  • Best for small projects.
  • Rate limited to 1 call per second.