Get Started NextjsTutorial — 7. Building the About page
About Page
Let’s build the About page. Here we can take two approaches:
- One would be to build the HTML statically in Next.js and then hook up the data from Content Island.
- The other is to first fetch the data from Content Island, build a raw HTML layout, wire it all together, and once everything works, apply the styles.
Both approaches are equally valid. It depends on your preferences and workflow. Maybe, as a developer, you’d prefer to ensure that the data loads first, and then hand off the styling to a designer. :)
We’ll go with the second option, put on our dev hat and get to work.
First, since we’re working with TypeScript, we’ll define the model for the About page data.
In Content Island, under the model tab, we can see the About model includes the following fields:
- picture: Photo.
- fullname: Name.
- shortBio: Short bio.
- extendedBio: Extended bio.
We can click on the "TypeScript" button to copy the interface definition and paste it into our project.
Let’s create a file named api.model.ts at the root of the project, where we will define all the Content Island models. For now, we’ll paste the About interface we just copied:
./src/api.model.ts
import type { Media } from '@content-island/api-client';
export interface About {
id: string;
language: 'en';
picture: Media;
fullname: string;
shortBio: string;
extendedBio: string;
}Now let’s define an API to fetch this data.
What are we doing here?
- Define an async function called
getAboutto fetch About data from Content Island. - We use the Content Island library to fetch the About data using the
getContentfunction, passing thecontentTypeof About. - Since we only have one content of this type, we don’t need to pass an
id.
./src/api.ts
import type { About } from "./api.model";
import { client } from "./lib/client";
export async function getAbout(): Promise<About> {
return await client.getContent<About>({ contentType: "About" });
}
With that done, let’s create a basic layout on the About page to display the data.
Since we’re using the App Router in Next.js, we can declare the component as async and use await to wait for the data. By default, components run on the server, so using async functions is not a problem.
./src/app/about/page.tsx
+ import { getAbout } from "../../api";
- export default function About() {
+ export default async function About() {
+ const about = await getAbout();
return (
+ <div>
<h1>About</h1>
+ <img src={about.picture.url} alt={about.fullname} />
+ <h2>{about.fullname}</h2>
+ <p>
+ <strong>Short Bio:</strong> {about.shortBio}
+ </p>
+ <p>
+ <strong>Extended Bio:</strong> {about.extendedBio}
+ </p>
+ </div>
);
}Looking good but there’s one thing: the extended bio field is Markdown. Since we’re rendering it dynamically, let’s install a library to process it.
npm install markedNow let’s process the markdown:
./src/app/about/page.tsx
+ import { marked } from 'marked';
import { getAbout } from "../../api";
export default async function About() {
const about = await getAbout();
+ const aboutExtendedBio = marked(about.extendedBio || '');
return (
<div>
<h1>About</h1>
<img src={about.picture.url} alt={about.fullname} />
<h2>{about.fullname}</h2>
<p>
<strong>Short Bio:</strong> {about.shortBio}
</p>
<p>
- <strong>Extended Bio:</strong> {about.extendedBio}
+ <strong>Extended Bio:</strong>
+ <div dangerouslySetInnerHTML={{ __html: aboutExtendedBio }} />
</p>
</div>
);
}Nice, it’s looking better! Let’s add some styling:
./src/app/about/page.module.css
.about {
display: flex;
flex-direction: column;
gap: var(--space-lg);
}
.about__header {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
@media screen and (min-width: 768px) {
.about__header {
flex-direction: row;
align-items: flex-end;
}
}
.about__intro {
display: flex;
flex-direction: column;
gap: var(--space-xs);
& > h1 {
font-size: var(--fs-4xl);
font-family: var(--ff-title);
width: 10ch;
line-height: 1.1;
}
& > p {
max-width: 30ch;
font-size: var(--fs-md);
}
}
@media screen and (min-width: 768px) {
.about__intro {
padding-bottom: var(--space-md);
}
}
.about__image {
width: 100%;
height: auto;
object-fit: cover;
aspect-ratio: 16/9;
border-radius: var(--border-radius);
}
@media screen and (min-width: 768px) {
.about__image {
width: 50%;
aspect-ratio: 10/16;
}
}
@media screen and (min-width: 1024px) {
.about__image {
width: 30%;
}
}
.about__bio {
max-width: 800px;
margin-right: auto;
margin-left: auto;
display: flex;
flex-direction: column;
gap: var(--space-lg);
& img {
width: 100%;
object-fit: cover;
aspect-ratio: 21/9;
border-radius: var(--border-radius);
}
}And update the HTML:
./src/app/about/page.tsx
import { marked } from "marked";
import { getAbout } from "../../api";
+ import classes from "./page.module.css";
export default async function About() {
const about = await getAbout();
const aboutExtendedBio = marked(about.extendedBio || "");
return (
- <div>
+ <div className={`container ${classes.about}`}>
- <h1>About</h1>
+ <div className={classes.about__header}>
<img
+ className={classes.about__image}
src={about.picture.url}
alt={about.fullname}
+ width={100}
+ height={200}
/>
- <h2>{about.fullname}</h2>
- <p>
- <strong>Short Bio:</strong> {about.shortBio}
- </p>
+ <div className={classes.about__intro}>
+ <h1>
+ I'm, <br /> {about.fullname}
+ </h1>
+ <p>{about.shortBio}</p>
+ </div>
+ </div>
- <p>
- <strong>Extended Bio:</strong>
- <div dangerouslySetInnerHTML={{ __html: aboutExtendedBio }} />
- </p>
+ <div
+ className={classes.about__bio}
+ dangerouslySetInnerHTML={{ __html: aboutExtendedBio }}
+ />
</div>
);
}Looking good! :)
Ready to move on to the post list?