【ポートフォリオ】大会特設サイト新設

# 1. はじめに
私は、大学の英語ディベートサークルにも所属しているのですが、そこでの運営チームにサイト制作スタッフとして参加しました。

8月の開催まで時間がなく、二、三週間程度で急いで作り上げたのですが、TDに承認されるレベルではあったのである程度のサイトは作れたのではないかと思います。
折角ならと、ポートフォリオとしてこの記事にまとめておこうと思う次第です。
あまりコーディングの詳しい話はしないので、技術的な情報の収集を目的にされている場合は、この記事は合わないと思われます。申し訳ありません。

::::note warn
この記事は非公式です。
大会のお問い合わせは、QDO運営のGmailにメールを送るか、[QDO 2024 特設サイトのコンタクトフォーム](https://kyushu-debate-open-2024.vercel.app/contact
)から連絡を取るようお願いいたします。
::::


# 目次
- [1. はじめに](#1-はじめに)
- [2. サイト制作の経緯](#2-サイト制作の経緯)
- [3. フロントエンドライブラリー一覧](#3-フロントエンドライブラリー一覧)
- [4. Home, Aboutページ](#4-home-aboutページ)
- [5. Tournament, Constructページ](#5-tournament-constructページ)
- [6. その他の工夫点](#6-その他の工夫点)
    - [6-1. プログレスバー](#6-1-プログレスバー)
    - [6-2. 画面遷移](#6-2-画面遷移)
    - [6-3. フェードイン](#6-3-フェードイン)
- [7. SEO対策](#7-seo対策)
- [8. おわりに](#8-おわりに)

# 2. サイト制作の経緯
QUDSがBritish Parliamentary Styleのオープン大会として開催していたKyushu Debate Openを再開するにあたり、これを告知する特設サイトをつくらねばならないという話が持ち上がりました。
しかし、コロナが流行して2020年の大会が中止になって以降、4年程度期間が開いてしまったため、過去サイトの管理アカウントが行方知れずになってしまいました。
ひとまずは、今年の大会のサイトは新しく作っておこうということで、このサイトの制作が始まりました。

# 3. フロントエンドライブラリー一覧
<dl>
  <dt>zod</dt>
  <dd>値のバリデーションのために利用しました</dd>
  <dt>mui</dt>
  <dd>CSSスタイル適用の効率化のために利用しました</dd>
  <dt>framermotion</dt>
  <dd>サイトに動きをつけやすくするために利用しました</dd>
　<dt>swiper</dt>
  <dd>主に自動スライドショーをつくるために利用しました</dd>
  <dt>nodemailer</dt>
  <dd>ユーザーからコミへの連絡手段を確保するために利用しました</dd>
  <dt>typescript</dt>
  <dd>変数やオブジェクトの型定義のために利用しました</dd>
</dl>

もっとも利用したのはMUIです。その理由は、MUIがUIコンポーネントのライブラリーであり、一からスタイリングしなければならないTailwindcssよりも開発スピードが優れるうえ、UIコンポーネントの種類が豊富でカスタム性も高いためです。今回は特に、大会開催前に少しでも早くサイトを制作して公開しなければならないという制約があったために、基本的にはMUIを利用してサイトを制作しました。

今回のプロダクトを制作した感想としては、やはりバックエンドやデータベースを利用しない分、システム的な難易度は低いと感じました。さくッとつくれた印象です。
一応、Contactページ用にForm Actionは利用しているのですが、以前制作した学内知恵袋のように、様々な場面でフォームが利用できるようにUIコンポーネントやServer Actionsを再利用可能な形に整えたり、API取得のメソッドを共通化したりなどと言った工夫も必要ではありませんでした。

ただ、ウェブデザインやUIに拘ることが思いのほか難しく、UXをあげるための工夫は難度が高かったです。
単純に知識不足もあるのかもしれません。
個人的には、レイアウトをあれこれと調整するよりも、ちょっと工夫が必要なシステムを意図通りに動かすために試行錯誤しながらプログラムを組む方が楽しいので、あまり肌に合わないのかもしれません。

とはいえ、サイトを制作して広く世間様に公開する以上、UI/UXに関する工夫はできる限り取り組みました。以降の章では各ページにおいて、どんな点を工夫したのかを紹介します。

# 4. Home, Aboutページ
トップページやAboutページにおいては、大会や開催地（福岡県や大学）の雰囲気がわかるようにすることを目指しました。
トップページでは自動スクロールのスライドショーを掲載したり、AboutページではMUIのImage Listを利用して福岡県や以前の大会の写真を一覧的に掲載しました。
紹介しやすいのはabout fukuokaのページですので、そのスクリーンショットをお見せします。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/cc0a89bd-c091-fc34-c004-d1263b8157db.png)


このページでは福岡県の様子を伝える写真をMUIのImage Listによって制作していますが、MUIのデモンストレーションページにはレスポンシブ対応の手引きが書いておらず、ここは自力で対応しました。

```typescript:FukuokaImpression.tsx
export default function FukuokaImpression() {
  const matches = useMediaQuery("(max-width:600px)");

  return (
    <Fragment>
      <Box justifyContent="center">
        <Box>
          <Typography variant="h4" align="center">
            Impression
          </Typography>
          <Typography variant="caption" align="center">
            ©Fukuoka prefecture travel association
          </Typography>
        </Box>
      </Box>
      <ImageList
        sx={{ width: { xs: 300, sm: 400, md: 650 }, height: matches ? 3125 : 1250 }}
        variant="quilted"
        cols={matches ? 1 : 4}
        rowHeight={matches ? 150 : 121}
      >
        {images.map((image) => (
          <ImageListItem key={image.title} cols={matches ? 1 : image.col || 1} rows={matches ? 1 : image.row || 1}>
            <PhotoDialog image={{ title: image.title, url: image.url }} />
          </ImageListItem>
        ))}
      </ImageList>
    </Fragment>
  );
}
```
ImageListItemのおおもとはGrid Layoutであり、colsやrowsの数値によって縦や横の幅を決定します。
MUIのuseMediaQueryというHooksを用いることで、600px以下の時には全て縦横共に1とし、600px以上の時に先ほどの画像のように工夫を施した配列になります。

# 5. Tournament, Constructページ

これらのページでは、何が話題になっているのかを直感的に分かりやすくするための画像作成や配置を意識しました。

例えば、以下の画像はtournament registrationページのスクリーンショットです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/854e9aea-c814-6c7c-bbad-07eada1107f4.png)


Phase1が申し込みに関する話題で、Phase2が料金支払いに関するものであると、直感的に理解できると思います。
そのトピックにあう背景画像をMS Designerで生成して配置し、画像よりも手前で表示する文字はその背景をグレーに着色することで見やすくしました。

似たような工夫をしたページとして、「作成中」と表示したページがあります。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2826164/0b5490b7-307a-7b3b-6f0a-53a10a630a70.png)


まだ掲載情報が不確定でページの作成ができていないために「作成中」になってはいますが、その際も単に文字だけを表示するのではなく、工事現場の画像を背景に表示して貧相に見えないように工夫しました。

# 6. その他の工夫点。
この章では、UIに関するその他の工夫点を紹介しようと思います。

## 6-1. プログレスバー
プログレスバーとは、画面スクロールに応じて横に伸びていくバーのことです。
Framer motionを用いて実装しました。
以下の通りです。

```typescript:ProgressBar.tsx
"use client";

import { motion, useTransform, useScroll } from "framer-motion";

export default function ProgressBar() {
  const { scrollYProgress } = useScroll();
  const scaleX = useTransform(scrollYProgress, [0, 1], [0, 1]);

  return (
    <motion.div
      style={{
        width: "100%",
        height: "5px",
        backgroundColor: "lightblue",
        position: "fixed",
        top: 0,
        left: 0,
        zIndex: 9999,
      }}
    >
      <motion.div
        style={{
          width: "100%",
          height: "100%",
          backgroundColor: "midnightblue",
          scaleX: scaleX,
          transformOrigin: "left",
        }}
      />
    </motion.div>
  );
}
```
transformOriginがleftになっていることで、バーが伸び始めるのが左端になっています。たしか、デフォルトではセンターだったように思います。
そして、scaleXに、useTransformの返り値を指定しており、これによりスクロール量とバーの進行量を同期させることができます。

## 6-2. 画面遷移
画面遷移については、以下の記事を参考にしました。


https://zenn.dev/takna/articles/next-framer-motion-page-fade-in-animation


具体的なコードとしては、以下の通りです。
```typescript:template.tsx
"use client";

import { motion } from "framer-motion";
import { usePathname } from "next/navigation";
import { ReactNode } from "react";

export default function Template({ children }: { children: ReactNode }) {
  const pathname = usePathname();
  return (
    <motion.div
      key={pathname}
      variants={{
        hidden: { opacity: 0 },
        enter: { opacity: 1 },
      }}
      initial="hidden"
      animate="enter"
      transition={{
        type: "linear",
        duration: 1,
      }}
    >
      {children}
    </motion.div>
  );
}
```

一点だけ違うのは、motion.divに対してkeyを設定したことです。
これにより、urlの変更、即ち画面遷移の際にtemplate内のframer motionコンポーネントが発火するようになります。

## 6-3. フェードイン

フェードインに関しては、以下の記事を参考にしました。

https://zenn.dev/funteractiveinc/articles/in-view-transition

具体的なコードとしては、以下の通りです。
```typescript:FadeIn.tsx
"use client";

import { ReactElement, } from "react";
import { motion } from "framer-motion";

export default function FadeIn({ children }: { children: ReactElement }) {
  return (
    <motion.div
      variants={{
        offscreen: {
          y: 100,
          opacity: 0,
        },
        onscreen: {
          y: 0,
          opacity: 1,
          transition: {
            duration: 1,
          },
        },
      }}
      initial="offscreen"
      whileInView="onscreen"
      viewport={{ once: true, amount: 0 }}
    >
      {children}
    </motion.div>
  );
}
```
一点異なるのは、onceをtrueにしていることです。
これがなければ一度コンポーネントが画面外に移った後、スクロールして再び画面内に入ってきた時にまたフェードインしてしまいます。
これは少し煩わしくなるので、onceプロパティはtrueにしました。

# 7. SEO対策

SEO対策においては、以下の記事を参考にしました。

https://qiita.com/powdersugar828828/items/e7f6a131c65e1820f6b8

Next.jsでできるSEO対策が包括的にまとめられており、非常に参考になります。

個人的に、最も効果があったのはGoogle Search Consoleの実装でした。
Google Search ConsoleとNext.jsの連携に関する多くの記事は、Vercelのプロジェクト管理ダッシュボードのドメイン設定から追加するに解説されてありましたが、私の場合その操作ができませんでした。

このため、Google Search Consoleとの連携は、ドメインではなくURLプレフィクスからメタタグを取得し、RootLayoutコンポーネントのhead要素の中に追加することで実現しました。

RootLayoutコンポーネントにおいてはこのほかにも、Open Graph Protocolの設定やサイト構造に関する記述も行いました。

具体的には、以下の通りです。

```typescript:layout.tsx
import ProgressBar from "@/ui/ProgressBar";
import Footer from "@/ui/Footer";
import Header from "@/ui/Header";
import type { Metadata } from "next";
import { Box } from "@mui/material";
import "./globals.css";
import SubNav from "@/ui/SubNav";
import { ContextProvider } from "@/ui/ContextProvider";
import { ReactNode } from "react";
import Script from "next/script";

const description =
  "We announce that the Kyushu Debate Open(QDO) resumes this year. We constructed a special website for this resume of the event.";

export const metadata: Metadata = {
  title: "Kyushu Debate Open 2024 - QDO 2024 ",
  description: description,
  openGraph: {
    type: "website",
    siteName: "QDO 2024",
    title: "QDO 2024",
    description: description,
    images: "https://kyushu-debate-open-2024.vercel.app/opengraph-image.png",
  },
};

export default function RootLayout({
  children,
}: Readonly<{
  children: ReactNode;
}>) {
  return (
    <html>
      <head>
        <meta name="google-site-verification" content="_some_letters" />
        <link rel="shortcut icon" href="/icon.ico" />
        <link rel="icon" href="/icon.ico" />
      </head>
      <body className="bg-[rgba(255,250,205,0.2)]">
        <ContextProvider>
          <ProgressBar />
          <Header />
          <SubNav />
          <Box component="main" minHeight="80vh">
            {children}
          </Box>
          <Footer />
        </ContextProvider>
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify({
              "@context": "https://schema.org",
              "@type": "WebSite",
              name: "QDO 2024",
              image: "https://kyushu-debate-open-2024.vercel.app/icon.ico",
              description: description,
            }),
          }}
        />
      </body>
    </html>
  );
}
```

また、Google Search Consoleにおいて、サイトマップを登録できる機能があります。

これに対応するために、サイトマップも作成いたしました。
以下の通りです。

```typescript:sitemap.tsx
import type { MetadataRoute } from "next";

const rootUrl = "https://kyushu-debate-open-2024.vercel.app";

export default function sitemap(): MetadataRoute.Sitemap {
  const paths = [
    { path: "/", priority: 1 },
    { path: "/about", priority: 0.9 },
    { path: "/about/fukuoka", priority: 0.7 },
    { path: "/about/kyushu-university", priority: 0.6 },
    { path: "/staff", priority: 0.6 },
    { path: "/tournament", priority: 0.8 },
    { path: "/tournament/registration", priority: 0.5 },
    { path: "/tournament/visit", priority: 0.4 },
    { path: "/tournament/schedule", priority: 0.3 },
    { path: "/partners", priority: 0.2 },
    { path: "/contact", priority: 0.1 },
  ];

  return paths.map((path) => ({
    url: rootUrl + path.path,
    lastModified: new Date(),
    changeFrequency: "yearly",
    priority: path.priority,
  }));
}
```
Google Search Consoleにサイトマップとして登録する際のurlは、`https://{base url}/sitemap`とすると追加できます。

公式ガイドは以下のページにありますので、より詳しい解説はこちらをご参照ください。

https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap

# 8. おわりに
比較的短期間でサイトを一つ制作することができ、それなりに満足できました。
ただ、はじめての受託開発だったのに、要件をヒアリングしようとしても「好きに作ってもらっていい」と言われ、あまりちゃんとした開発工程を進めることはできませんでした。
一応、過去の特設サイトを参考にして情報を伝えるために必要なページを洗い出し、サイトマップとして提示することなどは最低限取り組めましたが、それ以降のことは自己判断で進めさせていただいたので、あまり上流工程っぽい経験はできなかったのかなぁと思います。

今はプログラミングサークルにおいて、別の受託開発案件に関わらせていただいているので、この企画では上流の方から意識的に取り組んでみたいと思います。