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.