Next.js prismic custom blog post tags

July 13th, 2020 - Justin Friebel
Next.js, prismic

This will show you how to setup blog post tags with Next.js and prismic. First you'll want to create a new custom type in prismic with the following configuration.

1{
2  "Main": {
3    "uid": {
4      "type": "UID",
5      "config": {
6        "label": "UID",
7        "placeholder": "blogposttag"
8      }
9    },
10    "title": {
11      "type": "StructuredText",
12      "config": {
13        "single": "heading1, heading2, heading3, heading4, heading5, heading6",
14        "label": "Title",
15        "placeholder": "Tag Title"
16      }
17    }
18  }
19}

Then you'll want to add the new tag type to your blog post type.

1"blog_post_tags": {
2  "type": "Group",
3  "config": {
4    "fields": {
5      "tag": {
6        "type": "Link",
7        "config": {
8          "select": "document",
9          "customtypes": ["blog_post_tag"],
10          "label": "Tag",
11          "placeholder": "Select a tag"
12        }
13      }
14    },
15    "label": "Blog Post Tags"
16  }
17},

We can then add a component to handle the listing of tags.

1import { RichText } from "prismic-reactjs";
2import Link from "next/link";
3import { tagHrefResolver, tagLinkResolver } from "prismic-configuration";
4
5export interface TagsProps {
6  blogPostTags: [];
7}
8
9const Tags = ({ blogPostTags }: TagsProps) => {
10  return (
11    <div>
12      {blogPostTags.map(({ tag }: any, index: number) => {
13        if (!tag?.data) return;
14
15        const { title } = tag?.data;
16
17        return (
18          <span key={tag.uid}>
19            {index ? ", " : ""}
20            <Link
21              href={tagHrefResolver()}
22              as={tagLinkResolver(tag.uid)}
23              passHref
24            >
25              <a className="blogPostTag">{RichText.asText(title)}</a>
26            </Link>
27          </span>
28        );
29      })}
30    </div>
31  );
32};
33
34export { Tags };

Next you'll need to add some tag link resolvers to your prismic-configuration.js file.

1export const tagLinkResolver = (tag) => {
2  return `/blog/tag/${tag}`;
3};
4
5export const tagHrefResolver = () => {
6  return `/blog/tag/[uid]`;
7};

In our pages/blog/index.tsx file we'll go ahead and add the code needed to get the tags with the posts in getStaticProps. We also use the Tags.tsx component we created earlier to list the tags for each post.

1import React from "react";
2import Prismic from "prismic-javascript";
3import { RichText } from "prismic-reactjs";
4import { GetStaticProps } from "next";
5import Link from "next/link";
6import { linkResolver, hrefResolver } from "prismic-configuration";
7import { Layout } from "components/Layout";
8import { PageHeading } from "components/PageHeading";
9import { Head } from "components/Head";
10import { PrettyDate } from "components/PrettyDate";
11import { Author } from "components/Author";
12import { colors } from "colors";
13import { Tags } from "components/Tags";
14
15const BlogHome = ({ home, posts }) => {
16  const { headline, meta_title, meta_description } = home.data;
17
18  return (
19    <Layout>
20      <Head title={meta_title} description={meta_description} />
21      <PageHeading heading={RichText.asText(headline)} />
22
23      <ul>
24        {posts.results.map((post: any) => {
25          const { blog_post_tags, date, author } = post.data;
26
27          return (
28            <li key={post.uid} className="blogPost">
29              <Link href={hrefResolver(post)} as={linkResolver(post)} passHref>
30                <a>
31                  <h2 className="subtitle">
32                    {RichText.asText(post.data.title)}
33                  </h2>
34                </a>
35              </Link>
36
37              <span className="dateAuthorContainer">
38                {PrettyDate(date)}
39                <Author author={author.data} />
40              </span>
41
42              <Tags blogPostTags={blog_post_tags} />
43            </li>
44          );
45        })}
46      </ul>
47      <style jsx>{`
48        .blogPost {
49          margin-bottom: 40px;
50        }
51        .subtitle {
52          margin-bottom: 12px;
53          line-height: 30px;
54          color: ${colors.link};
55        }
56        .subtitle:hover {
57          -webkit-filter: drop-shadow(0px 0px 3px ${colors.linkHover});
58          filter: drop-shadow(0px 0px 3px ${colors.linkHover});
59        }
60        .dateAuthorContainer {
61          display: flex;
62          align-items: center;
63        }
64      `}</style>
65    </Layout>
66  );
67};
68
69export const getStaticProps: GetStaticProps = async () => {
70  if (!process.env.PRISMIC_API_ENDPOINT || !process.env.PRISMIC_ACCESS_TOKEN)
71    return { props: {} };
72
73  const home = await Prismic.client(process.env.PRISMIC_API_ENDPOINT, {
74    accessToken: process.env.PRISMIC_ACCESS_TOKEN,
75  }).getSingle("blog_home", {});
76
77  const posts = await Prismic.client(process.env.PRISMIC_API_ENDPOINT, {
78    accessToken: process.env.PRISMIC_ACCESS_TOKEN,
79  }).query(Prismic.Predicates.at("document.type", "blog_post"), {
80    orderings: "[my.blog_post.date desc]",
81    fetchLinks: [
82      "author.author_name",
83      "author.author_image",
84      "blog_post_tag.title",
85    ],
86  });
87
88  return { props: { home, posts } };
89};
90
91export default BlogHome;

Next we'll want to update pages/blog/[uid].tsx, which is handles the pages for our individual blog posts. As you can see it also uses fetchLinks to get the tag titles with the posts and passes them the the Tags.tsx file we created earlier.

1import React from "react";
2import { GetStaticProps, GetStaticPaths } from "next";
3import Prismic from "prismic-javascript";
4import { RichText } from "prismic-reactjs";
5import { Layout } from "components/Layout";
6import { PageHeading } from "components/PageHeading";
7import { Head } from "components/Head";
8import { PrettyDate } from "components/PrettyDate";
9import { Tags } from "components/Tags";
10import { Author } from "components/Author";
11import { CodeSlice } from "components/CodeSlice";
12import { TextSlice } from "components/TextSlice";
13
14const Post = ({ post }) => {
15  const {
16    author,
17    blog_post_tags,
18    title,
19    date,
20    post_body,
21    body,
22    meta_title,
23    meta_description,
24  } = post.data;
25
26  const blogContent = body.map((slice, index) => {
27    if (slice.slice_type === "text") {
28      return <TextSlice slice={slice} key={index} />;
29    } else if (slice.slice_type === "code_snippet") {
30      return <CodeSlice content={slice.primary.code_snippet} key={index} />;
31    } else {
32      return null;
33    }
34  });
35
36  return (
37    <Layout>
38      <Head title={meta_title} description={meta_description} />
39      <PageHeading heading={RichText.asText(title)} />
40
41      <span className="dateAuthorContainer">
42        {PrettyDate(date)}
43        <Author author={author.data} />
44      </span>
45
46      <Tags blogPostTags={blog_post_tags} />
47
48      {blogContent}
49
50      <style jsx>{`
51        .dateAuthorContainer {
52          display: flex;
53          align-items: center;
54        }
55      `}</style>
56    </Layout>
57  );
58};
59
60export const getStaticPaths: GetStaticPaths = async () => {
61  if (!process.env.PRISMIC_API_ENDPOINT || !process.env.PRISMIC_ACCESS_TOKEN)
62    return { paths: [], fallback: false };
63
64  const posts = await Prismic.client(process.env.PRISMIC_API_ENDPOINT, {
65    accessToken: process.env.PRISMIC_ACCESS_TOKEN,
66  }).query(Prismic.Predicates.at("document.type", "blog_post"), {
67    orderings: "[my.blog_post.date desc]",
68  });
69
70  const paths = posts.results.map((post) => `/blog/${post.uid}`);
71
72  return { paths, fallback: false };
73};
74
75export const getStaticProps: GetStaticProps = async ({ params }) => {
76  if (
77    !params?.uid ||
78    !process.env.PRISMIC_API_ENDPOINT ||
79    !process.env.PRISMIC_ACCESS_TOKEN
80  )
81    return { props: {} };
82
83  const post = await Prismic.client(process.env.PRISMIC_API_ENDPOINT, {
84    accessToken: process.env.PRISMIC_ACCESS_TOKEN,
85  }).getByUID("blog_post", params?.uid as string, {
86    fetchLinks: [
87      "author.author_name",
88      "author.author_image",
89      "blog_post_tag.title",
90    ],
91  });
92
93  return { props: { post } };
94};
95
96export default Post;

Last we'll add a new pages/blog/tag/[uid].tsx file to handle the listing of blog posts that have a specific tag.

1import React from "react";
2import Prismic from "prismic-javascript";
3import { RichText } from "prismic-reactjs";
4import { GetStaticProps, GetStaticPaths } from "next";
5import Link from "next/link";
6import { linkResolver, hrefResolver } from "prismic-configuration";
7import { Layout } from "components/Layout";
8import { PageHeading } from "components/PageHeading";
9import { Head } from "components/Head";
10import { PrettyDate } from "components/PrettyDate";
11import { Author } from "components/Author";
12import { colors } from "colors";
13import { Tags } from "components/Tags";
14
15const BlogTagListing = ({ posts, singleTagTitle }) => {
16  const tagPageTitle = `${RichText.asText(singleTagTitle)} blog posts`;
17
18  return (
19    <Layout>
20      <Head
21        title={tagPageTitle}
22        description={`Find and read ${tagPageTitle}.`}
23      />
24      <PageHeading heading={tagPageTitle} />
25
26      <ul>
27        {posts.results.map((post) => {
28          const { blog_post_tags, date, author } = post.data;
29
30          return (
31            <li key={post.uid} className="blogPost">
32              <Link href={hrefResolver(post)} as={linkResolver(post)} passHref>
33                <a>
34                  <h2 className="subtitle">
35                    {RichText.asText(post.data.title)}
36                  </h2>
37                </a>
38              </Link>
39
40              <span className="dateAuthorContainer">
41                {PrettyDate(date)}
42                <Author author={author.data} />
43              </span>
44
45              <Tags blogPostTags={blog_post_tags} />
46            </li>
47          );
48        })}
49      </ul>
50      <style jsx>{`
51        .blogPost {
52          margin-bottom: 40px;
53        }
54        .subtitle {
55          margin-bottom: 12px;
56          line-height: 30px;
57          color: ${colors.link};
58        }
59        .subtitle:hover {
60          -webkit-filter: drop-shadow(0px 0px 3px ${colors.linkHover});
61          filter: drop-shadow(0px 0px 3px ${colors.linkHover});
62        }
63        .dateAuthorContainer {
64          display: flex;
65          align-items: center;
66        }
67      `}</style>
68    </Layout>
69  );
70};
71
72export const getStaticPaths: GetStaticPaths = async () => {
73  if (!process.env.PRISMIC_API_ENDPOINT || !process.env.PRISMIC_ACCESS_TOKEN)
74    return { paths: [], fallback: false };
75
76  const tags = await Prismic.client(process.env.PRISMIC_API_ENDPOINT, {
77    accessToken: process.env.PRISMIC_ACCESS_TOKEN,
78  }).query(Prismic.Predicates.at("document.type", "blog_post_tag"), {});
79
80  const paths = tags.results.map((tag) => `/blog/tag/${tag.uid}`);
81
82  return { paths, fallback: false };
83};
84
85export const getStaticProps: GetStaticProps = async ({ params }) => {
86  if (
87    !params?.uid ||
88    !process.env.PRISMIC_API_ENDPOINT ||
89    !process.env.PRISMIC_ACCESS_TOKEN
90  )
91    return { props: {} };
92
93  const allTags = await Prismic.client(process.env.PRISMIC_API_ENDPOINT, {
94    accessToken: process.env.PRISMIC_ACCESS_TOKEN,
95  }).query([Prismic.Predicates.at("document.type", "blog_post_tag")], {});
96
97  const singleTag = allTags.results.filter((tag) => {
98    return tag.uid === params.uid;
99  });
100
101  const tagId = singleTag.map((tag) => {
102    return tag.id;
103  });
104
105  const tagTitle = singleTag.map((tag) => {
106    return tag.data.title;
107  });
108
109  const singleTagId = tagId.values().next().value;
110  const singleTagTitle = tagTitle.values().next().value;
111
112  const posts = await Prismic.client(process.env.PRISMIC_API_ENDPOINT, {
113    accessToken: process.env.PRISMIC_ACCESS_TOKEN,
114  }).query(
115    [
116      Prismic.Predicates.at("document.type", "blog_post"),
117      Prismic.Predicates.at("my.blog_post.blog_post_tags.tag", singleTagId),
118    ],
119    {
120      orderings: "[my.blog_post.date desc]",
121      fetchLinks: [
122        "author.author_name",
123        "author.author_image",
124        "blog_post_tag.title",
125      ],
126    }
127  );
128
129  return { props: { posts, singleTagTitle } };
130};
131
132export default BlogTagListing;

The complete working code can be found in my Next.js prismic blog GitHub repo.

© 2020 Justin Friebel. Powered by Next.js, prismic, & Vercel.