🎉

hacoCMS×SolidJsを触ってみた。

2023/10/08に公開

うえむーです。
今回はhacoCMS×SolidJsについてお話しをしていきたいと思います。
まずは、hacoCMSについて説明していきます。

hacoCMSとは?

hacoCMSとはシーサー株式会社が開発したmicroCMS・Newtと同じAPI型のヘッドレスCMSです。
コンテンツ管理のためサーバー管理は一切不要で、アカウント登録するだけでそのサービスがご利用できます。

ヘッドレスCMSとは

ヘッドレスCMSの「ヘッド」はビュー(表示する画面)を表すものであり、ヘッドレスなのでビューがないものです。
つまりビューがないCMSです。

hacoCMS(ヘッドレスCMS)の特徴

hacoCMS及びヘッドレスCMSの特徴には以下のような特徴があります。

表示速度が速い
マルチデバイスに対応できる(コンテンツをWeb、iOSアプリ、Androidアプリetc..)
サイトを部分的にCMS化できるまた、サーバー停止による影響が少ない
フロントエンドの自由度が高まる
サーバーレスなのでアカウント登録するだけでサービスが利用できる。
WordPress・MovableTypeのようにバージョン管理をする必要が
フロント側とデータ管理が別々であるためセキュリティが高い

hacoCMS(ヘッドレスCMS)は上記のようにいろんな特徴がありますが、
最大の特徴としては無料プランが充実しているところです。

API数が10個までで何記事も投稿できようです。
また、ビジネスプランでも税込3,190円と比較的安めです。

https://hacocms.com/

hacoCMSを登録する

hacoCMSを登録してみます。
(1)hacoCMSの公式サイトから新規登録を押下すると(2)アカウント登録のページに遷移するので
メールアドレス・パスワードを入力します。
メールアドレス・パスワードを入力すると(3)アカウント有効化メールのページに遷移され、アカウント有効化のメールが届くの有効化します。
アカウント登録後、(4)アカウント設定を行います。「アカウント名」と「アカウント識別子」を入力して、アカウント設定を完了させます。

hacoCMSにログインする

では早速ログインしましょう。hacoCMSの公式サイトからログインを押下すると以下のようにログイン画面に遷移され、ログイン・パスワードを入力します。

ログイン・パスワードを入力すると以下のような画面に遷移されます。

プロジェクト・APIを作成する

まずは、プロジェクト作成します。
プロジェクト作成画面を開いて、「プロジェクト名」と「サブドメイン」の登録を行います。

すると以下のようにプロジェクトが作成されます。

また、プロジェクトを管理しやすいように、ロゴ・説明文を追加することができます。
該当するプロジェクト(1)をクリックすると(2)のページに遷移され、そこのページにプロジェクト設定があるのでそれをクリックします。
すると、プロジェクト設定のページに遷移されるのでロゴ・説明文を編集することができます。

次に、APIを作成します。
APIはWordPressを例えると投稿(ブログ)になります。
プロジェクト一覧からプロジェクトをクリックするとAPI一覧に遷移されるので新規追加をクリックしてAPIを追加します。

新規追加をクリックすると以下のように基本情報入力ページに遷移されます。
API名・エンドポイント・説明文を入力します。

入力して次のステップに行くとAPI形式を選択します。

リスト形式

JSON配列を返却するAPIです。ブログ記事やニュースの一覧などに適しています。

シングル形式

JSONオブジェクトを返却するAPIです。サイト全体の設定項目や会社概要などに適しています。

選択して次のステップに行くとAPIスキーマ定義に遷移されます。
API出力する内容(フロントに出力する内容)を定義して、フィールドを選択してフィールド名・IDを設定します。

選択できるフィールドは以下になります。

・テキストフィールド・・・1行のテキストを入力するフィールド
・テキストエリア・・・複数行のテキストを入力するフィールド
・リッチテキスト・・・pタグ・h2などのタグを利用してhtml形式に入力するフィールド
・画像フィールド・・・画像(jpg・webp)を添付するフィールド
・ファイルフィールド・・HTML・mp4などをアップロードするフィールド
・日時フィールド・・・日時を入力するフィールド
・数字フィールド・・・半角数字を入力するフィールド
・真偽値フィールド・・・booleanのtrue・falseを選択するフィールド
・セレクトフィールド・・・セグメントを表示するフィールド
・参照フィールド・・・別のAPIのコンテンツを参照するフィールド
・カスタムフィールド・・・フィールド定義したフィールド

詳細は以下にてご確認ください。
https://hacocms.com/docs/entry/api-schema

これで、APIが作成できました。

エンドポイントはコンテンツを出力する時に利用するのでメモを控えてください

APIトークンを取得する

次にAPIを取得してコンテンツを出力する為に、microCMSのようにAPIトークンを取得します。
プロジェクト一覧 -> プロジェクト設定 -> APIトークンの順に遷移していきます。

すると以下の画面が表示されるので更新ボタンを押下してアクセストークンを発行します。

APIトークンはコンテンツを出力する時に利用するのでメモを控えてください

hacoCMSの登録方法を解説したので、コンテンツを出力させます。
今回はSolidJsというフレームワークを利用してhacoCMSで登録したものを出力したいと思います。
まずは、SolidJsについて説明していきます。

余談

Next.js・Nuxt.jsのフレームワークでhacoCMSで登録したコンテンツを出力する方法は以下の公式ドキュメントに書いているみたいなのでこちらにてご確認ください。
https://hacocms.com/docs/entry/tutorial-nuxt-js-ssg
https://hacocms.com/docs/entry/tutorial-next-js-ssg

SolidJsとは?

SolidJsはVueやReactと同じ宣言型のJavaScriptのフレームワークです。
特に記述方法はReactに似ており、JSX, Typescript等のモダンフレームワークが採用する機能を持ち、React Hooksの関数もあり、一度Reactで構築した方は大変馴染みやすいフレームワークです。
また、VueやReactのように仮想DOMを利用せず、一度作成したDOMをSignal・Memoなどの関数を利用して部分的に動的に更新することでパフォーマンス性にも優れております。

https://www.solidjs.com/

コンテンツを出力する

それではコンテンツを出力します。
今回は以下のようにコンテンツを出力したいと思います。

solidJs・hacocms-js-sdkをインストールする

まずは、solidJsをインストールします。

npx degit solidjs/templates/js [ファイル名]

または TypeScript 向けだと以下のようにインストールしますが、
今回はjsでインストールします。

npx degit solidjs/templates/ts [ファイル名]

インストールすると以下のようなディレクトリになります。

├── README.md
├── index.html
├── jsconfig.json
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── src
│   ├── App.jsx
│   ├── App.module.css
│   ├── assets
│   │   └── favicon.ico
│   ├── index.css
│   ├── index.jsx
│   └── logo.svg
└── vite.config.js

次に、hacoCMSでコンテンツを出力するために、hacocms-js-sdkをインストールします。
hacocms-js-sdkはhacoCMSのJavaScript/TypeScript用APIクライアントライブラリです。
以下のコマンドを打ってインストールします。

npm install hacocms-js-sdk

or

yarn add hacocms-js-sdk

envファイルを設定する

次にenvファイルを開き以下のようにトークンを設定します。
これは先ほど発行したアクセストークンを入力します。

VITE_HACOCMS_API=xxxxxxxxxxxxxxxxxxxxxxxxx

実装する

トークンを設定した後は以下のようにHTMLファイルを作成します。

sample.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link id="favicon" rel="shortcut icon" type="image/ico" />
  </head>
  <body>
    <div id="root"></div>
    <div id="modal"></div>
    <script src="/src/page/item.jsx" type="module"></script>
  </body>
</html>

id属性がrootの要素はitem.jsxからDOMを生成する時に設定します。
id属性がmodalの要素はモーダルを表示する時に設定します。
次にjsxでカード型コンポーネントをリストで出力するように実装します。

item.jsx
import { render } from 'solid-js/web';
import ItemList from '../components/itemextend';
import styles from '../assets/css/module/App.module.css';
function Item() {
  return (
    <>
      <div class={styles.l_container}>
        <main class={styles.l_main}>
          <ul class={styles.c_itemlist}>
            <ItemList />
          </ul>
        </main>
      </div>
    </>
  );
}
render(() => <Item />, document.getElementById('root'));

カード型のコンポーネントは「ItemList.jsx」のファイルを生成して、コンポーネントを作成して行きます。

itemextend.jsx
import styles from '../assets/css/module/App.module.css';
import { render } from 'solid-js/web';
import { createSignal, onMount, For, createEffect } from 'solid-js';
import { HacoCmsClient, SortQuery } from 'hacocms-js-sdk';

const PROJECT_ACCESS_TOKEN = import.meta.env.VITE_HACOCMS_API
export default function ItemExtend() {

  const [cats, setCats] = createSignal()
      , [data, setData] = createSignal()
      , [current, setCurrent] = createSignal("is_disabled");

  onMount(async () => {
    const client = new HacoCmsClient(`https://xxxxxxxxxxxx.hacocms.com`, PROJECT_ACCESS_TOKEN)
    const res = await client.getList(Object, '/[エンドポイント]', { s: SortQuery.build(['createdAt', 'desc']), limit: 30 });
    const data = await res.data
    setCats(await data);
  });

  createEffect(() => {
    if( current() === 'is_anabled' ) {
      render(() => <Modal data={data()} />, document.getElementById('modal'));
    }
  });

  const close = () => {
    setCurrent('is_disabled');
    document.getElementById('modal').removeChild(document.getElementById('modalinner'));
  }

  const Modal = (data) => {
    if( data.data !== null ) {
      return (
        <>
          <div id="modalinner">
            <div class={styles.m_modal}>
              <button class={styles.m_modal_close} onClick={() => {close()}}>CLOSE</button>
              <figure class={styles.m_modal_figure}>
                <img
                  src={data.data.image}
                  class={styles.m_modal_img}
                  alt={data.data.name}
                  loading="lazy"
                />
                <figcaption class={styles.m_modal_detail}>
                  <p class={styles.m_modal_name}>{data.data.name}</p>
                  <p class={styles.m_modal_price}>{data.data.price}</p>
                  <p class={styles.m_modal_description}>{data.data.description}</p>
                </figcaption>
              </figure>
            </div>
            <div class={styles.m_background} onClick={() => {close()}}></div>
          </div>
        </>
      );
    }
  }
  return (
    <>
      <For each={cats()}>{(cat, i) =>
        <li class={styles.c_itemlistli}>
          <button class={styles.c_itemlistbutton} onClick={() => {setData(cat); setCurrent('is_anabled');}}>
            { !data.data ?
              <>
                <img
                  class={styles.c_itemlistimage}
                  src={`${cat.image}`}
                  loading="lazy"
                  alt={styles.c_itemlistname}
                />
                <p class={styles.c_itemlistname}>{cat.name}</p>
                <p class={styles.c_itemlistdesc}>{cat.price}</p>
              </>
              : null
            }
          </button>
        </li>
      }</For>
    </>
  );
}

上記のitemextend.jsxのファイルを一つ一つ説明していきます。

まずはレンダリングするために、以下のようにインポートします。
React.jsと言ったら「import ReactDOM from "react-dom";」をインポートして「ReactDOM.render」でレンファリングすることと一緒になります。

import { render } from 'solid-js/web';

次に、hacoCMSのコンテンツを出力させるために
solid-jsからcreateSignal, onMount, For, createEffectをインポートしていきます。

import { createSignal, createEffect, onMount, For } from 'solid-js';

「createSignal」はReactの「useState」のように、
関数コンポーネントを保持したり、更新したりするための関数処理です。
カード出力・モーダル出力するために以下のようにセットします。

  const [cats, setCats] = createSignal()
      , [data, setData] = createSignal()
      , [current, setCurrent] = createSignal("is_disabled");

「createEffect」はReactの「useEffect」のように、
コンポーネントの画面生成後、また更新後に自動実行する関数処理です。
今回はモーダルを開閉させるために以下のように記述します。

  createEffect(() => {
    if( current() === 'is_anabled' ) {
      render(() => <Modal data={data()} />, document.getElementById('modal'));
    }
  });

「onMount」はSvelteのように、一度DOMを生成した後に実行する関数処理です。
「createEffect」のように何回も自動実行されません。以下のようにAPiを呼び出してjsonなどで出力する時に適している関数処置です。

  onMount(async () => {
    const client = new HacoCmsClient(`https://xxxxxxxxxxxxxx.hacocms.com`, PROJECT_ACCESS_TOKEN)
    const res = await client.getList(Object, '/xxxxxxxxx', { s: SortQuery.build(['createdAt', 'desc']), limit: 30 });
    const data = await res.data
    setCats(await data);
  });

「For」は文字通り配列する関数です。ほとんどの場合はオブジェクトの配列でfor文・map関数などで利用して配列を回してコンテンツを出力していますが、SolidJsは「For」関数があるので以下のようにセットすれば配列を回して出力することができます。

      <For each={cats()}>{(cat, i) =>
        <li class={styles.c_itemlistli}>
          <button class={styles.c_itemlistbutton} onClick={() => {setData(cat); setCurrent('is_anabled');}}>
            { !data.data ?
              <>
                <img
                  class={styles.c_itemlistimage}
                  src={`${cat.image}`}
                  loading="lazy"
                  alt={styles.c_itemlistname}
                />
                <p class={styles.c_itemlistname}>{cat.name}</p>
                <p class={styles.c_itemlistdesc}>{cat.price}</p>
              </>
              : null
            }
          </button>
        </li>
      }</For>

SolidJsを解説したのでhacocms-js-sdkについて説明します。
hacocms-js-sdkを利用して、HacoCmsClient, SortQueryをインポートします。

import { HacoCmsClient, SortQuery } from 'hacocms-js-sdk';

インポートして以下のように実装すればコンテンツが出力されます。

const PROJECT_ACCESS_TOKEN = import.meta.env.VITE_HACOCMS_API 

  onMount(async () => {
    const client = new HacoCmsClient(`https://xxxxxxx-xxxxxx.hacocms.com`, PROJECT_ACCESS_TOKEN)
    const res = await client.getList(Object, '/yyyyyy', { s: SortQuery.build(['createdAt', 'desc']), limit: 30 });
    const data = await res.data
    setCats(await data);
  });

● import.meta.env.VITE_HACOCMS_API はenvファイルでアクセストークンを設定したものです。
https://xxxxxxx-xxxxxx.hacocms.comはプロジェクト作成した時に生成したサブドメインです
● yyyyyyはAPI作成する時に設定したエンドポイントになります。

それらをインポートしてHTML実行した後に、
App.module.cssのファイルでレイアウト調整していきます。

App.module.css
:root {
  --white: #ffffff;
  --purple: #6D5458;
  --black: #333333;
}
/**
* common
**/
body {
  margin: 0;
  font-size: 62.5%;
}
@media (max-width: 991px) {
  body {
    font-size: 57.5%;
  }
}
@media (max-width: 767px) {
  body {
    font-size: 50%;
  }
}
body:after {
  content: '';
  width: 100%;
  height: 100vh;
  background-color: var(--white);
  z-index: 10000000;
  position: fixed;
  top: 0;
  left: 0;
  animation: bodyfadeout .5s linear 0s infinite;
  animation-fill-mode: forwards;
  animation-iteration-count: 1;
}
@keyframes bodyfadeout {
  0% { opacity: 1; }
  60% { opacity: 1; }
  100% { opacity: 0; z-index: -1; }
}
* {
  font-size: 1.0rem;
  line-height: 2;
}
@media (max-width: 991px) {
  * {
    font-size: 0.9rem;
    line-height: 2;
  }
}
@media (max-width: 767px) {
  * {
    font-size: 0.9rem;
    line-height: 2;
  }
}
ol, ul {
  margin: 0;
  padding: 0;
}
ol li,
ul li {
  list-style-type: none;
}
a {
  color: var(--black);
  text-decoration: none;
}
figure {
  margin: 0;
}
img {
  max-width: 100%;
}
.App {
  text-align: center;
}
h2{
  color: var(--purple);
}
h2 {
  font-size: 1.4rem;
}
@media (max-width: 767px) {
  h2 {
    font-size: 1.2rem;
  }
}
/**
* layout
**/
.l_container {
  display: flex;
  flex-wrap: wrap;
  margin: 2rem auto;
  max-width: 1400px;
  padding: 0 1rem;
  opacity: 0;
  animation: fadeIn 1s ease .5s infinite;
  animation-fill-mode: forwards;
  animation-iteration-count: 1;
}
@keyframes fadeIn{
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
.l_main {
  width: calc(100% - 20px);
  margin: 0 20px 0 0;
}
@media (max-width: 991px) {
  .l_main {
    width: calc(100% - 1rem);
    margin: 0 1rem 0 0;
  }
}
@media (max-width: 767px) {
  .l_main {
    width: 100%;
    margin: 0 0 1rem;
  }
  .l_container {
    margin: .5rem auto;
    padding: 0 .5rem;
  }
}
/**
* components
**/
.c_itemlist {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.c_itemlist .c_itemlistli {
  width: calc(100% / 2 - 5px);
  text-align: center;
}
.c_itemlist .c_itemlistimage {
  width: 100%;
}
.c_itemlist .c_itemlistbutton {
  border: none;
  background: transparent;
  cursor: pointer;
}
.c_itemlist .c_itemlistname,
.c_itemlist .c_itemlistdesc {
  margin: .25rem 0;
}
@media (max-width: 767px) {
  .c_itemlist {
    gap: 0px;
  }
  .c_itemlist .c_itemlistli {
    width: calc(100% / 2);
  }
}
.m_modal {
  position: fixed;
  top: 50%;
  left: 50%;
  min-width: 700px;
  transform: translate(-50%, -50%);
  background: var(--white);
  padding: 1rem 1rem;
  z-index: 150;
  min-height: 50%;
  overflow: scroll;
  opacity: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  animation: modalfadeout .5s ease 0s infinite;
  animation-fill-mode: forwards;
  animation-iteration-count: 1;
}
@keyframes modalfadeout {
  0% { opacity: 0; z-index: -1; transform: translate(-50%, -30%);}
  100% { opacity: 1; transform: translate(-50%, -50%); }
}
.m_modal_figure {
  width: 100%;
}
.m_modal_img {
  width: 100%;
} 
.m_modal_close {
  position: absolute;
  text-indent: -9999px;
  border: none;
  background: transparent;
  top: 0.5rem;
  right: 1rem;
  display: block;
  width: 30px;
  height: 30px;
  font-size: 0;
  padding: 0;
  cursor: pointer;
}
.m_modal_close:before,
.m_modal_close:after {
  position: absolute;
  content: '';
  display: block;
  width: 100%;
  height: 1px;
  background: black;
  transform: rotate(45deg);
}
.m_modal_close:after {
  transform: rotate(-45deg);  
}
.m_background {
  position: fixed;
  top: 0%;
  left: 0%;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  background: #000000;
  opacity: .7;
  z-index: 100;
}
.m_modal_detail p {
  margin-top: .5rem;
  margin-bottom: 0;
}
.m_modal_detail p:empty {
  margin:0;
}
@media (max-width: 991px) {
  .m_modal {
    width: 75%;
  }
}
@media (max-width: 767px) {
  .m_modal {
    width: 90%;
    min-width: auto;
  }
}

実行結果は以下のようになりました。

Githubにもデプロイしてるのでぜひご覧ください。
https://github.com/uemura5683/hacocms-solidjs

参考記事・ドキュメント

https://hacocms.com/docs/
https://www.solidjs.com/

Discussion