Now that we have a basic blog page, let's fill it with posts! First, we'll create individual post pages (dynamically), then we'll come back and add a post feed on the blog page with a reusable post card component.

Lesson #2 Concepts:

  • Content modeling
  • Rendering markdown content
  • Next.js dynamic routes
  • Rendering and annotating components

Add Post Model

As we did with the blog page, let's begin by adding a Post model.

# .stackbit/models/Post.yaml

name: Post
layout: Post
fields:
  - type: string
    name: title
    required: true
  - type: date
    name: date
    required: true
  - type: string
    name: excerpt
    default: Excerpt goes here ...

We've given the posts required title and date fields, along with an excerpt field, which we'll use to render a preview of the post content.

Because we omitted the hideContent option, there will be a content area to edit the main content of the page. We'll see this in action later in this lesson.

Configure Post Page

Like the BlogPost model, we also want posts to be of the page type, so let's add that configuration to stackbit.yaml.

# stackbit.yaml

hello: world
# ...
contentModels:
  # ...
  Post:
    isPage: true
    urlPath: '/blog/{slug}'
    newFilePath: 'blog/{slug}.md'

Note here that we are interpolating each post's slug in the URL. We'll see this come to life as we create posts, which is the next step.

Add Post Content

Now you can add a new page of type Post in Stackbit. You'll have to fill in the two required fields, along with the slug.

Add new post dialog

The value you add to the slug field here becomes the slug value from your model configuration to define the URL for that page. For example, if you used my-first-post as the slug, that means the content file will be created at content/pages/blog/my-first-post.md and that the page itself is expected to be rendered at /blog/my-first-post.

The post content file will look something like this:

---
title: My First Post
date: '2022-02-22'
excerpt: Excerpt goes here ...
layout: Post
---

Etiam facilisis lacus nec pretium lobortis. Praesent dapibus justo non efficitur efficitur. Nullam viverra justo arcu, eget egestas tortor pretium id. Sed imperdiet mattis eleifend. Vivamus suscipit et neque imperdiet venenatis ...

Notice that excerpt and the main content area are populated even though you didn't add any content for them. Stackbit works to provide sensible defaults to avoid breaking your site when a new page is added.

Add a few posts. After creating each post, you'll be taken to a 404 page, just as you were with the blog page. We'll resolve those errors in the next step by creating the post page component.

Post Page Component

Let's add the post page, keeping things simple as we did with the blog page. We'll add a title and parse the markdown to create the body content.

// pages/blog/[slug].js

import Markdown from 'markdown-to-jsx'

import { pageUrlPath } from '../../utils/page-utils'
import { pagesByLayout } from '../../utils/sourcebit-utils'

const allPosts = pagesByLayout('Post')

const PostPage = ({ post }) => {
  return (
    <div data-sb-object-id={post?.__metadata?.id}>
      <h1 data-sb-field-path="title">{post.frontmatter.title}</h1>
      <Markdown data-sb-field-path="markdown_content">{post.markdown}</Markdown>
    </div>
  )
}

export const getStaticProps = async ({ params }) => {
  const currentPath = `/blog/${params.slug}`
  const post = allPosts.find((page) => pageUrlPath(page) === currentPath)
  return { props: { post } }
}

export const getStaticPaths = async () => {
  return {
    paths: allPosts.map((page) => pageUrlPath(page)),
    fallback: false
  }
}

export default PostPage

There are several items to make note of with this code:

  • The annotation (data-sb-field-path) for the main body area is called markdown_content. This is a reserved field specific to the main content area of the source file.
  • We're using markdown-to-jsx to transform the markdown content to HTML. This is to keep the process simple. Ideally you'd do this work within getStaticProps() to keep your component code lighter (for better performance in production).
  • getStaticPaths() is using a helper function pageUrlPath to dynamically build the individual Next.js pages. Look in utils/page-utils.js for this method. Learn more about dynamic routing with Next.js.

Because we already added the annotations, your Stackbit editor should refresh to show the page content, and it should now be visually editable!

Post detail page

Add Posts to Blog Page

With individual post pages in place, we can add a feed of these posts to the main blog page. We'll render a post card component, which will represent and link to an individual post.

Add Post Card Component

Our components in this project live in the components directory. Add a PostCard component.

// components/PostCard.jsx

import * as React from 'react'
import Link from 'next/link'
import Markdown from 'markdown-to-jsx'

import { pageUrlPath } from '../utils/page-utils'

export const PostCard = ({ post }) => {
  return (
    <Link href={pageUrlPath(post)}>
      <a data-sb-object-id={post.__metadata.id}>
        <h3 data-sb-field-path=".title">{post.frontmatter.title}</h3>
        {post.frontmatter.excerpt && <Markdown data-sb-field-path=".excerpt">{post.frontmatter.excerpt}</Markdown>}
      </a>
    </Link>
  )
}

Once again we're using the pageUrlPath() helper to build the URL.

Also note that we've once again used the data-sb-object-id annotation. This is because these cards will be rendered on pages that are not the post page. So we have to tell Stackbit where to store this information, which is in the individual post source file (e.g. content/pages/blog/my-first-post.md).

Add Cards to Blog Page

With the component in place, we're ready to add the cards to the main blog page. (Comments describe the new lines of code.)

// pages/blog/index.jsx

// Import post card
import { PostCard } from '../../components/PostCard'
import { pagesByLayout } from '../../utils/sourcebit-utils'

const BlogPage = ({ page, posts }) => {
  return (
    <div data-sb-object-id={page?.__metadata?.id}>
      <h1 data-sb-field-path="title">{page.frontmatter.title}</h1>
      {/* Loop through posts and add post card for each one. */}
      {(posts ?? []).map((post) => (
        <PostCard key={post.__metadata.id} post={post} />
      ))}
    </div>
  )
}

export default BlogPage

export const getStaticProps = async () => {
  const page = pagesByLayout('Blog')[0]
  // Retrieve all posts and send to the page component as a prop, sorted by date.
  const posts = pagesByLayout('Post').sort((a, b) => {
    return new Date(b.frontmatter.date) - new Date(a.frontmatter.date)
  })
  return { props: { page, posts } }
}

Look back at your browser and you should see a list of posts. Try adding a few new posts and you should see them appear in the list, sorted by date.

Blog page with post cards

Next, we'll take a look at creating a post feed component that can be reused on other pages.