Help us understand the problem. What is going on with this article?

esaで新着と注目記事をトップページに表示する

CleanShot 2020-12-22 at 20.44.37.jpg

この記事は、Goodpatch Advent Calendar 2020 18日目の記事です。

はじめに

グッドパッチでは社内ポータルとしてesaを活用しています。
社員の増加に比例して、esaの記事も増えます。そうすると新着記事を追うのが難しくなります。

esapadがあるじゃない

そこでhogelogさんが作ってくださったesapadです。
esapadを使ってトップページのReadmeに新着記事を出すことができます。

gasで書き直して改造しました

hogelogさんのesapadはRubyですが、サーバーを立てずに済むgasで書き直して、自分好みに改造しました。名付けて『esapad_custom』です。

実はかなり前からesapad_customを導入していました。
改めて、これが他のところでも使えると便利かもしれないと思い公開することにしました。

esapad_customとは

CleanShot 2020-12-22 at 20.44.37.jpg

画像にあるように、esaのトップのReadmeで新着と注目記事を表示しています。
10分毎に更新されるようになっており、会社の新鮮なナレッジを回遊しやすいようにしています。
おそらく社内のほとんどの人がここから記事を回遊しています。

工夫したところ

最新記事と人気記事を一覧で表示

実はesaでは注目の記事順で一覧表示ができます。

CleanShot 2020-12-22 at 20.50.48.jpg

総合的な記事のスコア

esaのAPIでは上記の説明があり、コメントやスターの数、投稿日などからランク付けされているようです。

このソートを利用して一定期間の中で盛り上がっている記事を一覧で表示するようにしました。
その結果、記事へのアクセスが増えて、スターが付く記事も増えました。
スターが増えると投稿者にとっても次の記事を書くモチベーションになるため記事数も増えて良いサイクルが回ります。

表示方法もリストからテーブルにして2列表示にしました。
モザイクがかかっているので画像からわからないのですが…可読性を重視して、カテゴリ入れずにタイトルだけとしています。

新着記事ではwipを非表示

wipの記事がトップに表示されたくないという声が社内からあがり、投稿のしやすさを鑑みて新着記事ではwip記事を出さないようにしました。
人気記事ではwipが表示されるのですがこちらはタイトルの接頭辞に [wip]と表示されるようにしています。

スター、ウォッチ、コメント数を一覧に表示

CleanShot 2020-12-22 at 21.06.27.jpg

より注目されて読みたくなるようにスター、ウォッチ、コメント数を出すようにしました。
あくまで私の所感ですがスター数が多い記事は真面目な記事、コメント数が多い記事は面白い記事であることが多いです。

利用手順

1. esaのアクセストークンを取得

dev/esa/api/v1 #noexpand - docs.esa.ioをご一読ください

2. 以下の文字列をesapad_customを挿入する記事に記入する

<table>
    <thead>
    <tr>
        <th><b> :+1:   NEW POSTS </b></th>
        <th><b> :star2: HOT TOPICS</b></th>
    </tr>
    </thead>
<!-- RECENTLY-UPDATED-ALTERNATE-POSTS-START -->
<!-- RECENTLY-UPDATED-ALTERNATE-POSTS-END -->
</tbody>
</table>

3. 次のソースコードをgasに貼って実行

updatePagesList() を実行してください。esaの指定ページにesapad_customが表示されたら成功です。


var ACCESS_TOKEN = '';//esaのWebHookを記入
var ESA_TEAM = 'yourteam';//yourteam.esa.io ならyourteamを入力
var DEFAULT_PER_PAGE_ID = 1;//挿入先のページIDを入力
var API_URL = 'https://api.esa.io/v1/teams/';


function updatePagesList(){
  var newUpdateMd = generateUpdatedMd("new");
  var hotUpdateMd = generateUpdatedMd("hot");
  var margedUpdateMd = margeMd(newUpdateMd,hotUpdateMd);

  var targetPage = fetchTargetPage(DEFAULT_PER_PAGE_ID);
  var targetPageMd = targetPage["body_md"];

  targetPageMd = replacePagesListMd(targetPageMd,"alternate",margedUpdateMd);

  Logger.log(targetPageMd);
  if (targetPageMd != targetPage["body_md"]){

    updatePost(DEFAULT_PER_PAGE_ID, targetPageMd);
    Logger.log("Updated: "+ targetPageMd["url"]);
  }
}


function updatePost(postNumber, bodyMd){
  var stringifiedPayload = JSON.stringify({
    post: {
      name: 'Readme.md',
      body_md: bodyMd,
      tags: '',
      category: '',
      wip: false,
      message: '[skip notice]',
      updated_by: 'esa_bot'
    }
  })

 UrlFetchApp.fetch(
   API_URL+ESA_TEAM+'/posts/'+postNumber+'?access_token='+ACCESS_TOKEN,
   {
     method: 'PUT',
     contentType: 'application/json',
     payload: stringifiedPayload
   });
}

function generateUpdatedMd(kind) {
  var posts = fetchUpdatedPages(kind);

  var html = posts.map(function(post){
    var updatedAt = new Date(post["updated_at"]);
    var formatUpdatedDate = (updatedAt.getMonth()+1)+"/"+updatedAt.getDay()+ "&nbsp;" + updatedAt.getHours() + ":" + updatedAt.getMinutes();

    var html = "<td>"
      + "<a href=" + post["url"] + " style=\"font-size: 100%;\">"
        + "<img src=\"" + post["created_by"]["icon"] + "\"  width=\"20px\" height=\"20px\" />&nbsp;"+ checkWip(post["wip"]) +post["name"]
      + "</a>"
      + "<div class=\"recently-updated-posts-metadata\" style=\"font-size: 100%;\">"
        + "<span class=\"post-list__reaction\">"
          + "<i class=\"viewer-action__icon fa fa-star\" style = \"font-size: 100%\" ></i>"+"<font color=\"#606060\">"+post["stargazers_count"]+"&ensp;</font>"
          + "<i class=\"viewer-action__icon fa fa-eye\" style = \"font-size: 100%\" ></i>"+"<font color=\"#606060\">"+post["watchers_count"]+"&ensp;</font>"
          + "<i class=\"viewer-action__icon fa fa-comments\" style = \"font-size: 100%\" ></i>"+"<font color=\"#606060\">"+post["comments_count"]+"&ensp;</font>"
        + "</span><font color=\"#606060\">by&nbsp;</font>"
      + "<a href=\"https://"+ESA_TEAM+".esa.io/users/"+post["created_by"]["screen_name"]+"\">"+post["created_by"]["screen_name"]+"</a>"
      + "</div>"
      + "</td>";
    switch (kind) {
      case "new":
        return "<tr>"+html;
        break;
      case "hot":
        return html+"</tr>"
        break;
      default:
    }
  })
  return html;
}

function fetchUpdatedPages(kind){
  var query;

  switch (kind) {
    case "hot":
      var today = new Date();
      var before1weekDate =  getOfBeforeAfterDays(today,-7);
      var year = before1weekDate.getFullYear().toString();
      var month = (before1weekDate.getMonth() + 1).toString();
      var day = before1weekDate.getDate().toString();
      query = "created%3A%3E"+year+"-"+month+"-"+day+"&sort=best_match&order=desc"; 

      break;
    case "new":
      var shipped = new Boolean(false);
      query = "wip%3A"+shipped+"&sort=created&order=desc";
      break;
  }
  var url = JSON.parse(UrlFetchApp.fetch(API_URL+ESA_TEAM+'/posts/'+'?access_token='+ACCESS_TOKEN+'&q='+ query).getContentText());
  return url["posts"];
}


function fetchTargetPage(targetPageId){
  return JSON.parse(UrlFetchApp.fetch(API_URL+ESA_TEAM+'/posts/'+targetPageId+'?access_token='+ACCESS_TOKEN).getContentText());
}

function margeMd(newUpdateMd,hotUpdateMd){
  var alternatedNewAndHotList = [];
  for (var i = 0; i < 12; i++) {//12件に絞る
    alternatedNewAndHotList.push(newUpdateMd[i]);
    alternatedNewAndHotList.push(hotUpdateMd[i]);
  }
  var alternatedNewAndHotMd = alternatedNewAndHotList.join('').toString();
  return alternatedNewAndHotMd;
}


function replacePagesListMd(originalMd, kind, updatedMd){
  var fetchRelpaceBody = "<!-- RECENTLY-UPDATED-"+kind.toUpperCase()+"-POSTS-START -->"+Parser.data(originalMd).from("<!-- RECENTLY-UPDATED-"+kind.toUpperCase()+"-POSTS-START -->").to("<!-- RECENTLY-UPDATED-"+kind.toUpperCase()+"-POSTS-END -->").build()+"<!-- RECENTLY-UPDATED-"+kind.toUpperCase()+"-POSTS-END -->";

  return originalMd.replace(fetchRelpaceBody,"<!-- RECENTLY-UPDATED-"+kind.toUpperCase()+"-POSTS-START -->"+"\n"+updatedMd+"\n"+"<!-- RECENTLY-UPDATED-"+kind.toUpperCase()+"-POSTS-END -->");
}


function checkWip(boolean){
  if (boolean) {
    return "[wip]&nbsp;";
  }else {
    return "";
  }
}

var getOfBeforeAfterDays = function(dateObj, number) {
    var result = false;
    if (dateObj && dateObj.getTime && number && String(number).match(/^-?[0-9]+$/)) {
        result = new Date(dateObj.getTime() + Number(number) * 24 * 60 * 60 * 1000);
    }
    return result;
};

4. gasのトリガーを設定する

esaのAPIはユーザ毎に15分間に75リクエストまで受け付けてくれますので、叩いてるAPIの数などを目安に間隔を調整してください。

CleanShot 2020-12-22 at 20.36.02.jpg

むすび

グッドパッチではesapad_customを導入してから記事の投稿数も閲覧頻度もかなり増えました。

数年前に書いたこのesapad_customが初めてgasを書いたのに等しく、その後の実務に活かせる経験を積ませていただきました。
esapadを作ってくださったhogelogさん、それを世に広めてくれたtanukiti1987さんには感謝しかありません。この場を借りてお礼を伝えたいです。

hogelogさん、tanukiti1987さんありがとうございました!

goodpatch
Goodpatch(株式会社グッドパッチ)はUI/UXデザインを強みにビジネスモデルやブランド、組織をデザインし、デザインの価値向上を目指すグローバルデザインカンパニーです。2020年6月30日、デザイン会社初の東証マザーズ上場。サービスやプロダクトの企画設計から関わりコンセプトメイキング、UX設計、プロトタイピング、UIデザイン、実装までワンストップで提供しています。
https://goodpatch.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away