Quick JAMStack Integration: Prismic CMS and NextJS

Quick JAMStack Integration: Prismic CMS and NextJS

Prismic tools abstract the CMS queries making development a bit faster

Jamstack is an architecture designed to make the web faster, more secure, and easier to scale. It builds on many of the tools and workflows which developers love, and which bring maximum productivity. The core principles of pre-rendering, and decoupling, enable sites and applications to be delivered with greater confidence and resilience than ever before. Explore more of the benefits of Jamstack. From: jamstack.org/what-is-jamstack

Just tried Prismic.io, which is a headless CMS similar to other competitors, you name them, DatoCMS , GraphCMS , or Strapi.io. But the first impression when I looked at the documentation , was like wow..., these guys have many tools and clients to speed up the development time ๐Ÿš€ ... And it was there, the javascript library and NextJS hooks. Of course a CLI tool, we will see all of them. Maybe just one downside the hooks library is still not supporting typescript OOTB but we can fix it.

Contents

  1. Requirements and what we are building
  2. Prismic repository
  3. Setup NextJS and Prismic client
  4. Cleanup
  5. Create API endpoint on NextJS and consume from home
  6. Query what you need
  7. Setup static generated pages
  8. Conclusions

Requirements and what we are building

What we are building

Kind of movie landing and movie detail pages, where landing page data will be filled from the FE and only the shell will be static, but the detail pages will be full statically generated: image.png

Prismic repository

Like mentioned in the requirements section, we need a Prismic.io account.

Create a new repository, I called it ticoflix, I'm bad at giving names.

Once you get created, go inside that repository and click custom types on the left side panel and begin creating a new content type. It should be a repeatable type, will be many of them and I used movie as the type name.

Fields

On the main tab we place:

  • UID type. Name: uid.
  • Image type. Name: thumbnail. Size: 250x250
  • Title type. Name: title. Allow only h1
  • Rich Text type. Name: synopsis. Allow paragram, anchors, lists
  • Number type. Name: year

We create another tab for SEO, on this one we place couple of fields:

  • Key Text type. Name: meta_title.
  • Key Text type. Name: meta_description.

image.png Now, let's save the type, go to documents on your left side panel, and create a couple of documents using our brand new content type.

Setup NextJS and Prismic client

Now back to our local computer, I'll create first the NextJS application supporting typescript

npx create-next-app --typescript

Name the app, e.g ticoflix, once created, get inside this new folder containing the generated code.

It's the moment to set up Prismic.io, as we mentioned in requirements, we need the Prismic CLI installed, so after that we can setup the project running:

prismic sm --setup --no-prismic

This will add the prismic dependencies to our app, but won't create any repo (--no-prismic), because we already created it before, also. will add the prismic configuration files to our app in order to connect to Prismic.io .

Clean prismic.js

We won't need code generated on this file, mostly for links generation, so after cleanup should look like:


import Prismic from "prismic-javascript";

import smConfig from "./sm.json";

export const apiEndpoint = smConfig.apiEndpoint;

// -- Access Token if the repository is not public
// Generate a token in your dashboard and configure it here if your repository is private
export const accessToken = "";

export const Client = (req = null, options = {}) =>
  Prismic.client(apiEndpoint, { req, ...options });

As you see we have the option to use accessToken in order to make the repo not accessible by anybody. For us, not going to use it. But is important for real production scenario.

Configure the prismic endpoint

Go to prismic.io on the dashboard and copy your repo endpoint:

image.png

When you grab it, return back to your local and modify the sm.json file:

{
  "apiEndpoint": "ENDPOINT_HERE",
  "libraries": ["@/slices", "essential-slices"],
  "_latest": "0.0.46"
}

Configure images

As we are going to consume images from prismic.io, we have to let NextJS allow to use the Image component properly, we set next.config.js accordingly, also from NextJS 11, using Image is enforced with linting rules :

module.exports = withTM({
  images: {
    domains: ["images.prismic.io"],
  },
});

Clean up

Create slices folder in the root of the app and add .keepdir inside (if you omit, only you get a warning on terminal when running), by default prismic hook library looks this folder to find slices components. Note: However we are not going to cover on this post the slices. But you can use them anytime via SliceZone component we kept sm-resolver.js file just in case.

Finally on this step we get rid of files *.js files from pages folder since we are using typescript.

Time to run to discard any issue, on terminal: npm run dev and open your browser at http://localhost:3000

Create API endpoint on NextJS and consume from home

API endpoint

Will create an API endpoint on the app, the purpose is to encapsulate the query to Prismic.io for retrieving movies. This endpoint will return an array in JSON format.

Under pages/api I created a pelis.ts, and using the Prismic client we query the movies and return the result, as you see our code uses async/await to keep clean:

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import Prismic from "prismic-javascript";
import { Client } from "../../prismic";

export default async function handler(
    req: NextApiRequest,
    res: NextApiResponse<any>
) {
    const client = Client();
    const queryResults = await client.query([
        Prismic.Predicates.at("document.type", "movie"),
    ]);
    res.status(200).json(queryResults.results);
}

Client is the one that Prismic CLI helped us to generate. By using Prismic.Predicates.at, we query only type movies documents.

Ok now try this endpoint on your browser and confirm you are getting a JSON response with all the movies previously created on Prismic.io repository: http://localhost:3000/api/pelis.

Fetch API endpoint from Home

The Home as you see on What we are building section, will have code running on client-side to fetch async any time this page is loaded, so we get fresh list of movies.

Update the index.ts file, adding useEffect and the fetch for making the http request to the API endpoint:

const [pelis, setPelis] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
  const loadPelis = async () => {
    setLoading(true);
    const response = await fetch('/api/pelis');
    const pelis = await response.json();
    setPelis(pelis);
    setLoading(false)
  }
  loadPelis();
}, [])

I'm telling, every time home component page is mounted, we request our API, and place in state the result, also loading status is switched. Now we will use this status to render the list.

return (
    <div className={styles.container}>
        <Head>
            <title>Ticoflix</title>
            <link rel="icon" href="/favicon.ico" />
        </Head>

        <main className={styles.main}>
            <h1 className={styles.title}>Ticoflix</h1>
            {loading ? <h3>Cargando peliculas...</h3> : null}
            <div className={styles.grid}>
                {pelis.map((p: Document) => (
                    <Link key={p.uid} href={`/p/${p.uid}`}>
                        <a className={styles.card}>
                            <RichText render={p.data.title} />
                            <Image
                                src={p.data.thumbnail.url}
                                alt="Movie thumbnail"
                                width={250}
                                height={250}
                              />
                        </a>
                    </Link>
                ))}
            </div>
        </main>

        <footer className={styles.footer}>
        </footer>
    </div>
);

We do here a simple loop through the pelis array and render Link component, this component is optimized for incremental builds, so it's good to use it instead of plain anchors. Also added a loading indicator, as this will be fetched in the browser, there is RichText component is required to render the movie title, because Prismic creates special structure for Rich Text values, is good to rely on that component to handle rendering.

Let's check again from the browser http://localhost:3000/ and let's check the list working properly.

Query what you need

Just found something interesting, you are able to query only what you need, for example for listing, this is really helpful, in this case for our pages/api/pelis.ts endpoint we just need title and thumbnail, then query function provides a second param where we can pass different query options such as fetch:

const queryResults = await client.query([
    Prismic.Predicates.at("document.type", "movie"),
  ], {
    fetch: ['movie.title', 'movie.thumbnail'],
});

Setup static generated pages

Now it's time to work the movies pages on pages/p/[uid].tsx file, these pages will be generated statically, first, we are going to rely on the prismic hooks :

export const getStaticPaths = useGetStaticPaths({
    client: Client(),
    type: 'movie',
    formatPath: ({ uid }: Document) => ({ params: { uid } }),
    fallback: true
})

export const getStaticProps = useGetStaticProps({
    client: Client(),
    type: 'movie',
    uid: ({ params }: any) => params.uid,
    getStaticPropsParams: {
        revalidate: 5
    }
});

getStaticPaths and getStaticPropsare part of NextJS, then what are doing these prismic hooks is to encapsulate the queries.

For getStaticPaths, we configure the type, pass the Client, formatPath builds the params that getStaticProps will receieve, fallback enables incremental builds .

For getStaticProps, we configure the type, pass the Client, uid helps to make the query to retrieve the movie data. And revalidate, this keeps pulling fresh data every 5 seconds after any page request and regenerates the page.

On the component side, I will use the data retrieved from static props to render the page:

export default function Pelicula(props: Document) {
    const { data } = props;
    const router = useRouter();
    if (router.isFallback) {
        return <h3>Cargando...</h3>
    }
    return (
        <>
            <Head>
                <title>{data.meta_title}</title>
            </Head>
            <div>
                <RichText render={data.title} />
                <RichText render={data.synopsis} />
                <strong>{data.year}</strong>
            </div>
        </>
    )
}

router.isFallback is a boolean that let us know when the page is being generated incrementally, we display a loading indicator. Otherwise we statically generated the page already.

Conclusions

Please comment if there is any question or if was helpful. Also found CLI and node dependencies provided by Prismic.io are great, but I would like to see they support typescript and also keep updated the packages and documentation.

IMPORTANT: Also is known that hooks packages will be deprecated soon prismic.io/docs/technologies/technical-refe.., so will try to keep this post updated once they come with a different module.

Full source code is here .