Dyrected
Guides

Live Preview

See content changes in real time while editing in the Admin UI.

Live preview renders your frontend in an iframe inside the Admin UI. As the editor types, draft data is sent via postMessage and your page re-renders instantly — no publish, no page reload.


1. Configure live preview on the collection

// dyrected.config.ts  (same for both frameworks)
{
  slug: 'posts',
  admin: {
    livePreview: {
      url: ({ doc }) =>
        `${process.env.NEXT_PUBLIC_SITE_URL}/blog/${doc.slug}/preview`,
    },
  },
  fields: [...],
}

2. Create the preview route

Option A — raw postMessage

// app/blog/[slug]/preview/page.tsx
'use client'
import { useEffect, useState } from 'react'

export default function PostPreviewPage() {
  const [post, setPost] = useState<any>(null)

  useEffect(() => {
    window.addEventListener('message', (e) => {
      if (e.data?.type === 'dyrected-live-preview') setPost(e.data.data)
    })
    window.parent.postMessage({ type: 'dyrected-live-preview-ready' }, '*')
  }, [])

  if (!post) return <div>Waiting for preview data…</div>

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Option B — useLivePreview hook

'use client'
import { useLivePreview } from '@dyrected/react'

export default function PostPreviewPage({ initialData }: { initialData: any }) {
  const { data: post } = useLivePreview({
    initialData,
    serverURL: process.env.NEXT_PUBLIC_DYRECTED_URL!,
    depth: 1,
  })

  return (
    <article>
      <h1>{post.title}</h1>
    </article>
  )
}

Pass initialData from a server component wrapper so the page renders before the first postMessage:

// app/blog/[slug]/preview/page.tsx
import { createClient } from '@dyrected/sdk'

const client = createClient({
  baseUrl: process.env.NEXT_PUBLIC_DYRECTED_URL!,
  apiKey:  process.env.DYRECTED_API_KEY!,
})

export default async function PreviewWrapper({ params }: any) {
  const { docs } = await client.collection('posts').find({
    where: { slug: { equals: params.slug } },
    depth: 1
  })
  
  return <PostPreviewPage initialData={docs[0] ?? {}} />
}

Option A — raw postMessage

<!-- pages/blog/[slug]/preview.vue -->
<script setup lang="ts">
const post = ref<any>(null)

onMounted(() => {
  window.addEventListener('message', (e) => {
    if (e.data?.type === 'dyrected-live-preview') post.value = e.data.data
  })
  window.parent.postMessage({ type: 'dyrected-live-preview-ready' }, '*')
})
</script>

<template>
  <div v-if="!post">Waiting for preview data…</div>
  <article v-else>
    <h1>{{ post.title }}</h1>
    <div v-html="post.content" />
  </article>
</template>

Option B — useDyrectedLivePreview composable

<!-- pages/blog/[slug]/preview.vue -->
<script setup lang="ts">
const { data: post } = useLivePreview({ initialData: null, depth: 1 })
</script>

<template>
  <article v-if="post">
    <h1>{{ post.title }}</h1>
    <div v-html="post.content" />
  </article>
  <div v-else>Waiting for preview data…</div>
</template>

3. Secure the preview route

// middleware.ts
export function middleware(req: NextRequest) {
  const referer = req.headers.get('referer') ?? ''
  const isPreview = req.nextUrl.pathname.includes('/preview')
  const fromAdmin = referer.includes(process.env.NEXT_PUBLIC_ADMIN_URL ?? '/admin')

  if (isPreview && !fromAdmin) {
    return NextResponse.redirect(new URL('/', req.url))
  }
  return NextResponse.next()
}
// middleware/preview-guard.ts
export default defineNuxtRouteMiddleware((to) => {
  if (!to.path.includes('/preview')) return

  const referer = useRequestHeaders(['referer']).referer ?? ''
  const adminUrl = useRuntimeConfig().public.adminUrl ?? '/admin'

  if (!referer.includes(adminUrl)) {
    return navigateTo('/')
  }
})

Testing

  1. Open the Admin UI and edit a post
  2. Click Preview in the top-right corner
  3. The preview pane loads your frontend in an iframe
  4. Edit the title — it updates in real time

See Live Preview reference for full config options.

On this page