shinyaz.com

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

Table of Contents

Overview

I added a "Featured" section to the home page that lets me pin selected posts in a grid. Adding featured: true to a post's frontmatter is all it takes to include it.

Here's the full implementation broken down into steps:

  1. Extend the Velite posts schema with a featured field
  2. Add a getFeaturedPosts() helper
  3. Build a FeaturedPostCard component
  4. Handle tag overflow
  5. Wire everything up on the home page

Extending the Velite Schema

Velite defines frontmatter shape via schemas. Adding a field to velite.config.ts is straightforward:

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

Using default(false) means existing posts need no changes — only posts that explicitly set featured: true are affected.

Adding getFeaturedPosts()

In src/lib/posts.ts, a one-liner on top of the existing getPublishedPosts() does the job:

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

Because getPublishedPosts() already filters out unpublished posts and sorts by date descending, this function inherits both behaviors for free. A post with published: false and featured: true will correctly be excluded.

Building FeaturedPostCard

Featured posts needed a visually distinct card — rounded border, subtle background — rather than the plain border-bottom style used in the regular post list. A dedicated component keeps the two concerns independent:

// 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>
        {/* category and tag badges */}
      </div>
    </article>
  );
}

Keeping this separate from PostCard also makes it easy to tune the tag display limit independently for each layout.

Handling Tag Overflow

Posts with many tags would overflow the card layout. The fix: show at most MAX_TAGS tags and display a +N indicator for the rest.

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 MAX_TAGS constant means a single edit updates both the slice and the overflow count. The same pattern was applied to the existing PostCard to keep behavior consistent across both components.

Wiring Up the Home Page

In src/app/[locale]/page.tsx, the section only renders when there's at least one featured post:

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 switches to a two-column layout on small screens and above. The conditional render means removing all featured: true markers simply hides the section — no UI breakage.

UI strings live in src/lib/i18n.ts:

// ja
home: { featuredPosts: "注目記事" }
 
// en
home: { featuredPosts: "Featured" }

Usage

Mark any post as featured by adding one line to its frontmatter:

---
title: "Post Title"
date: 2026-03-07
published: true
featured: true   # add this
categories:
  - programming
---

Posts without featured default to false, so nothing breaks for existing content.

Conclusion

The Velite schema extension was the cleanest part — default(false) made the migration zero-impact on existing posts. Separating FeaturedPostCard from PostCard gave each component its own MAX_TAGS constant and styling, making future tweaks straightforward. The home page section is purely additive and self-hiding when empty, so it fits naturally into the existing layout.

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