目次
概要
前回の記事Pagefindを使った検索機能の実装で検索機能を扱いました。今回の記事では、ブログ全体のレイアウトとコンポーネントアーキテクチャについて説明します。
Astroでは、ページの共通要素(Head、Navbar、Footerなど)をコンポーネントとして作成し、レイアウトで組み合わせることで一貫した構造を維持します。Jekyllの_layouts/と_includes/ディレクトリに対応する構造ですが、AstroではTypeScript型サポートとJSX式のおかげで、はるかに直感的に管理できます。
Astroコンポーネントの基本構造
Astroコンポーネント(.astroファイル)は大きく2つの部分に分かれます。上部のfrontmatter領域と下部のテンプレート領域です。
---
// 1. Frontmatter(サーバーサイドJavaScript/TypeScript)
// --- の間に書いたコードはビルド時にサーバーで実行されます。
import Component from './Component.astro';
interface Props {
title: string;
}
const { title } = Astro.props;
const data = await fetchData();
---
<!-- 2. テンプレート(HTML + JSX式) -->
<!-- frontmatter以下のHTMLはビルド時に静的HTMLに変換されます。 -->
<div>
<h1>{title}</h1>
<Component />
</div>
- Frontmatter(
---の間):サーバーサイドで実行されるコードです。データの取得、props処理、ロジック実行などを担当します。ここで書いたコードはブラウザに送信されません。 - Template:HTMLとJSX式を使ってUIをレンダリングします。
{変数名}でfrontmatterで定義した値を出力できます。ビルド時に静的HTMLに変換されます。
この構造がJekyllのLiquidテンプレートと最も大きく異なる点です。Liquidではテンプレート内にロジックを混在させる必要がありましたが、Astroではロジックとマークアップがきれいに分離されます。
BaseLayout — すべてのページの骨格
BaseLayout.astroはブログのすべてのページを包む最上位レイアウトです。HTMLドキュメントの基本構造(<!DOCTYPE html>、<html>、<head>、<body>)を定義し、共通コンポーネントを配置します。
---
// src/layouts/BaseLayout.astro
import Head from '../components/Head.astro';
import Navbar from '../components/Navbar.astro';
import Footer from '../components/Footer.astro';
import type { Lang } from '../i18n/translations';
interface Props {
title: string;
description: string;
image?: string;
lang: Lang;
permalink: string;
isPost?: boolean;
date?: string;
categoryPosts?: CategoryPostItem[];
noindex?: boolean;
category?: string;
isHome?: boolean;
isCategory?: boolean;
}
const { title, description, image, lang, permalink, isPost, date,
categoryPosts, noindex, category, isHome, isCategory } = Astro.props;
---
<!DOCTYPE html>
<html lang={lang}>
<Head
title={title} description={description} image={image}
lang={lang} permalink={permalink} isPost={isPost} date={date}
categoryPosts={categoryPosts} noindex={noindex} category={category}
isHome={isHome} isCategory={isCategory}
/>
<body>
<Navbar lang={lang} />
<slot /> <!-- ページごとのコンテンツが入る位置 -->
<Footer lang={lang} permalink={permalink} />
</body>
</html>
Propsがかなり多いですが、これはHead.astroでページの種類に応じて異なるSEOタグ(JSON-LDなど)を出力する必要があるためです。各Propsの役割は以下の通りです。
<Head>:SEOメタタグ、JSON-LD、スタイル、スクリプトなど<head>領域全体を管理します。以前のポストSEO実装で詳しく解説しました。<Navbar>:ナビゲーションバーと言語切り替え機能を提供します。<slot />:この位置に各ページ固有のコンテンツが挿入されます。Jekyllの{{ content }}に相当します。<Footer>:フッターとソーシャル共有ボタンを表示します。
PostLayout — ブログポスト専用レイアウト
PostLayout.astroはブログポストにのみ使用されるレイアウトです。BaseLayoutを包みつつ、ポストに必要な追加要素(ヒーローヘッダー、パンくず、広告、関連ポスト、コメントなど)を提供します。
---
// src/layouts/PostLayout.astro
import BaseLayout from './BaseLayout.astro';
import Breadcrumbs from '../components/Breadcrumbs.astro';
import Comments from '../components/Comments.astro';
import TopAds from '../components/ads/TopAds.astro';
import BottomAds from '../components/ads/BottomAds.astro';
import LeftAds from '../components/ads/LeftAds.astro';
import RightAds from '../components/ads/RightAds.astro';
// ...
interface Props {
title: string;
description: string;
image?: string;
lang: Lang;
permalink: string;
category: string;
date: string;
comments: boolean;
}
---
<BaseLayout title={title} description={description} ...>
<!-- ヒーローヘッダー:ポストタイトルと背景画像 -->
<header class="masthead" style={image ? `background-image: url('${image}')` : undefined}>
<div class="overlay"></div>
<div class="container">
<h1>{title}</h1>
<div class="heading-date">{formattedDate}</div>
<h2 class="subheading">{description}</h2>
</div>
</header>
<TopAds />
<div class="container container-contents">
<div class="row">
<Breadcrumbs items={breadcrumbItems} />
</div>
<div class="row">
<LeftAds />
<article class="col-lg-8 col-md-10 mx-auto" data-pagefind-body>
<slot /> <!-- マークダウンコンテンツがレンダリングされる位置 -->
</article>
<RightAds />
</div>
<BottomAds />
<!-- 同じカテゴリの関連ポスト一覧 -->
<div class="post-list-container">...</div>
<Comments enabled={comments} />
</div>
</BaseLayout>
ポストページを開くと、最上部に背景画像が敷かれたヒーローヘッダーが表示され、その下にパンくずナビゲーション、本文コンテンツ、関連ポスト一覧、コメントの順で表示されます。広告は上下左右の4箇所に配置されます。
関連ポスト一覧データ
ポスト下部には同じカテゴリの他のポスト一覧が表示されます。このデータはビルド時にgetCollection()で取得してJSONとしてクライアントに渡します。
// src/layouts/PostLayout.astroのfrontmatter領域
const allPosts = await getCollection(
'blog',
(post) => post.data.lang === lang && post.data.published !== false
);
const catPosts = allPosts.filter((p) => p.data.category === category);
const sortedPosts = catPosts.sort(
(a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);
このデータをクライアントJavaScriptで受け取り、ページネーションとポスト内検索機能を実装します。別途のAPI呼び出しなしにビルド時にすべてのデータが準備されるのが静的サイトの利点です。
Hit counter
各ポストの閲覧数はmyhits.vercel.app APIを使用して表示します。別途のバックエンドなしに外部サービスで簡単に実装できます。
主要コンポーネントの紹介
ブログで使用している主要コンポーネントを簡単に紹介します。
Navbar.astro
ナビゲーションバーと言語切り替え機能を提供するコンポーネントです。Home、Latest、Category、Apps、Contactメニューとともに、現在のページの他言語バージョンに移動できる言語切り替えボタンを含みます。
<!-- src/components/Navbar.astro -->
<nav>
<a href={getLocalizedPath('/', lang)}>Home</a>
<a href={getLocalizedPath('/latest/', lang)}>Latest</a>
<!-- 言語切り替え:現在のページの他言語バージョンに移動 -->
{Object.entries(languages).map(([code, name]) => (
<a href={getLocalizedPath(currentPath, code)}>{name}</a>
))}
</nav>
CategoryCard.astro
ホームページで各カテゴリをカード形式で表示するコンポーネントです。カテゴリ画像、タイトル、説明、「詳しく見る」リンクを含みます。
---
// src/components/CategoryCard.astro
interface Props {
slug: string;
image: string;
title: string;
description: string;
permalink: string;
seeMore: string;
lang: Lang;
}
---
PostCard.astro
ポスト一覧(最新記事ページなど)で各ポストをカード形式で表示するコンポーネントです。タイトル、説明、日付、カテゴリ情報を含みます。
Breadcrumbs.astro
現在のページの位置を階層構造で表示するナビゲーションコンポーネントです。ユーザーが上位ページに簡単に移動できるようにします。
Home > Astro > レイアウトとコンポーネントアーキテクチャ
SponsorButtons.astro
スポンサーボタンを表示するコンポーネントです。ポストのヒーローヘッダーと本文下部の2箇所に配置して、記事が役に立った時にスポンサーできるリンクを提供します。
AppPromo.astro
自作アプリを宣伝するコンポーネントです。言語に応じて異なるコンテンツを表示し、韓国語ページではReact Native関連書籍の宣伝も含まれます。
動的ルーティングでポストページを生成する
ブログの各ポストページは一つ一つ手動で作るのではなく、[...path].astroというcatch-allルーティングファイルで自動的に生成されます。getStaticPaths関数ですべてのポストのURLを返すと、ビルド時にそれぞれに対する静的HTMLファイルが作成されます。
---
// src/pages/ko/[...path].astro
import type { GetStaticPaths } from 'astro';
import PostLayout from '../../layouts/PostLayout.astro';
import { getCollection, render } from 'astro:content';
import { getLocalizedPath } from '../../i18n/translations';
const lang = 'ko';
export const getStaticPaths = (async () => {
const posts = await getCollection('blog', (post) =>
post.data.lang === 'ko' && post.data.published !== false
);
return posts.map((post) => {
const pathStr = post.data.permalink.replace(/^\//, '').replace(/\/$/, '');
return {
params: { path: pathStr },
props: { post },
};
});
}) satisfies GetStaticPaths;
const { post } = Astro.props;
const { Content } = await render(post);
---
<PostLayout
title={post.data.title}
description={post.data.description}
image={post.data.image}
lang={lang}
permalink={getLocalizedPath(post.data.permalink, lang)}
category={post.data.category}
date={post.data.date.toISOString()}
comments={post.data.comments}
>
<Content />
</PostLayout>
例えばpermalink: /astro/installation/の韓国語ポストがある場合、params.pathがastro/installationとなり、最終URLが/ko/astro/installation/として生成されます。日本語はsrc/pages/[...path].astro、英語はsrc/pages/en/[...path].astroで同じ構造で処理します。
Jekyll Liquidテンプレートとの比較
JekyllからAstroに移行して、テンプレートの書き方が完全に変わりました。以下の表で主な違いを比較します。
| 項目 | Jekyll (Liquid) | Astro |
|---|---|---|
| レイアウト継承 | layout: post frontmatter | importと<slot /> |
| 変数出力 | {{ "{{ page.title " }}}} | {title}(JSX式) |
| 条件付きレンダリング | {% raw %}{% if %}{% endraw %} | {condition && <Element />} |
| ループ | {% raw %}{% for %}{% endraw %} | {items.map(item => <Element />)} |
| コンポーネント取り込み | {% raw %}{% include component.html %}{% endraw %} | import + <Component /> |
| 型安全性 | なし | TypeScript interface Props |
| データアクセス | site.posts, page.title | getCollection(), Astro.props |
個人的に最も満足している部分はTypeScript型サポートです。JekyllではPropsにタイプミスがあっても何の警告もなく空の値が出力されていましたが、Astroではinterface Propsで型を定義すればIDEで自動補完と型チェックを受けられます。コンポーネントが増えるほどこの違いが大きく感じられます。
完了
今回の記事では、Astroブログのレイアウトとコンポーネントアーキテクチャを見てきました。
BaseLayout.astro:Head、Navbar、Footerを包む最上位レイアウトPostLayout.astro:ヒーローヘッダー、広告、関連ポスト、コメントなどを含むポスト専用レイアウト- Navbar、Breadcrumbs、CategoryCard、PostCard、SponsorButtons、AppPromoなど様々なコンポーネント
[...path].astrocatch-all動的ルーティングでポストページを自動生成- Jekyll Liquidテンプレートに比べてTypeScript型安全性と直感的なJSX構文の利点
次の記事GitHub Pagesデプロイでは、ビルドされたサイトをGitHub Pagesにデプロイする方法を解説します。
シリーズ案内
このポストはJekyllからAstroへのマイグレーションシリーズの一部です。
- JekyllからAstroへマイグレーションした理由
- Astroのインストールとプロジェクト構成
- Content Collectionsとマークダウンマイグレーション
- 多言語(i18n)実装
- SEO実装
- 画像最適化 — カスタムrehypeプラグイン
- コメントシステム(Utterances)
- 広告連携(Google AdSense)
- Pagefindを利用した検索実装
- レイアウトとコンポーネントアーキテクチャ
- GitHub Pagesデプロイ
- ソーシャル共有自動化スクリプト
- トラブルシューティングとヒント
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Dekuが開発したアプリを使ってみてください。Dekuが開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。