Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docusaurus/docs/dev-docs/configurations/admin-panel.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ The `./config/admin.js` file can include the following parameters:
| `forgotPassword.emailTemplate` | Email template as defined in [email plugin](/dev-docs/plugins/email#using-the-sendtemplatedemail-function) | object | [Default template](https://github.com/strapi/strapi/blob/main/packages/core/admin/server/config/email-templates/forgot-password.js) |
| `forgotPassword.from` | Sender mail address | string | Default value defined in <br />your [provider configuration](/dev-docs/providers#configuring-providers) |
| `forgotPassword.replyTo` | Default address or addresses the receiver is asked to reply to | string | Default value defined in <br />your [provider configuration](/dev-docs/providers#configuring-providers) |
| `preview.enabled` | Enable or disable the [Preview](/user-docs/content-manager/previewing-content) feature |
| `preview.config` | Configure the [Preview](/dev-docs/preview) feature |
| `rateLimit` | Settings to customize the rate limiting of the admin panel's authentication endpoints, additional configuration options come from [`koa2-ratelimit`](https://www.npmjs.com/package/koa2-ratelimit) | object | {} |
| `rateLimit.enabled` | Enable or disable the rate limiter | boolean | `true` |
| `rateLimit.interval` | Time window for requests to be considered as part of the same rate limiting bucket | object | `{ min: 5 }` |
Expand Down
305 changes: 305 additions & 0 deletions docusaurus/docs/dev-docs/preview.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm realizing now there's one last step needed to get the whole thing to work. Apologies for not seeing this earlier.

The permissions to load an iframe go both ways:

  • the parent window needs to allow loading the child window (that we do, it's the allowedOrigins thing)
  • the child window (the preview site) needs to allow being embedded in the parent (the admin)

That second step requires the preview frontend to have its own header directive: the CSP frame-ancestors directive. The way to set it up will depend on how they build their site. For nextjs it requires a middleware config: https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many thanks for all these details, @remidej ! I've just updated the docs. Are we good to merge the PR this afternoon?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is perfect 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
---
title: Setting up the Preview feature
description: Learn to set up the Preview feature to link your front end application to Strapi's Content Manager Preview feature.
displayedSidebar: devDocsSidebar
tags:
- content manager
- preview
- configuration
---

# Setting up the Preview feature <BetaBadge />

Strapi's Preview feature enables previewing content in a frontend application directly from the Strapi admin panel.

The present page describes how to set up the Preview feature in Strapi. Once set up, the feature can be used as described in the [User Guide](/user-docs/content-manager/previewing-content).

:::prerequisites
* The following environment variables must be defined in your `.env` file, replacing example values with appropriate values:

```bash
CLIENT_URL=https://your-frontend-app.com
PREVIEW_SECRET=your-secret-key # optional, required with Next.js draft mode
```

* A front-end application for your Strapi project should be already created and set up.
:::

## Configuration components

The Preview feature configuration is stored in the `preview` object of [the `config/admin` file](/dev-docs/configurations/admin-panel) and consists of 3 key components:

### Activation flag

Enables or disables the preview feature:
```javascript title="config/admin.ts|js" {3}
// …
preview: {
enabled: true,
// …
}
// …
```

### Allowed origins

Controls which domains can access previews:

```javascript title="config/admin.ts|js" {5}
// …
preview: {
enabled: true,
config: {
allowedOrigins: [env("CLIENT_URL")], // Usually your frontend application URL
// …
}
}
// …
```

### Preview handler

Manages the preview logic and URL generation, as in the following basic example where `uid` is the content-type identifier (e.g., `api::article.article` or `plugin::my-api.my-content-type`):

```jsx title="config/admin.ts|js" {6-11}
// …
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}`
},
}
}
// …
```

An example of [URL generation logic](#2-add-url-generation-logic) in given in the following basic implementation guide.

#### Previewing draft entries

The strategy for the front end application to query draft or published content is framework-specific. At least 3 strategies exist:

- using a query parameter, having something like `/your-path?preview=true` (this is, for instance, how [Nuxt](https://nuxt.com/docs/api/composables/use-preview-modehow) works)
- redirecting to a dedicated preview route like `/preview?path=your-path`(this is, for instance, how [Next's draft mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode) works)
- or using a different domain for previews like `preview.mysite.com/your-path`.

When [Draft & Publish](/user-docs/content-manager/saving-and-publishing-content.md) is enabled for your content-type, you can also directly leverage Strapi's `status` parameter to handle the logic within the Preview handler, using the following generic approach:

```javascript
async handler(uid, { documentId, locale, status }) {
const document = await strapi.documents(uid).findOne({ documentId });
const pathname = getPreviewPathname(uid, { locale, document });
if (status === 'published') {
// return the published version
}
// return the draft version
},
```

A more detailed example using the draft mode of Next.js is given in the [basic implementation guide](#3-add-handler-logic).

## Basic implementation guide

Follow these steps to add Preview capabilities to your content types.

### 1. Create the Preview configuration

Create a new file `/config/admin.ts` (or update it if it exists) with the following basic structure:

```javascript title="config/admin.ts"
export default ({ env }) => ({
// Other admin-related configurations go here
// (see docs.strapi.io/dev-docs/configurations/admin-panel)
preview: {
enabled: true,
config: {
allowedOrigins: env('CLIENT_URL'),
async handler (uid, { documentId, locale, status }) => {
// Handler implementation coming in step 3
},
},
},
});
```

### 2. Add URL generation logic

Add the URL generation logic with a `getPreviewPathname` function. The following example is taken from the [Launchpad](https://github.com/strapi/LaunchPad/tree/feat/preview) Strapi demo application:

```typescript title="config/admin.ts"
// Function to generate preview pathname based on content type and document
const getPreviewPathname = (uid, { locale, document }): string => {
const { slug } = document;

// Handle different content types with their specific URL patterns
switch (uid) {
// Handle pages with predefined routes
case "api::page.page":
switch (slug) {
case "homepage":
return `/${locale}`; // Localized homepage
case "pricing":
return "/pricing"; // Pricing page
case "contact":
return "/contact"; // Contact page
case "faq":
return "/faq"; // FAQ page
}
// Handle product pages
case "api::product.product": {
if (!slug) {
return "/products"; // Products listing page
}
return `/products/${slug}`; // Individual product page
}
// Handle blog articles
case "api::article.article": {
if (!slug) {
return "/blog"; // Blog listing page
}
return `/blog/${slug}`; // Individual article page
}
}
return "/"; // Default fallback route
};

// … main export (see step 3)
```

### 3. Add handler logic

Create the complete configuration, expanding the basic configuration created in step 1. with the URL generation logic created in step 2., adding an appropriate handler logic:

```typescript title="config/admin.ts" {8-9,18-35}
const getPreviewPathname = (uid, { locale, document }): string => {
// … as defined in step 2
};

// Main configuration export
export default ({ env }) => {
// Get environment variables
const clientUrl = env("CLIENT_URL"); // Frontend application URL
const previewSecret = env("PREVIEW_SECRET"); // Secret key for preview authentication

return {
// Other admin-related configurations go here
// (see docs.strapi.io/dev-docs/configurations/admin-panel)
preview: {
enabled: true, // Enable preview functionality
config: {
allowedOrigins: clientUrl, // Restrict preview access to specific domain
async handler(uid, { documentId, locale, status }) {
// Fetch the complete document from Strapi
const document = await strapi.documents(uid).findOne({ documentId });

// Generate the preview pathname based on content type and document
const previewPathname = getPreviewPathname(uid, { locale, document });

// Use Next.js draft mode passing it a secret key and the content-type status
const urlSearchParams = new URLSearchParams({
url: getPreviewPathname(uid, { locale, document }),
secret: previewSecret,
status,
});
return `${clientUrl}/api/preview?${urlSearchParams}`;
},
},
},
};
};
```

### 4. Set up the front-end preview route

Setting up the front-end preview route is highly dependent on the framework used for your front-end application.

For instance, [Next.js draft mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode) and
[Nuxt preview mode](https://nuxt.com/docs/api/composables/use-preview-mode) provide additional documentation on how to implement the front-end part in their respective documentations.

If using Next.js, a basic implementation could be like in the following example taken from the [Launchpad](https://github.com/strapi/LaunchPad/tree/feat/preview) Strapi demo application:

```typescript title="/next/api/preview/route.ts"
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";

export async function GET(request: Request) {
// Parse query string parameters
const { searchParams } = new URL(request.url);
const secret = searchParams.get("secret");
const url = searchParams.get("url");
const status = searchParams.get("status");

// Check the secret and next parameters
// This secret should only be known to this route handler and the CMS
if (secret !== process.env.PREVIEW_SECRET) {
return new Response("Invalid token", { status: 401 });
}

// Enable Draft Mode by setting the cookie
if (status === "published") {
draftMode().disable();
} else {
draftMode().enable();
}

// Redirect to the path from the fetched post
// We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
redirect(url || "/");
}
```

### Next steps

Once the preview system is set up, you need to adapt your data fetching logic to handle draft content appropriately. This involves:

1. Create or adapt your data fetching utility to check if draft mode is enabled
2. Update your API calls to include the draft status parameter when appropriate

The following, taken from the [Launchpad](https://github.com/strapi/LaunchPad/tree/feat/preview) Strapi demo application, is an example of how to implement draft-aware data fetching in your Next.js front-end application:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that these links are pointing to the feat/preview branch of the launchpad repo, which will 404 when we merge our PR on the main branch. Hopefully next week after the CMS release

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reminder! Yes, I'm used to it, and still haven't found a satisfying solution to automate this in some way. Any idea? 🤔 Or maybe is there another format of URLs that would persist across merges? 🤔
I'll update it once the strapi/strapi branch is merged.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you could link to the repo's tree for a specific commit, this will never 404: https://github.com/strapi/LaunchPad/tree/faa960bf8ca824f5b92dc984c4627abeb87b85eb

But the downside is you wouldn't get any potential improvement that comes in the future. Maybe with a self reminder to update the commit once in a while it could work?


```typescript {8-18}
import { draftMode } from "next/headers";
import qs from "qs";

export default async function fetchContentType(
contentType: string,
params: Record = {}
): Promise {
// Check if Next.js draft mode is enabled
const { isEnabled: isDraftMode } = draftMode();

try {
const queryParams = { ...params };
// Add status=draft parameter when draft mode is enabled
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;
}
}
```

This utility method can then be used in your page components to fetch either draft or published content based on the preview state:

```typescript
// In your page component:
const pageData = await fetchContentType('api::page.page', {
// Your other query parameters
});
```
52 changes: 52 additions & 0 deletions docusaurus/docs/user-docs/content-manager/previewing-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: Previewing content
description: With the Preview feature, you can preview your front-end directly from the Content Manager
displayedSidebar: userSidebar
tags:
- content manager
- preview
---

# Previewing content <BetaBadge />

With the Preview feature, you can preview your front end application directly from Strapi's admin panel. This is helpful to see how updates to your content in the Edit View of the Content Manager will affect the final result.

<!-- TODO: add a dark mode GIF -->
<ThemedImage
alt="Previewing content"
sources={{
light: '/img/assets/content-manager/previewing-content.gif',
dark: '/img/assets/content-manager/previewing-content.gif',
}}
/>

<!-- <div style={{position: 'relative', paddingBottom: 'calc(54.43121693121693% + 50px)', height: '0'}}>
<iframe id="zpen5g4t8p" src="https://app.guideflow.com/embed/zpen5g4t8p" width="100%" height="100%" style={{overflow:'hidden', position:'absolute', border:'none'}} scrolling="no" allow="clipboard-read; clipboard-write" webkitallowfullscreen mozallowfullscreen allowfullscreen allowtransparency="true"></iframe>
</div> -->

:::prerequisites
- The Strapi admin panel user should have read permissions for the content-type.
- The Preview feature should be configured in the code of the `config/admin` file (see [Developer Docs](/dev-docs/preview) for details).
- A front-end application should already be created and running so you can preview it.
:::

When the Preview feature is properly set up, an **Open preview** button is visible on the right in the Edit View of the Content Manager. Clicking it will display the preview of your content as it will appear in your front-end application, but directly within Strapi's the admin panel:

<!-- TODO: add a dark mode screenshot -->
<ThemedImage
alt="Previewing content"
sources={{
light: '/img/assets/content-manager/previewing-content.png',
dark: '/img/assets/content-manager/previewing-content.png',
}}
/>

From the Preview screen, you can:

- click the close button ![Close button](/img/assets/icons/close-icon.svg) in the upper left corner to go back to the Edit View of the Content Manager,
- switch between previewing the draft and the published version (if [Draft & Publish](/user-docs/content-manager/saving-and-publishing-content) is enabled for the content-type),
- and click the link icon ![Link icon](/img/assets/icons/v5/Link.svg) in the upper right corner to copy the preview link. Depending on the preview tab you are currently viewing, this will either copy the link to the preview of the draft or the published version.

:::note
In the Edit view of the Content Manager, the Open preview button will be disabled if you have unsaved changes. Save your latest changes and you should be able to preview content again.
:::
6 changes: 6 additions & 0 deletions docusaurus/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@ const sidebars = {
label: 'Internationalization (i18n)',
id: 'dev-docs/i18n',
},
{
type: 'doc',
label: 'Preview',
id: 'dev-docs/preview',
},
{
type: 'doc',
id: 'dev-docs/cli',
Expand Down Expand Up @@ -645,6 +650,7 @@ const sidebars = {
type: 'doc',
id: 'user-docs/content-manager/saving-and-publishing-content',
},
'user-docs/content-manager/previewing-content',
'user-docs/content-manager/adding-content-to-releases',
],
},
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.