prismicio / prismic-next

Helpers to integrate Prismic into Next.js apps
https://prismic.io/docs/technologies/nextjs
Apache License 2.0
57 stars 7 forks source link

RFC: @prismicio/next #1

Closed angeloashmore closed 2 years ago

angeloashmore commented 3 years ago

Overview

@prismicio/next is a new, yet to be written package designed to ease development when integrating Prismic with Next.js. It is both a library and a set of architecture recommendations for Next.js users.

It is targeted for users using Next.js directly, whether alongside other data sources, such as APIs and the file system, or exclusively with Prismic.

This package should make adoption of Prismic within Next.js a more pleasant experience, with most manual work automated through helpful functions.

This package and the surrounding concepts have the following goals:

"Easily" means easy to setup, but also easy to maintain and produce sustainable projects.

Background Information

Prismic's integration with Next.js is currently handled in two ways:

  1. A first-class Slice Machine integration composed of custom code generators, Slice Machine-specific functions, and React components.
  2. Starter projects with a suggested project organization to orchestrate fetching and displaying Prismic data.

This leaves a gap between the two approaches: developers who are familiar with Next.js with existing projects and developers who will be using Prismic outside of Slice Machine.

Developers in those groups are currently left to wire up solutions themselves. This can lead to error-prone code, additional maintenance burdens, and frustration.

Ultimately, this could drive users away from Prismic.

Proposal

Concepts from Slice Machine's integration with Next.js can be made more general. By making them less Slice Machine-specific, any user of Next.js—and, in some cases, React—can take advantage of the abstractions.

This is not a duplication of efforts. Instead, code generated and used by Slice Machine will come from @prismicio/next.

The following concepts and abstractions can be provided by the new package. Some come from Slice Machine, while others are new and unique to this approach.

Embrace Next.js' concepts

Users should use Next.js and its features as the Next.js team intends. By doing so, all documentation, examples, and best practices for Next.js will apply to these users' projects. As new Next.js features are introduced, they can be integrated into users' projects by the users themselves.

It should follow, then, that @prismicio/next works within the paradigms of Next.js. It should embrace the framework's core features, such as getStaticPaths and getStaticProps. It should provide solutions that are flexible and configurable, much like Next.js itself.

It should not try to hide Next.js features. It should not provide solutions that close off users from extending functionality.

Working to Next.js' strengths will also make the Prismic integration stronger.

Embrace Prismic core libraries

This proposal builds upon previous proposals and efforts to strengthen and simplify lower-level Prismic libraries. For example, by simplifying the core JavaScript client library, @prismicio/client, we have made data fetching in Next.js simpler as well.

These efforts should be embraced and used directly within Next.js.

For context, the opposite of this approach would involve hiding lower-level functionality behind single-task black boxes. This leads to technical debt as users can no longer extend that single task without updates to the library with fragmentation of usage.

Consider the following example for setting up getStaticPaths and getStaticProps:

import { useGetStaticProps, useGetStaticPaths } from "next-slicezone/hooks";

import { Client } from "../prismic-configuration";

export const getStaticProps = useGetStaticProps({
    client: Client(),
    uid: ({ params }) => params.uid,
});

export const getStaticPaths = useGetStaticPaths({
    client: Client(),
    type: "page",
    fallback: process.env.NODE_ENV === "development",
    formatPath: ({ uid }) => ({ params: { uid } }),
});

And the more flexible, albeit more verbose, counterpart:

import * as prismicNext from "@prismicio/next";
import * as prismicH from "@prismicio/helpers";

import { createClient, linkResolver } from "../prismic";

export const getStaticProps = async (context) => {
    const client = createClient();
    prismicNext.enableClientServerSupportFromContext(client, context);

    const uid = context.params.uid;
    const page = await client.getByUID("page", uid);

    return {
        props: { page },
    };
};

export const getStaticPaths = async () => {
    const client = createClient();
    const pages = await client.getAllByType("page");
    const paths = pages.map((page) => ({
        params: {
            uid: page.uid,
            pagePath: prismicH.documentAsLink(page, linkResolver).split("/"),
        },
    }));

    return { paths, fallback: true };
};

The first example showcases single-task black boxes to output data Next.js expects. It allows a user to configure its output through different string and function options. Someone familiar with Next.js, but unfamiliar with next-slicezone/hooks, may not understand what is happening or what the options affect.

The second example performs the same tasks as the first, but takes a more functional approach. Rather than providing single-task black boxes, @prismicio/next provides small functions to be used throughout a user's own logic. In this example, that logic includes using @prismicio/client directly to fetch documents, allowing the user to perform that work as they see fit. Someone familiar with Next.js, but unfamiliar with @prismicio/next, could read the code, have a general understanding of its effects, and know how to extend it.

Although neither example shows how TypeScript would be integrated, it should be clear how it can be added to the second example.

Provide small helper functions

The following helper functions abstract common tasks while allowing a user to use them as they see fit.

How could it be implemented

The library would consist of the above small helper functions. As such, implementations should be straightforward.

// enableClientServerSupportFromContext.ts

import * as nextT from "next/types";
import * as prismic from "@prismicio/client";

import { getPreviewRefFromContext } from "./getPreviewRefFromContext";

export const enableClientServerSupportFromContext = (
    client: prismic.Client,
    context: nextT.GetStaticPropsContext
): void => {
    const ref = getPreviewRefFromContext(context);

    if (ref) {
        client.queryContentFromRef(ref);
    }
};
// getPreviewRefFromContext.ts

import * as nextT from "next/types";

import { PrismicNextPreviewData } from "../types";

const isPrismicNextPreviewData = (
    previewData: nextT.PreviewData
): previewData is PrismicNextPreviewData =>
    typeof previewData === "object" && "ref" in previewData;

export const getPreviewRefFromContext = (
    context: nextT.GetStaticPropsContext
): string | undefined => {
    if (isPrismicNextPreviewData(context.previewData)) {
        return context.previewData.ref;
    }
};
// buildPreviewDataFromReq.ts

import * as nextT from "next/types";

import { PrismicNextPreviewData } from "../types";

export const buildPreviewDataFromReq = (
    req: nextT.NextApiRequest
): PrismicNextPreviewData => {
    if (Array.isArray(req.query.token)) {
        return {
            ref: req.query.token[0]
        };
    }

    return {
        ref: req.query.token
    };
};

From these functions, users can setup Preview Mode easily:

// pages/api/preview.ts

import * as nextT from "next/types";
import * as prismicNext from "@prismicio/next";

import { buildPreviewDataFromReq } from "../../lib/buildPreviewDataFromReq";
import { createClient, linkResolver } from "../../prismic";

export default async function handler(
    req: nextT.NextApiRequest,
    res: nextT.NextApiResponse
): Promise<void> {
    const client = createClient();
    client.enableServerSupportFromReq(req);

    const previewData = prismicNext.buildPreviewDataFromReq(req);
    const resolvedURL = await client.resolvePreviewURL({ linkResolver });

    res.setPreviewData(previewData);
    res.redirect(resolvedURL);
}
// pages/api/exit-preview.ts

import * as nextT from "next/types";

export default function handler(
    _req: nextT.NextApiRequest,
    res: nextT.NextApiResponse
): void {
    res.clearPreviewData();
}

The Preview Mode entry handler (pages/api/preview.ts) is recommended to be implemented within a project rather than via a helper method (e.g. createPrismicPreviewModeAPIHandler). This simplifies @prismicio/client instance management and allows a user to customize the handler.

If you have a good suggestion on how this could be more automated, please share!

How to provide feedback

Everything described above is open for feedback.

Do you think @prismicio/next should do more? Be more opinionated? Or less?

If you have comments, please share them here. ✌️

lihbr commented 3 years ago

LGTM! Maybe you should make it clearer that Next users will also benefit from @prismicio/react's RFC moving on :) https://github.com/prismicio/prismic-reactjs/issues/92

hypervillain commented 3 years ago

looks good to me too! I'm curious what the pagePath param in the second example would be used for

hypervillain commented 3 years ago

Could we also discuss a sort of enableMockDataSupportFromContext? :)

angeloashmore commented 3 years ago

@hypervillain

pagePath

pagePath is the parameter for the src/pages/[[...pagePath]].ts page parameter. It's what sets the URL for the document/page. This would end up looking like ['blog', 'category', 'my-category'] for a resolved URL like /blog/category/my-category.

The uid parameter in the example is actually not allowed to be passed to getStaticProps. We'll have to figure out how we can convert a URL/pagePath parameter to a document's ID or UID.

Does the current next-slicezone/hooks functions allow complex nested URLs? Something with route resolvers?

enableMockDataSupportFromContext

Are you thinking the client should return mock data if enabled? Is this connected to the mock.json files in the current next-slicezone/hooks?