Astro のブログを「トップページ+セクション」構成に作り替える(ツール一覧も追加)
この回でやること
連載「Astro × microCMS でブログを作る」の第6回で、この連載の最終回です。ここまではトップページ(/)がそのまま記事一覧でしたが、ブログ専用ではなく、Webツールなどほかのコンテンツも載せるサイトにする方針にしたので、構成を作り替えます。
/をサイトの紹介と各セクションの新着を並べるトップページにする- ブログを
/blog/以下の1セクションとして整理し、部品をsrc/components/blog/にまとめる - ヘッダーのナビを定数から生成し、表示中のセクションを強調する
- 記事カードの見出しレベルを、置く場所に合わせて切り替えられるようにする
- 2つ目のセクションとして、microCMS で管理するツール一覧(
/tools/)を作る
前回(第5回:連載機能)までで、ブログとしての機能はそろっています。この回のコードは、ブランチ 07-homepage と、その後のツール一覧のコミットの時点のものです(間のブランチ 06-deploy はデプロイの設定なので、連載「Cloudflare Pages で公開する」で扱います)。
作り替えの前後で、URL の役割は次のように変わりました。
URL | 変更前 | 変更後 |
|---|---|---|
| 新着記事(6件) | サイトの紹介、ブログの新着記事(3件)、ツール(3件) |
| 記事一覧 | ブログのトップ(タイトル・説明文つきの記事一覧) |
| なし | ツール一覧 |
説明文を「サイト」と「ブログ」に分ける
それまでの SITE_DESCRIPTION は、ブログの説明文でした。サイト全体の説明文を新しく書き、ブログの説明文は BLOG_DESCRIPTION として分けます。
export const SITE_NAME = 'cyber-blow';
export const SITE_DESCRIPTION =
'実際に手を動かして確かめたIT技術の情報と、Webツールを公開しているサイトです。';
export const BLOG_DESCRIPTION =
'エラー解決・検証・比較・AIツール活用術など、実際に手を動かして確かめたIT技術情報を発信するブログ。';SITE_DESCRIPTION は、共通レイアウトで description を指定しなかったページ(トップページなど)の meta description に使われます。ブログの一覧ページでは BLOG_DESCRIPTION を渡すようにします。
トップページを作る
トップページは、サイト名と紹介文のあとに、セクションごとのブロックを並べる形にしました。
---
import ArticleList from '../components/blog/ArticleList.astro';
import { SITE_DESCRIPTION, SITE_NAME } from '../consts';
import BaseLayout from '../layouts/BaseLayout.astro';
import { getBlogSummaries } from '../lib/microcms';
const LATEST_POST_COUNT = 3;
const posts = (await getBlogSummaries()).slice(0, LATEST_POST_COUNT);
---
<BaseLayout>
<section class="hero">
<h1 class="hero-title">{SITE_NAME}</h1>
<p class="hero-lead">{SITE_DESCRIPTION}</p>
</section>
<!-- セクション(/tools など)を増やしたら、ここに同じ形で並べる -->
<section class="home-section">
<div class="home-section-header">
<h2>ブログ</h2>
<a href="/blog/">すべての記事 →</a>
</div>
<ArticleList posts={posts} headingLevel={3} />
</section>
</BaseLayout>各セクションのブロックは「見出し(h2)」「すべて見るリンク」「新着の一覧」の3点セットです。セクションを増やすときは、この <section> を同じ形で足していきます。headingLevel={3} については後で説明します。
/blog/ をブログのトップにする
/blog/ は、ページタイトルを「記事一覧」から「ブログ」に変え、ブログの説明文を出すようにしました。
<BaseLayout title="ブログ" description={BLOG_DESCRIPTION}>
<h1 class="page-title">ブログ</h1>
<p class="page-lead">{BLOG_DESCRIPTION}</p>
<ArticleList posts={posts.slice(0, POSTS_PER_PAGE)} />
<Pagination currentPage={1} totalPages={totalPages} />
</BaseLayout>2ページ目以降(/blog/page/<n>/)も、タイトルを「ブログ(2ページ目)」のようにそろえ、同じ説明文を渡しています。
部品をセクションごとのフォルダに分ける
ブログでしか使わない部品は、src/components/blog/ に移しました。ほかのセクションを足したときに、どの部品がどこで使われるのかが分かりやすくなります。
src/components/
├── blog/
│ ├── ArticleCard.astro
│ ├── ArticleList.astro
│ ├── Pagination.astro
│ └── SeriesNav.astro
└── tools/ ← 後でツール一覧を作るときに追加
├── ToolCard.astro
└── ToolList.astroページのほうも、src/pages/blog/ と src/pages/tools/ のようにセクションごとに分かれています。新しいセクションを足すときに触る場所は、次の3つです。
src/pages/<セクション>/にページを作る(専用の部品はsrc/components/<セクション>/に置く)src/consts.tsのNAV_ITEMSにリンクを追加する(ヘッダーに表示される)src/pages/index.astroにセクションのブロックを追加する
ヘッダーのナビを定数から作る
ヘッダーのナビは、それまで「記事一覧」へのリンクを1つ直接書いていました。これを定数の配列から作るようにします。
/** ヘッダーのナビゲーション。/tools などのセクションを増やしたらここに追加する */
export const NAV_ITEMS = [{ href: '/blog/', label: 'ブログ' }];<nav class="site-nav">
{
NAV_ITEMS.map((item) => (
<a
href={item.href}
aria-current={Astro.url.pathname.startsWith(item.href) ? 'true' : undefined}
>
{item.label}
</a>
))
}
</nav>表示中のページの URL が、ナビのリンク先で始まっていれば aria-current を付けます。/blog/ だけでなく、記事詳細(/blog/<id>/)や連載の目次(/blog/series/<id>/)を開いているときも「ブログ」が強調されます。
リンク先そのもののページを開いているとは限らないので、値は「ページの集合の中の現在のページ」を表す page ではなく、汎用の true にしています。見た目は CSS で太字にしました。
.site-nav a[aria-current] {
font-weight: 700;
}記事カードの見出しレベルを切り替える
記事カードのタイトルは、もともと h2 でした。記事一覧ページでは「h1 ブログ → h2 記事タイトル」で問題ありませんが、トップページでは「h1 サイト名 → h2 ブログ → 記事タイトル」となるので、カードのタイトルは h3 にしたいところです。
そこで、見出しのレベルを Props で受け取れるようにしました。
interface Props {
post: BlogSummary;
/** 記事タイトルの見出しレベル。ページ内の見出し構造に合わせる */
headingLevel?: 2 | 3;
}
const { post, headingLevel = 2 } = Astro.props;
const Heading = `h${headingLevel}` as const;<Heading class="card-title">{post.title}</Heading>Astro では、タグ名の文字列を変数に入れて、その変数をタグとして書けます(動的タグ)。このとき変数名は大文字で始める必要があります。heading のように小文字にすると、Astro は <heading> という名前の HTML タグとして出力してしまいます。
一覧の部品(ArticleList)は受け取った headingLevel をそのままカードに渡すだけなので、トップページでは headingLevel={3}、記事一覧ページでは指定なし(h2)になります。
参考:Template expressions reference | Astro Docs
ツール一覧のセクションを作る
構成を作り替えたので、2つ目のセクションとしてツール一覧を作ります。これまでに作った Webツールはそれぞれサブドメインで動いていて、/tools/ はそれらへのリンク集です。一覧は microCMS で管理します。
microCMS の tools API
フィールドID | 表示名 | 種類 | 必須 | 備考 |
|---|---|---|---|---|
| ツール名 | テキストフィールド | ✓ | |
| 説明 | テキストエリア | ✓ | 一覧のカードに表示 |
| URL | テキストフィールド | ✓ | ツール本体のURL(サブドメインなど) |
| サムネイル | 画像 | 一覧のカード画像 | |
| 関連記事 | 複数コンテンツ参照( | 使い方などの記事。カードの下にリンクが出る |
ツールの使い方を書いた記事を relatedBlogs で紐づけておくと、ツールのカードから記事へ案内できます。
取得:参照先のフィールドも絞り込む
export type Tool = MicroCMSListContent & {
name: string;
/** 一覧に出す説明 */
description: string;
/** ツール本体のURL */
url: string;
thumbnail?: MicroCMSImage;
/** 使い方などの関連記事 */
relatedBlogs?: Pick<Blog, 'id' | 'title'>[];
};
// 関連記事は本文まで返ってくると重いので、id と title だけに絞る
const TOOL_FIELDS = ['id', 'name', 'description', 'url', 'thumbnail', 'relatedBlogs.id', 'relatedBlogs.title'];
/** 全ツール。orders を指定しないので、microCMS 管理画面での並び順になる */
export function getTools(): Promise<Tool[]> {
return client.getAllContents<Tool>({
endpoint: 'tools',
queries: { fields: TOOL_FIELDS },
});
}ポイントは2つです。
relatedBlogsは記事を参照するフィールドなので、そのまま取得すると、参照先の記事の本文まで返ってきます。fieldsにrelatedBlogs.idのようにドットでつないで書くと、参照先のフィールドも絞り込めます。- 並び順の指定(
orders)は付けていません。microCMS はordersを省略すると、管理画面でのコンテンツの表示順で返します。管理画面で並べ替えた順番が、そのまま一覧の順番になります。
ツールのカード:リンクの中にリンクを入れない
記事のカードは、カード全体を1つの <a> で包んでいました。ツールのカードも同じようにしたいのですが、カードの下に関連記事へのリンクを出すので、そのまま包むと <a> の中に <a> が入ってしまいます。HTML では、<a> の中に別の <a> やボタンなどの対話型の要素を入れることはできません。
そこで、カード本体(画像・ツール名・説明)だけを <a> で包み、関連記事のリンクはその外に置きました。
<article class="card">
<a href={tool.url}>
{/* …省略(サムネイル) */}
<div class="card-body">
<Heading class="card-title">{tool.name}</Heading>
<p class="tool-description">{tool.description}</p>
</div>
</a>
{
relatedBlogs.length > 0 && (
<div class="tool-related">
<p class="tool-related-heading">関連記事</p>
<ul>
{relatedBlogs.map((blog) => (
<li>
<a href={`/blog/${blog.id}/`}>{blog.title}</a>
</li>
))}
</ul>
</div>
)
}
</article>それまでカードの枠線や影は、カードの中の <a> に付けていました。これを外側の .card に移し、<a> の外にある関連記事も枠の中に収まるようにしています。
.card {
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius);
transition: box-shadow 0.15s;
}
.card:hover {
box-shadow: 0 4px 16px rgb(0 0 0 / 0.08);
}
.card > a {
display: block;
color: inherit;
text-decoration: none;
}参考:<a>: アンカー要素 - HTML | MDN
ページとナビへの追加
/tools/ のページは、ブログのトップと同じ形です。
---
import ToolList from '../../components/tools/ToolList.astro';
import { TOOLS_DESCRIPTION } from '../../consts';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getTools } from '../../lib/microcms';
const tools = await getTools();
---
<BaseLayout title="ツール" description={TOOLS_DESCRIPTION}>
<h1 class="page-title">ツール</h1>
<p class="page-lead">{TOOLS_DESCRIPTION}</p>
<ToolList tools={tools} />
</BaseLayout>ナビには、配列に1行足すだけです。
/** ヘッダーのナビゲーション。セクションを増やしたらここに追加する */
export const NAV_ITEMS = [
{ href: '/blog/', label: 'ブログ' },
{ href: '/tools/', label: 'ツール' },
];トップページには、ツールの先頭3件を出すブロックを足しました。ツールが0件のときは、ブロックごと出しません。
{
tools.length > 0 && (
<section class="home-section">
<div class="home-section-header">
<h2>ツール</h2>
<a href="/tools/">すべてのツール →</a>
</div>
<ToolList tools={tools} headingLevel={3} />
</section>
)
}一方、/tools/ のページでは、0件のときに「ツールは準備中です。」と表示します。トップページでは空のセクションを見せず、一覧ページでは何もない理由を伝える、という使い分けです。
寄り道:iCloud の競合コピーがコミットに混ざった
この回の作り替えのコミットに、意図していない .node-version 2 と public/robots 2.txt というファイルが入っていました。
プロジェクトを iCloud Drive で同期しているデスクトップのフォルダに置いていたため、同期の競合で「〜 2」という名前のコピーができていたのが原因です。次のコミットで削除しました。
public/ に置いたファイルは、ビルドでそのまま出力先にコピーされます。気づかずにデプロイしていれば、robots 2.txt も公開サイトに置かれていたことになります。
デスクトップや書類フォルダを iCloud で同期している Mac では、次のどちらかをおすすめします。
- 開発用のフォルダを、iCloud で同期しない場所に置く
- コミットの前に
git statusで、身に覚えのないファイルが増えていないか確認する
まとめ
/はサイトの紹介とセクションごとのブロックを並べるトップページにし、ブログは/blog/の1セクションにした- 部品とページをセクションごとのフォルダに分け、セクションを足すときに触る場所を3つに絞った
- ナビは定数の配列から作り、URL の前方一致で表示中のセクションに
aria-currentを付ける - 見出しレベルは Astro の動的タグで切り替え、ページ内の見出しの階層を崩さない
- ツール一覧は microCMS で管理し、参照先のフィールドは
fieldsのドット記法で絞る。カード内のリンクは入れ子にしない
連載「Astro × microCMS でブログを作る」はこの回で終わりです。作ったサイトを Cloudflare Pages で公開する手順と、そこでハマったことは、連載「Cloudflare Pages で公開する」の第1回から書いています。