Skip to main content

How to build a blog with NextJS and TakeShape | Part II

· 24 min read
Ashutosh K Singh

This article assumes that you have read "How to build a blog with NextJS and TakeShape" if not, I will recommend reading it because this tutorial builds on top of the project created in the that article.

You can check out the final project here and the complete code on GitHub.

Prerequisites

How to Access Environment Variables from Browser

In the last article, we created the following environment variables in the .env file to store the project's API keys.

# .env
TAKESHAPE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TAKESHAPE_PROJECT="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

By default, these environment variables are not exposed to the browser, but we will need to access them from our browser as we develop the project in this tutorial.

Using the NEXT_PUBLIC_ prefix exposes the environment variable to the browser.

Update .env file like this.

# .env
NEXT_PUBLIC_TAKESHAPE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
NEXT_PUBLIC_TAKESHAPE_PROJECT="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

You will also need to update the lib/api.js file to access these new environment variables.

Update API_ENDPOINT and TAKESHAPE_API_KEY variables in lib/api.js like this.

lib/api.js
// lib/api.js
const API_ENDPOINT = `https://api.takeshape.io/project/${process.env.NEXT_PUBLIC_TAKESHAPE_PROJECT}/graphql`;
const TAKESHAPE_API_KEY = process.env.NEXT_PUBLIC_TAKESHAPE_API_KEY;

How to Display Date on Posts

In this section, we will discuss how to display published or updated date on posts.

The first step is to update the queries in getAllPosts() and getPostBySlug() functions to fetch _createdAt, which, as it sounds, refers to the date when you created the post.

You can also use _updatedAt to show the date when the post was modified or updated.

In this tutorial, we will show only the created date of the post.

Modify the getAllPosts() and getPostBySlug() functions and in lib/api.js:

lib/api.js
// get all posts to display on landing page
export async function getAllPosts() {
const data = await fetchData(
`
query AllPosts {
allPosts: getPostList {
items {
_id
title
deck
slug
**author {
name
slug
}
_createdAt
tags{
name
_id
}**
}
}
}
`
);
return data.allPosts.items;
}

// get single post based on the slug passed
export async function getPostBySlug(slug) {
const data = await fetchData(
`
query PostBySlug($slug: String) {
post: getPostList(where: {slug: {eq: $slug}}) {
items {
_id
title
slug
deck
bodyHtml
**author{
name
slug
}
tags{
name
_id
}
_createdAt**
}
}
}`,
{
variables: {
slug,
},
}
);
return data.post.items[0];
}

You will notice that the queries have also been updated to include the following.

 author {
name
slug
}
tags {
name
_id
}

We will use the above data when making dynamic routes to the author and tag pages. You can ignore them for now; we will discuss them in the next sections.

Now, update the .map() method on posts in pages/index.js to include _createdAt.

pages/index.js
{
posts.length>0 && posts.map((post) => (
<PostContainer
key={post._id}
title={post.title}
slug={post.slug}
author={post.author}
deck={post.deck}
date={post._createdAt}
/>
))
}
{
posts.length>0 && posts.map((post) => (
<PostContainer
key={post._id}
title={post.title}
slug={post.slug}
author={post.author}
deck={post.deck}
date={post._createdAt}
/>
))
}

In the above code, we have also modified the JSX expression to check if the posts array is empty or not? And show the PostContainer component only when the posts array is not empty, i.e., posts.length>0.

The next step is to update the PostContainer component to show this date.

The _createdAt returned from TakeShape CMS looks something like this.

"_createdAt": "2020-10-27T05:33:49.751Z",

We convert this date into a more human-readable form by passing date into JavaScript Date constructor and then using it with .toDateString(). Read more about .toDateString() here.

components/post-container.js
// components/post-container.js
import Link from "next/link";
import styles from "../styles/PostContainer.module.css";

export default function PostContainer({ title, deck, slug, author, date }) {
const createdDate = new Date(date);

return (
<div className={styles.container}>
<div className={styles.title}>
<Link href={`/blog/${slug}`}>{title}</Link>
</div>
<div className={styles.author}> {author.name}</div>
<div className={styles.date}> {createdDate.toDateString()}</div>

<div className={styles.deck}>
<p>{deck}</p>
</div>
<div className={styles.read}>
<Link href={`/blog/${slug}`}>Read More</Link>
</div>
</div>
);
}

Add the following CSS for the date in styles/PostContainer.module.css.

PostContainer.module.css
.date {
color: inherit;
font-family: monospace;
font-size: 1rem;
padding: 5px 10px;
display: inline-block;
margin: 0 2px;
font-weight: 300;
}

Run the following command in your project's root directory to start the development server.

npm run dev

Head over to http://localhost:3000 , here is how your app will look like.

Now, to show this date on blog page, update the blog/[slug].js file like this.

blog/[slug].js
// pages/blog/[slug].js
import Head from "next/head";
import Link from "next/link";
import styles from "..//../styles/Posts.module.css";
import { getAllSlugs, getPostBySlug } from "../../lib/api";
import Header from "../../components/header";

function Posts({ post }) {
const createdAt = new Date(post._createdAt);

return (
<div>
<Head>
<title key={post.title}>{post.title}</title>
</Head>
<div className={styles.container}>
<div className={styles.header}>
<Header title={post.title} />
<h2 className={styles.home}>
<Link href={`/`}>🏠 Home</Link>
</h2>
</div>

**<div className={styles.info}>
<div className={styles.author}>By: {post.author.name}</div>
<div className={styles.date}> {createdAt.toDateString()}</div>
</div>**

<div className={styles.body}>
<main dangerouslySetInnerHTML={{ __html: post.bodyHtml }} />
</div>
</div>
</div>
);
}

export async function getStaticPaths() {
const allPosts = await getAllSlugs();
const paths = allPosts.map((post) => ({
params: { slug: post.slug },
}));
return {
paths,
fallback: false,
};
}

export async function getStaticProps({ params }) {
const post = await getPostBySlug(params.slug);

return {
props: {
post,
},
};
}

export default Posts;

Add the styling for date, info and auhtor div in styles/Posts.module.css.

Posts.module.css
.info {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
font-size: 1.5rem;
}

.author {
margin: 5px;
font-weight: 300;
font-family: 'Syne Mono', monospace;
width: fit-content;
}

.date {
color: inherit;
font-family: monospace;
font-size: 1rem;
padding: 5px 10px;
display: inline-block;
margin: 0 2px;
font-weight: 300;
}

Here is how the blog page will look.

How to Display and Add Dynamic Routes to Tags

In this section, we will first display tags on posts and then create dynamic routes to each tag where all the posts having that particular tag are shown.

Since, we have already modified the functions to get tags from TakeShape, we just need modify the .map() method in index.js to pass tags to the PostContainer component.

index.js
{posts.length>0  && posts.map((post) => (
<PostContainer
key={post._id}
title={post.title}
slug={post.slug}
author={post.author}
deck={post.deck}
date={post._createdAt}
tags={post.tags}
/>
))}

Now, update the PostContainer component to show the tags.

export default function PostContainer({
title,
deck,
slug,
author,
tags,
date,
}) {
const updatedDate = new Date(date);
return (
<div className={styles.container}>
<div className={styles.title}>
<Link href={`/blog/${slug}`}>{title}</Link>{" "}
</div>
<div className={styles.author}> {author.name}</div>
<div className={styles.date}> {updatedDate.toDateString()}</div>

<div className={styles.tags}>
{tags.map((tag) => (
<Link key={tag._id} href={`/tag/${tag._id}`}>
<div className={styles.tag}>
{tag.name}
</div>
</Link>
))}
</div>
<div className={styles.deck}>
<p>{deck}</p>
</div>

<div className={styles.read}>
<Link href={`/blog/${slug}`}>Read More</Link>
</div>
</div>
);
}

We have added the link to tags but these routes doesn't exist yet.

Before we create them, add the CSS to style these tags in styles/PostContainer.module.css.

PostContainer.module.css
.tags {
font-size: 0.9rem;
padding: 5px;
display: flex;
}

.tag {
background-color: rgb(224, 201, 238);
margin: 2px;
padding: 6px;
font-weight: 600;
border-radius: 20px;
}

.tag:hover,
.tag:focus,
.tag:active {
font-weight: 800;
cursor: pointer;
}

Here is how the post containers look like with these tags.

Now, add these tags to blog/[slug].js. Add the following code for tags in between author and date divs inside info div.

info div
<div className={styles.info}>
<div className={styles.author}>By: {post.author.name}</div>
<div className={styles.tags}>
{post.tags.map((tag) => (
<Link key={tag._id} href={`/tag/${tag._id}`}>
<div className={styles.tag}>{tag.name}</div>
</Link>
))}
</div>
<div className={styles.date}> {createdAt.toDateString()}</div>
</div>

Add the following CSS in styles/Posts.module.css to style the tags.

styles/Posts.module.css
.tags {
font-size: 0.9rem;
padding: 5px;
display: flex;
}

.tag {
background-color: rgb(224, 201, 238);
margin: 2px;
padding: 6px;
font-weight: 600;
border-radius: 20px;
}

.tag:hover,
.tag:focus,
.tag:active {
font-weight: 800;
cursor: pointer;
}

Here is how your blog page will look.

To create dynamic routes to tags, we need to create a function in lib/api.js that takes the _id of tag as an argument and fetches data or posts having that tag.

Inside api.js create two functions named getPostsByTag() and getAllPostsTags() like this.

getPostsByTag
//get posts by tags
export async function getPostsByTag(_id) {
const data = await fetchData(
`
query PostsByTag($_id: ID!) {
getTags(_id: $_id) {
postSet {
items {
_id
title
deck
slug
author {
name
slug
}
tags{
name
_id
}
_createdAt
}
}
name
}
}
`,
{
variables: {
_id,
},
}
);
return data.getTags;
}

// get all tags of the posts to generate static paths
export async function getAllPostsTags() {
const data = await fetchData(`
{
allTags: getTagsList {
items {
_id
name
}
}
}
`);
return data.allTags.items;
}

The function getAllPostsTags() fetches the _id of all tags to generate static paths for getStaticPaths() function.

Now, run the following command in your project's root directory to create [_id].js under tag directory in the pages folder.

mkdir pages/tag
cd pages/tag
touch [_id].js

Add the following code to tag/[_id].js

tag/[_id].js
// pages/tag/[slug].js
import Head from "next/head";
import Link from "next/link";
import styles from "..//../styles/Tag.module.css";
import { getAllPostsTags, getPostsByTag } from "../../lib/api";
import Header from "../../components/header";
import PostContainer from "../../components/post-container";

function Tag({ post }) {
return (
<div>
<Head>
<title key={post.name}>{post.name}</title>
</Head>
<div className={styles.container}>
<div className={styles.header}>
<Header title={`Tag - ${post.name}` } />
<h2 className={styles.home}>
<Link href={`/`}>🏠 Home</Link>
</h2>
</div>
<div className={styles.posts}>
<div className={styles.posts_body}>
{post.postSet.items.map((post) => (
<PostContainer
key={post._id}
title={post.title}
slug={post.slug}
author={post.author}
deck={post.deck}
tags={post.tags}
date={post._createdAt}
/>
))}
</div>
</div>
</div>
</div>
);
}

export async function getStaticPaths() {
const allTags = await getAllPostsTags();
const paths = allTags.map((tag) => ({
params: { _id: tag._id },
}));
return {
paths,
fallback: false,
};
}

export async function getStaticProps({ params }) {
const post = await getPostsByTag(params._id);

return {
props: {
post,
},
};
}

export default Tag;

To style this page, run the following command in your root directory to create Tag.module.css under the styles directory.

cd styles
touch Tag.module.css

Add the following CSS to Tag.module.css.

Tag.module.css
/* Tag.module.css */

.container {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: #fafafa;
}

.home {
background-color: #1abc9c;
font-size: 1.5rem;
color: white;
text-align: center;
display: inline;
padding: 5px 7.5px;
margin: 5px;
cursor: pointer;
border-radius: 20px;
}

.home:hover,
.home:active,
.home:focus {
background-color: teal;
}

.header {
padding: 0.5rem 1.5rem;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
background-color: white;
}

@media only screen and (max-width: 500px) {
.header {
display: inline;
}
.home {
font-size: 1.2rem;
}
}

Restart your development server and head over to http://localhost:3000/tag/1badb385-9905-4930-9b0b-3b538b748bc1, i.e., the page for Pharetra tag.

Here is how this page will look.

Here is a GIF illustrating the tag routes.

A screenshot of the full page.

How to Add Dynamic Routes to Authors

In this section, we will add dynamic routes to the author page and then create the author page with the author's bio, image, and posts written by them.

First, create getAllAuthorSlugs() and getAuthorBySlug() functions in lib/api.js to fetch author data.

Add the following code in lib/api.js file.

lib/api.js
// get all slugs of the authors to generate static paths
export async function getAllAuthorSlugs() {
const data = await fetchData(
`
{
allAuthors: getAuthorList {
items {
slug
}
}
}

`
);
return data.allAuthors.items;
}

// get single author based on the slug passed
export async function getAuthorBySlug(slug) {
const data = await fetchData(
`
query AuthorBySlug($slug: String) {
author: getAuthorList(where: {slug: {eq: $slug}}) {
items {
biographyHtml
name
photo {
path
}
slug
postSet {
total
items {
title
deck
_id
slug
tags{
name
_id
}
_createdAt
}
}
}
}
}`,
{
variables: {
slug,
},
}
);
return data.author.items[0];
}

The above function getAllAuthorSlugs() returns all authors' slugs to generate static paths, whereas the function getAuthorBySlug() fetches a single author's data based on its unique slug.

Since we have already modified the getAllPosts() function in the last section to fetch the author's slug, we can adjust the PostContainer component to add dynamic routes to the author page.

components/post-container.js
// components/post-container.js

export default function PostContainer({ title, deck, slug, author, date, tags }) {
const createdDate = new Date(date);

return (
<div className={styles.container}>
<div className={styles.title}>
{" "}
<Link href={`/blog/${slug}`}>{title}</Link>{" "}
</div>
{author && (
<Link href={`/author/${author.slug}`}>
<div className={styles.author}> {author.name}</div>
</Link>
)}
<div className={styles.date}> {createdDate.toDateString()}</div>
<div className={styles.tags}>
{tags.map((tag) => (
<Link key={tag._id} href={`/tag/${tag._id}`}>
<div className={styles.tag}>{tag.name}</div>
</Link>
))}
</div>
<div className={styles.deck}>
<p>{deck}</p>
</div>
<div className={styles.read}>
<Link href={`/blog/${slug}`}>Read More</Link>
</div>
</div>
);
}

Update the CSS for author in styles/PostContainer.module.css like this.

styles/PostContainer.module.css
.author {
color: inherit;
font-family: "Syne Mono", monospace;
font-style: italic;
font-size: 1rem;
padding: 5px 10px;
display: inline-block;
margin: 0 2px;
font-weight: 300;
}

.author:hover,
.author:focus,
.author:active {
font-weight: 900;
cursor: pointer;
text-decoration: underline;
}

To style the author page, create a file named Authors.module.css under the styles directory by running the following command.

cd styles
touch Authors.module.css

Add the following CSS to Authors.module.css.

Authors.module.css
/* Authors.module.css */

.container {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: #fafafa;
}

.home {
background-color: #1abc9c;
font-size: 1.5rem;
color: white;
text-align: center;
display: inline;
padding: 5px 7.5px;
margin: 5px;
cursor: pointer;
border-radius: 20px;
}

.home:hover,
.home:active,
.home:focus {
background-color: teal;
}

.header {
padding: 0.5rem 1.5rem;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
background-color: white;
}

.author_bio {
display: flex;
margin: 10px 40px;
justify-content: space-around;
align-items: center;
}

.body {
font-size: 1.2rem;
margin: 0 10px;
width: 800px;
}

.posts {
margin: 10px 50px;
}

.posts_title {
font-size: 1.7rem;
font-weight: 400;
width: fit-content;
padding: 10px;
font-family: Cambria, Cochin, Georgia, Times, "Times New Roman", serif;
}

.posts_body {
display: flex;
flex-direction: column-reverse;
}

@media only screen and (max-width: 500px) {
.header {
display: inline;
}
.home {
font-size: 1.2rem;
}
}

@media only screen and (max-width: 768px) {
.author_bio {
flex-direction: column;
justify-content: center;
margin: 10px 20px;
}
.body {
margin: 0;
width: 300px;
height: 10rem;
overflow: hidden;
}
.posts {
margin: 10px 20px;
}
}

Now, run the following command to create [slug].js under pages/author directory.

mkdir pages/author
cd pages/author
touch [slug].js

Add the following code to author/[slug].js.

author/[slug].js
// pages/author/[slug].js
import Head from "next/head";
import Link from "next/link";
import styles from "..//../styles/Authors.module.css";
import { getAllAuthorSlugs, getAuthorBySlug } from "../../lib/api";
import Header from "../../components/header";
import PostContainer from "../../components/post-container";

function Authors({ author }) {
return (
<div>
<Head>
<title key={author.name}>{author.name}</title>
</Head>
<div className={styles.container}>
<div className={styles.header}>
<Header title={author.name} />
<h2 className={styles.home}>
<Link href={`/`}>🏠 Home</Link>
</h2>
</div>
<div className={styles.author_bio}>
<img
width={250}
height={300}
src={`https://images.takeshape.io/${author.photo.path}`}
alt={author.name}
/>
<div className={styles.body}>
<main dangerouslySetInnerHTML={{ __html: author.biographyHtml }} />
</div>
</div>
<div className={styles.posts}>
<div className={styles.posts_title}>Posts by {author.name}</div>
<div className={styles.posts_body}>

{author.postSet.items.map((post) => (
<PostContainer
key={post._id}
title={post.title}
slug={post.slug}
deck={post.deck}
date={post._createdAt}
tags={post.tags}
/>
))}</div>
</div>
</div>
</div>
);
}

export async function getStaticPaths() {
const allAuthors = await getAllAuthorSlugs();
const paths = allAuthors.map((author) => ({
params: { slug: author.slug },
}));
return {
paths,
fallback: false,
};
}

export async function getStaticProps({ params }) {
const author = await getAuthorBySlug(params.slug);

return {
props: {
author,
},
};
}

export default Authors;

Focus on how we are displaying author's image.

<img />
<img
width={250}
height={300}
src={`https://images.takeshape.io/${author.photo.path}`}
alt={author.name}
/>

The getAuthorBySlug() function returns the author's photo's path, which looks something like this.

"path": "6f78e8ac-527a-4e09-9c5e-510876b096bf/dev/b74d8b18-b7bb-4a34-9e2f-77deb1c5fc2c/robert-l-stevenson.png"

We prefix the path with https://images.takeshape.io/ to get the actual image URL.

An alternative to prefixing the path with https://images.takeshape.io/ would be to install the @takeshape/routing module by running the following command.

npm i @takeshape/routing

And then, use the getImageUrl() function from @takeshape/routing and pass the path to the author's photo in this function.

import {getImageUrl} from '@takeshape/routing';

...
function Authors({ author }) {
...
<img
width={250}
height={300}
src={getImageUrl(author.photo.path)}
alt={author.name}
/>
...
}

Here is how http://localhost:3000/author/lewis-carroll, i.e., the page for Lewis Carroll looks.

Now update blog/[slug].js to add the links to author's page like this.

blog/[slug].js
function Posts({ post }) {
const createdAt = new Date(post._createdAt);

return (
<div>
<Head>
<title key={post.title}>{post.title}</title>
</Head>
<div className={styles.container}>
<div className={styles.header}>
<Header title={post.title} />
<h2 className={styles.home}>
<Link href={`/`}>🏠 Home</Link>
</h2>
</div>

<div className={styles.info}>
<div className={styles.author}>
<Link href={`/author/${post.author.slug}`}>
<a> By: {post.author.name}</a>
</Link>
</div>
<div className={styles.tags}>
{post.tags.map((tag) => (
<Link key={tag._id} href={`/tag/${tag._id}`}>
<div className={styles.tag}>{tag.name}</div>
</Link>
))}
</div>
<div className={styles.date}> {createdAt.toDateString()}</div>
</div>

<div className={styles.body}>
<main dangerouslySetInnerHTML={{ __html: post.bodyHtml }} />
</div>
</div>
</div>
);
}

You can click on author's name on blog posts and then go to their respective page.

How to Add Sorting Functionality to the Blog

In this section, we will add a button that sorts the posts on the landing page based on their creation date, i.e., _createdAt.

You can also sort them based on _updatedAt, i.e., based on the updated date.

First we need to update the getAllPosts() function in lib/api.js to add the sorting queries.

lib/api.js
// get all posts to display on landing page
export async function getAllPosts(order) {
const data = await fetchData(
`
query AllPosts($order : String = "desc") {
allPosts: getPostList(sort: {field: "_createdAt", order: $order}) {
items {
_id
title
deck
slug
author {
name
slug
}
tags {
name
_id
}
_createdAt
}
}
}

`,
{
variables: {
order,
},
}
);
return data.allPosts.items;
}

In the above code, we have added an order variable in the AllPosts query; we have also given this variable a default value of desc, i.e., descending order.

Here field represents the data on which sorting is done, you can change it to _updatedAt, and the posts will sort based on the date they were updated.

query AllPosts($order : String = "desc") {
allPosts: getPostList(sort: {field: "_createdAt", order: $order}) {
...
}
}

You can test out this query in the API Explorer of your project.

The next step is to import the useState() hook in index.js.

pages/index.js
// pages/index.js
import React, { useState } from "react";
import Head from "next/head";
import styles from "../styles/Home.module.css";
import Header from "../components/header";
import PostContainer from "../components/post-container";
import { getAllPosts } from "../lib/api";

Now, define a state named post and direction to track the order of sorting, i.e., asc or desc. Give desc as the initial value of direction state, same as that of the order variable's default value.

We will pass the posts argument as the initial state of the post state, and instead of using the .map() method on the posts argument, we will use it on the post state.

export default function Home({ posts }) {
const [post, setPost] = useState(posts);
const [direction, setDirection] = useState("desc");

return (
<div className={styles.container}>
<Head>
<title>TakeShape Blog with NextJS</title>
</Head>
<Header title="TakeShape Blog with NextJS" />
{post.length>0 && post.map((post) => (
<PostContainer
key={post._id}
title={post.title}
slug={post.slug}
author={post.author}
deck={post.deck}
date={post._createdAt}
tags={post.tags}
/>
))}
</div>
);
}

When the button is clicked, we will make a new request to TakeShape but this time we will pass desc or asc in the getAllPosts function based on the direction state.

Now, add the code for button in index.js just above the .map() method on post.

<div className={styles.sort}>
<button onClick={setValueAndOrder}>
Sort - Current Order {direction.toLocaleUpperCase()}
</button>
</div>

Now, create the setValueAndOrder() function which is passed to onClick event of sort button.

Add the following code above return.

const setValueAndOrder = async (e) => {
await e.preventDefault();
if (direction == "asc") {
const res = await getAllPosts("desc");
await setPost(res);
await setDirection("desc");
}
if (direction == "desc") {
const res = await getAllPosts("asc");
await setPost(res);
await setDirection("asc");
}
};

The above code is a simple if/else statement without the else. It checks the current value of direction and passes the opposite value to the getAllPosts() function; then it updates the post state with the new response from TakeShape. Finally, the function updates the value of direction.

Add the following CSS to styles/Home.module.css to style this button.

styles/Home.module.css
.sort button {
background-color: #1abc9c;
font-size: 1.3rem;
color: white;
text-align: center;
padding: 10px;
width: fit-content;
cursor: pointer;
margin: 0 5px;
border-radius: 30px;
}

Since this project was bootstrapped with sample data from TakeShape, all the posts were created simultaneously.

You are free to add any sample or dummy data you want.

In your TakeShape Dashboard, navigate to the Data tab and select the Tags shape in the sidebar. Select New Tags. When you're done creating your data, ensure the new content is Enabled.

Head over to http://localhost:3000 and click the sort button and see your tags in action.

How to Add Search Functionality to the Blog

In this section, we will add a search bar on the landing page to search among the posts based on the user's query. This functionality is quite helpful when you have a large number of posts on your blog.

First, create a function named searchPosts() in lib/api.js. This function takes the searched query as argument and search for the same using getPostList(terms: $terms).

search
// search for posts based on the query passed
export async function searchPosts(terms) {
const data = await fetchData(
`
query SearchPosts ($terms: String ) {
allPosts: getPostList(terms: $terms) {
items {
_id
title
deck
slug
author {
name
slug
}
tags{
name
_id
}
_createdAt
}
}
}

`,
{
variables: {
terms,
},
}
);
return data.allPosts.items;
}

In your pages/index.js add the following code to create the form for searching.

pages/index.js
<form onSubmit={handleSubmit} className={styles.form}>
<input
className={styles.input}
placeholder="Search for Posts ... "
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button>Search</button>
</form>

Now, add the following CSS to styles/Home.module.css to style this button.

styles/Home.module.css
.form {
display: flex;
justify-content: center;
align-items: center;
width: fit-content;
margin: 10px 0;
}

.input {
border: none;
background-color: #e6e6e6;
padding: 10px;
outline: none;
color: black;
font-size: 1.4rem;
border-radius: 20px;
margin-right: 10px;
}

.form button {
background-color: #1abc9c;
font-size: 1.4rem;
color: white;
text-align: center;
padding: 10px;
cursor: pointer;
border-radius: 30px;
}

.form button:hover,
.form button:active,
.form button:focus {
background-color: teal;
}

@media only screen and (max-width: 640px) {
.form {
display: grid;
width: 100%;
grid-gap: 4px;
}
.form button,
.input {
font-size: 1rem;
margin-right: 0;
}
.sort {
display: flex;
justify-content: center;
align-items: center;
}
.sort button {
font-size: 1rem;
}
}

Since we have not yet created query state where user's input is stored and the handleSubmit() function that is triggered when the Search button is clicked, you will see and error on http://localhost:3000.

Lets create the function handleSubmit() in index.js.

First, import useRouter hook in index.js. useRouter hook lets you access the router object inside any function component with in your app. You can read more about useRouter() hook here.

Add the following code at the top of index.js file along with other imports.

import { useRouter } from "next/router";

Now, add the following code for query state and handleSubmit() function.

export default function Home({ posts }) {
const [post, setPost] = useState(posts);
const [direction, setDirection] = useState("desc");
const [query, setQuery] = useState("");

const router = useRouter();

const handleSubmit = async (e) => {
await e.preventDefault();
await router.push({ pathname: "/search", query: { query } });
};
...
}

The handleSubmit() takes the query from the user and uses router.push method to send that query state as query object to /search route.

The next step is to create the /search route or search.js file. We will first create Search.module.css to style this page.

Run the following command to create Search.module.css.

cd styles
touch Search.module.css

Add the following CSS to Search.module.css.

Search.module.css
.container {
min-height: 100vh;
padding: 0 1.5rem;
display: flex;
flex-direction: column;
background-color: white;
}

.query {
font-size: 1.2rem;
font-weight: 700;
color: teal;
padding: 1rem;
width: fit-content;
border: 1px solid #1abc9c;
border-radius: 30px;
}

.home {
background-color: #1abc9c;
font-size: 1.5rem;
color: white;
text-align: center;
padding: 5px 7.5px;
margin: 5px;
cursor: pointer;
border-radius: 20px;
width: fit-content;
}

.home:hover,
.home:active,
.home:focus {
background-color: teal;
}

@media only screen and (max-width: 500px) {
.home {
font-size: 1.2rem;
}
.query {
font-size: 1rem;
}
}

Run the following command in your root directory to create search.js.

cd pages
touch search.js

Add the following code inside search.js.

pages/search.js
// pages/search.js
import { searchPosts } from "../lib/api";
import Head from "next/head";
import styles from "../styles/Search.module.css";
import Link from "next/link";
import Header from "../components/header";
import PostContainer from "../components/post-container";

export default function Search({ post, searchedQuery }) {
return (
<div className={styles.container}>
<Head>
<title>{searchedQuery}</title>
</Head>
<Header title="TakeShape Blog with NextJS" />
<h2 className={styles.home}>
<Link href={`/`}>🏠 Home</Link>
</h2>
{searchedQuery && (
<div className={styles.query}>Searched Query: {searchedQuery}</div>
)}
{post.length > 0 ? (
<div className={styles.posts}>
{post.map((post) => (
<PostContainer
post={post}
key={post._id}
title={post.title}
slug={post.slug}
deck={post.deck}
author={post.author}
tags={post.tags}
date={post._createdAt}
/>
))}
</div>
) : (
<h1>No Results</h1>
)}
</div>
);
}

export async function getServerSideProps({ query }) {
const post = await searchPosts(query.query);
return {
props: {
post,
searchedQuery: query.query,
},
};
}

In the above code, we show posts only when post.length > 0, i.e., the searched query exists and there is a response from TakeShape CMS.

If the searched query doesn't exist, we show the No Results message to the user.

You can access the query sent by the router.push() method by passing a query argument to the getServerSideProps() function.

It may be a little confusing to use query.query.

Here's an explanation for this, the first query is the argument passed in getServerSideProps() that can be used to access all the queries, while the second query is the user input or query sent by the router.push() method.

router.push({ pathname: "/search", query: { query } })

If you have another query named id, you can use query.id to access it.

Here's a GIF to show search functionality in action.

A gif of the search functionality in action.

How to Build a Dynamic Sitemap for Your Blog

Sitemaps are a fundamental component of most websites and essential if your site needs to be indexed by a search engine.

Creating a sitemap that is automatically updated whenever your content is updated is simple and easy with TakeShape.

Create a function named getDataForSitemap() in lib/api.js. This function fetches the slug or -id and _updatedAt , i.e., the last updated date of posts, authors and tags.

getDataForSitemap
// get data for sitemap
export async function getDataForSitemap() {
const data = await fetchData(`

query Sitemap {
getAuthorList {
total
items {
_updatedAt
slug
}
}
getPostList {
total
items {
_updatedAt
slug
}
}
getTagsList {
total
items {
_id
_updatedAt
}
}
}
`);
return data;
}

Run the following command to create sitemap.xml.js under pages directory.

cd pages
touch sitemap.xml.js

Add the following code to sitemap.xml.js.

pages/sitemap.xml.js
//pages/sitemap.xml.js
import { getDataForSitemap } from "../lib/api";

const createSitemap = (data, origin) => {

return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
${data.getAuthorList.items
.map((author) => {
return `
<url>
<loc>${`${origin}/author/${author.slug}`}</loc>
<lastmod>${author._updatedAt}</lastmod>
</url>
`;
})
.join("")}
${data.getPostList.items
.map((post) => {
return `
<url>
<loc>${`${origin}/blog/${post.slug}`}</loc>
<lastmod>${post._updatedAt}</lastmod>
</url>
`;
})
.join("")}
${data.getTagsList.items
.map((tag) => {
return `
<url>
<loc>${`${origin}/tag/${tag._id}`}</loc>
<lastmod>${tag._updatedAt}</lastmod>
</url>
`;
})
.join("")}
</urlset>
`;
};

export async function getServerSideProps({ res }) {
const data = await getDataForSitemap();

res.setHeader('Content-Type', 'text/xml')
res.write(createSitemap(data, "http://localhost:3000"))
res.end()
return {
props: {

},
};
}

const SitemapIndex = () => null
export default SitemapIndex

Head over to http://localhost:3000/sitemap.xml; here is how this page would look.

When deploying, you should change the origin in the createSitemap() function inside the getServerSideProps() function to your domain or deployed URL.

res.write(createSitemap(data, "https://takeshape-next-blog-2.vercel.app"))

You can visit https://takeshape-next-blog-2.vercel.app/sitemap.xml to see the sitemap for this project.

The last step is to create a robots.txt file under the public directory.

Run the following command to create robots.txt.

cd public
touch robots.txt

Add the following text to the robots.txt file.

User-agent: *
Sitemap: https://takeshape-next-blog-2.vercel.app/sitemap.xml

Conclusion

In this article, we discussed how to add multiple features and functionalities to a blog site made with Next.js and TakeShape CMS. You can follow this tutorial and create your own version of this project. There are many features and functionality that you can add to this project.

Here are a few ideas to get you started:

  • Add an RSS feed to your blog.
  • Add comment functionality to your blog posts.
  • Create custom Social Media share buttons for platforms like Facebook, Twitter, Reddit, LinkedIn to your blog posts.
  • Style the app using UI libraries like Chakra UI, Material UI, etc.

Here are some additional resources that can be helpful.

Happy coding!