预览(Preview)

页面摘要: Preview(预览)将内容管理器(Content Manager)连接到前端,让编辑者在发布前查看更改。本文档包含:配置预览 URL 的步骤。

借助 Preview(预览)功能,你可以直接从 Strapi 的管理面板预览你的前端应用。这有助于查看在内容管理器(Content Manager)编辑视图(Edit View)中对内容的更新将如何影响最终结果。

  • 套餐:免费功能 Live Preview(实时预览)仅适用于 CMS Growth 和 Enterprise 套餐。
  • 角色与权限:在「角色 > 插件 - 用户与权限(Users & Permissions)」中的读取(Read)权限
  • 启用:应在 config/admin 文件中配置
  • 环境:在开发(Development)和生产(Production)环境中均可用

预览内容

配置

说明
  • 必须在你的 .env 文件中定义以下环境变量,将示例值替换为适当的值:

    CLIENT_URL=https://your-frontend-app.com
    PREVIEW_SECRET=your-secret-key
    

    PREVIEW_SECRET 密钥是可选的,但使用 Next.js 草稿模式(draft mode)时需要。

  • 你的 Strapi 项目的前端应用应已创建并配置好。

配置组件

Preview(预览)功能的配置存储在 the config/admin file 的 preview 对象中,由 3 个关键组件组成:

启用标志

启用或禁用预览功能:

// …
preview: {
  enabled: true,
    // …
}
// …

允许的源(Allowed origins)

控制哪些域可以访问预览:

// …
preview: {
  enabled: true,
  config: {
    allowedOrigins: env("CLIENT_URL"),  // 通常是你的前端应用 URL
    // …
  }
}
// …

预览处理器(Preview handler)

管理预览逻辑和 URL 生成,如下面的基本示例所示,其中 uid 是内容类型标识符(例如 api::article.article 或 plugin::my-api.my-content-type):

// …
preview: {
  enabled: true,
  config: {
    // …
    async handler(uid, { documentId, locale, status }) {
      const document = await strapi.documents(uid).findOne({ documentId });
      const pathname = getPreviewPathname(uid, { locale, document });

      return `${env('PREVIEW_URL')}${pathname}`
    },
  }
}
// …

URL 生成逻辑 的示例在以下基本实现指南中给出。

预览草稿条目

前端应用查询草稿或已发布内容的策略取决于具体框架。至少存在 3 种策略:

  • 使用查询参数,类似 /your-path?preview=true(例如,Nuxt 就是这样工作的)
  • 重定向到专用的预览路由,如 /preview?path=your-path(例如,Next 的草稿模式(draft mode) 就是这样工作的)
  • 或使用不同的域进行预览,如 preview.mysite.com/your-path。

当为你的内容类型启用了 草稿与发布(Draft & Publish) 时,你也可以直接在 Preview handler 中利用 Strapi 的 status 参数来处理逻辑,使用以下通用方法:

async handler(uid, { documentId, locale, status }) {
   const document = await strapi.documents(uid).findOne({ documentId });
   const pathname = getPreviewPathname(uid, { locale, document });
   if (status === 'published')  { 
      // 返回已发布版本
   }
   // 返回草稿版本
},

使用 Next.js 草稿模式的更详细示例在 基本实现指南 中给出。

基本实现指南

按照以下步骤为你的内容类型添加 Preview(预览)能力。

1. [Strapi] 创建 Preview(预览)配置 {#1-create-config}

创建一个新文件 /config/admin.ts(如果已存在则更新它),结构如下:

export default ({ env }) => ({
  // 其他与管理面板相关的配置写在这里
  // (见 docs.strapi.io/cms/configurations/admin-panel)
  preview: {
    enabled: true,
    config: {
      allowedOrigins: env('CLIENT_URL'),
      async handler (uid, { documentId, locale, status }) => {
        // 处理器实现见第 3 步
      },
    },
  },
});

2. [Strapi] 添加 URL 生成逻辑 {#2-add-url-generation}

使用 getPreviewPathname 函数添加 URL 生成逻辑。以下示例取自 Launchpad Strapi 演示应用:

// 根据内容类型和文档生成预览路径名的函数
const getPreviewPathname = (uid, { locale, document }): string => {
  const { slug } = document;
  
  // 使用各自的 URL 模式处理不同的内容类型
  switch (uid) {
    // 处理具有预定义路由的页面
    case "api::page.page":
      switch (slug) {
        case "homepage":
          return `/${locale}`; // 本地化的首页
        case "pricing":
          return "/pricing"; // 定价页面
        case "contact":
          return "/contact"; // 联系页面
        case "faq":
          return "/faq"; // 常见问题页面
      }
    // 处理产品页面
    case "api::product.product": {
      if (!slug) {
        return "/products"; // 产品列表页
      }
      return `/products/${slug}`; // 单个产品页面
    }
    // 处理博客文章
    case "api::article.article": {
      if (!slug) {
        return "/blog"; // 博客列表页
      }
      return `/blog/${slug}`; // 单个文章页面
    }
    default: {
      return null;
    }
  }
};

// … 主导出(见第 3 步)
NOTE

如果某些内容类型没有意义,则不需要有预览,因此 default 分支会返回 null。例如,带有一些站点元数据的全局单一类型(Global single type)就没有匹配的前端页面。在这些情况下,处理器函数应返回 null,预览 UI 将不会在管理面板中显示。这就是你按内容类型启用或禁用预览的方式。

3. [Strapi] 添加处理器逻辑 {#3-add-handler}

创建完整的配置,在第 1 步创建的基础配置中扩展第 2 步创建的 URL 生成逻辑,添加适当的处理器逻辑:

const getPreviewPathname = (uid, { locale, document }): string => {
  // … 如第 2 步所定义
};

// 主配置导出
export default ({ env }) => {
  // 获取环境变量
  const clientUrl = env("CLIENT_URL"); // 前端应用 URL
  const previewSecret = env("PREVIEW_SECRET"); // 用于预览身份验证的密钥

  return {
    // 其他与管理面板相关的配置写在这里
    // (见 docs.strapi.io/cms/configurations/admin-panel)
    preview: {
      enabled: true, // 启用预览功能
      config: {
        allowedOrigins: clientUrl, // 将预览访问限制在特定域
        async handler(uid, { documentId, locale, status }) {
          // 从 Strapi 获取完整的文档
          const document = await strapi.documents(uid).findOne({ documentId });
          
          // 根据内容类型和文档生成预览路径名
          const pathname = getPreviewPathname(uid, { locale, document });

          // 如果未找到路径名,则禁用预览
          if (!pathname) {
            return null;
          }

          // 使用 Next.js 草稿模式(draft mode),向其传递密钥和内容类型状态
          const urlSearchParams = new URLSearchParams({
            url: pathname,
            secret: previewSecret,
            status,
          });
          return `${clientUrl}/api/preview?${urlSearchParams}`;
        },
      },
    },
  };
};

4. [前端] 设置前端预览路由 {#4-setup-frontend-route}

设置前端预览路由高度依赖于前端应用所使用的框架。

例如,Next.js 草稿模式(draft mode) 和 Nuxt 预览模式 在其各自的文档中提供了如何实现前端部分的额外文档。

如果使用 Next.js,一个基本实现可能类似于以下取自 Launchpad Strapi 演示应用的示例:

import { draftMode } from "next/headers";
import { redirect } from "next/navigation";

export async function GET(request: Request) {
  // 解析查询字符串参数
  const { searchParams } = new URL(request.url);
  const secret = searchParams.get("secret");
  const url = searchParams.get("url");
  const status = searchParams.get("status");

  // 检查 secret 和 next 参数
  // 此 secret 应只有该路由处理器和 CMS 知晓
  if (secret !== process.env.PREVIEW_SECRET) {
    return new Response("Invalid token", { status: 401 });
  }

  // 通过设置 cookie 启用草稿模式(Draft Mode)
  if (status === "published") {
    draftMode().disable();
  } else {
    draftMode().enable();
  }

  // 重定向到所获取文章的路径
  // 我们不重定向到 searchParams.slug,因为那可能导致开放重定向漏洞
  redirect(url || "/");
}

5. [前端] 允许前端被嵌入 {#5-allow-frontend-embed}

在 Strapi 一侧,the allowedOrigins configuration parameter 允许管理面板在 iframe 中加载前端窗口。但允许嵌入是双向的,因此在前端一侧,你也需要允许该窗口被嵌入到 Strapi 的管理面板中。

这要求前端应用拥有自己的响应头指令,即 CSP frame-ancestors 指令。设置此指令取决于你的网站是如何构建的。例如,在 Next.js 中设置它需要一个中间件配置(见 Next.js 文档)。

6. [前端] 调整数据获取以适配草稿内容 {#6-fetch-draft-content}

预览系统设置好后,你需要调整数据获取逻辑,以适当地处理草稿内容。这涉及以下步骤:

  1. 创建或调整你的数据获取工具,以检查草稿模式是否启用
  2. 在适当的时候更新你的 API 调用,以包含草稿状态参数

以下示例取自 Launchpad Strapi 演示应用,展示了如何在你的 Next.js 前端应用中实现能够感知草稿的数据获取:

import { draftMode } from "next/headers";
import qs from "qs";

export default async function fetchContentType(
  contentType: string,
  params: Record = {}
): Promise {
  // 检查 Next.js 草稿模式是否启用
  const { isEnabled: isDraftMode } = await draftMode();
  
  try {
    const queryParams = { ...params };
    // 当草稿模式启用时,添加 status=draft 参数
    if (isDraftMode) {
      queryParams.status = "draft";
    }
    
    const url = `${baseURL}/${contentType}?${qs.stringify(queryParams)}`;
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(
        `Failed to fetch data from Strapi (url=${url}, status=${response.status})`
      );
    }
    return await response.json();
  } catch (error) {
    console.error("Error fetching content:", error);
    throw error;
  }
}

然后,这个工具方法可以在你的页面组件中使用,根据预览状态获取草稿或已发布内容:

// 在你的页面组件中:
const pageData = await fetchContentType('api::page.page', {
  // 你的其他查询参数
});

Live Preview(实时预览)实现

(Growth 计划) (Enterprise 计划)

设置好基础的 Preview(预览)功能后,你可以通过实现 Live Preview(实时预览)来增强体验。

窗口消息(Window messages)

Live Preview(实时预览)通过在管理面板和你的前端之间进行通信,创造了更具交互性的体验。它依赖于通过 window 对象上的 the postMessage() API 发送的事件。

你需要在应用中添加一个事件监听器。它应当存在于所有页面上,最好是在包裹整个应用的布局(layout)组件中。监听器需要过滤消息,仅对 Strapi 发起的消息做出反应。

有 2 种消息需要监听:

  • strapiUpdate:当内容更新已保存到数据库时由 Strapi 发送。这是一个获取内容更新版本并刷新预览的时机。在 Next.js 中,刷新 iframe 内容的推荐方式是使用 the router.refresh() method。
  • previewScript:由 Strapi 发送,为你提供一个驱动 Live Preview(实时预览)功能的脚本。这个脚本应当被注入到页面的 <head> 标签中。它负责在预览中高亮可编辑区域,并在双击某个区域进行编辑时向 Strapi 回传消息。

为了接收 previewScript 消息,你需要让 Strapi 知道你的前端已准备好接收它。这通过向父窗口发送 previewReady 消息来完成。

综合起来,一个准备添加到你的全局布局中的组件可能如下所示:

JavaScript

'use client';

export default function LivePreview() {
  // …
  const router = useRouter();

  useEffect(() => {
    const handleMessage = async (message) => {
      const { origin, data } = message;

      if (origin !== process.env.NEXT_PUBLIC_API_URL) {
        return;
      }

      if (data.type === 'strapiUpdate') {
        router.refresh();
      } else if (data.type === 'strapiScript') {
        const script = window.document.createElement('script');
        script.textContent = data.payload.script;
        window.document.head.appendChild(script);
      }
    };

    // 添加事件监听器
    window.addEventListener('message', handleMessage);

    // 让 Strapi 知道我们已准备好接收脚本
    window.parent?.postMessage({ type: 'previewReady' }, '*');

    // 在卸载时移除事件监听器
    return () => {
      window.removeEventListener('message', handleMessage);
    };
  }, [router]);

  return null;
}

TypeScript

'use client';

export default function LivePreview() {
  // …
  const router = useRouter();

  useEffect(() => {
    const handleMessage = async (message: MessageEvent<any>) => {
      const { origin, data } = message;

      if (origin !== process.env.NEXT_PUBLIC_API_URL) {
        return;
      }

      if (data.type === 'strapiUpdate') {
        router.refresh();
      } else if (data.type === 'strapiScript') {
        const script = window.document.createElement('script');
        script.textContent = data.payload.script;
        window.document.head.appendChild(script);
      }
    };

    // 添加事件监听器
    window.addEventListener('message', handleMessage);

    // 让 Strapi 知道我们已准备好接收脚本
    window.parent?.postMessage({ type: 'previewReady' }, '*');

    // 在卸载时移除事件监听器
    return () => {
      window.removeEventListener('message', handleMessage);
    };
  }, [router]);

  return null;
}

Next.js 中的缓存:

在 Next.js 中,缓存持久化 可能需要额外的步骤。你可能需要通过从客户端向服务器发起 API 调用来使缓存失效,由服务器处理重新验证(revalidation)逻辑。详情请参阅 Next.js 文档,例如 revalidatePath() 方法。

内容源映射(Content source maps)

Live Preview(实时预览)能够识别前端中对应于 Strapi 字段的部分。这是通过内容源映射(content source maps)实现的,这些元数据被编码为你基于字符串的内容(例如文本字段)中的隐藏字符。它使用 @vercel/stega 库来编码和解码此元数据。

只有在 strapi-encode-source-maps 响应头被设为 true 时,这些元数据才会被添加到你的 Content API 响应中。你可以在数据获取工具中设置此响应头。务必只在检测到你的站点在预览上下文中渲染时才传递该响应头。

:::caution 内容源映射(content source maps)只会添加到 REST API 响应中。如果你的前端通过 GraphQL API 获取内容,则双击编辑不可用。 :::

对于 Next.js 应用,你可以使用 next/headers 中的 draftMode() 方法来检测草稿模式是否启用,并在所有 API 调用中相应地设置该响应头:

import { draftMode } from "next/headers";
import qs from "qs";

export default async function fetchContentType(
  contentType: string,
  params: Record = {}
): Promise {
  // 检查 Next.js 草稿模式是否启用
  const { isEnabled: isDraftMode } = await draftMode();
  
  try {
    const queryParams = { ...params };
    // 当草稿模式启用时,添加 status=draft 参数
    if (isDraftMode) {
      queryParams.status = "draft";
    }
    
    const url = `${baseURL}/${contentType}?${qs.stringify(queryParams)}`;
    const response = await fetch(url, {
      headers: {
        // 在预览模式下启用内容源映射
        "strapi-encode-source-maps": isDraftMode ? "true" : "false",
      },
    });
    if (!response.ok) {
      throw new Error(
        `Failed to fetch data from Strapi (url=${url}, status=${response.status})`
      );
    }
    return await response.json();
  } catch (error) {
    console.error("Error fetching content:", error);
    throw error;
  }
}

使用

使用该功能的路径: 内容管理器(Content Manager),你的内容类型的编辑视图

Preview(预览)与 Live Preview(实时预览)的对比

根据你的 CMS 套餐,你使用 Preview(预览)的体验会有所不同:

  • 使用免费(Free)套餐时,Preview(预览)仅为全屏模式。
  • 使用 (Growth 计划) 和 (Enterprise 计划) 套餐时,你可以获得增强的体验,称为 Live Preview(实时预览)。借助 Live Preview(实时预览),你可以与内容管理器(Content Manager)的编辑视图并排查看预览,并且你还可以通过双击任意内容直接在预览本身中编辑内容。

一旦 Preview(预览)功能设置妥当,内容管理器(Content Manager)的编辑视图 右侧会出现一个 打开预览(Open preview) 按钮。点击它将在你的前端应用中的显示方式预览你的内容,但直接在 Strapi 的管理面板内。

预览内容

预览打开后,你可以:

  • 点击左上角的关闭按钮 返回内容管理器(Content Manager)的编辑视图,
  • 使用预览内容上方下拉菜单在桌面(Desktop)和移动(Mobile)预览之间切换,
  • 在草稿和已发布版本的预览之间切换(如果为内容类型启用了 草稿与发布(Draft & Publish)),
  • 点击右上角的链接图标 以复制预览链接。根据你的当前预览标签页,这将复制草稿或已发布版本的预览链接。
NOTE

在内容管理器(Content Manager)的编辑视图中,如果你有未保存的更改,打开预览(Open preview) 按钮将被禁用。保存你最新的更改后,你应该能够再次预览内容。

Live Preview(实时预览)

(Growth 计划) (Enterprise 计划)

Live Preview(实时预览)是 Strapi 付费 CMS 套餐中可用的增强型 Preview(预览)体验。

借助 Live Preview(实时预览),除了免费套餐中包含的内容外,你还可以:

  • 使用侧边编辑器(Side Editor)并排查看条目的编辑视图(内容管理器(Content Manager)中)和前端预览。你也可以使用 和 按钮在全屏和并排预览之间切换。
  • 双击预览窗格中的任意内容以就地编辑。这会打开一个弹出框,将前端内容与 Strapi 中对应的字段同步。

预览内容

:::caution 实验性功能 此功能目前为实验性。欢迎随时与 Strapi 团队分享 反馈 或 问题。

当前版本的 Live Preview(实时预览)存在以下限制: