【初心者】Next.jsでホームページをつくろう⑧〜Zenの記事を埋め込もう〜その1
はじめに
こんにちは!「選挙×テクノロジー」で民主主義をアップデートすべく、個人開発をしながら勉強中です!今回はNext.jsでホームページを作成していくシリーズの第8回です。
前回はアイコンやボタンを設置しました。今回はZenの記事をページに埋め込んでみましょう。

前回の記事はコチラ
対象読者
- 初学者として同じ目線で学びたい方
- 初学者の私にアドバイスしてくださる聖人
もくじ
- ヘッダーの作成
- 記事の有無による条件分岐
- ループによる実装
1.ヘッダーの作成
まずは、このセクション全体のレイアウトと、見出し(ヘッダー)の部分です。
<section className="flex flex-col gap-6">
<header className="space-y-2">
<h2 className="text-xl font-semibold text-foreground md:text-2xl">
Zennの記事
</h2>
<p className="text-sm text-muted-foreground md:text-base">
勉強のためにZennで記事を投稿しています。
</p>
</header>
<section className="flex flex-col gap-6">
全体を縦並び(flex-col)にして、このヘッダー部分と、この後に続く「記事一覧」の間に均等な余白(gap-6:24px)を作っています。
<header className="space-y-2">
見出し(h2)と説明文(p)の間の余白を自動で空けてくれます。プロフィール部分でも使った便利なクラスですね。
見出しやテキストの色(text-foreground など)は、ダークモードに対応するshadcn/uiのクラスを使っています。
text-foregroundは主役の文字、text-muted-foregroundは脇役の文字に使われます。
2.記事の有無による条件分岐
「データがある時・ない時で表示を切り替える」処理をおこないます。
{articles.length === 0 ? (
<p className="rounded-lg bg-card p-6 text-sm text-muted-foreground shadow-sm">
現在表示できる記事がありません。時間をおいて再度お試しください。
</p>
) : (
// ...このあと記事一覧を表示するコードが続く...
articles.length === 0 ? ( ない時の表示 ) : ( ある時の表示 )
これは「三項演算子」と呼ばれる構文です。ZennのAPIから記事をうまく取得できず「0件」だった場合は、エラーで画面が真っ白になるのを防ぐために「現在表示できる記事がありません」というメッセージを表示するようにしています(フェールセーフ)。
メッセージの className には rounded-lg(角丸)や bg-card(カード風の背景)、shadow-sm(うっすら影)をつけて、ただの文字ではなく「空っぽの状態を示すカード」として綺麗にデザインしています。
3.ループによる実装による実装
<div className="grid gap-4 md:grid-cols-2">
{articles.map((article) => (
<ArticleCard
key={article.id}
article={{
id: article.id,
title: article.title,
emoji: article.emoji,
path: article.path,
publishedAt: article.published_at,
}}
formattedPublishedAt={formatter.format(
new Date(article.published_at),
)}
runId={runId}
/>
))}
</div>
① グリッドレイアウトによるレスポンシブ対応
<div className="grid gap-4 md:grid-cols-2">
ここは記事のカードを並べる親要素です。Tailwind CSSを使って、アイテム間の隙間を gap-4(16px)に設定しています。
ここで特に便利なのが md:grid-cols-2 というクラスです。これは「画面サイズが中くらい(mdサイズ)以上のときは、グリッドを2列にする」という指定です。
② .map() メソッドを使った繰り返し処理
{articles.map((article) => ( ... ))}
配列データから要素を一つずつ取り出して処理を行う、JavaScriptの map メソッドを使用しています。Reactで一覧画面(リストやカードの羅列など)を作るときの超・定番の書き方です。
APIから取得した articles(記事データのまとまり)から1記事ずつ article として取り出し、見た目を担当する <ArticleCard /> コンポーネントに当てはめて連続で出力しています。
[.map() メソッドの解説]
1. .map() メソッドとは?(一言でいうと)
配列の要素を先頭から1つずつ順番に取り出し、「何かの処理」をして、まったく新しい配列を作るためのJavaScriptの標準機能です。
▼ JavaScriptの基本的な例
const numbers = [1, 2, 3];
// 1つずつ取り出して(num)、2倍にして返す
const doubled = numbers.map((num) => num * 2);
console.log(doubled); // [2, 4, 6] という新しい配列ができる!
// ※元の numbers は [1, 2, 3] のまま
▼React(next.js)での例
- なぜReactでこんなに多用されるの?
Reactでは、「データの配列」を「UI(見た目)の配列」に変換するために .map() を使います。
通常のHTMLなら <li> タグなどを手作業でコピペして増やしますが、ReactではAPIなどから取得したデータをもとに、自動でタグを生成します。
const fruits = ["りんご", "みかん", "ぶどう"];
export default function FruitList() {
return (
<ul>
{/* データの配列を、<li>タグの配列に変換して表示 */}
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
);
}
③ 要素を識別する key とProps(データ)の受け渡し
<ArticleCard key={article.id} ... />
Reactで .map() を使ってループ処理をする際、必ず設定しなければならないのが key プロパティです。これは、Reactが「リストの中のどのアイテムが変更・追加・削除されたか」を裏側で効率的に把握するための「背番号」のような役割を果たします。重複しない一意の値を設定する必要があるため、今回はZenn側のデータが持っている固有の id を指定しています。
さらに、取り出した記事のデータ(id, title, emoji, path など)や、JavaScript標準の機能で日本の日付形式(YYYY/MM/DDなど)に見やすく整えた公開日時(formattedPublishedAt)を、子コンポーネントである ArticleCard にProps(プロップス)として渡しています。実際のカードのデザインやレイアウトは、この ArticleCard 側に任せているというわけですね。
④日付をキレイに整えて渡す
formattedPublishedAt={formatter.format(
new Date(article.published_at),
)}
ここは、Reactの「Props(プロップス)」という機能を使って、親(このページ)から子(ArticleCard部品)へデータを手渡している部分です。
formattedPublishedAt=:
【訳】「子部品の <ArticleCard /> さん、このデータを『整形済み公開日時(formattedPublishedAt)』という名前で渡すから、そっちで使ってね!」
formatter.format(...):
【訳】「その日付データを、このコードの上の方で作っておいた formatter(日付キレイにするマシーン)に入れて、『2024/03/06』みたいに人間が読みやすい形にしてから渡してあげて!」
new Date(article.published_at):
【訳】「Zennから送られてきた公開日時(article.published_at)は 2024-03-06T09:00:00Z みたいな機械用の文字(文字列)で読みにくいから、まずはJavaScriptが理解できる『日付データ(Dateオブジェクト)』に変換して!」
[formatterの定義]
const formatter = new Intl.DateTimeFormat("ja-JP", {
year: "numeric",
month: "short",
day: "numeric",
});
const formatter =
【訳】「今から『formatter(日付の形を整える係)』という名前の箱を作って、そこに完成したマシーンを入れます!」
"ja-JP",
【訳】「まずは大前提として、このマシーンの言語・地域設定は『日本(ja-JP)』に合わせてね!」
(※ これを指定することで、後で出てくる「月」や「日」が、日本のカレンダーの常識に沿って処理されるようになります。)
year: "numeric",
【訳】「『年』の部分は、普通の数字(numeric)として出力してね!」
(結果:2026 など)
month: "short",
【訳】「『月』の部分は、短い形式(short)で出力してね!」
(※ 実はここが一番の魔法です!日本語設定(ja-JP)の場合、ただの数字の「3」や「03」ではなく、「3月」というように漢字付きの自然な表記に自動変換してくれます。英語設定なら「Mar」になります。)
day: "numeric",
【訳】「『日』の部分は、ゼロ埋めしない普通の数字(numeric)で出力してね!」
(結果:06日ではなく、スッキリとした 6 になります。)
必須レベルで必要になります
2. 「どんな具材(Props)が来るか」のルール決め(型定義)
type ArticleForCard = { id: number; title: string; ... };
export type ArticleCardProps = { article: ArticleForCard; ... };
ここではTypeScriptを使って、**「親から渡されるProps(具材)の絶対ルール」**を決めています。
「titleは絶対に文字列(string)で送ってこいよ!」「idは数字(number)な!」と指定することで、親が間違ったデータを渡した瞬間にエラーを出して守ってくれます。
- Propsの受け取りと、ちょっとした技
export function ArticleCard({
article,
formattedPublishedAt,
runId: _runId,
}: ArticleCardProps)
① export function ArticleCard(部品の公開)
function ArticleCard: 「ここから ArticleCard という名前の部品(関数)を作りますよ」という宣言です。
export: 「この部品を、他のファイル(親ページなど)からも呼び出して使えるように公開します」という意味です。これがないと、このファイルの中に引きこもってしまい、外から使えなくなります。
② ({ ... }) の波括弧(JavaScriptの便利技「分割代入」)
Reactのコンポーネントは、親から渡されたデータを props という1つの大きな箱(オブジェクト)として受け取ります。
しかし、引数のところで { }(波括弧)を使うと、「箱ごと受け取るのではなく、届いた瞬間に箱を開けて、中身だけを取り出す」ことができます。これを分割代入と呼びます。
これのおかげで、コンポーネントの中では props. を付けずに、いきなり article や formattedPublishedAt という名前でデータを使えるようになっています。
③ 受け取っている3つのデータ(具材)
親から送られてきた箱の中に入っている、具体的なデータたちです。
article: 記事のタイトル、絵文字、URLのパスなどが入ったメインのデータセットです。
formattedPublishedAt: 「2026/03/09」のように、親側ですでに見やすく整えられた日付の文字列です。(※子コンポーネントに計算させない、役割分担)
runId: _runId: 親から runId というデータも送られてきていますが、「受け取るけど、このカードのデザインの中では使わないからね」という意思表示のために、名前の先頭に _(アンダーバー)をつけて _runId という名前に変更しています(エンジニアのお作法)。
④ : ArticleCardProps(契約書の適用)
最後に、一番外側についているコロン(:)以降の部分です。
これはTypeScriptの機能で、**「今受け取ったこの { article, ... } のデータたちは、上で決めた ArticleCardProps というルール(型)に絶対に従っていますよ!」**と宣言しています。
もし親が formattedPublishedAt を渡し忘れたり、違う名前で渡してきたりすると、この受付窓口で「ルール違反です!」と赤いエラーを出して開発者に教えてくれます。
【まとめ】
つまり、このブロックを人間の言葉に翻訳するとこうなります。
「外から使える ArticleCard コンポーネントを作ります!(export function)
親から渡されたPropsの箱をパカッと開けて({ })、
『article』と『formattedPublishedAt』と『runId』を取り出します!
あ、runIdは使わないので『_runId』って名前にしときますね。
もちろん、これらはすべて事前に決めたルール(ArticleCardProps)通りに受け取っています!(:)」
Discussion