Patterns

Post grid

View in Figma

The insights index — fixed row choreography over flat content-card records, paginated by URL. CMS-serializable.

Install

npx shadcn@latest add @alix/postGrid

Preview

Usage

import PostGridBlock, { type PostGridBlockProps } from "@/components/postGrid";

const fromCMS: PostGridBlockProps = await getInsightsIndex({
  page: Number(searchParams.page) || 1,
});

<PostGridBlock {...fromCMS} />

<PostGridBlock
  page={1}
  pageHref="/insights?page={page}"
  articles={[
    {
      media: { type: "image", src: "/article-placeholder.svg", alt: "" },
      alignment: "vertical",
      title: "Liquidity planning when cash windows compress",
      excerpt: "A concise insight for leaders making decisions under pressure.",
      href: "/insights/liquidity-planning",
    },
  ]}
/>

Examples

01

Editorial and video cards in the feed

The forced news lead sits above an equal-column row that keeps each card's own variant (editorial) and media type (video).

<PostGridBlock
  page={1}
  articles={[
    // featured lead + sidebar — the block renders these as news
    { media: { type: "image", src: "/brand/ship.webp", alt: "" }, alignment: "vertical", title: "Port disruption is now a working-capital issue", excerpt: "…", href: "/insights/port-disruption" },
    // …three more news cards…
    // equal-column row keeps each card's own discriminant
    { variant: "editorial", media: { type: "image", src: "/brand/portrait-1.jpg", alt: "" }, alignment: "vertical", title: "The case for a standing cash office", excerpt: "…", href: "/insights/cash-office" },
    { variant: "editorial", media: { type: "video", src: "/media/turnaround-loop.mp4", poster: "/brand/ship.webp", muted: true, loop: true }, alignment: "vertical", title: "Turnaround delivery in motion", excerpt: "…", href: "/insights/turnaround-delivery" },
    { variant: "editorial", media: { type: "image", src: "/brand/people.webp", alt: "" }, alignment: "vertical", title: "Diligence findings that translate to value", excerpt: "…", href: "/insights/diligence-to-value" },
  ]}
/>
02

Server-paginated with a page window

Pass one server-fetched page plus totalPages and the block renders it as-is; the pager collapses to a first/last window with ellipses.

// App Router page — one page fetched server-side, the block renders it as-is.
const page = Number((await searchParams).page) || 1;
const { posts, pageCount } = await getInsightsPage(page);

<PostGridBlock
  articles={posts}
  page={page}
  totalPages={pageCount}
  pageHref="/insights?page={page}"
/>

When to use it

Use it for

  • The full insights or newsroom index, where the block owns the row choreography and pagination.
  • URL-based paging that works without JavaScript and stays crawlable, with page read from the route.
  • Server-fetching one page and passing totalPages when the full list is too large to ship.

Reach for something else

  • A single equal-width row of cards — use PostRowBlock instead.
  • One teaser card on its own — use ContentCard instead.
  • A bespoke row arrangement — call renderPostRow inside your own frame instead.

Props

PropTypeDefaultDescription
articlesContentCardProps[]Flat CMS article records. Pass the full list for client slicing, or one server-fetched page with `totalPages`.
ContentCardProps.variant"news" | "editorial""news"Card visual variant for each article.
ContentCardProps.media.type"image" | "video""image"Media discriminant. Image needs `src` and `alt`; video needs `src` plus optional playback fields.
ContentCardProps.alignment"vertical" | "horizontal"Required card media/text orientation. The closing row forces one item to horizontal.
pagenumber1Current 1-based page, usually read from the route.
totalPagesnumberRequired only when `articles` already contains one server-fetched page.
pageHrefstring"?page={page}"Serializable href template. `{page}` is replaced per link.
linkComponentElementTypeFramework-link escape hatch for pagination. Omit for plain crawlable anchors.
escape hatchcompose lower layerFor a bespoke row arrangement, compose ContentCard and `renderPostRow` directly.

See also