🌱

strapi-starter-next-blogでTailwind CSSを使ってみる

11 min read

Gridsomeの開発・メンテナンス状況があまり思わしくないことと自由度の高さから、Next.jsのstrapiのスターターを試してみました。

バックエンドには下記のstrapiプロジェクトを使用します。

https://zenn.dev/mseto/articles/strapi-starter-blog

Next.jsの開発環境構築

手順はGridsomeのときと同様です。

ここではstrapi-starter-gridsome-blog/next-blogディレクトリを作成し、VSCodeで開いて下記のファイルを作成します。

.devcontainer/devcontainer.json
{
  "name": "next-blog",
  "dockerComposeFile": "docker-compose.yml",
  "service": "next-blog",

  "workspaceFolder": "/src",

  "settings": {
  },

  "extensions": [
  ],
}

GridsomeのときにはDockerのネットワークを作成してコンテナ名での通信でうまくいっていたのですが、Next.jsでは通信はできるものの、画像が表示されなかったため、network_modehostにしました。

.devcontainer/docker-compose.yml
version: "3"

services:
    next:
        image: node:14-slim
        volumes:
            - ../:/src
        working_dir: /src
        command: /bin/sh -c "while sleep 1000; do :; done"
        network_mode: "host"

これに合わせてstrapiプロジェクト側のdocker-compose.ymlも下記のように変更しています。

.devcontainer/docker-compose.yml
version: "3"

services:
    strapi-blog:
        container_name: strapi-blog
        image: node:14-slim
        volumes:
            - ../:/src
        working_dir: /src
        command: /bin/sh -c "while sleep 1000; do :; done"
        depends_on:
            - mysql
        network_mode: "host"

    mysql:
        image: mysql:5.7
        command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
        environment:
            MYSQL_DATABASE: strapi-blog
            MYSQL_USER: strapi-blog
            MYSQL_PASSWORD: strapi-blog
            MYSQL_ROOT_PASSWORD: strapi-blog
            TZ: "Asia/Tokyo"
        volumes:
            - db-data:/var/lib/mysql
        network_mode: "host"
        
volumes:
    db-data:

また、strapiプロジェクトのbackend/config/database.jsも下記のように'host'のmysqllocalhostに変更しています。

backend/config/database.js
module.exports = ({ env }) => ({
  defaultConnection: 'default',
  connections: {
    default: {
      connector: 'bookshelf',
      settings: {
        client: 'mysql',
        host: env('DATABASE_HOST', 'localhost'),
        port: env.int('DATABASE_PORT', 3306),
        database: env('DATABASE_NAME', 'strapi-blog'),
        username: env('DATABASE_USERNAME', 'root'),
        password: env('DATABASE_PASSWORD', 'strapi-blog'),
        ssl: env.bool('DATABASE_SSL', false),
      },
      options: {}
    },
  },
});

ファイル作成後はVSCodeでRemote-Containers: Reopen in Containerを実行します。

$ yarn create strapi-starter . next-blog

Next.jsのインストールが終わると下記の様に続けてstrapiのインストールが開始されますが、ここではいったんQuickstartを選択し、インストールが始まったらcontrol + cで強制終了します。
その後、作成されたbackendディレクトリを削除します。

Next.jsの起動

下記コマンドを実行してNext.jsを起動します。

$ cd frontend
$ yarn install
$ yarn yarn develop

Next.jsのバージョンアップ

strapi-starter-next-blogのNext.jsはバージョンが9なので最新のバージョン11にしてみます。
いったんNext.jsを停止し、下記を実行します。

$ yarn add react@latest react-dom@latest
$ yarn add next@latest

バージョンアップ後、問題なく起動することを確認します。

$ yarn yarn develop

動作確認したらNext.jsを停止します。

TypeScriptに対応する

下記を実行してtypescriptと@types/react、@types/nodeをインストールします。

$ yarn add --dev typescript @types/react @types/node

下記コマンドを実行し、空のtsconfig.jsonを作成し、Next.jsを起動するとnext-env.d.tsが作成され、tsconfig.jsonにデフォルトの設定が出力されます。

$ touch tsconfig.json
$ yarn develop

next-env.d.tsは削除も編集もしてはいけません。
また、tsconfig.jsonでデフォルトのstrictモードはfalseになっています。

動作確認したらNext.jsを停止します。

ESLintを設定する

下記コマンドでeslintとeslint-config-nextをインストールします。

$ yarn add --dev eslint eslint-config-next

strapi-starter-next-blogにもともとある.eslintrcを削除し、下記コマンドを実行します。

$ yarn run next lint

Next.js用の.eslintrcが作成されます。

Tailwind CSSのインストール

$ yarn add --dev tailwindcss@latest postcss@latest autoprefixer@latest

tailwind.config.jsの作成

下記コマンドを実行してtailwind.config.jsとpostcss.config.jsを作成します。

$ yarn run tailwindcss init -p

tailwind.config.jsの編集

作成したfrontend/tailwind.config.jsを下記のように編集してPurgeの設定を追加します。

frontend/tailwind.config.js
module.exports = {
  purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Tailblocksのコンポーネントをベースにトップページを編集

Gridsomeの場合と同様に編集します。

下記のようにfrontend/pages/_app.jsのimport "../assets/css/style.css";import 'tailwindcss/tailwind.css'に変更し、UIkitのCSS部分を削除します。

frontend/pages/_app.js
import App from "next/app";
import Head from "next/head";
import 'tailwindcss/tailwind.css'
import { createContext } from "react";
import { getStrapiMedia } from "../lib/media";
import { fetchAPI } from "../lib/api";

// Store Strapi Global object in context
export const GlobalContext = createContext({});

const MyApp = ({ Component, pageProps }) => {
  const { global } = pageProps;

  return (
    <>
      <Head>
        <link rel="shortcut icon" href={getStrapiMedia(global.favicon)} />
      </Head>
      <GlobalContext.Provider value={global}>
        <Component {...pageProps} />
      </GlobalContext.Provider>
    </>
  );
};

// getInitialProps disables automatic static optimization for pages that don't
// have getStaticProps. So article, category and home pages still get SSG.
// Hopefully we can replace this with getStaticProps once this issue is fixed:
// https://github.com/vercel/next.js/discussions/10949
MyApp.getInitialProps = async (ctx) => {
  // Calls page's `getInitialProps` and fills `appProps.pageProps`
  const appProps = await App.getInitialProps(ctx);
  // Fetch global site settings from Strapi
  const global = await fetchAPI("/global");
  // Pass the data to our page via props
  return { ...appProps, pageProps: { global } };
};

export default MyApp;

frontend/pages/index.jsのJSX部分を下記のように変更します。

frontend/pages/index.js
import React from "react";
import Articles from "../components/articles";
import Layout from "../components/layout";
import Seo from "../components/seo";
import { fetchAPI } from "../lib/api";

const Home = ({ articles, categories, homepage }) => {
  return (
    <Layout categories={categories}>
      <Seo seo={homepage.seo} />
      <Articles articles={articles} />
    </Layout>
  );
};

export async function getStaticProps() {
  // Run API calls in parallel
  const [articles, categories, homepage] = await Promise.all([
    fetchAPI("/articles"),
    fetchAPI("/categories"),
    fetchAPI("/homepage"),
  ]);

  return {
    props: { articles, categories, homepage },
    revalidate: 1,
  };
}

export default Home;

frontend/components/nav.jsのJSXを変更します。

frontend/components/nav.js
import React from "react";
import Link from "next/link";

const Nav = ({ categories }) => {
  return (
    <header className="text-gray-600 body-font">
    <div className="container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center">
      <Link href="/" className="flex title-font font-medium items-center text-gray-900 mb-4 md:mb-0"><a>Strapi Blog</a></Link>
      <nav className="md:ml-auto flex flex-wrap items-center text-base justify-center">
        {categories.map((category) => {
          return (
            <Link key={category.id} as={`/category/${category.slug}`} href="/category/[id]">
              <a className="mr-5 text-gray-600 hover:text-gray-900">{category.name}</a>
            </Link>
          );
        })}
      </nav>
    </div>
  </header>
  );
};

export default Nav;

frontend/components/articles.jsのJSXを変更します。

frontend/components/articles.js
import React from "react";
import Card from "./card";

const Articles = ({ articles }) => {
  return (
    <section className="text-gray-600 body-font">
      <div className="container px-5 py-4 mx-auto">
        <div className="flex flex-wrap -m-4">
          {articles.map((article, i) => {
            return (
              <Card article={article} key={article.slug} />
            );
          })}
        </div>
      </div>
    </section>
  );
};

export default Articles;

frontend/components/card.jsのJSXを変更します。

frontend/components/card.js
import React from "react";
import Link from "next/link";
import Image from "./image";

const Card = ({ article }) => {
  return (
    <div className="p-4 md:w-1/3">
      <Link as={`/article/${article.slug}`} href="/article/[id]">
        <a>
          <div className="h-full border-2 border-gray-200 border-opacity-60 rounded-lg overflow-hidden">
            <Image
              image={article.image}
              alt={`Hero image`}
              className="lg:h-48 md:h-36 w-full object-cover object-center"
            />
            <div className="p-6">
              <h2 className="tracking-widest text-xs title-font font-medium text-gray-400 mb-1">{article.category.name}</h2>
              <h1 className="title-font text-lg font-medium text-gray-900 mb-3">{article.title}</h1>
              <p className="leading-relaxed mb-3 text-gray-600">{article.description }</p>
              <div className="flex items-center flex-wrap ">

                <span className="text-gray-400 inline-flex items-center lg:ml-auto md:ml-0 ml-auto leading-none text-sm py-1">
                  {article.author.name}
                  {article.author.picture && (
                      <Image
                      image={article.author.picture}
                      alt={`Picture of ${article.author.name}`}
                      className="rounded-full h-12 w-12 flex items-center justify-center ml-3"
                      />
                  )}
                </span>
              </div>
            </div>
          </div>
        </a>
      </Link>
    </div>
  );
};

export default Card;

frontend/components/image.jsを下記のように変更します。

frontend/components/image.js
import { getStrapiMedia } from "../lib/media";

const Image = ({ image, style, className }) => {
  const imageUrl = getStrapiMedia(image);

  return (
    <img
      src={imageUrl}
      alt={image.alternativeText || image.name}
      style={style}
      className={className}
    />
  );
};

export default Image;

変更前
strapi-blog変更前

変更後
strapi-blog変更後

まとめ

今回Next.jsのバージョンを上げてみたり、TypeScriptに対応するためのパッケージのインストールはしましたが、結局TypeScriptに対応まではしませんでした。
スターターは便利ですが、最新の状態にはメンテナンスされてなく、これをベースにするよりは最初から作ったほうがいいかなと思いました。