Live preview with NextJS

default discord avatar
Sandro Wegmannlast year
15

I just recently discovered Payload and fell in love with it immediately.



I'm now trying to set up a live preview with the NextJS Frontend which is deployed on vercel. The goal would be to have 2 windows open, edit the content in payload on the left side, see the changes on the preview website in real time (small delays are fine).



My Idea was to set up a separate deploy with an auth wall on a subdomain (preview.xyz.com) that get's the data in draft mode after the user has logged in with his payload credentials. Using the autosave feature, the content on the preview page should be updated every 2 seconds.



One unsolved question is how to actually refresh the page content: I could let the page query the content every 2 seconds, or maybe set up a websocket connection to the payload express app? Since I haven't done anything similar before, I'd love to hear if anyone thinks this will work, or if there are better/simpler ways to implement this.

  • discord user avatar
    jmikrut
    last year

    Hey @Sandro Wegmann — I do think that a simple websocket or messaging service would be the best way to do this



    and this is on our radar to build into Payload for sure, natively, although as of right now, you could do this on your own using SocketIO or similar

  • default discord avatar
    FireGuy_42last year

    Sandro you are doing the lords work, if you figure it out lmk. Would love to implement a live preview, I wish there was documentation on implementation of just the preview functionality with next similar to what Prismic, DatoCMS, etc... Trying to hack something together to show team proof of concept

  • default discord avatar
    AngeloKlast year

    Same here, would love this! As of right now I just refetch the draft data each 2seconds on a preview route.



    Using SocketIO seems interesting for sure.

  • default discord avatar
    Sandro Wegmannlast year

    I'll post in here when I've figured sth out 😉



    @FireGuy_42 @AngeloK I actually managed to get it working. Check my post in the Showcase Channel or hmu 🙂

  • default discord avatar
    AngeloKlast year

    Very nice! I would love to try and implement this in my project. It's SvelteKit tho, but the functionality stays the same I guess.

  • default discord avatar
    Sandro Wegmannlast year

    Yup, you just have to implement a functionality in the frontend to 1. Authenticate a user (send a login request to payload) and 2. if logged in, listen to update calls from the server. This is a bit tricky if you're using Nextjs along with SSG etc. but I don't know about sveltekit



    Here's what you have to do serverside

    // Instead of just calling app.listen
    // app.listen(process.env.PAYLOAD_PUBLIC_INTERNAL_SERVER_URL.split(':')[2]);
    
    // create an http server (or https if you aren't using a reverse proxy)
    const server = http.createServer(app)
    server.listen(process.env.PAYLOAD_PUBLIC_INTERNAL_SERVER_URL.split(':')[2], () => {
      console.log(`HTTP Server running on port ${process.env.PAYLOAD_PUBLIC_INTERNAL_SERVER_URL.split(':')[2]}`);
    });
    
    // Socket.io
    // Now pass the server instance to socket io, to create a websocket server on the same port
    const io = require('socket.io')(server, {
      cors: {
        origin: process.env.WHITELIST_ORIGINS.split(','),
      }
    });
    
    io.on('connection', (socket) => {
      console.log('New client connected');
    
      socket.on('disconnect', () => {
        // console.log('Client disconnected')
      });
    })
    
    // You can't call io.emit directly from the payload hooks, it throws errors. 
    // As a workaround, you can set up a simple endpoint that emits the update 
    app.post('/updatePreview', (req, res) => {
      console.log('update preview called')
      // Check if authorization header contains PAYLOAD_SECRET
      if (req.headers.authorization !== process.env.PAYLOAD_PUBLIC_SERVER_SECRET) {
        res.sendStatus(401)
        return
      }
      io.emit('update')
    })


    Now, in the payload hooks, add an afterChange Hook:


    if (args.doc._status !== 'published') {
                try {
                  axios({
                    method: 'post',
                    url: `${process.env.PAYLOAD_PUBLIC_INTERNAL_SERVER_URL}/updatePreview`,
                    headers: {
                      'Authorization': `${process.env.PAYLOAD_PUBLIC_SERVER_SECRET}`
                    }
                  })
                } catch (e) {
                  console.error(e)
                }
                return console.log('Not published, skipping revalidation')

    This will just call the api endpoint you've just set up and emit the update message

  • default discord avatar
    Jarrodlast year

    if you write code blocks and want them to be syntax highlighted for readability, you can append the language after the 3 back ticks. For typescript it would be 3 back ticks followed by the word

    typescript
  • default discord avatar
    Sandro Wegmannlast year

    Oh damn thanks @Jarrod ! This is my first time really using discord haha

  • default discord avatar
    Jarrodlast year

    No prob! Was just letting you know 😃

  • default discord avatar
    AngeloKlast year

    @Sandro Wegmann finally had the chance to test this out. Works like a charm!

  • default discord avatar
    Ellielast year

    Gosh I'm a real grave-digger here, but if anyones interested, my solution (developed accidentally in parallel with this one haha) is perhaps a bit more involved, but the authentication part is quite interesting so I figured I should probably post it. It does require the two apps (frontend and payload) to be one the same-ish URL (ie. www.example.com and cms.example.com) but works pretty well I would say!



    export const io = new Server(server, {
      cors: {
        origin: WEB_URL,
        credentials: true,
      },
      allowRequest: (req, callback) => {
        const cookies = Object.fromEntries(
          req.headers.cookie
            ?.split("; ")
            .map((cookie) => cookie.split("="))
            .filter((tuple) => tuple.length === 2) ?? []
        );
        const payloadToken = cookies["payload-token"];
        if (typeof payloadToken !== "string") {
          callback("Not signed in", false);
          return;
        }
        jwt.verify(payloadToken, payload.secret, (err) => {
          if (err) {
            callback("Invalid token", false);
            return;
          }
          callback(null, true);
        });
      },
    });
Star on GitHub

Star

Chat on Discord

Discord

online

Can't find what you're looking for?

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