Blogブログ

headlessCMS

strapi

2023.03.05

headless cmsのstrapi解説|10分でヘッドレスCMS完了!

今回は革命的なheadless cmsのstrapiを解説します。

こんにちは山本です。

先日nuxt3の解説を書きましたが、
ブログを書くのが面倒にならないうちに、
headless cmsのstrapiの解説もしておきます。

公式はここです。
クラスメソッドさんも解説しています。見てはいませんがたぶんこのブログよりも詳しく解説してます。

yarnが入ってる方はデスクトップから下記のコマンドを実行しましょう。

cd
cd Desktop
mkdir headless
yarn create strapi-app study-strapi --quickstart

すると、

これを実行してしばらく待ちましょう。
するとですね。。
恐るべきことにいきなりブラウザが立ち上がりこの画面が出てきます。

そして、会員登録をしましょう。

会員登録をするとこのような画面をなります。

あれ?開発関係ないの?GUIなの? って思いましたよね。

まぁもうちょっと見てみましょう。

これをクリックしましょう。

すると。。

ここに
Pluralはクエリで一覧で取得するときのパラメータ名でblogs
Singlar はクエリで単数のデータを取得するときのパラメータ名blog
Display nameは単に表示名なのでblogs
と入力しましょうか。

そしたら次はGUIに従って
なんとブログテーブルのカラムを追加できるんです。
試しに
title
body
image

を追加してみます。

同じような感じで、

tagsも作ってみましょう。
id
name
だけでいいです。

ではリレーションを作ってみます。

blogsに新しくカラムを追加するように

ここをクリックし、
下の方にあるrelationをクリックします。
すると。。

このようにリレーションをGUIで設定できるんですね。

ちょっとこれを初めてみた時は本当に驚きました
saveを押すのを忘れないでください。

そして、次に画面左のsettingをクリックしましょう。
roles public editの順にクリックしてください

すると

なんとなくわかりますよね。

これもうAPIの設定をGUIでやっちゃってるんですよ。

これを見た時もマジでビビりました。

Tagsも設定しちゃいましょう。

APIのURIはどこに?

それはここです。

そうです。
ttp://localhost:1337/api/blogs

ということです。

初めてこれをみた時はマジでビビりました。

ではアクセスしてみると。。

すごくないですか?

そしてこのようにブログを普通に書いていけるんですよね。

そうすると、

APIで取得できるというわけです。

とんでもなく便利な無料サービスがありましたね。

このようなサイトで解説されています。
どのサイトもこのブログよりは詳しいでしょう。

ここで、実際にNuxt3でheadless CMS strapiで情報を取得してポストをするコードサンプルを書いてみようと思います。

ちなみにnuxt3くっそ初心者向けに書いていきます。

では
127.0.0.1:3000/menus
のページでstrapi側で作ったメニューを取得して表示し、
そして、
選択したメニューを編集し
strapiのAPIを使って値を更新するというコードを書いてみようと思います。

今回APIのURIは

headless CMSのstrapi側のエンドポイントは
http://localhost:1337/api/menus にして、

ここでpagesの配下にmenusフォルダをつくり、
その中にindex.vueを書きましょう そうすると、
http://localhost:3000/menus でアクセスできるようになります。

では 最小単位のコードを書いてだんだんしっかり書いていく感じで説明しますので、 まずはこのコードを書いて実行してみましょう。

<script setup lang="ts">
interface Menu {
  id: number
  attributes: {
    title: string
    price: string
    description: string
  }
}

const id = ref<number>();
const title = ref<string>('');
const price = ref<string>('');
const description = ref<string>('');


//const menus = ref<Array<Menu>>([]);
const menus = ref<Menu[]>([]);


const getMenus = async () => {
  try {
    const response = await fetch('http://localhost:1337/api/menus');
    if (response.ok) {
      const { data } = await response.json();
      menus.value = data;
    }
  } catch (error) {
    console.error(error);
  }
}


</script>

<template>
  <div>
    <p v-for="(menu, index) in menus" :key="menu.id">
      <div>
         ここにメニューがきます。
        <button>編集</button>
        <hr>
      </div>
    </p>

    <button @click="getMenus">メニュー取得</button>
  </div>
</template>

これからやろうとしてることは、 メニュー取得ボタンをおしたらメニューが一覧で表示され、そのメニューの編集ボタンを押すと

フォームがでてきて、更新ボタンをおすとその情報が更新されるということをやろうとしています。

まずは上のコードを解説します。

まずは型!

tsでは型が重要になってきます。

コードのコメントに解説をしてきます。

//typeはちょっと前までinterfaceと書いていましたが
//このinterfaceはtypeと置き換えるとより新しい記述になります。
interface Menu {
  id: number
  attributes: {
    title: string
    price: string
    description: string
  }
}
//↑これはMenuという型を定義しています。
//Menuという型を付与されたオブジェクトは必ずidというキーとattributesというキーをもち、
//さらにattributesの中にはtitleやpriceやdescritpionというオブジェクトがないといけないよということを
//定義したものになります。
//で、「なんで、この構造を思いつくんや....attributesってどこからでてきたんや...神か?」と思うかもですが
//これは、実際にAPIを叩いて返り値を確認しましたwなので、
//どういう構造で返ってきたか知っている状態で型を定義しています。
//毎回そんなもんです。

//これはリアクティブな状態にするための定義でnumber型です。
//もし、型定義をしないなら
//const id =ref(0);とかでもいいです。0は初期値を与えただけ。
//今はtsで書いてるので型を書きました。
const id = ref<number>();

//これらも同様にstring型で初期値を空文字にして定義しました。
const title = ref<string>('');
const price = ref<string>('');
const description = ref<string>('');

//この配列の型定義は上のMenuの形をもった配列が、入るものがMenusだよという定義になります。
//ちなみに const menus = ref<Array<Menu>>([]); と書いても同じです。
//つまり <Array<Menu>>の型定義は<Menu[]>とも書けます。
const menus = ref<Menu[]>([]);
//ref<Menu[]>([])の([])は初期値に空配列いれてるよという意味です。

次に関数部分!(もはやheadless CMS strapiの解説ではなくて若干ただのnuxt3の書き方になってる)

const getMenus = async () => {
  try {
    const response = await fetch('http://localhost:1337/api/menus');
    if (response.ok) {
      const { data } = await response.json();
      menus.value = data;
    }
  } catch (error) {
    console.error(error);
  }
}

これは、アロー関数の記法で書いてます。 つまり function(){}が ()⇒{}になってる記法のこと。 非同期にするためには async function(){ await fetch()}と書く必要があるので、 アロー関数だと async () ⇒ { await fetch}になってることがわかります。 この書き方で非同期でデータを持ってきたりできます。

responseをみるとresponse.okとか書いてありますが、 何でokってのがあるとかそんなこと知ってるの?って感じですが、 それは、、、ルールとして知ってます。 今、みなさん覚えたと思うのでもう覚えたということで。

で、responseオブジェクトにはokという変数の中にboolianでたぶんtrueとか入ってるんですが、 それ以外にもオブジェクトなので、いろんな変数や関数や別のオブジェクトたちをもちます。 json()で、返り値をjson型で返してくれるようにしてます。

その結果をdataにいれているということです。 ここでconsole.log(data)をしてもわかりやすいかもしれません。

そして、このdataの値をmenus.valueの中に入れてますが、 nuxt3の仕様で、reactiveな変数に値をいれるときはthis.menusではなく、menus.valueで入れるようになっているというのは前回話した通りです。

これで、動的に取り出せるリアクティブなmenusの中に、 strapiのAPIのレスポンスが入りました。

ちなみにmenusの型は上で、

const menus = ref<Menu[]>([]);

と決めたばかりで、このMenuもその上で、

type Menu {
  id: number
  attributes: {
    title: string
    price: string
    description: string
  }
}

のように決めたので、 apiの結果がこの型の通りになってないとエラーになります。 が、今回はAPIの結果を先に調べてから書いてるのでエラーにはなりません。

では、
次に、headless cmsのstrapiから取ってきたこの情報を表示することをしてみます。

コードをこのように変更します。

<script setup lang="ts">
interface Menu {
  id: number
  attributes: {
    title: string
    price: string
    description: string
  }
}

const id = ref<number>();
const title = ref<string>('');
const price = ref<string>('');
const description = ref<string>('');

//const menus = ref<Array<Menu>>([]);
const menus = ref<Menu[]>([]);
const editShowFlg = ref<boolean>(false);

const getMenus = async () => {
  try {
    const response = await fetch('http://localhost:1337/api/menus');
    if (response.ok) {
      const { data } = await response.json();
      menus.value = data;
    }
  } catch (error) {
    console.error(error);
  }
}

</script>

<template>
  <div>
    <p v-for="(menu, index) in menus" :key="menu.id">
      <div>
        {{ menu.id }}:{{ menu.attributes.title }}({{menu.attributes.price }})「{{ menu.attributes.description }}」
        <button @click="showEditMenu(menu)">編集</button>
        <hr>
      </div>
    </p>

    <button @click="getMenus">メニュー取得</button>
  </div>
</template>

追記したところはv-forの中身が動的になったのと、 titleやprice 等の型も追加しました。

menuのそれぞれの項目をリアクティブな値に入れてることもわかると思います。

vue2のころにthis.menuなどをHTMLの中で使わなかったように、 vue3でもmenu.valueなどはHTMLの中では使う必要はありません。 menu.valueを書くのはいつでもjsやtsの中で書いていきます。

あとは型をみるとobjectの階層がわかるので

{{menu.attributes.title}}をみても「そういう階層で返ってきてるからね〜」くらいでわかると思います。

編集ボタンを押したら情報をフォームに入れるコードを書いていきます。

編集ボタンを押すと初めは表示されていなかったフォームがでてきてそこに値が入ったフォームがでてくるようにしてみます。 formが含まれるHTMLを書いて、v-showをはじめはfalseにしておいて、 編集が押されたらtrueにするようなコードを書いていきます。

<script setup lang="ts">
interface Menu {
  id: number
  attributes: {
    title: string
    price: string
    description: string
  }
}

const id = ref<number>();
const title = ref<string>('');
const price = ref<string>('');
const description = ref<string>('');
const editingMenu = ref<Menu | null>(null);

//const menus = ref<Array<Menu>>([]);
const menus = ref<Menu[]>([]);
const editShowFlg = ref<boolean>(false);

const getMenus = async () => {
  try {
    const response = await fetch('http://localhost:1337/api/menus');
    if (response.ok) {
      const { data } = await response.json();
      menus.value = data;
    }
  } catch (error) {
    console.error(error);
  }
}

const showEditMenu = (menu: Menu) => {
  editingMenu.value = menu;
  id.value = menu.id;
  title.value = menu.attributes.title;
  price.value = menu.attributes.price;
  description.value = menu.attributes.description;
  editShowFlg.value = true;
}

</script>

<template>
  <div>
    <p v-for="(menu, index) in menus" :key="menu.id">
      <div>
        {{ menu.id }}:{{ menu.attributes.title }}({{menu.attributes.price }})「{{ menu.attributes.description }}」
        <button @click="showEditMenu(menu)">編集</button>
        <hr>
      </div>
    </p>

    <div v-show="editShowFlg">
      <input type="text" v-model="title">
      <input type="text" v-model="price">
      <input type="text" v-model="description">
      <button @click="updateMenu()">更新</button>
    </div>

    <button @click="getMenus">メニュー取得</button>
  </div>
</template>

では、このコードの解説を書いていきます。

const editShowFlg = ref<boolean>(false);

はフォームを含むHTMLでv-showではじめはfalseで表示するためのフラグです、 true / falseの型であるbooleanで定義しています。

初めはfalseなので、

    <div v-show="editShowFlg">
      <input type="text" v-model="title">
      <input type="text" v-model="price">
      <input type="text" v-model="description">
      <button @click="updateMenu()">更新</button>
    </div>

このeditShowFlgがfalseで初めは表示されないけれど、

<button @click=”showEditMenu(menu)”>編集</button>

を押したときに, このshowEditMenu関数にそれぞれのmenuを引数で渡していて、 それを受け取って、

const showEditMenu = (menu: Menu) => {
  editingMenu.value = menu;
  id.value = menu.id;
  title.value = menu.attributes.title;
  price.value = menu.attributes.price;
  description.value = menu.attributes.description;
  editShowFlg.value = true;
}

これが実行されますが、 引数のmenuの横の:Menuは、この引数は上で定義した型じゃないといけませんよという表記になります。そして渡されたmenuから menu.id やmenu.attributes.titleなどを リアクティブな値として上で定義したオブジェクト(例えばtitle.value)の中に入れて行っています。

そしてeditShowFlg.valueもtrueにしたので、上のHTMLが表示されるというロジックです。

では、HTML部分をもう一度見ると、、

    <div v-show="editShowFlg">
      <input type="text" v-model="title">
      <input type="text" v-model="price">
      <input type="text" v-model="description">
      <button @click="updateMenu()">更新</button>
    </div>

ここで、v-modelが登場していて、 titleやprice,descriptionが書かれています。 v-modelで書かれているということは リアクティブでないといけないので、 そういう意味で、

const id = ref<number>();
const title = ref<string>('');
const price = ref<string>('');
const description = ref<string>('');
const editingMenu = ref<Menu | null>(null);

//const menus = ref<Array<Menu>>([]);
const menus = ref<Menu[]>([]);
const editShowFlg = ref<boolean>(false);

のような個別の定義を書いていったということになります。

型を見ると、 「この型は値がないこと(nullなこと)もあるよ」という表示として

const editingMenu = ref<Menu | null>(null); という表記を記載しています。

今回のコードでは、 このようにeachで回ってきたそれぞれのmenuの値たちを リアクティブなオブジェクトにいれてv-modelでvalueとしてリアルタイムに持たせることをしていることになります。

そしてそのリアクティブなオブジェクトにいれた値たちを最終的にAPIに飛ばすのですが

<button @click=”updateMenu()”>更新</button>

この更新ボタンを押して飛ばすことになります。

では、最後に

<button @click=”updateMenu()”>更新</button>

を押した後にv-modelで保有してるデータをstrapiのAPIに飛ばすコードを追記し、 完成としましょう。

<script setup lang="ts">
interface Menu {
  id: number
  attributes: {
    title: string
    price: string
    description: string
  }
}

const id = ref<number>();
const title = ref<string>('');
const price = ref<string>('');
const description = ref<string>('');
const editingMenu = ref<Menu | null>(null);

//const menus = ref<Array<Menu>>([]);
const menus = ref<Menu[]>([]);
const editShowFlg = ref<boolean>(false);

const getMenus = async () => {
  try {
    const response = await fetch('http://localhost:1337/api/menus');
    if (response.ok) {
      const { data } = await response.json();
      menus.value = data;
    }
  } catch (error) {
    console.error(error);
  }
}

const showEditMenu = (menu: Menu) => {
  editingMenu.value = menu;
  id.value = menu.id;
  title.value = menu.attributes.title;
  price.value = menu.attributes.price;
  description.value = menu.attributes.description;
  editShowFlg.value = true;
}

const updateMenu = async (menu: Menu) => {
  try {
    const response = await fetch(`http://localhost:1337/api/menus/${id.value}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        data: {
            title: title.value,
            price: price.value,
            description: description.value,
        },
      }),
    })
    if (response.ok) {
      console.log('Menu updated successfully!')
      editShowFlg.value = false;
      console.log(response)
    } else {
      console.error('Failed to update menu.')
    }
  } catch (error) {
    console.error(error)
  }
}
</script>

<template>
  <div>
    <p v-for="(menu, index) in menus" :key="menu.id">
      <div>
        {{ menu.id }}:{{ menu.attributes.title }}({{menu.attributes.price }})「{{ menu.attributes.description }}」
        <button @click="showEditMenu(menu)">編集</button>
        <hr>
      </div>
    </p>

    <div v-show="editShowFlg">
      <input type="text" v-model="title">
      <input type="text" v-model="price">
      <input type="text" v-model="description">
      <button @click="updateMenu()">更新</button>
    </div>

    <button @click="getMenus">メニュー取得</button>
  </div>
</template>

です。

引数にMenu型のmenuを渡していて、

try {} catch {}でAPIを試行しています。 async awaitなので非同期で実行されています。

APIの送信先は

http://localhost:1337/api/menus/${id.value} ですが、 これはstrapiのAPIのエンドポイント(送信するためのURL)を見ると

こう書いてあるので、 idをURLの乗せて更新すれば良いことがわかります。

ちなみに更新はPUTというので、

 method: 'PUT',

と書かれています。

よーくコードを見ると

fetch関数の第一引数はエンドポイント、 第二引数はmethod header body だけです。

methodはPUTのような操作を明示的に渡すことになっています。

そしてHTTP通信では、 人間の目に見える(というか感覚的にこれを送ってるってわかる)bodyと 人間の目に見えない(HTTPの仕様上伝えないといけない)headという情報を

同時に渡さないと通信できません。

これはAPIに関わらずネットの通信全てがそうです。

なので、headとbodyの情報を送っています

      headers: {
        'Content-Type': 'application/json',
      },

「bodyで送る情報の内容なんだけど、jsonです。」ってことを言ってるだけです。

次に重要なbody部分

      body: JSON.stringify({
        data: {
            title: title.value,
            price: price.value,
            description: description.value
        },
      }),

JSON.stringifyは、配列やオブジェクトを通信で読み取ってくれるJSON型に変換してくれるという関数なので、それを使って data: {}の値を送ってるということになります。

「いやなんで、dataとかがそもそも存在するって分かるの?」

「attributesの中にidあってもいいのに、なんでattributesとidが同階層なの?なんで事前にわかるの?」

って思ったと思います。 (俺はもしこれははじめてみたら絶対にそう思いました。)

これは、strapiのマニュアルにdataに囲んでidとattribute同階層で送ってくれと 書いてあったからです。 だから気づきました。 ちなみに
初心者「それ気づかなかったら永遠にできないじゃん」
って思いますよね

その通りです。

ちなみに私はマニュアル記載の形式を見間違えて正しい形式に気づかずに
40分くらい無駄な時間を過ごしました。
ここのStrapi Documentをみてしっかりと理解しましょう。

これで、送信をして、 if (response.ok) { console.log(‘Menu updated successfully!’) editShowFlg.value = false; console.log(response) } else { console.error(‘Failed to update menu.’) }

このように、もし、送信が成功したらokが返ってくるので

試してみましょう。

これで、
headless cms strapiを使ってnuxt3で値を取得して、
編集してpostして変更するという最小単位のことができました。