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

1 comment: