Before we start this lesson, let's pause and consider what you've done. In just two lessons, you've already built a Next.js blog site that is visually editable with Stackbit! That's some great progress.

Near the end of the last lesson, you added the blog posts to a static blog landing page. That's a good first step to showcase your posts. But you may also want to give your content editor(s) the ability to surface recent posts on other pages.

In this lesson, we're going to build a component similar to the list of posts on the blog page.

In this lesson:

  • Object content modeling
  • Model groups
  • Dynamic components

Add Post Feed Model

We're going to create a post feed component. As you already know, the first step in this process is to create the content model.

Since we know that we're going to add dynamic post content to the component, let's give it two simple fields — heading and subheading — as a means to introduce the list of posts.

# .stackbit/models/PostFeed.yaml

type: object
name: PostFeed
label: Post Feed
labelField: heading
groups:
  - sectionComponent
fields:
  - type: string
    name: heading
    label: Title
    default: Post Feed Title
  - type: markdown
    name: subheading
    default: Subheading goes here ...

See the next couple subsections for more info on a couple new concepts introduced here.

Understanding the Label Field Property

labelField tells Stackbit which field to use to represent the item in the visual and page editors. This is title by default. If we don't have a title field, it's good practice to set this property. Learn more about the labelField option.

Understanding Model Groups

groups is a method for grouping components together for use by other models. Open the main Page model (.stackbit/models/Page.yaml), and look at the sections field.

# .stackbit/models/Page.yaml

# ...
fields:
  # ...
  - type: list
    name: sections
    label: Sections
    items:
      type: model
      groups:
        - sectionComponent

For this model, sections is a list of object models. Rather than having to spell each one out individually, we can use groups to tell Stackbit that any model with a matching group name should be included.

Because we added the sectionComponent group to our post feed component, we're going to be able to add the component to any new Page.

Add Post Feed Component

We've gone through this exercise a few times already. Let's create a new component for the post feed:

// components/PostFeed.jsx

import * as React from 'react'
import Markdown from 'markdown-to-jsx'

import { PostCard } from './PostCard'

export const PostFeed = (props) => {
  return (
    <div data-sb-field-path={props['data-sb-field-path']}>
      <h2 data-sb-field-path=".heading">{props.heading}</h2>
      {props.subheading && <Markdown data-sb-field-path=".subheading">{props.subheading}</Markdown>}
      <div>
        {(props.posts ?? []).map((post) => (
          <PostCard key={post.__metadata.id} post={post} />
        ))}
      </div>
    </div>
  )
}

The code here looks similar to the blog landing page code. That's because we're essentially doing the same thing — rendering a couple text fields, and then listing post card components.

The biggest difference is that we aren't fetching the post data directly, but accepting them as props. We'll talk more about that soon.

Add Post Feed to Home Page

The home page is the only page on the site using the Page model by default. Navigate to the home page in Stackbit and try adding a Post Feed section.

Post feed component with posts

Notice that we just get the heading and subheading, but not the posts. Let's address that.

Delivering Post Data

Let's make our post data available at the page level and pass it down to components as needed. Update getStaticProps() in pages/[[...slug]].js with the following:

// pages/[[...slug]].js

export const getStaticProps = async ({ params }) => {
  const pagePath = '/' + (params?.slug || []).join('/')
  const page = allPages.find((page) => pageUrlPath(page) === pagePath)
  // Get all posts and sort them by date, most recent first.
  const posts = pagesByLayout('Post').sort((a, b) => {
    return new Date(b.frontmatter.date) - new Date(a.frontmatter.date)
  })
  // Retrieve the data from the current page, and if there are any PostFeed
  // components in it, add the post data to those components.
  page.frontmatter.sections.map((section) => {
    if (section.type === 'PostFeed') section.posts = posts
  })

  return { props: { page, footer: siteConfig.footer } }
}

This is a somewhat unique operation. If you're curious to understand why and what's going on, read the next couple subsections.

Otherwise, the feed should now be populated with posts and you can move on to the next lesson!

Post feed component with posts

If you're itching to get styling, fear not! We'll cover a few different approaches to writing CSS over the next few sections.

Data-Loading Consequences

There are many ways to populate data in Next.js components. Technically, because the data is static JSON and markdown files, we could have brought it in directly. However, this can drastically increase the bundle size and lead to degraded performance for your users.

The better approach is to do as much data work as possible in getStaticProps(). Or, rather, to minimize the amount of work components need to do with data transformations.

Because of this, what we've done is fetch and send only the data we need for each individual page. So, for example, if you wanted to limit the number of posts rendered by the post feed component, you should do that work in getStaticProps(), rather than deferring that work to the page component.

Building for Scale

It's also important to note that this is not a scalable solution. As your site grows, you'll have more components and models, which means more data calculations and transformations.

What you'll likely want to do is break that logic out into a series of utility functions. We've started you with a few in the utils directory, but you can make it your own.

Again, the important piece is to minimize the amount of work components need to do with data transformations.