SUBMONTANE STUDIOができるまで(七転八倒編)

JavaScript

再び更新がだいぶ遅くなってしまいました…

前回から引き続き、SUBMONTANE STUDIOができるまで、今回は最終回のコーディング七転八倒編です。前回まででデザインと技術選定はできたので、いよいよ実際にコードを書いていく作業に入ります。

Next.jsの書き方

前回、Reactのコードを少し掲載しました。Next.jsはReactをベースに開発されているので、基本的なコードは変わりません。

サンプルとして、SUBMONTANE STUDIOのトップページのお知らせ部分のコードを記載します。

import styles from "../../styles/home.module.scss";
import InformationItem from "./InformationItem";

export default function Information() {
  return (
    <section className={styles.information}>
      <h2>お知らせ</h2>
      <ul id="information" className={styles["information-list"]}>
        <InformationItem />
      </ul>
    </section>
  );
}
import type { BlogList } from "@/@types/blog";
import { format, parse } from "@formkit/tempo";
import { headers } from "next/headers";
import Link from "next/link";
import styles from "../../styles/home.module.scss";
type Item = {
  id: string;
  publishedAt: Date;
  updatedAt?: Date;
  title: string;
};
export default async function InformationItem() {
  const res = await fetch(
    /* MicroCMSのエンドポイント */,
    {
      headers: {
        "X-MICROCMS-API-KEY": /* envファイル内のAPIキー */|| "",
      },
    },
  );

  const data = await res.json();

  return (data as BlogList).contents.map((item: Item) => (
    <li key={item.title}>
      <Link href={`/posts/${item.id}`}>
        <p className={styles.time}>
          <time dateTime={format(item.publishedAt, "YYYY-MM-DD")}>
            {format(item.publishedAt, "YYYY.MM.DD")}
          </time>
        </p>
        <h3>{item.title}</h3>
      </Link>
    </li>
  ));
}

まず、Information.tsxではimportでhome.module.scssとInformationItem.tsxを読み込んでいます。Next.jsは、SassをインストールするだけでCSS ModulesでSASS/SCSSを使用できるようになります。Webpackやnpm-scriptsの設定を書かなくていいのが楽ですね。home.module.scssでは

.information{
  display:flex;
  /* その他もろもろ */
  p{
    font-size: 1rem;
    /* その他もろもろ */
  }
}

のようなSCSSの記法でスタイリングしています。

ひとつ特徴的なのは、className属性です。HTMLでは

<section class="information">
  <!-- section要素の中身 -->
</section>

のようにクラスを指定してCSSと関連付けますが、CSS Modulesを使用したReact/Next.jsでは

import styles from 拡張子が.moduleで終わるSCSS/CSSファイル
export default function Information() {
  <section className={styles.information}>
    // section要素の中身
  </section>
}

のようにclassName属性を記述します。これは、Reactが使用しているJavaScriptの仕様上class属性が使用できないためです。

上記のコードではstylesという名前でCSSを読み込み、className属性でstylesの中のinformationクラスを適用しています。import styles from ~~stylesという部分は決まっているわけではなく、testでもhomeでもなんでも構いません。

このコードでビルドすると、

.home_information__27C_t {
  /* informationの中身 */
}

のように、SCSSファイルのファイル名_クラス名__ランダムな文字列といった感じでクラスセレクタが作成され、ランダムな文字列.cssというファイルに出力されます。HTMLも

<section class="home_information__27C_t">
  <!-- section要素の中身 -->
</section>

という感じでCSSに合わせてクラスが設定されます。これの何がいいかというと、IDやクラスの衝突を気にしなくてよくなることです。「Aというクラスを2箇所で使っちゃってスタイルが上書きされてうわあああああ!!」というマークアップ・フロントエンドエンジニアあるあるがなくなるのはかなり大きいです。

CSS Modulesの仕様で誤解していたこと

import styles from 'home.module.scss';
import button from 'button.module.scss';

上記のように書いた場合、CSSファイルはどんな感じで出力されると思いますか?(exportなどは適切に書いたとします)

  • abcde.css
  • fghijk.css

のようにCSSファイルが2つ出力されると思った方、あなたは正しいです。

冷静に考えればそうはならないだろうと分かるのですが、コーディング作業中の僕としては

  • abcde.css

一つによき感じでまとめられると思っていました。**まあ、そうはなりませんでした。**現在ではこの挙動が問題になることはそうそうないとは思いますが、調子に乗ってCSS Modulesを細かく作りすぎると、そのモジュールごとに外部CSSファイルが追加され、HTTPリクエストがどんどん増えていくことになるので、少し注意が必要です。

(HTTP/2の普及した現在では、リクエストがどれだけあろうが、一般的な範疇であればそれほどページパフォーマンスに変化はありません。スクリプトやスタイルをBundlerにかけて1つにまとめる…というのは、過去のものとなりつつあります…)

CSSの:global問題

CSS Modulesでは、基本的にclassNameのあとに固有のハッシュが付きます。しかし、時としてこのハッシュを付けないスタイルを適用したいときがあります。(.info__acs_tの子要素に.newクラスを付けたい場合など)

この場合に.newにいちいちハッシュを付けていると、同じコードを何度も書く必要が出てきてしまい、逆に非効率になってしまう場合があります。

その場合は、:globalセレクタを使用することでスコープを解除し、グローバルなスタイルとして記述できるようになります。

.info { // <- この部分はハッシュが末尾につき、スコープ化されます
  :global { // <- 以下の要素をグローバルなスタイルとして記述するよう指定するセレクタ
    .new {
      color: #f00;
    }
  }
}

ただ、この:globalセレクタは妙にクセがあるのか、SCSS→CSSのコンパイルの際にエラーが発生してしまうことが多々ありました。

なぜエラーになるのか分かりにくい部分が多く、かなり苦労しました。おそらく私のCSS Modulesへの理解が足りていないことが根底にあるのでしょうが、この部分は特に苦労しました。

お問い合わせフォームでCloudflare Workersが動かない

Cloudflare Pagesを選んだ理由がお問い合わせフォームを無料で作れる(どれだけ送信されても無料)というところだったのですが、肝心のこのメールフォームの連携が全くうまく行きませんでした。どうも当初見込んでいたMailChannelsの無料APIがEOL(サービス終了)になったことが絡んでいそうですが、その時点ではよくわからず、結局Cloudflare連携のメールフォーム作成は断念し、SSGFormというサービスを使うことにしました。

SSGFormは

タグのaction属性にSSGForm発行のURLを設定し、あとはinputやselectなどのフォーム部品に名前やvalueを付けると簡単にフォームを送信してくれる、というサービスです。自動応答メールの送信や reCAPTCHAの導入、送信完了ページへの転送なども可能です。日本発のサービスということもあり、クライアントワークでも大いに活躍しそうです。

キャッシュ強すぎ問題

Next.jsは、ページをあらかじめ生成し、キャッシュすることですばやく閲覧者へ届けることが可能です。ホスティングに使っているCloudflareにもページをキャッシュし、配信する機能があります。

特にNext.js側では、最近のApp Routerへの仕様変更によってキャッシュを効かせるかどうかの選択が曖昧になっています。当初、ブログ一覧をデフォルトの設定(≒オプションを設定しない)で実装していたのですが、公開して初めてブログ記事を更新する段になって、Facebookで繋がっている友人からこんなメッセージが届きました。

「ブログ一覧、更新されていないよ」

………そう、キャッシュが更新されず、閲覧者側ではいつまで経っても更新されていないということに、ここに来てようやく気づきました。なんで…?

そこで、キャッシュをクリアするためのスーパーリロードはもちろん、Cloudflare Pagesのキャッシュクリアなどを試した結果、Next.js側のキャッシュが更新されていないことに気づいたのでした。そこで、

fetch('https://...', { next: { revalidate: 3600 } })

のようにオプションを指定し、3600秒でキャッシュが更新されるように設定しました。というわけで、1時間後にやっとブログ一覧ページが更新されたのでした。

今後の課題

細かい反省点・改良すべき点は多いのですが、ひとまずこの単純に時間でキャッシュが更新される仕組みは最優先でどうにかしたいところです。revalidateTag機能を使用すればもう少しスマートにキャッシュ更新ができそうなので、実装を検討中です。

というわけで、開設から半年以上かかって今回のサイト開設について説明しました。今後の更新では最新の技術について使ってみた感想や使用感などを上げていく予定です。といいつつ、いつ更新できるかは不明ですが、気長にお待ちいただけますと幸いです。

記事一覧へ戻る

お見積り・お問い合わせこちら

お困りの際は、お気軽にご相談ください
お問い合わせはこちらからお願いいたします

お問い合わせ