Guide: setting up Next.js previews

We can serve up the dashboard while developing a Next.js website by tweaking the dev command in our package.json just a little bit.

package.json
{
  "scripts": {
    "dev": "alinea serve -- next dev"
  }
}

This runs both the alinea and next development servers meaning we're able to host live Next.js pages within the alinea dashboard.

Preview API route

To set up preview we follow the recommended Next.js way by creating a preview API route. This API route receives a preview token from alinea and uses it to tell Next.js to redirect to the page we're currently viewing and query draft data. Note that during development this is not strictly required, but getting it out of the way now makes sure this works just the same when we're ready to deploy to production.

pages/api/preview.ts
import {backend} from '@alinea/generated/backend.js'
import type {NextApiRequest, NextApiResponse} from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // We'll parse the previewToken from the url /api/preview?token
  const previewToken = req.url!.split('?').pop()

  // We can ask alinea to parse and validate the preview token.
  // We'll receive the url of the entry we're currently previewing.
  const {url} = await backend.parsePreviewToken(previewToken)
  
  // Store the preview token in the Next.js context so we
  // can use it in the next route to query drafts data. Next.js
  // uses a temporary cookie to persist this.
  res.setPreviewData(previewToken)

  // Finally redirect to the page we actually want to view
  res.redirect(url)
}

Querying preview data

CMS data will typically be queried within the getStaticProps function of Next.js routes. Normally Next.js will call these functions at build time, but because we used the setPreviewData function it will call them directly making sure we don't get cached results.

pages/recipe/[slug].tsx
import {initPages} from '@alinea/generated/pages'
import {GetStaticPropsContext} from 'next'

export async function getStaticProps(context: GetStaticPropsContext) {
  // Pass the previewToken that we receive in previewData to initPages
  const pages = initPages(context.previewData as string)

  // Anything we query on this pages instance will reflect drafts data
  const recipe = await pages.whereType('Recipe').first(page => page.path.is(slug))
  return {props: recipe}
}

export default RecipeView

Setting up the preview panel

For previews to show up in the alinea dashboard we have to adjust the preview property of the workspace we want to preview. We use the BrowserPreview component and point it at our running Next.js development server. You should see the webpage appear on the right side in the dashboard.

alinea.config.tsx
import {alinea, BrowserPreview} from 'alinea'

export const config = createConfig({
  workspaces: {
    web: alinea.workspace('My workspace', {
      // ... workspace options
      preview({entry, previewToken}) {
        // During dev point at running Next.js development server, 
        // in production use the current domain
        const location = process.env.NODE_ENV === 'development' 
          ? 'http://localhost:3000' 
          : ''
        return (
          <BrowserPreview
            url={`${location}/api/preview?${previewToken}`}
            // The preview pane will display this url to the user
            prettyUrl={entry.url}
          />
        )
      }
    })
  }
})

Reloading props on content changes

With the above changes we can now see a live preview during editing. It reloads the iframe a short time after content changes. However, we can do much better by asking Next.js to reload our props and rerender the page instead of doing a full page reload. Alinea exports a hook in the @alinea/preview package (< 1kB) which can be used in _app.tsx or a similar high level component.

pages/app.tsx
import {useNextPreview} from 'alinea/preview/next'
import type {AppProps} from 'next/app'

export default function App({Component, pageProps}: AppProps) {
  // If we're in an iframe listen to content changes and reload props
  useNextPreview()

  // This is just the minimal required view for _app,
  // your implementation will likely be much different
  return <Component {...pageProps} />
}