Get Started NextjsTutorial — 8. Page with list of posts
8. Page with list of posts
Now let’s build the main page. In this case, we want to display a list of published posts.
How does this work in static mode with Next.js? At build time, we fetch all the posts and generate the listing page. The final result is a static HTML page served to the client with the list of posts.
To do this, we’ll define the model and add the API to fetch the list.
Just like we did for About, we go to the Content Island portal, open the Model tab, click on the Post model TypeScript button, copy it and update our api.model.ts file:
./src/api.model.ts
+ export interface Post {
+ id: string;
+ language: 'en';
+ title: string;
+ slug: string;
+ date: string; // Stores the date in ISO 8601 format. For example: 2021-09-10T19:30:00.000Z
+ author: string;
+ summary: string;
+ image: Media;
+ content: string;
+ }Now we build the API. Remember, for the About page we used getContent, but that only fetches a single item. This time, we want a list of posts, so we’ll use getContentList:
- Import the new model.
- Create an async method
getPoststhat returns an array ofPost. - Use
getContentList, filtering by thePostcontentType.
./src/api.ts
- import type { About } from "./api.model";
+ import type { About, Post } from "./api.model";
import { client } from "./lib/client";
export async function getAbout(): Promise<About> {
return await client.getContent<About>({
contentType: "About",
});
}
+ export async function getPosts(): Promise<Post[]> {
+ return await client.getContentList<Post>({
+ contentType: "Post",
+ });
+ }
Then in the homepage, we fetch and display the posts. As before, we’ll first show the data without styles, then enhance it later.
- Import the
getPostsAPI. - Mark the component as
asyncand useawaitto fetch the data. - Display each field’s data in HTML.
./src/app/page.tsx
+ import { getPosts } from "../api";
- export default function Home() {
+ export default async function Home() {
+ const posts = await getPosts();
return (
+ <>
<h1>Hello Blog!</h1>
+ <ul>
+ {posts.map((post) => (
+ <li key={post.id}>
+ <h2>{post.title}</h2>
+ <p>{post.author}</p>
+ <p>{post.summary}</p>
+ </li>
+ ))}
+ </ul>
+ </>
);
}It works… though it looks pretty plain. Let’s add some styling.
./src/app/page.module.css
.posts__header {
text-align: center;
margin-bottom: var(--space-lg);
& > h1 {
font-size: var(--fs-3xl);
font-family: var(--ff-title);
}
}
.posts__list {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
justify-items: center;
gap: var(--space-lg);
}
.post {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.post__image {
width: 100%;
height: auto;
object-fit: cover;
object-position: top;
aspect-ratio: 16/9;
border-radius: var(--border-radius);
}
.post__text {
display: flex;
flex-direction: column;
gap: var(--space-2xs);
}
.post__header {
display: flex;
flex-direction: column;
gap: 2px;
}
.post__date {
font-size: var(--fs-2xs);
opacity: 0.7;
}
.post__title {
text-decoration: none;
color: var(--text-color);
transition: color 0.3s ease-in-out;
&:hover {
color: var(--title-hover-color);
}
& > h2 {
font-size: var(--fs-lg);
font-family: var(--ff-title);
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
line-height: 1.175;
}
}
.post__description {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
overflow: hidden;
}Now update the component to use the new styles and markup:
./src/app/page.tsx
import { getPosts } from "../api";
+ import { Post } from "../api.model";
+ import classes from "./page.module.css";
export default async function Home() {
const posts = await getPosts();
+ const formatDate = (date: string) =>
+ new Date(date).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
return (
- <>
+ <section className="container">
- <h1>Hello Blog!</h1>
+ <div className={classes.posts__header}>
+ <h1>CurioVerso Blog</h1>
+ <p>All Posts</p>
+ </div>
- <ul>
+ <ul className={classes.posts__list}>
{posts.map((post) => (
<li key={post.id}>
- <h2>{post.title}</h2>
- <p>{post.author}</p>
- <p>{post.summary}</p>
+ <article className={classes.post}>
+ <a href={`/posts/${post.slug}`}>
+ <img
+ src={post.image.url}
+ alt={post.image.name}
+ width={250}
+ height={100}
+ className={classes.post__image}
+ loading="eager"
+ />
+ </a>
+ <div className={classes.post__text}>
+ <div className={classes.post__header}>
+ <p className={classes.post__date}>
+ <time dateTime={post.date}>{formatDate(post.date)}</time>
+ </p>
+ <a href={`/posts/${post.slug}`} className={classes.post__title}>
+ <h2>{post.title}</h2>
+ </a>
+ </div>
+ <div>
+ <p className={classes.post__description}>{post.summary}</p>
+ </div>
+ </div>
+ </article>
</li>
))}
</ul>
- </>
+ </section>
);
}
Looks great! :)
Shall we move on to the single post page?