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
- Knowledge of HTML, CSS, JavaScript
- Basics of React and GraphQL
- Node/NPM installed on your local dev machine
- React Dev tools (Optional)
- Any code editor of your choice
- Downloaded or Forked the previous project from its GitHub repository.
- Added your API keys to the
.env.example
file and rename it to.env
.
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
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
:
// 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
.
{
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
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
.
.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.
// 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
.
.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.
{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
.
.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.
<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.
.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.
//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
// 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 */
.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.

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.
// 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
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.
.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 */
.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
.
// 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
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.
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.
// 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
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.
.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 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.
<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.
.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
.
.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
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.

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.
// 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
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!