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

![CleanShot 2020-12-22 at 20.44.37.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66020/a3322867-cb1a-bb69-585d-15e2ea076796.jpeg)

この記事は、[Goodpatch Advent Calendar 2020](https://qiita.com/advent-calendar/2020/goodpatch) 18日目の記事です。




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


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

- [hogelog/esapad](https://github.com/hogelog/esapad)
- [esa のトップページを活発にするなら esapad があるじゃない - おいしい健康　開発者ブログ](https://oishi-kenko.hatenablog.com/entry/2017/08/18/205417)

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

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


## esapad_customとは
![CleanShot 2020-12-22 at 20.44.37.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66020/a3322867-cb1a-bb69-585d-15e2ea076796.jpeg)

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


# 工夫したところ
## 最新記事と人気記事を一覧で表示
実はesaでは[注目の記事順で一覧表示ができます。](https://docs.esa.io/posts?q=sort%3A+best_match-desc+)

![CleanShot 2020-12-22 at 20.50.48.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66020/ec9e36cd-2b42-9662-f4b4-0866ba2d8b8c.jpeg)


>総合的な記事のスコア

[esaのAPI](https://docs.esa.io/posts/102#URI%E3%82%AF%E3%82%A8%E3%83%AA%E6%96%87%E5%AD%97%E5%88%97-1)では上記の説明があり、コメントやスターの数、投稿日などからランク付けされているようです。


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

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


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

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

![CleanShot 2020-12-22 at 21.06.27.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66020/6616031a-7004-1ca1-d151-ce30a4f5c09e.jpeg)

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


# 利用手順

## 1. esaのアクセストークンを取得
[dev/esa/api/v1 #noexpand - docs.esa.io](https://docs.esa.io/posts/102)をご一読ください


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

```html
<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が表示されたら成功です。


```javascript


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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/66020/88c07b8c-302c-4556-8ec9-96f26abef4af.jpeg)


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

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

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