841-biborokuWebフロントエンドの備忘録

ヘッドレスCMS Newtを使ったコンテンツ管理とAstroでのページ構築①

Newtには登録していたものの、最近はmicroCMSばかり使っていました。そこで、久々にNewtを使ってみることにしました。

このサイトの記事はmicroCMSを利用して作っています。この記事はmicroCMSと同じフィールド構成でNewtとAstroを使って実装した備忘録です。

また、microCMSからNewtへ置き換えた際の違いや、Newtの魅力・便利な点も合わせてまとめました。

※この記事では、Newtの登録や初期設定に関する説明は省略しています。

モデルとフィールド構成

Newtでは、モデルとフィールドは以下のように構成しました(一部フィールドは除いています)。

  • モデル=microCMSでいう「コンテンツ(API)」
  • フィールド=microCMSでいう「APIスキーマ(フィールド)」

モデル:ブログ

項目

フィールドの種類

タイトル

テキスト(1行)

スラッグ

テキスト(1行)

カテゴリー

参照(複数値)

サムネイル画像

画像

セクション

マルチタイプ(複数値)

メタデータ

マルチタイプ

モデル:カテゴリ

項目

フィールドの種類

タイトル

テキスト(1行)

スラッグ

テキスト(1行)

イメージカラー

テキスト(1行)

Newtのフィールドのいいところ

microCMSと似ている部分も多いNewtですが、異なるポイントもいくつかあります。ここではその違いに注目していきます。

単一と複数

Newtでは、フィールドの種類に対して「単一」または「複数」を選択できる仕様になっています。
例えば、テキストフィールドの場合:

  • microCMSの場合:1行テキストとテキストエリアは別のフィールドとして用意されている。
  • Newtの場合:テキストフィールドを選択した後に、「1行」または「複数行」を設定可能。

この仕組みは画像や参照フィールドでも同様で、用途に応じて柔軟に選択できます。

メディア

Newtでは、アップロードしたメディア自体に次のような情報を設定可能です。

  • タイトル
  • 代替テキスト
  • 説明
  • タグ

画像のalt用フィールドを別途作成する手間を省けます。さらに、画像フィールドに設定したメディアから、このmetadataを取得することもできます。

ただし、これらの情報は現在の仕様ではメディア管理画面からしか編集できないため、記事編集画面から直接編集できるとさらに便利だと感じました。

ファイル

Newtではフリープランでもファイルのアップロードが可能です。

選択フィールド

  • Label(管理画面に表示される項目名)
  • Value(戻り値として取得される値)

LabelはAPIから取得することはできませんが、管理画面用に編集者がわかりやすい名前を設定できる点が便利です。

マルチタイプ

Newtでは、microCMSでいう「繰り返しフィールド」が「マルチタイプ」として提供されています。

  • デフォルト値:初めからフィールドを表示させておくことが可能。
  • 表示タイプ
    • アコーディオン(デフォルトで閉じた状態、上下入れ替えがしやすい)
    • フラット(全て展開された状態、内容を一目で把握しやすい)

どちらを使うかは好みによりますが、それぞれ利点があるので用途に応じて選択するといいと思います。

NewtのJavaScript SDK を使った記事表示

Newtには newt-client-js というJavaScript SDKが提供されています。このSDKを利用して、記事データを取得し、ページに表示する方法を見ていきます。

npm i newt-client-js

型定義

NewtのSDKでは、TypeScriptを活用できるよう、型が用意されています。これはmicroCMSのSDKで提供される型(例: MicroCMSImage, MicroCMSContentId, MicroCMSDate)に似ています。

  • Content: コンテンツ共通のプロパティを定義した型。
  • Image: 画像フィールドのプロパティを定義した型。
  • File: ファイルフィールドのプロパティを定義した型。

Newt JavaScript SDK - TypeScriptでの利用方法

クライアントの作成とコンテンツ一覧の取得

NewtのJavaScript SDKを使用してクライアントを作成し、コンテンツ一覧を取得する関数を実装しました。

import { createClient } from 'newt-client-js'

export interface Blog extends Content {
  title: string
  slug: string
  category: Category[]
  thumb?: Image
  section?: (Richeditor|HtmlCode|Highlight|Gallery)[]
  metadata?: Metadata
}

export const newtClient = createClient({
  spaceUid: import.meta.env.NEWT_SPACE_UID,
  token: import.meta.env.NEWT_CDN_API_TOKEN,
  apiType: 'cdn',
})

export const blogList = async() => {
  const {items} = await newtClient.getContents<Blog>({
    appUid: 'YOUR_APP_UID ',  //コンテンツのApp UID
    modelUid: 'YOUR_MODEL_UID',  //コンテンツのモデルUID
  })
  return items
}

Astroで記事詳細ページの作成

記事一覧を取得し、slug フィールドを基に記事詳細ページのパスを生成する方法を紹介します。

パス生成の仕組み

以下のコードは、記事一覧を取得して各記事の slug を使って動的なパスを生成しています。
※実際のフィールド定義は簡略化しています。

src/pages/[...path]/index.astro

---
import Layout from "../../layouts/Layout.astro";
import { blogList } from "../../lib/newt";
import dayjs from "dayjs";
import "dayjs/locale/ja";

export const getStaticPaths = async () => {
  const posts = await blogList();
  const paths = posts.map((post) => {
    return {
      params: {
        path: `entry/${post.slug}`,
      },
      props: {
        post,
      },
    };
  });
  return paths;
};
const { post } = Astro.props;
const { title, category, section, _sys } = post;
const datetime = dayjs(_sys.createdAt).locale("ja").format("YYYY-MM-DD");
const publishDate = dayjs(_sys.createdAt).locale("ja").format("YYYY/MM/DD");
---

<Layout>
  <h1>{title}</h1>
  <time datetime={datetime}>{publishDate}</time>
  <ul>
    {category.map((cat) => <li>{cat.title}</li>)}
  </ul>
  {
    section &&
      section.map((sec) => {
        switch (sec.type) {
          case "richeditor":
            return <div set:html={sec.data.body} />;
          case "htmlCode":
            return <div set:html={sec.data.htmlCode} />;
          case "highlight":
            return (
              <div>
                <p>{sec.data.lang}</p>
                <p>{sec.data.filename}</p>
                <pre>
                  <code>{sec.data.body}</code>
                </pre>
              </div>
            );
          case "gallery":
            return (
              <div>
                {sec.data.images.map((img) => (
                  <img src={img.src} alt={img.altText} />
                ))}
              </div>
            );

          default:
            return null;
        }
      })
  }
</Layout>

📝参考

シェア