Attempting to use payload-visual-editor with Nextjs App router.

default discord avatar
reepicheep05
2 months ago
1

Here is the gist of it:



In

payload.config.ts

I just mapped the collection slugs to generate a link to the draft route that includes the collection slug as a query parameter.





payload.config.ts
const collections = [...example, example2]

export default buildConfig({
plugins: [
visualEditor({
      previewUrl: () => `${draftURL}?secret=1234`,
      showPreview: false,
      collections: (() => {
        const collectionPreviewLinks = {};
        Array.from(collections).forEach(collection => {
          collectionPreviewLinks[collection.slug] = {
            previewUrl: (previewURLData) => {
              return `${draftURL}?secret=1234&pageTemplate=${collection.slug}`
            }
          }
        })
        return collectionPreviewLinks;
      })()
    }),
]
})


Here is the draft route in

app/api/draft/route.ts


app/api/draft/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'

// route handler enabling draft mode
import { draftMode } from 'next/headers'
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const secret = searchParams.get('secret')
  const pageTemplate = searchParams.get('pageTemplate')
  
  if (secret != process.env.SOMESECRET) {
    return new Response('Invalid token', { status: 401 })
  }

  

  switch (pageTemplate) {
        case 'pages':
            redirectURL = `http://localhost:4001/pages?draft=true`; // draft parameter set here in case the cookie persists outside of payload.
            break
        case: 'more-pages-here'
        ...
        default:
  }

  draftMode().enable()
  redirect(redirectURL)
}


Now the request is redirected with the draft cookie set and a query parameter of

draft=true



At this point, I have my page component check if the cookie is set and determine which component to render. Both use the same template so you don't have maintain two versions.



app/pages/[[...slug]]/page.tsx
import Client from "./Client";
import Server from "./Server";
import { draftMode } from "next/headers";

export default async function Page({
  params,
  searchParams,
}: {
  params: { slug: string };
  searchParams?: { [key: string]: string | string[] | undefined };
}) {
  const { isEnabled } = draftMode();

  if (isEnabled && searchParams?.draft) {
    console.log("Loading Preview...");
    return <Client />;
  } else {
    console.log("Loading Live Page...");
    // prettier-ignore
    {/* @ts-expect-error Server Component */} // Ignore promise warning?
    return <Server params={{ slug: params.slug }} />;
  }
}


app/pages/[[...slug]]/Client.tsx
"use client";
import { useEffect, useState } from "react";
import { Post } from "@payload-types";
import Template from "./PageTemplate";

export default function Preview() {
  const [props, setProps] = useState<Post | null>(null);
  useEffect(() => {
    const listener = (event: MessageEvent) => {
      if (event.data.cmsLivePreviewData) {
        setProps(event.data.cmsLivePreviewData);
      }
    };
    window.addEventListener("message", listener, false);
    return () => {
      window.removeEventListener("message", listener);
    };
  }, []);

  if (!props) {
    return <div>Loading...</div>;
  }

  return <Template {...props} />;
}


app/pages/[[...slug]]/Server.tsx
import Template from "./PageTemplate";

async function getData(slug: string) {
  // Custom Endpoint to get by slug, replace as needed.
  const url = `http://localhost:4001/api/pages/slug/${slug}`; 
  
  
  const res = await fetch(url, { cache: "no-cache" });
  const pages = await res.json();
  let page;
  if (pages.hasOwnProperty("docs")) {
    page = pages.docs.length > 0 ? pages.docs[0] : pages.docs;
  }
  return await page;
}

export default async function Post({ params }: { params: { slug: string } }) {
  const props = await getData(params.slug);
  return <Template {...props} />;
}


app/pages/[[...slug]]/PageTemplate.tsx
import { Post as PostType } from "@payload-types";

export const PostTemplate = (props: PostType) => {
  const { title, slug, author, publishedDate, category, tags, content } = props;
  return <div className="max-w-xl m-auto">{props.title}</div>;
};

export default PostTemplate;
  • default discord avatar
    janpeini
    2 months ago

    That looks nice, is very similar to what we do in app router, but we did not use the draft mode functionality thus far. Great work!



    But we also use a template in a separate file so we can dynamically load it from the preview or the actual page.tsx

Open the post
Continue the discussion in Discord
Like what we're doing?
Star us on GitHub!

Star

Connect with the Payload Community on Discord

Discord

online

Can't find what you're looking for?

Get help straight from the Payload team with an Enterprise License.