@shinyaz

Adding a Featured Posts Section to the Home Page with Velite & Next.js

Table of Contents

Overview

I wanted a way to pin selected posts to the home page. Not a "latest posts" feed — a curated section where I control which articles get highlighted. Adding featured: true to a post's frontmatter should be all it takes.

The implementation itself is simple, but it surfaced three patterns that I've found useful beyond this specific feature.

Pattern 1: Safe Schema Evolution with default(false)

Adding a featured field to the Velite posts schema:

velite.config.ts
const posts = defineCollection({
  schema: s.object({
    // ...existing fields
    featured: s.boolean().default(false), // new
  }),
});

The key insight: default(false) means zero migration effort. Every existing post implicitly gets featured: false without touching a single file. Only posts that explicitly set featured: true are affected.

This is a pattern worth remembering whenever you extend a content schema. Use default() values that preserve the current behavior, and new fields become purely additive. No migration scripts, no bulk file edits, no risk of breaking existing content.

The query function is a one-liner on top of the existing getPublishedPosts():

src/lib/posts.ts
export function getFeaturedPosts(locale?: Locale) {
  return getPublishedPosts(locale).filter((post) => post.featured);
}

Since getPublishedPosts() already filters unpublished posts and sorts by date, this inherits both behaviors for free. A post with published: false and featured: true is correctly excluded.

Pattern 2: Tag Overflow with MAX_TAGS

Posts with many tags can break card layouts. The fix: show at most N tags and display a +N indicator for the rest.

src/components/blog/featured-post-card.tsx
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>
  );
}

Extracting the limit into a constant means a single edit updates both the slice and the overflow count. I applied the same pattern to the regular PostCard component to keep behavior consistent.

Pattern 3: Self-Hiding Sections

The featured section on the home page only renders when there's at least one featured post:

src/app/[locale]/page.tsx
{
  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>
  );
}

This means removing all featured: true markers simply hides the section — no code changes needed, no UI breakage. The feature is purely additive: it exists when there's content for it and disappears when there isn't.

Summary

  • default(false) makes schema changes safe — existing content is unaffected, new fields are purely additive. Use this pattern whenever extending Velite (or any content schema) with new optional fields.
  • MAX_TAGS prevents layout breakage — a simple constant-based slice with a +N overflow indicator keeps card layouts stable regardless of how many tags a post has.
  • Conditional rendering creates self-managing sections{items.length > 0 && <Section />} means the feature manages its own visibility. No feature flags, no separate configuration.

Share this post

Shinya Tahara

Shinya Tahara

Solutions Architect @ AWS

I'm a Solutions Architect at AWS, providing technical guidance primarily to financial industry customers. I share learnings about cloud architecture and AI/ML on this blog.

Related Posts