A photo of Shane

Shane Chaffe

2 min read ⏳

How to use Contentful to power your knowledge base or blog

I will initially outline how I built out my blog and then go on to how you could configure a knowledge base as well.

Prerequisites:

The key to Contentful is content modelling, it's essential to have a composable content model that reflects what you are going to consume and on a larger scale, is reusable.

The Content Model

Content model screenshot

The interesting thing here is references, they're really powerful because you can link other entries and limit your users to use only existing entries to prevent content duplication which is key.

Screenshot of Author reference field

Other than that, the other fields are self-explanatory I would say, the title is quite self-reflecting of what purpose it should serve. You would then be able to create entries using this content model so that you can consume them in your application.

The code

The next part is the rich text field, using a package called "@contentful/rich-text-react-renderer". This is magically able to render the items you put in a rich text field as you would expect, the only thing you need to do is pass some rich text options to the documentToReactComponents method available from the package, your options might look like this for example:

import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { BLOCKS, MARKS, INLINES } from "@contentful/rich-text-types";
import Image from "next/image";
import Link from "next/link";

export const richTextOptions = {
  renderNode: {
    [BLOCKS.HEADING_1]: (node: any, children: any) => {
      return <h1 className="font-bold text-5xl mb-4 dark:text-white">{children}</h1>;
    },
    [BLOCKS.HEADING_2]: (node: any, children: any) => {
      return <h2 className="font-bold text-2xl mb-4 dark:text-white">{children}</h2>;
    },
    [BLOCKS.HEADING_3]: (node: any, children: any) => {
      return <h3 className="font-bold text-xl mb-4 dark:text-white">{children}</h3>;
    },
    [BLOCKS.HEADING_4]: (node: any, children: any) => {
      return <h4 className="font-bold text-lg mb-4 dark:text-white">{children}</h4>;
    },
    [BLOCKS.HEADING_5]: (node: any, children: any) => {
      return <h5 className="font-bold text-lg mb-4 dark:text-white">{children}</h5>;
    },
    [BLOCKS.HEADING_6]: (node: any, children: any) => {
      return <h6 className="font-bold text-lg mb-4 dark:text-white">{children}</h6>;
    },
    [BLOCKS.UL_LIST]: (node: any, children: any) => {
      return <ul className="list-disc dark:text-slate-200">{children}</ul>;
    },
    [BLOCKS.LIST_ITEM]: (node: any, children: any) => {
      return <li className="my-4">{children}</li>;
    },
    [BLOCKS.OL_LIST]: (node: any, children: any) => {
      return <li className="my-4">{children}</li>;
    },
    [BLOCKS.PARAGRAPH]: (node: any, children: any) => {
      return <p className="mb-2 text-slate-700 dark:text-slate-200">{children}</p>;
    },
    [BLOCKS.TABLE]: (node: any, children: any) => {
      return (
        <table>
          <tbody>{children}</tbody>
        </table>
      );
    },
    [BLOCKS.EMBEDDED_ASSET]: (node: any, children: any) => {
      return (
        <Image
          src={`https:${node.data.target.fields.file.url}`}
          alt={node.data.target.fields.title}
          width={500}
          height={500}
          className="sm:w-1/4 md:w-2/4 lg:w-3/4 my-12 rounded-lg shadow-lg"
        />
      );
    },
    [INLINES.HYPERLINK]: (node: any, children: any) => {
      return (
        <Link
          target="_blank"
          href={node.data.uri}
          className="text-blue-600 hover:underline"
          style={{ wordWrap: "break-word" }}
        >
          {children}
        </Link>
      );
    },
    [BLOCKS.EMBEDDED_ENTRY]: (node: any, children: any) => {
      // node.data.fields holds description, language, code
      const { codeSnippet, language } = node.data.target.fields;
      return (
        <SyntaxHighlighter style={vscDarkPlus} language={language}>
          {codeSnippet}
        </SyntaxHighlighter>
      );
    },
  },
};

You'll notice I've defined what my components should look like and what styles they should have, in addition to that what HTML should be rendered. You will then pass it to the method I mentioned before:

const BlogDetailPage = async ({ slug }: SlugProps) => {
  const blogPost = await getSingleBlogPost(slug);

  // content is the object I pass to documentToReactComponents, this is the
  // name of your field where the body of your content is in Contentful
  const { content, author, tags, dateOfEntry } = blogPost[0].fields;

  const date = formatDate(dateOfEntry as string);

  return (
    <section>
      <Author
        author={author}
      />
      <time className="mb-2 block text-zinc-400 dark:text-slate-200">
        {date}       
      </time>
      <Link href={"/blog"}>
        <ArrowLeftIcon className="h-6 w-6 text-slate-600 mb-8 dark:text-slate-200" />
      </Link>
      {content &&
        documentToReactComponents(content, richTextOptions)}
      <p className="mt-12">Technology used: </p>
      {
        tags?.map((tag, idx) => (
          <span
            key={idx}
            className="inline-block mt-2 mr-4 rounded bg-sky-400 p-2 text-white font-bold"
          >
            {tag as string}
          </span>
        ))
      }
    </section>
  );
};

export default BlogDetailPage;

Now since this is an entry we've created and we have the code to display what we want, we need to make sure in our application it is consumed in the right path. In my file directory, I'll consume this in a path like this: chaffe.dev/blog/[blogId] - the dynamic segment here is where I will consume my entries using a request like this to Contentful:

export const getSingleBlogPost = async (slug: string) => {
  const post = await client.getEntries({
    content_type: "blogPost",
    "fields.slug[match]": slug,
  });

  return post.items;
};

Here I can filter by my content type and then use my slug to request Contentful to deliver the exact entry that is in the path which is defined by me ultimately based on what I create in the web app.

In hindsight, another article would be necessary to cover a knowledge base tutorial.

Technology used:

ContentfulNext.jsVercelContent ModellingReactJavascript