JAIST Tech サークル Advent Calendar 2025 の1日目の記事です。
背景
JAIST Tech サークルは、2025年4月に設立された 北陸先端科学技術大学院大学 公認サークルです。
活動開始から日が浅いものの LT 会やハッカソン、他大学との交流会など精力的に活動しています。そうした流れの中で「外部発信の拠点となるページが欲しい」「活動実態をサークル内外に共有したい」といった声が上がり、ホームページ制作プロジェクトが立ち上がりました。
そこで今回は、まず情報を着実に公開できる状態を整えることを重視して、下記のような 2段階リリース の戦略を立てました。
- Phase 1: 情報公開を最優先し、シンプルかつ堅牢な構成でリリースする
- Phase 2: サークルのブランドイメージを反映した、よりリッチなデザインへアップデートする
この記事では将来的なデザイン刷新(Phase 2)を見据えつつ、開発者が少数でも無理なく運用・改善を続けられるように工夫した「Next.js における変更に強い設計」について整理します。同じような状況の方にとって参考になる点があれば幸いです。
Phase 1 で実装する内容とそれに対する戦略
サイトは主に「トップページ」「プロジェクトページ」「入会ページ」の3ページから構成されています。
検索機能等は Phase 1 で実装しても良かったのですが、検索体験(UX)の設計も含めて Phase 2 でじっくり取り組むこととしたので、Phase 1 では本当に基本的なテキストコンテンツを整備して公開するだけです。
もう少しアプリケーションとして動きや魅せ方にこだわりたいという意見もあるため、デザインは大きく変わる可能性が高いです。
そのため、 HTML 構造と見た目との結合度を下げてデザイン変更による手戻りを最小化する目的で、変更容易性を重視した設計を採用しました。
実装
コンテンツとレイアウトの分離
コンテンツ内容とレイアウトが密結合してしまっていると Phase 2 でのデザイン実装時に扱うコードが複雑なものとなります。その結果、改修コストが増加すると予想されます。そのため、コンテンツとレイアウトをなるべく疎結合にすることを目的に下記のような実装を行いました。
import { TextContent } from "@/types/ui";
import { SectionFrame } from "@/components/shared/SectionFrame";
import { Heading } from "@/components/shared/Heading";
import { MultilineText } from "@/components/shared/MultilineText";
type Props = {
title: string;
about: TextContent;
};
export const HeroSection = ({ title, about }: Props) => {
return (
// レイアウトの責務は SectionFrame に分離(padding, max-width, align など)
<SectionFrame centered={true}>
<Heading level={1} title={title} />
<MultilineText content={about} />
</SectionFrame>
);
};
ポイントとなる部分は SectionFrame です。基本的な実装であれば <div className="..."> のように書く部分ですが、このような構造にすることでデザイン変更時は SectionFrame に変更を加えるだけで全ページのレイアウトを調整することが可能です。
また、仮に今とは全く異なるレイアウト構成が採用された場合でも、SectionFrame を消してコンポーネント内の要素を組み直せば良いだけなので比較的容易に対応できます。
データとビューの分離
現在は静的サイトであるため、テキストデータをコンポーネントから追い出し、疎結合にするという方針を決めました。また、JAIST は国際色豊かな大学であるため、将来的な国際化対応に着手する足掛かりを作っておくことも念頭に置いています。Phase 1 では過剰なエンジニアリングを避けるため、あえてライブラリ(next-intl 等)を使わず、プレーンなオブジェクトで管理しています。
全体像としてはテキストコンテンツをオブジェクトとして定義し、ページコンポーネントはそれを参照して表示するという構成です。
export const ja = {
top: {
welcome: "JAIST Tech サークルへようこそ",
about: [
"JAIST Tech サークルは「作りたい」を形にする北陸先端科学技術大学院大学公認の技術系サークルです!",
"アプリ開発、ハッカソン、AI、電子工作など、楽しみながらスキルを磨いていけるサークルを目指しています!"
],
}
} as const;
import { HeroSection } from "@/components/top/HeroSection";
import { ja } from "@/locales/ja";
const { top } = ja;
export default function Top() {
return (
<main>
<HeroSection
title={top.welcome}
about={top.about}
/>
</main>
);
}
コンポーネント側(上記の例だと HeroSection)はデータを受け取ることに徹しており、具体的な文言を一切知りません。それらを繋ぐのがページコンポーネントの役割です。
データ構造の揺らぎを吸収する UI 設計
データとビューの分離を進める中で問題になったのは、今後変わりうるテキストコンテンツが「1行の文字列」になるか「複数行のリスト」になるか、現時点では未確定な箇所が多いという点です。 そこで string や string[] のどちらの形でも受け入れて適切に表示する MultilineText コンポーネントを作成しました。
export type TextContent = string | readonly string[];
import { TextContent } from "@/types/ui";
import { Fragment } from "react";
type Props = {
content: TextContent;
as?: "p" | "span";
className?: string;
};
export const MultilineText = ({ content, as: Tag = "p", className }: Props) => {
const lines = Array.isArray(content) ? content : [content];
return (
<Tag className={className}>
{lines.map((line, index) => (
<Fragment key={index}>
{line}
{index < lines.length - 1 && <br />}
</Fragment>
))}
</Tag>
);
};
これにより辞書となるデータ側で行数を変更してもビュー側での修正が不要になります。
まとめ
今回は「デザインが決まっていない」という制約を逆手に取り、変更容易性を意識した実装に取り組みました。
- コンテンツとレイアウトを分離する(
SectionFrame) - データとビューを分離する(
ja.ts/page.tsx) - データの揺らぎを吸収する(
MultilineText)
これらの工夫により、Phase 2 でどんなリッチなデザインが降りてきてもデータ構造やロジックを壊すことなくスムーズに移行できそうです。