Velite × Next.js でトップページに注目記事セクションを追加する
目次
はじめに
トップページに選んだ記事をピン留めできるセクションがほしかった。「最新記事」フィードではなく、どの記事をハイライトするかを自分でコントロールできるキュレーションセクションだ。記事のフロントマターに featured: true を書くだけで動くのが理想。
実装自体はシンプルだが、この機能を超えて使える3つのパターンが浮かび上がった。
パターン 1: default(false) による安全なスキーマ進化
Velite の posts スキーマに featured フィールドを追加する:
const posts = defineCollection({
schema: s.object({
// ...既存フィールド
featured: s.boolean().default(false), // 追加
}),
});ポイントは default(false) だ。既存ファイルの変更がゼロで済む。すべての既存記事は暗黙的に featured: false として扱われ、featured: true を明示的に書いた記事だけが対象になる。
コンテンツスキーマを拡張するときに覚えておくべきパターンだ。現在の挙動を保つデフォルト値を設定すれば、新しいフィールドは純粋に追加的になる。マイグレーションスクリプトも、ファイルの一括編集も、既存コンテンツが壊れるリスクもない。
クエリ関数は既存の getPublishedPosts() の上に1行足すだけ:
export function getFeaturedPosts(locale?: Locale) {
return getPublishedPosts(locale).filter((post) => post.featured);
}getPublishedPosts() が非公開記事のフィルタと日付降順ソートを済ませているため、両方の挙動を無料で継承する。published: false かつ featured: true の記事は正しく除外される。
パターン 2: MAX_TAGS によるタグ溢れ対策
タグが多い記事はカードレイアウトを壊す。対策:最大 N 件を表示し、超過分は +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>
);
}上限値を定数にまとめることで、変更が1箇所で済む。同じパターンを通常の PostCard にも適用し、挙動を統一した。
パターン 3: 自己非表示セクション
トップページの注目記事セクションは、1件以上の注目記事があるときだけレンダリングされる:
{
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>
);
}つまり、すべての featured: true マーカーを外せばセクションは自動的に消える — コード変更不要、UI が壊れることもない。機能は純粋に追加的だ:コンテンツがあれば存在し、なければ消える。
まとめ
default(false)でスキーマ変更を安全に — 既存コンテンツに影響なし、新フィールドは純粋に追加的。Velite(や他のコンテンツスキーマ)をオプショナルフィールドで拡張するときのパターンとして覚えておく。- MAX_TAGS でレイアウト崩れを防止 — 定数ベースの slice と
+N表示で、タグ数に関係なくカードレイアウトが安定する。 - 条件付きレンダリングで自己管理型セクションを作る —
{items.length > 0 && <Section />}で、機能が自身の表示・非表示を管理する。フィーチャーフラグも個別の設定も不要。
