Movable Type 7 のコンテンツタイプ+Data API Ver4 で、レシピサイトのウェブアプリを開発してみる

[前回の記事](https://qiita.com/TakeshiNickOsanai/items/a3bdabc385f3c72f6a9f)の続きです。　


前回作成したレシピサイトをベースに、Movable Type 7 に実装予定の「Data API Ver.4」を利用して、コンテンツタイプのデータと連携したアプリを作ってみます。

※ 2018年4月4日時点、MT7ベータ3を前提に作成・記述しています。仕様の変更に伴い、動作が異なる場合があります。変更がありましたら随時修正します。

## Data API Ver 4 のエンドポイント

Movable Type 7 の Data API のエンドポイントは、Ver3 までと変わらず

```
http://your-mt/mt-data-api.cgi/v4/sites/サイトID/
```

となります。

* Data API のCGIファイル
* APIのバージョン
* sites
* サイトID

の順番に指定してきます。

コンテンツタイプのデータを操作する場合、キャメルタイプで「contentTypes」と入力し、合わせてコンテンツタイプのIDを指定します。

例として

* サイトIDが1
* コンテンツタイプのIDが2
* コンテンツタイプに登録されているコンテンツのデータ一覧を取得したい

この場合、エンドポイントは以下のようになります。

```
http://your-mt/mt-data-api.cgi/v4/sites/1/contentTypes/2/data
```

* サイトIDが3
* コンテンツタイプのIDが4
* コンテンツタイプに登録されているコンテンツデータのIDが5
* 5の詳細データを取得したい

この場合、エンドポイントは以下のようになります。

```
http://your-mt/mt-data-api.cgi/v4/sites/3/contentTypes/4/data/5
```

Data API経由でデータの操作を行う場合、環境変数でCORS(Cross-Origin Resource Sharing) の設定を適切に行う必要があります。

環境変数は以下のようなものがあります。

* [DataAPICORSAllowOrigin](https://www.movabletype.jp/documentation/appendices/config-directives/dataapicorsalloworigin.html)
* [DataAPICORSAllowMethods](https://www.movabletype.jp/documentation/appendices/config-directives/dataapicorsallowmethods.html)
* [DataAPICORSAllowHeaders](https://www.movabletype.jp/documentation/appendices/config-directives/dataapicorsallowheaders.html)
* [DataAPICORSExposeHeaders](https://www.movabletype.jp/documentation/appendices/config-directives/dataapicorsexposeheaders.html)



## Data API Ver4 でコンテンツタイプのデータを取得する際の注意

Data APIから取得できるデータの順番は、コンテンツタイプの設定画面で登録した順番に準じます。

今回の例では、コンテンツタイプを

1. レシピ名
2. 写真
3. 材料
4. 作り方


の順番に登録しました。この場合、Data API Ver4で取得できるデータサンプルは以下のようになります。

サンプルJSON

```json
{
    "author": {
        "displayName": "tosanai",
        "userpicUrl": null
    },
    "basename": "d02298c174f3b63275de13c6b071b8753eff6c74",
    "blog": {
        "id": "1"
    },
    "createdDate": "2018-04-04T14:59:51+09:00",
    "data": [
        {
            "data": "味たまご",
            "id": "8",
            "label": "レシピ名",
            "type": "single_line_text"
        },
        {
            "data": [
                "3"
            ],
            "id": "9",
            "label": "写真",
            "type": "asset_image"
        },
        {
            "data": [
                "たまご",
                "めんつゆ",
                "みりん"
            ],
            "id": "10",
            "label": "材料",
            "type": "list"
        },
        {
            "data": "めんつゆとみりん、水の比率が 1:1:2 で混ぜ合わせ、鍋で一度沸騰させ、冷まします。\r\n鍋でお湯を沸かします。\r\n常温にした卵をお湯に入れ中火で6分半煮ます。\r\n卵を冷水などで冷やし、卵の殻をむきます。\r\n冷ました付け汁に卵を入れ、冷蔵庫で1日以上置きます。",
            "id": "11",
            "label": "作り方",
            "type": "multi_line_text"
        }
    ],
    "date": "2018-04-04T14:58:31+09:00",
    "id": 3,
    "label": "味たまご",
    "modifiedDate": "2018-04-04T14:59:51+09:00",
    "permalink": "http://example.com/d02298c174f3b63275de13c6b071b8753eff6c74.html",
    "status": "Publish",
    "updatable": false
}
```


コンテンツタイプの設定画面で、以下のように登録順序を変えた場合

1. 写真
2. 材料
3. 作り方
4. レシピ名

取得できるJSONデータは以下のように変更になります。

サンプルJSON

```json

{
    "author": {
        "displayName": "tosanai",
        "userpicUrl": null
    },
    "basename": "d02298c174f3b63275de13c6b071b8753eff6c74",
    "blog": {
        "id": "1"
    },
    "createdDate": "2018-04-04T14:59:51+09:00",
    "data": [
        {
            "data": [
                "3"
            ],
            "id": "9",
            "label": "写真",
            "type": "asset_image"
        },
        {
            "data": [
                "たまご",
                "めんつゆ",
                "みりん"
            ],
            "id": "10",
            "label": "材料",
            "type": "list"
        },
        {
            "data": "めんつゆとみりん、水の比率が 1:1:2 で混ぜ合わせ、鍋で一度沸騰させ、冷まします。\r\n鍋でお湯を沸かします。\r\n常温にした卵をお湯に入れ中火で6分半煮ます。\r\n卵を冷水などで冷やし、卵の殻をむきます。\r\n冷ました付け汁に卵を入れ、冷蔵庫で1日以上置きます。",
            "id": "11",
            "label": "作り方",
            "type": "multi_line_text"
        },
        {
            "data": "味たまご",
            "id": "8",
            "label": "レシピ名",
            "type": "single_line_text"
        }
    ],
    "date": "2018-04-04T14:58:31+09:00",
    "id": 3,
    "label": "名前がありません。",
    "modifiedDate": "2018-04-04T14:59:51+09:00",
    "permalink": "http://example.com/d02298c174f3b63275de13c6b071b8753eff6c74.html",
    "status": "Publish",
    "updatable": false
}

```

data オブジェクトの順番が入れ替わっていることが分かります。

Ver 4 では、コンテンツタイプのエンドポイントからは、画像などのアイテムの詳細情報が取得できません。このため、画像の詳細データを取得する場合

* コンテンツタイプのJSONからアイテムのIDを取得
* アイテムIDを元に、再度Data APIに問い合わせて、詳細情報を取得

という手続きを踏む必要があります。

例として、下記のようなコンテンツタイプデータだった場合

```json

 "data": [
        {
            "data": "味たまご",
            "id": "8",
            "label": "レシピ名",
            "type": "single_line_text"
        },
        {
            "data": [
                "3"
            ],
            "id": "9",
            "label": "写真",
            "type": "asset_image"
        },
        {
            "data": [
                "たまご",
                "めんつゆ",
                "みりん"
            ],
            "id": "10",
            "label": "材料",
            "type": "list"
        },
        {
            "data": "めんつゆとみりん、水の比率が 1:1:2 で混ぜ合わせ、鍋で一度沸騰させ、冷まします。\r\n鍋でお湯を沸かします。\r\n常温にした卵をお湯に入れ中火で6分半煮ます。\r\n卵を冷水などで冷やし、卵の殻をむきます。\r\n冷ました付け汁に卵を入れ、冷蔵庫で1日以上置きます。",
            "id": "11",
            "label": "作り方",
            "type": "multi_line_text"
        }
    ],

```

写真のアイテムIDは「3」です。このため、アイテムのデータを取得するエンドポイントに対して、アイテムIDの番号[3]を指定して必要なデータを取得する必要があります。

## レシピ一覧のページをData API Ver 4 で生成する

以上を踏まえて、Data API Ver4を利用してレシピコンテンツのアプリを作ってみます。


### アプリの仕様

アプリの仕様は以下とします。


* [前回の記事](https://qiita.com/TakeshiNickOsanai/items/a3bdabc385f3c72f6a9f)で作成したレシピサイトのコンテンツタイプを利用する

![apli_gaiyou.png](https://qiita-image-store.s3.amazonaws.com/0/83767/31259ba3-0be4-afd0-d2c8-0c4676bc1879.png)


* トップページ、詳細ページの2枚のhtmlで構成
* Data API Ver4 からJSONデータを取得して、JavaScript でレンダリングする
* トップページのURL
 * http://example.com/index.html
* 詳細ページのURL
 * http://example.com/detail.html?id=2
 * IDをパラメータに含める
 * ID番号を元に、Data APIへ問い合わせを行う
* [前回の記事](https://qiita.com/TakeshiNickOsanai/items/a3bdabc385f3c72f6a9f) で作成したウェブサイトと同じデザインとする

以下

* レシピの一覧データの表示
* レシピの詳細データの表示


の順に記述します。

最初は、レシピの一覧データの操作について記述します。

### 一覧画面の開発

最初は、前回のトップページに相当する一覧ページを作ります。
コンテンツタイプの一覧データのJSONを取得し、[axios](https://github.com/axios/axios)で通信を行い、[Vue.js](https://jp.vuejs.org/index.html) でレンダリングします。

* コンテンツタイプの一覧データを Data API へ問い合わせ
* 登録されたデータの数だけ for 文でループ処理を行い、以下の3つを含むオブジェクトを生成
 * コンテンツタイプに登録されたデータのID
 * 写真のアイテムID
 * レシピ名
* 生成したオブジェクトをPromise チェーンで次の関数へ引き渡す
* オブジェクトを元に再度ループ。写真のサムネイルデータを取得。以下のデータをVue.js のインスタンスへ格納する。
 * データのIDを含むリンク文字列
 * サムネイルのURL
 * レシピ名

以下、コードです。

```html

<!DOCTYPE html>
<html lang="ja" xml:lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,user-scalable=no,maximum-scale=1" />
<title>レシピブック</title>
  <script src="https://unpkg.com/vue"></script>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <link rel="stylesheet" href="https://your-site/style.css">
</head>
<body id="topPage" class="headerBg">

<div class="wrapper">

  <div id="TopHeader">
    <h1>レシピブック</h1>
  </div>


  <h2>新着一覧</h2>

  <div id="entries" class="listWrapper">

    <div class="list" v-for="item in json">
      <div class="thum">
        <a v-bind:href="item.link">
          <img :src="item.thumbnailURL">
          <div class="listDescription"><span class="listDescriptionTxt">{{ item.name }}</span></div>
        </a>
      </div>
    </div>

  </div>
</div>


  <script>

  "use strict";
  
  const list = axios('https://your-mt/mt-data-api.cgi/v4/sites/サイトID/contentTypes/コンテンツタイプID/data');

  const vm = new Vue({
    el: '#entries',
    data: {
      json: {},
    },
  });

  Promise.resolve(list).then(function(response) {

    let json = {};

    for (let i in response.data.items) {

      let ContentData = {};

      ContentData.id      = Number(response.data.items[i].id);
      ContentData.ImageID = Number(response.data.items[i].data[1].data);
      ContentData.name    = response.data.items[i].data[0].data;

      json[i] = ContentData;

    }

    return json;

  }).then(json => {

    for (let i in json) {

      axios('https://your-mt/mt-data-api.cgi/v4/sites/サイトID/assets/' + json[i].ImageID + "/thumbnail?width=220&square=true")
      
      .then(res => {

        let result = {};

        result.link         = "./detail.html?id=" + json[i].id;
        result.thumbnailURL = res.data.url;
        result.name         = json[i].name;

        Vue.set(vm.json, i, result);

      });
    }
  });

  </script>

</body>
</html>

```


### レシピ詳細画面の開発

次に、レシピの詳細画面を開発します。
トップページからリンクされたページには、パラメータとして「id=**」という文字列が付与されています。このIDを元に、Data API Ver.4 へ問い合わせを行います。

コンテンツタイプの詳細データJSONを取得し、Vue.js でレンダリングします。
以下のような考え方で作ってみました。

* コンテンツタイプのIDを元に Data API へ問い合わせ
* レシピ名、レシピの写真、材料、作り方のJSONデータを取得
* 写真のIDを元に、Data API へ再問い合わせ
* 最初のJSONデータと、写真のIDをもとに取得したJSONデータを合わせて、オブジェクトを生成
* オブジェクトをVue.js のインスタンスへ格納、レンダリングを実行する


以下、コードです。

```html

<!DOCTYPE html>
<html lang="ja" xml:lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,user-scalable=no,maximum-scale=1" />
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<link rel="stylesheet" href="https://your-site/style.css">
<title>レシピブック</title>

</head>
<body id="entryPage" class="headerBg">

  <div id="vm" class="wrapper">
    <div class="detailWrapper">
      <h1 class="mainTitle" v-if="label">{{label}}</h1>
      <div class="detail">

        <div class="mainImg" v-if="imgsrc"><img :src=imgsrc ></div>

        <div class="detailInner">
          <h2>材料</h2>

          <div v-if="ingredients" v-for="item in ingredients">
            <p class="mb5">{{item}}</p>
          </div>

          <h2>作り方</h2>

          <div v-if="recipe">
            {{recipe}}
          </div>

        </div>
      </div>
      <div class="backBtn">
        <a href="./index.html">トップへ戻る</a>
      </div>
    </div>
  </div>

  <script>

  "use strict";

  // コンテツタイプのデータIDを取得するため、GETで渡された文字列を解析する関数 [getUrlParams] を定義

  function getUrlParams() {
    const query = window.location.search.substring(1);
    const vars = query.split('&');
    let args = {};
    for (let i = 0; i < vars.length; i++) {
      let tmp = vars[i].split('=');
      args[tmp[0]] = tmp[1];
    }
    return args;

  }

  // 関数 [getUrlParams] を使って、GETの引数を変数 params に格納

  const params = getUrlParams();

  // Vue.js のインスタンスを生成

  const vm = new Vue({
    el: '#vm',
    data: {
      label       : '',
      ingredients : '',
      recipe      : '',
      imgsrc      : '',
    },
    created: function() {
      this.getData();
    },

    methods: {

      getData: function() {

        axios('https://your-mt/mt-data-api.cgi/v4/sites/サイトID/contentTypes/コンテンツタイプID/data/' + params.id).then(res => {

          const json     = res.data;

          vm.label       = json.data[0].data;
          vm.ingredients = json.data[2].data;
          vm.recipe      = json.data[3].data;

          return json;

        }).then(json => {

          const AssetEndpoint = 'https://your-mt/mt-data-api.cgi/v4/sites/サイトID/assets/' + json.data[1].data;
          axios(AssetEndpoint).

          then (AssetResponse => {
            this.imgsrc = AssetResponse.data.url;
          });

        }).catch(function(error) {
          console.log(error);
        });
      }
    },
  })
  .catch(function(error) {

     console.log(error);

   });

  </script>

</body>
</html>
```

## jQuery 版

Vue.js で書いた内容を、jQueryでも書いてみました。

一覧画面

```html
<!DOCTYPE html>
<html lang="ja" xml:lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,user-scalable=no,maximum-scale=1" />
  <title>レシピブック</title>
  <link rel="stylesheet" href="https://your-site/style.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body id="topPage" class="headerBg">

<div class="wrapper">

  <div id="TopHeader">
    <h1>レシピブック</h1>
  </div>

  <h2>新着一覧</h2>

  <div id="entries" class="listWrapper">
  </div>

</div>


  <script>

  "use strict";
  
  const list = $.get('https://your-mt/mt-data-api.cgi/v4/sites/サイトID/contentTypes/コンテンツタイプID/data');

  Promise.resolve(list).then(response => {

    let json = {};

    for (let i in response.items) {

      let ContentData = {};

      ContentData.id = Number(response.items[i].id);
      ContentData.ImageID = Number(response.items[i].data[1].data);
      ContentData.name = response.items[i].data[0].data;

      json[i] = ContentData;

    }

    return json;

  }).then(json => {

    for (let i in json) {

      $.get('https://your-mt/mt-data-api.cgi/v4/sites/サイトID/assets/' + json[i].ImageID + "/thumbnail?width=220&square=true")
      
      .then(res => {

        let result = '';
        
        result += '<div class="list"><div class="thum">';
        result += '<a href="./detail.html?id=' + json[i].id + '">';
        result += '<img src="' + res.url + '" />';
        result += '<div class="listDescription"><span class="listDescriptionTxt">' + json[i].name + '</span></div>';
        result += '</a>';
        result += '</div></div>';

        $("#entries").append(result);

      });
    }
  })

  .catch(function(error) {

     console.log(error);

   });

  </script>

</body>
</html>

```


詳細画面

```html

<!DOCTYPE html>
<html lang="ja" xml:lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,user-scalable=no,maximum-scale=1" />

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<title>レシピブック</title>

<link rel="stylesheet" href="https://your-site/style.css">

</head>
<body id="entryPage" class="headerBg">

  <div id="detail" class="wrapper">
    <div class="detailWrapper">
      <h1 class="mainTitle"></h1>
      <div class="detail">

        <div class="mainImg"></div>

        <div class="detailInner">
          <h2>材料</h2>

          <div id="ingList">

          </div>

          <h2>作り方</h2>

          <div id="recipe">

          </div>

        </div>
      </div>
      <div class="backBtn">
        <a href="./index.html">トップへ戻る</a>
      </div>
    </div>
  </div>

  <script>

  "use strict";

  // コンテツタイプのデータIDを取得するため、GETで渡された文字列を解析する関数 [getUrlParams] を定義

  function getUrlParams() {
    const query = window.location.search.substring(1);
    const vars = query.split('&');
    let args = {};
    for (let i = 0; i < vars.length; i++) {
      let tmp = vars[i].split('=');
      args[tmp[0]] = tmp[1];
    }
    return args;
  }

  // 関数 [getUrlParams] を使って、GETの引数を変数 params に格納

  const params = getUrlParams();

  $.get('https://your-mt/mt-data-api.cgi/v4/sites/サイトID/contentTypes/コンテンツタイプID/data/' + params.id)
   .then(res => {

    const label = res.data[0].data;
    const ingredients = res.data[2].data;
    const recipe = res.data[3].data;

    $(".mainTitle").text(label);

    for (let i in ingredients) {
      $("#ingList").append('<p class="mb5">' + ingredients[i] + '</p>');
    }

    $("#recipe").text(recipe);

    return res;

  }).then(res => {

    const AssetEndpoint = 'https://your-mt/mt-data-api.cgi/v4/sites/サイトID/assets/' + res.data[1].data;

    $.get(AssetEndpoint)
    .then(AssetResponse => {

      $(".mainImg").html('<img src="' + AssetResponse.url + '" />');

    });

  })

  .catch(function(error) {

    console.log(error);

  });

  </script>

</body>
</html>

```

以上、Data API Ver 4を利用した、コンテンツタイプのサンプルを考えてみました。

## 免責

ここで記述しているコードはサンプルコードとなり、サポートは致しかねる旨、ご理解とご了承ください。