Velite × Next.js でトップページに注目記事セクションを追加する
目次
はじめに
ブログのトップページに「注目記事」セクションを追加しました。記事のフロントマターに featured: true を書くだけで、選んだ記事をトップページのグリッドにピン留めできます。
実装の流れは次の通りです。
- Velite の posts スキーマに
featuredフィールドを追加 getFeaturedPosts()ヘルパー関数を追加FeaturedPostCardコンポーネントを作成- タグが多い場合のオーバーフロー対応
- トップページへの組み込み
Veliteスキーマへのフィールド追加
Velite はMDXファイルのフロントマターをスキーマで定義します。velite.config.ts の posts スキーマに featured を追加します。
// velite.config.ts
const posts = defineCollection({
schema: s.object({
// ...既存フィールド
published: s.boolean().default(true),
featured: s.boolean().default(false), // 追加
// ...
}),
});default(false) を指定することで、既存の記事ファイルは一切変更不要です。featured: true を書いた記事だけが対象になります。
getFeaturedPosts() の実装
src/lib/posts.ts に既存の getPublishedPosts() を利用したフィルタ関数を追加します。
export function getFeaturedPosts(locale?: Locale) {
return getPublishedPosts(locale).filter((post) => post.featured);
}getPublishedPosts() の内部で published: true のフィルタと日付降順ソートが済んでいるため、featured のフィルタを追加するだけです。未公開の記事(published: false)がたまたま featured: true になっていても除外されます。
FeaturedPostCard コンポーネントの作成
注目記事は通常の記事リストとは視覚的に区別したいため、専用の FeaturedPostCard コンポーネントを用意します。既存の PostCard(縦リスト用)とは別に、カード形式のデザインにしました。
// src/components/blog/featured-post-card.tsx
const MAX_TAGS = 3;
export function FeaturedPostCard({ title, description, date, permalink, categories, tags, locale }) {
return (
<article className="group rounded-lg border border-border bg-muted/30 p-4 hover:bg-muted/50 transition-colors">
<Link href={permalink} className="block">
<h3 className="font-semibold tracking-tight group-hover:underline">
{title}
</h3>
{description && (
<p className="mt-1 text-sm text-muted-foreground line-clamp-2">
{description}
</p>
)}
</Link>
<div className="mt-2 flex flex-wrap items-center gap-2">
<time dateTime={date} className="text-xs text-muted-foreground">
{formatDate(date, locale)}
</time>
{/* カテゴリ・タグバッジ */}
</div>
</article>
);
}PostCard と別コンポーネントにした理由は2つです。
- 視覚的区別:
rounded-lg border bg-muted/30でカード感を出す - 独立した上限値:後述のタグ表示上限をコンポーネントごとに管理する
タグオーバーフロー対応
タグが多い記事でカードレイアウトが崩れないよう、表示数を最大3件に制限し、超過分は +N で示します。
const MAX_TAGS = 3;
{tags.slice(0, MAX_TAGS).map((tag) => (
<TagBadge key={tag} slug={tag} locale={locale} />
))}
{tags.length > MAX_TAGS && (
<span className="text-xs text-muted-foreground">+{tags.length - MAX_TAGS}</span>
)}上限値を MAX_TAGS 定数にまとめることで、変更が1箇所で済みます。同様の対応を通常の PostCard にも適用し、両方で上限3件に統一しました。
トップページへの組み込み
src/app/[locale]/page.tsx で注目記事を取得し、件数が1件以上の場合だけセクションを表示します。
const featuredPosts = getFeaturedPosts(locale);
{featuredPosts.length > 0 && (
<section className="mt-8 md:mt-12">
<h2 className="text-xl font-semibold mb-4">{t.home.featuredPosts}</h2>
<div className="grid gap-3 sm:grid-cols-2">
{featuredPosts.map((post) => (
<FeaturedPostCard key={post.permalink} {...post} locale={locale} />
))}
</div>
</section>
)}sm:grid-cols-2 でスモール以上は2カラムになります。注目記事がゼロの場合はセクション自体が非表示になるため、記事を減らしても UI が壊れません。
UI文字列は src/lib/i18n.ts に追加します。
home: {
featuredPosts: "注目記事", // ja
// ...
}
// en
home: {
featuredPosts: "Featured",
// ...
}使い方
注目させたい記事のフロントマターに1行追加するだけです。
---
title: "記事タイトル"
date: 2026-03-07
published: true
featured: true # この行を追加
categories:
- programming
---featured: true を書かない記事はデフォルト false として扱われるため、既存の記事に影響はありません。
まとめ
Velite のスキーマ拡張はフィールドを1行追加するだけで既存コンテンツへの影響がゼロという点が気に入っています。コンポーネントを分けたことでタグ上限やカードスタイルをそれぞれ独立して調整できるため、今後の変更もしやすい構造になりました。
