r.blog

Next.js × docker-compose × microCMS × VercelでJAMstackブログを作る - 02

前回の続きです。
your-project-name your-app-nameという名前は適宜変えて読んでください。

JAMstackブログを作る

1.日付のフォーマット

詳細ページを見ると、2022-01-01T15:00:00.000Zといった感じで日付が表示されています。

日付データはISO 8601形式のUTC(協定世界時)にて返却しています。
ご利用の際にはフロントエンド側にて現地時間への変換が必要です。(日本時間はプラス9時間)
https://help.microcms.io/ja/knowledge/specification-of-utc-time

とのことなので、日時をフォーマットできるjsライブラリのdate-fnsをインストールし変換していきます。
(公式でdayjs紹介されていたのは終わってから気づきました)

インストールの仕方は前回のmicrocms-js-sdkのインストール方法と同じです。

your-project-name
$ docker-compose stop #コンテナ起動していたらこのコマンドでコンテナ停止
$ docker-compose run --rm your-app-name /bin/sh #コンテナを一時的に起動し中に入る
=====ここからコンテナ内=====
# ls #ディレクトリ内を確認 docker-compose.yml、Dockerfile、your-app-nameがあると思います
# cd your-app-name #your-app-nameディレクトリに入る
# npm install date-fns #date-fnsをインストール
# exit #コンテナを抜ける
=====ここまでコンテナ内=====


libs/date.jsを作成します。yyyy.MM.ddがフォーマット後のレイアウトなのでお好みで変えてください。

/your-app-name/libs/date.js
import { parseISO, format } from 'date-fns'

export default function Date({ dateString }) {
 const date = parseISO(dateString)
 return <time dateTime={dateString}>{format(date, 'yyyy.MM.dd')}</time>
}


[id].jsの編集をします。

/your-app-name/pages/blog/[id].js
import { client } from "../../libs/client";
import Date from "../../libs/date"; //追加

〜省略〜

<main>
 <h1>{blog.title}</h1>
 <p><Date dateString={blog.date} /></p> //Dateで囲む


日付が2022.01.01の形式になっていたら成功です🎉

2. Sassのインストールとスタイルの当て方

Next.jsは面倒な設定無しにsassが使えるようなので、sassを導入していきます。
インストールの仕方は先程やった通り。

# npm install sass

ちなみにチュートリアルにある--saveはなくてもpackage.jsonに記述されるようなので省いてます。
これで.scssが使えるようになりました。

Next.jsではCSS Modulesという考え方が推されているようなので、これに沿って設計していきます。BEM×Sassばっかりだったのでよくわからない新鮮です。

まずはサイト全体で読み込みたいスタイル=グローバルレベルのスタイルを書いてみます。
stylesディレクトリにglobalディレクトリ、その中にglobal.scssを作成します。もともとある.cssファイルは削除してOK。

/styles/global/global.scss
body {
 background-color: #f0f0f7;
}


全ページで使用されるレイアウトは/pages/_app.jsなので、ここでglobal.scssを読み込みます。

/pages/_app.js
import '../styles/global/global.scss' //scss読み込み


サイト全体に背景色がついたらOKです👏

次にコンポーネントレベルのスタイルを書いてみます。
stylesディレクトリにindex.module.scssを作成します。もともとあったHomeでもいいんですが名前揃わなかったのが違和感だったので…

/styles/index.module.scss
.list {
 font-weight: bold;
}

.item {   
  + .item {
   margin-top: 2.5rem;
  }
   
  > a {
   display: block;
   padding: 2rem;
   background: #fff;
  }
}


/pages/index.jsを編集します。

/pages/index.js
import styles from '../styles/index.module.scss'; //scss読み込み

〜省略〜

<ul className={styles.list}> //classを追加
 {blog.map((blog) => (
   <li key={blog.id} className={styles.item}> //classを追加

最初の一行でindex.module.scssを読み込みstylesという名前で定義、className={styles.classname}でclassをつけていきます。

一覧ページで各記事のリンクが白ベタになっていたらOKです👏

3. レイアウトの作成

ページの作りやスタイルの当て方についてある程度わかったところで、レイアウトを整えていきます。

ヘッダー、フッターは共通パーツになるのでそれぞれコンポーネントとして作成、それをすべてのコンテンツをラップするレイアウトコンポーネントを作成して配置していきます。

your-app-nameディレクトリ直下にcomponentsディレクトリ、その中にHeader.jsFooter.jsを作成します。

/your-app-name/components/Header.js
 import Link from "next/link";
import styles from '../styles/header.module.scss';

export default function Header() {
 return (
 <header className={styles.header}>
		 <h1 className={styles.title}>
		 <Link href="/">
		  <a>
		    Blog title
		  </a>
		 </Link>
	 	</h1>
 </header>
 )
}
/your-app-name/components/Footer.js
import styles from '../styles/footer.module.scss';

export default function Footer() {
 return (
 <footer className={styles.footer}>
  <small>Copyright &copy; Blog title All Rights Reserved.</small>
 </footer>
 )
}


続けてLayout.jsを作成します。

/your-app-name/components/Layout.js
import Header from './Header'
import Footer from './Footer'

export default function Layout({ children }) {
 return (
  <>
   <Header />
    <main>
      {children}
    </main>
   <Footer />
  </>
 )
}


_app.jsを編集します。

/pages/_app.js
import Layout from '../components/Layout' //追加

function MyApp({ Component, pageProps }) {
 return (
  <Layout> //Layoutで囲む
   <Component {...pageProps} />
  </Layout>
 );
}


レイアウトで囲んだことで、コンテンツがラップされ、ヘッダーとフッターが表示されるようになりました👏

scssがなくてページがエラーになっているので、scssを作成していきます。

4. CSSの構造、作成

sassの変数やmixin、reset.cssは全体で使用したいので、globalディレクトリに入れてみることにします。stylesディレクトリはこんな感じに。reset.cssDestyle.cssを入れました。

styles
├── global
│  ├── _mixin.scss //メディアクエリなどよく使うmixin
│  ├── _variables.scss //変数
│  ├── common.scss //上記のファイルの橋渡し的存在
│  ├── global.scss //レイアウトと要素型セレクターの指定 bobyとか
│  └── reset.css //リセットcss
├── detail.module.scss //詳細ページ
├── footer.module.scss //フッター
├── header.module.scss //ヘッダー
└── index.module.scss //一覧ページ

構造・管理方針についてはCSS Moduleのベストプラクティスがまだよくわかってないので今後の課題…

reset.css_app.jsで読み込みます。

/pages/_app.js
import '../styles/global/reset.css' //追加


変数などの使い方はこんな感じにしてみました。Dart sass対応✍

/styles/global/_variables.scss
// カラーなどの変数の指定
$color-primary: #6667AB;
$color-secondary: #DDDDEC;
$color-bg: #F0F0F7;
$color-font: #1F2037;
$color-font-thin: #ACACBE;
/styles/global/_mixin.scss
// mixinの設定
$breakpoint: (
 sp: "screen and (max-width: 767px)",
 pc: "screen and (min-width: 768px)",
);

@mixin mediaquery($device) {
 @media #{map-get($breakpoint, $device)} {
  @content;
 }
}
/styles/global/common.scss
// @forwardで各scss読み込み
@forward "variables";
@forward "mixin";


header.module.scssなどの中ではこのように記述します。

/styles/header.module.scss
@use "global/common" as common; //呼び出し元の宣言と名前定義

.header {
 padding: 1.5rem 2.5rem;
 font-size: 1.8em;
 font-weight: bold;
 color: #fff;
 background-color: common.$color-primary;
  
 @include common.mediaquery(sp) {
  padding: 1rem 1.4rem;
  font-size: 1.4em;
 }
}


common.scssを通じて、各変数、mixinを呼び出せています👏

後は各ページのjsにscss読み込み、HTML部分を修正・追加しつつ、cssをガシガシ書いていきます💪

5. headの設定(title設定、Googlefontの読み込み)

titleなどのheadも_app.jsで設定してしまいます。

/pages/_app.js
import Head from "next/head"; //追加

function MyApp({ Component, pageProps }) {
 return (
  <Layout>
   <Head> //追加
    <title>Blog title</title> //titleタグ
    <link //Googlefont読み込み
     href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP〜"
     rel="stylesheet"
    />
   </Head>
   <Component {...pageProps} />
  </Layout>
 );
}

export default MyApp


6. 詳細ページのスタイル

詳細ページでmicroCMSから受け取った本文にスタイルを当てる方法です。
microCMSで、各装飾を使った記事を一つ書いておくとやりやすいかもしれません。

[id].jsdetail.module.scssはこんな状態です。

/pages/blog/[id].js 
import { client } from "../../libs/client";
import Date from "../../libs/date";
import styles from '../../styles/detail.module.scss';

export default function BlogId({ blog }) {
 return (
  <div>
    <div className={styles.header}>
     <p className={styles.date}><Date dateString={blog.date} /></p>
     <h1 className={styles.title}>{blog.title}</h1>
    </div>
   <div
    className={styles.body}
    dangerouslySetInnerHTML={{
     __html: `${blog.body}`,
    }}
   />
  </div>
 );
}

〜省略〜
/styles/detail.module.scss
@use "global/common" as common;

.header {
 margin-bottom: 2.5rem;
 font-weight: bold;
  
 .date {
  margin-bottom: 0.6rem;
  color: common.$color-secondary;
  font-size: 1rem;
 }

 .title {
  font-size: 2rem;
  line-height: 1.6;
 }
}

.body {
 padding: 2rem;
 border-radius: 5px;
 background-color: #fff;
 box-shadow: 0 13px 24px 0 rgba(102, 103, 171, 5%);
}


ページを検証ツールなどで見てみると、.detail_body__XXXXの下層にh1タグやpタグ、blockquoteタグで内容が出力されていることがわかります。
なので子セレクタや子孫セレクタで装飾していきます。以下は例です。

/styles/detail.module.scss
@use "global/common" as common;

〜省略〜

.body {
  padding: 2rem;
  border-radius: 5px;
  background-color: #fff;
  box-shadow: 0 13px 24px 0 rgba(102, 103, 171, 5%);
  
  & > h1, h2, h3, h4, h5 {
    font-weight: bold;
    margin: 2em auto 0.5rem;
    
    &:first-child {
      margin-top: 0;
    }
  }
  
  & > h1 {
    font-size: 1.875rem; //30px h2〜以下お好みで
  }

  & > p {
    line-height: 1.8;
    letter-spacing: 0.2px;

    & > code {
      background-color: common.$color-bg;
      border-radius: 3px;
      padding: 0.1rem 0.2rem;
      color: #ff668a;
      margin: 0 0.1rem;
    }
  }
  
  & a {
    color: common.$color-font-thin;
    text-decoration: underline;
  }
    
  & img {
    max-width: 100%;
    height: auto;
  }

  & > blockquote {
    position: relative;
    padding: 1rem;
    border-left: common.$color-font-thin 0.3rem solid;
    color: common.$color-font-thin;
    z-index: 1;

    & > a {
      margin-top: 1rem;
      text-decoration: underline;
      text-underline-offset: 0.2rem;
      @include common.hover;
    }
  }

  & > pre {
    margin: 0.5rem 0;
    line-height: 1.5;
    border-radius: 5px;
    overflow-x: auto;
  }

  & > ol, ul {
    list-style-position: inside;
    
    > li + li {
      margin-top: 0.6rem;
    }
  }

  & > ol {
    list-style-type: decimal;
  }

  & > ul {
    list-style-type: disc;

    > li {
      > ul {
        list-style: circle;
        list-style-position: inside;
        padding-left: 1rem;
        margin-top: 0.5rem;
        
        > li + li {
          margin-top: 0.3rem;
        }
      }
    }
  }
}


ここを設定するとグッとブログっぽくなります✨

7. シンタックスハイライトの導入

コードブロックとして記述したコードに色をつけてわかりやすく(シンタックスハイライト)していきます。
HTMLパーサーのcheerioとシンタックスハイライトをしてくれるhighlight.jsのjsライブラリをインストールし設定します。
どちらも今までのライブラリと同じくnpmでインストール出来ます。

# npm install cheerio
# npm install highlight.js


[id].jsを編集します。まずはライブラリの読み込み。

/pages/blog/[id].js
import cheerio from 'cheerio'; //追加
import hljs from 'highlight.js'; //追加
import 'highlight.js/styles/monokai.css'; //追加


getStaticPropsにmicroCMSから返されるリッチエディタ部分を処理する部分を追加。

export const getStaticProps = async (context) => {
  const id = context.params.id;
  const data = await client.get({ endpoint: "blog", contentId: id });

  //ここから追加
  const $ = cheerio.load(data.body); 
  $("pre code").each((_, elm) => {
    const result = hljs.highlightAuto($(elm).text());
    $(elm).html(result.value);
    $(elm).addClass("hljs");
  });

  return {
    props: {
      blog: data,
      highlightedBody: $.html(), //追加
    },
  };
};


最後にソース部分を編集。

export default function blogId({ blog, highlightedBody }) { //追加
 return (
  <div>
    <div className={styles.header}>
     <p className={styles.date}><Date dateString={blog.date} /></p>
     <h1 className={styles.title}>{blog.title}</h1>
    </div>
   <div
    className={styles.body}
    dangerouslySetInnerHTML={{
     __html: highlightedBody, //変更
    }}
   />
  </div>
 );
}

これでコード部分にいい感じに色がついて見やすくなりました✨

カラーテーマはhighlight.jsデモページから選ぶことができ、importするcss名を変えることで反映されます。
かなり大好きなMarianaがないのが悲しみでした

import 'highlight.js/styles/monokai.css';

このとき、/your-app-name/node_modules/highlight.js/stylesにあるcss名で指定してください。


ただ判別の精度?がよくないのか、たまに意図されない箇所で色が変わってしまっているのが少し難点です。

If automatic detection doesn’t work for you, or you simply prefer to be explicit, you can specify the language manually in the using the class attribute:<pre><code class="language-html">...</code></pre>
“自動検出がうまくいかない場合や、単に明示的であることを好む場合は、class属性を使用して手動で言語を指定することができます。”
https://highlightjs.org/usage/

…とのことで、codeタグに言語名のclassを追加することでその言語でハイライトしてくれるようになるようです。



次回はVercelでホスティングと公開を紹介します。

参考

date-fns
https://note.com/karaage1970/n/ne0970347a9e9

css
https://qiita.com/tetsutaroendo/items/8e3351bc4bfbb419f662
https://zenn.dev/catnose99/scraps/5e3d51d75113d3

レイアウト
https://weseek.co.jp/tech/733/
https://qiita.com/akitxxx/items/277a7be35156c2677110

Googlefont
https://nextjs.org/docs/basic-features/font-optimization
https://penpen-dev.com/blog/next-webfont/

シンタックスハイライト
https://blog.microcms.io/syntax-highlighting-on-server-side/
https://qiita.com/cawauchi/items/ff6489b17800c5676908