🙆

1ページだけのシンプルな記事レイアウトをNext.jsとAIで作る

に公開

この記事では1ページだけのシンプルな記事レイアウトをNext.jsで構築、そして構築作業にはAIを利用します。シンプルな記事レイアウトは多くのWebコンテンツの基本となるので、応用が効くテクニックです。

題材として北海道の札幌を旅行したという架空の旅行記事を作成し、写真も挿入、ちょっとした装飾を追加して完成です。

Next.js初期構築とAIコーディングツール立ち上げ

まずは以下のコマンドでNext.jsの初期構築を行います。

npx create-next-app

https://nextjs.org/docs/app/getting-started/installation

完了したらAIコーディングツールを立ち上げます。私はGemini CLIを利用しましたが、Claude CodeでもCursorでも、お好きなものを選んでいただいて構いません。

Next.js初期構築とGemini CLI立ち上げ

最初に文字だけのコンテンツを生成

まずは簡単にするため、文字だけのコンテンツを作成しましょう。写真や装飾は後で追加します。

準備として、create-next-appにて自動作成されたpage.module.csspage.tsx内のデモを消去します。

プロンプト
@app/page.module.css と @app/page.tsx 全てをクリアして
"hello world"とだけ表示してください。

早速、文字コンテンツ生成です。

プロンプト
4000文字程度の架空の札幌旅行ブログ記事を@app/page.tsxに生成してください。
CSSは一切追加しないでください。

CSSによる装飾をほぼ適用していないので(global.cssのみ適用)、横いっぱいに広がった画面に文字を詰め込んだ窮屈なレイアウトになっています。

文字コンテンツのみ、CSSによる装飾なし

レイアウトを改善しましょう。

プロンプト
典型的な記事コンテンツ用のCSSを@app/page.module.cssに追加して、
@app/page.tsxで利用してください。

ただし、@app/page.module.css内のクラスは一番外側のクラス、
例えば`.container {...}`だけ命名して、それ以外は`.container > p {...}`,
`.container > h2 {...}`のように、子要素に対して要素種別ごとに適用する形で
記載してください
生成されたCSS
page.module.css
.container {
  max-width: 800px;
  margin: 0 auto;
  padding: 40px 20px;
  line-height: 1.8;
  font-size: 1.1rem;
  color: #333;
}

.container > h1 {
  font-size: 2.5rem;
  text-align: center;
  margin-bottom: 1.5em;
  line-height: 1.4;
}

.container > h2 {
  font-size: 2rem;
  margin-top: 2.5em;
  margin-bottom: 1.5em;
  padding-bottom: 0.5em;
  border-bottom: 2px solid #eee;
}

.container > p {
  margin-bottom: 1.5em;
}

.container > hr {
  border: 0;
  height: 1px;
  background-color: #ddd;
  margin: 3em 0;
}

文章を真ん中に寄せて左右に余白を設け、縦方向に適切な行間を空けただけでもかなり読みやすくなりました。

https://youtu.be/Lm3KWvntrTA

無料の写真素材サイト「ぱくたそ」より画像をダウンロード

次は記事に写真を追加していきます。

今回は写真素材サイト「ぱくたそ」さんから画像をダウンロードして使用させていただきました。ぱくたそ内で「札幌」と検索すると、以下のように札幌の美しい町並みの写真が多数見つかります。

ぱくたそで「札幌」と検索

ご自身でぱくたその写真を利用されたい場合、利用規約などを十分確認の上でご利用が問題ないかを検討して下さい。

https://www.pakutaso.com/userpolicy.html

Next.jsの<Image>コンポーネントに必要な属性

Next.jsでは画像を表示する際、HTML標準の<img>タグではなく、Next.jsによる最適化が効いた<Image>コンポーネントを利用します。

https://nextjs.org/docs/app/getting-started/images

<Image>コンポーネント利用時には以下の属性を指定します。

import Image from 'next/image'
 
<Image
  src="/AME963_sappro_TP_V.png"
  width={800}
  height={500}
  alt="札幌の夜景"
/>
Next.jsの<Image>でwidthとheightが必要な理由

Next.js公式ドキュメントでは<Image>コンポーネントにwidthheightが必須であると記載されています。

You must set both width and height properties unless:

  • The image is statically imported.
  • The image has the fill property

試しにwidthheightを取り除いてみると、ランタイムエラーになってしまいます。

widthとheigtがないと、Next.jsのImageコンポーネントはランタイムエラーを発生させる

なぜこうなっているか?というと、Next.jsの<Image>コンポーネントは、画像に必要な最適化を必ず適用するするように作られているためで、その最適化の中にはwidthheightを指定しなければ適用できないものがあるからです。

実は、このwidthheight指定による最適化は、HTML標準の<img>要素でも指定することが近年のベスト・プラクティスになっていて、その理由の解説はこちらのブログで丁寧に解説されています。

https://www.mizdra.net/entry/2020/05/31/192613

ちなみに、widthheightは「レンダリング時」の横幅と縦の高さではなく、「画像ファイルそのもの」のwidthheightを指定します。レンダリング時の横幅と縦の高さはモバイルやPCなど環境によって変わってしまいますし、widthheightを指定するのはアスペクト比を計算するためなので、比率さえあっていれば良いのです。

Next.jsの<Image>でaltが必要な理由

これはアクセシビリティの観点から必須とされています。

altを指定しないと、TypeScriptエラーになってしまいます。

を指定しないと、、Next.jsのImageコンポーネントはTypeScriptエラーを発生させる

AIチャットアプリで画像から属性を抽出

以上より、<Image>コンポーネントには画像だけではなく、srcwidthheightalt属性が必要なことがわかったので、AIチャットアプリを使って画像からそれらを抽出します。

ここでAIチャットアプリ(ChatGPT, Claude, Geminiなど)を使い、AIコーディング・ツールは使わないのは(Cursor, Claude Code, Gemini CLIなど)、その方が写真を扱いやすいからです。本記事ではWeb版のGeminiを使いました。

https://gemini.google.com/app

プロンプト
アップロードした写真から、以下の情報を含むJSON配列を生成してください。
`src`(ファイル名)、`width`(幅px)、`height`(高さpx)、`alt`属性。
AIから出力されたJSON配列
AIからの出力
[
  {
    "src": "BOB20614z004_TP_V.webp",
    "width": 1920,
    "height": 1440,
    "alt": "夜の札幌の繁華街、特にすすきの周辺のビル壁面に設置されたネオンサインや大きな広告看板(サッポロビール、PROMISEなど)が輝く様子。"
  },
  {
    "src": "AME963_sappro_TP_V.jpg",
    "width": 1024,
    "height": 576,
    "alt": "高層ビルが立ち並ぶ札幌の夜景を上空から捉えた俯瞰ショット。青みがかった照明と道路の街灯がグリッド状に広がる大都市の夜景。"
  },
  {
    "src": "BOB20614z006_TP_V.webp",
    "width": 1920,
    "height": 1440,
    "alt": "青空の下、高台に立つウィリアム・スミス・クラーク博士の銅像。右手を遠くに指し、「BOYS BE AMBITIOUS(少年よ、大志を抱け)」と刻まれた台座が見える。(さっぽろ羊ヶ丘展望台)"
  },
  {
    "src": "rinmon11703_TP_V.webp",
    "width": 1920,
    "height": 1440,
    "alt": "札幌の歴史的建造物である**札幌市時計台**(旧札幌農学校演武場)の屋根と、シンボルである時計塔部分をクローズアップした写真。白い木造の外壁と赤い屋根が特徴。"
  },
  {
    "src": "BOB96P8110175_TP_V4.webp",
    "width": 1080,
    "height": 1440,
    "alt": "青空を背景に、**さっぽろテレビ塔**を見上げるアングルで捉えた写真。鉄骨のトラス構造が特徴的で、展望台下のビル部分も写っている。"
  },
  {
    "src": "rinmon11701_TP_V.jpg",
    "width": 1024,
    "height": 576,
    "alt": "緑豊かな前庭越しに、**北海道庁旧本庁舎**(赤れんが庁舎)を正面から捉えた写真。特徴的な赤レンガ造りと緑色のドーム型屋根が見える。"
  },
  {
    "src": "BOB20614z000_TP_V.jpg",
    "width": 1024,
    "height": 768,
    "alt": "**大通公園**の景色。手前には芝生や花壇、噴水があり、奥には札幌のランドマークである**さっぽろテレビ塔**がそびえ立っている。"
  }
]

https://youtu.be/ibBFPN6Lrhg

AIチャットアプリで続けて文章を生成

先程のチャットアプリのウインドウを開いたまま、次のプロンプトを入力します。

プロンプト
アップロードした画像と上記のJSON配列を用いて、markdown形式で4000文字程度の札幌の
旅行ブログを生成してください。ただし写真部分は、以下のような形でReactコンポーネントを
直接記述する形式にしてください。

<Image
  src="/AME963_sappro_TP_V.webp"
  width={800}
  height={500}
  alt="札幌の夜景"
/>

同じチャットを継続することで、アップロードした画像と、先程生成されたJSON配列をうまく利用できます。

https://youtu.be/MbUoutQAMDY

AIコーディングエージェントで記事部分のJSXを生成

それでは以下のプロンプトをGemini CLIに渡して、記事コンテンツを生成します。

プロンプト
@app/page.tsxの既存のコンテンツはクリアし、以下のmarkdownをもとに、
@app/page.tsxに記事を生成してください。

markdownの画像部分は、以下のようにNext.jsの<Image>コンポーネントの形式に
なっているので、そのまま利用してください。

<Image
  src="/AME963_sappro_TP_V.webp"
  width={800}
  height={500}
  alt="札幌の夜景"
/>

markdownは以下のとおりです。

...(この部分にmarkdown貼り付け)

↑の...(この部分にmarkdown貼り付け)となっている部分に、先程のステップで生成したmarkdownを貼り付けてください。

https://youtu.be/xrR8W4WMJ90

上記の動画のとおり、写真付きの記事が生成されましたが、写真が大きすぎるようです。

プロンプト
写真が大きすぎます。CSSで記事の横幅いっぱいに収まるようにしてください。

上記のプロンプトで以下のCSSが挿入されました。

.container img {
  max-width: 100%;
  height: auto;
  display: block;
  margin: 1.5em auto;
}

https://youtu.be/-xqldQK0W2A

無事、写真サイズがちょうどよくなりました。

CSSの基本:Normal Flow

ここでAIによる作成から一旦離れて、Webページのレイアウトの基本である「Normal Flow(または単に Flow Layout)」を紹介します。現代のCSSにはFlexboxやGridといった強力なレイアウト手法がありますが、それらもNormal Flowを理解していると、より深く使いこなすことができます。

Normal Flowとは、CSSで特別なレイアウト指定をしなかった場合に、HTML要素がどう配置されるかを定めた基本ルールです。とてもシンプルで、HTMLのソースコードに書かれた順に、要素が画面の上から下へと自然に「流れる」ように配置されていきます。

簡単にするためにあえて単純化すると、Normal Flowは:

  • <h1>, <h2>, <h3>, <h4>, <h5>, <h6>による見出しと
  • <p>による段落

を組み合わせ、文章をレイアウトします。

これらの要素は横幅いっぱいに広がるので、縦の間隔をmarginによって制御するのが、基本であるNormal Flowの、さらに基本の考え方です。

https://youtu.be/8DqwQNCJxFw

記事コンテンツには<h1><h6><p>だけではなく、<img>なども含むことが多いですが、<h1><h6><p>によるレイアウトを覚えるのが基本です。

<h2>, <h3>, <strong>の装飾

再びAIによる作成作業に戻り、写真以外の文字部分を装飾していきましょう。<h2>は現状こう表示されています。

h2 initial

これを装飾してみます。

プロンプト
h2にCSSのborderを使って青い下線を引いてください。
page.module.css
.container > h2 {
   ...
-  border-bottom: 2px solid #eee;
+  border-bottom: 8px solid #0056b3;
}

h2 updated

下線が長すぎるので、文字にかかる部分だけ下線を引くように変更しましょう。

page.tsx
-<h2>街の中心:大通公園から見上げる札幌の象徴</h2>
+<h2>
+  <span>街の中心:大通公園から見上げる札幌の象徴</span>
+</h2>
page.module.css
.container > h2 {
   ...
-  border-bottom: 8px solid #0056b3;
}

+.container > h2 > span{
+  border-bottom: 8px solid #0056b3;
+}

こうなりました。

h2 updated again

もう一段階装飾を調整しましょう。

プロンプト
h2の下線を文字に重なるようにしてください。
h2の左隣にCSSの`:before`を使って四角形の装飾をおいてください。

プロンプトの結果そのままではなく、微調整を加えてこのようにしました。

結果のCSS
page.module.css
.container > h2 {
  font-size: 2rem;
  margin-top: 2.5em;
  margin-bottom: 1.5em;
  display: flex;
  align-items: center;
}

.container > h2::before {
  content: "";
  display: inline-block;
  width: 12px; /* Example size */
  height: 30px; /* Example size */
  background-color: #3d77b6ff; /* Solid blue */
  margin-right: 10px; /* Space between box and text */
  vertical-align: middle;
}

.container > h2 > span {
  position: relative;
  line-height: 1;
}

/* h2 の下線を:afterで表現 */
.container > h2 > span::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 4px; /* Move up to overlap with text */
  width: 100%;
  height: 6px;
  background-color: rgba(0, 111, 230, 0.3); /* semi-transparent blue */
}

h2 final

さらに<h3><strong>も装飾します。

プロンプト
h3にCSSで下線を引くことはできますか?色は rgba(0, 111, 230, 0.3)
h2と同様に、h3の内部にspanを挿入し、そのspanに対して下線を引くように変えてください
プロンプト
strongに下線を引くことはできますか?色はrgba(0, 111, 230, 0.3)
結果のCSS
page.module.css
.container > h3 {
  margin-bottom: 1em; /* Space below h3 */
}

.container > h3 > span {
  border-bottom: 4px solid rgba(0, 111, 230, 0.3); /* Underline for h3 span */
}

.container strong {
  position: relative;
  border-bottom: none; /* Remove border-bottom from strong itself */
}

.container strong::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0.2em; /* Adjust this value to control overlap */
  width: 100%;
  height: 4px; /* Underline thickness */
  background-color: rgba(0, 111, 230, 0.3); /* semi-transparent blue */
}

<h2><h3><strong>と統一感のある装飾ができました。

h3 and strong

まとめ

ここまで、AIで記事コンテンツを作成し、そのレイアウトや装飾手法を学びながら、CSS Normal FlowやNext.jsにおける<Image>コンポーネントといった話題を学んできました。

写真と文章の組み合わせは、現代のWebコンテンツの大半を占めます。そのレイアウト手法の基本を理解するのに、本記事が役に立てば幸いです。

Discussion