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 and in your code with the following. We keep the custom_types up to date in our repository so it's easy to keep track of their evolution.

custom_types/blog_post_tag.json

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 in both your code and in prismic.

custom_types/blog_post.json

1{
2  "SEO": {
3    "uid": {
4      "type": "UID",
5      "config": {
6        "label": "Unique ID",
7        "placeholder": "Type an SEO-friendly identifier..."
8      }
9    },
10    "meta_title": {
11      "type": "Text",
12      "config": {
13        "label": "Meta Title",
14        "placeholder": "Meta title..."
15      }
16    },
17    "meta_description": {
18      "type": "Text",
19      "config": {
20        "label": "Meta Description",
21        "placeholder": "Meta Description..."
22      }
23    },
24    "canonical": {
25      "type": "Link",
26      "config": {
27        "select": "document",
28        "customtypes": ["brewery", "country"],
29        "label": "Canonical URL",
30        "placeholder": "Select the category to use in the canonical URL"
31      }
32    }
33  },
34  "Blog Post": {
35    "title": {
36      "type": "StructuredText",
37      "config": {
38        "single": "heading1",
39        "label": "Title",
40        "placeholder": "Blog Post Title..."
41      }
42    },
43    "date": {
44      "type": "Date",
45      "config": {
46        "label": "Date"
47      }
48    },
49    "blog_post_tags": {
50      "type": "Group",
51      "config": {
52        "fields": {
53          "tag": {
54            "type": "Link",
55            "config": {
56              "select": "document",
57              "customtypes": ["blog_post_tag"],
58              "label": "Tag",
59              "placeholder": "Select a tag"
60            }
61          }
62        },
63        "label": "Blog Post Tags"
64      }
65    },
66    "author": {
67      "type": "Link",
68      "config": {
69        "select": "document",
70        "customtypes": ["author"],
71        "label": "Author",
72        "placeholder": "Select an author"
73      }
74    },
75    "body": {
76      "type": "Slices",
77      "fieldset": "Slice zone",
78      "config": {
79        "labels": {},
80        "choices": {
81          "text": {
82            "type": "Slice",
83            "fieldset": "Text",
84            "description": "A Rich Text section",
85            "icon": "text_fields",
86            "non-repeat": {
87              "rich_text": {
88                "type": "StructuredText",
89                "config": {
90                  "multi": "paragraph, preformatted, heading2, heading3, heading4, heading5, heading6, strong, em, hyperlink, embed, list-item, o-list-item",
91                  "allowTargetBlank": true,
92                  "label": "Rich Text",
93                  "labels": ["code"],
94                  "placeholder": "Enter your text..."
95                }
96              }
97            },
98            "repeat": {}
99          },
100          "code_snippet": {
101            "type": "Slice",
102            "fieldset": "Code Snippet",
103            "description": "A code snippet section for example code",
104            "icon": "code",
105            "non-repeat": {
106              "code_snippet": {
107                "type": "StructuredText",
108                "config": {
109                  "multi": "preformatted",
110                  "label": "Code Snippet",
111                  "placeholder": "Enter code snippet..."
112                }
113              }
114            },
115            "repeat": {}
116          },
117          "image": {
118            "type": "Slice",
119            "fieldset": "Image",
120            "description": "An image section",
121            "icon": "image",
122            "non-repeat": {
123              "image": {
124                "type": "Image",
125                "config": {
126                  "constraint": {},
127                  "thumbnails": [],
128                  "label": "Image"
129                }
130              }
131            },
132            "repeat": {}
133          }
134        }
135      }
136    }
137  }
138}

You can now add a shared component to handle the listing of tags wherever we'll need to list them.

components/Tags.tsx

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.

prismic-configuration.js

1// -- Link resolution rules
2// Manages links to internal Prismic documents
3// Modify as your project grows to handle any new routes you've made
4export const linkResolver = (doc) => {
5  if (doc.type === "blog_post") {
6    return `/blog/${doc.uid}`;
7  }
8  return "/";
9};
10
11// Additional helper function for Next/Link components
12export const hrefResolver = (doc) => {
13  if (doc.type === "blog_post") {
14    return `/blog/[uid]`;
15  }
16  return "/";
17};
18
19export const tagLinkResolver = (tag) => {
20  return `/blog/tag/${tag}`;
21};
22
23export const tagHrefResolver = () => {
24  return `/blog/tag/[uid]`;
25};

In the blog listing page you'll add the code needed to get the tags with the posts in getStaticProps. We also use the tags component we created earlier to list the tags for each post.

pages/blog/index.tsx

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={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 you'll want to update the file 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 shared tags component we created earlier.

pages/blog/[uid].tsx

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 you'll add a new component to handle the listing of blog posts that have a specific tag.

pages/blog/tag/[uid].tsx

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={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;

You now have a working blog post tag system using prismic and Next.js! The complete working code can be found in my Next.js prismic blog GitHub repo.

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