Skip to content

Commit cb57976

Browse files
pwizlaremidej
andauthored
Live Preview improvements (#2690) (#2703)
* Add first draft version * Fix badge text position within tab items * Update code & content + add noTooltip feature for badges * Re-arrange sections * Fix typo * separate live preview section * Use numbers instead of letters for numerals * Update capitalization * Add TODO for dark mode GIF --------- Co-authored-by: Rémi de Juvigny <[email protected]>
1 parent b1483a6 commit cb57976

File tree

8 files changed

+244
-76
lines changed

8 files changed

+244
-76
lines changed

docusaurus/docs/cms/features/preview.md

Lines changed: 158 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -293,80 +293,164 @@ On the Strapi side, [the `allowedOrigins` configuration parameter](#allowed-orig
293293

294294
This requires the front-end application to have its own header directive, the CSP `frame-ancestors` directive. Setting this directive up depends on how your website is built. For instance, setting this up in Next.js requires a middleware configuration (see [Next.js docs](https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy)).
295295

296-
#### 6. [Front end] Detect changes in Strapi and refresh the front-end {#6-refresh-frontend}
296+
#### 6. [Front end] Adapt data fetching for draft content {#6-fetch-draft-content}
297297

298-
Strapi emits a `strapiUpdate` message to inform the front end that data has changed.
298+
Once the preview system is set up, you need to adapt your data fetching logic to handle draft content appropriately. This involves the following steps:
299+
300+
1. Create or adapt your data fetching utility to check if draft mode is enabled
301+
2. Update your API calls to include the draft status parameter when appropriate
302+
303+
The following, taken from the <ExternalLink to="https://github.com/strapi/LaunchPad/tree/feat/preview" text="Launchpad" /> Strapi demo application, is an example of how to implement draft-aware data fetching in your Next.js front-end application:
304+
305+
```typescript {8-18}
306+
import { draftMode } from "next/headers";
307+
import qs from "qs";
308+
309+
export default async function fetchContentType(
310+
contentType: string,
311+
params: Record = {}
312+
): Promise {
313+
// Check if Next.js draft mode is enabled
314+
const { isEnabled: isDraftMode } = await draftMode();
315+
316+
try {
317+
const queryParams = { ...params };
318+
// Add status=draft parameter when draft mode is enabled
319+
if (isDraftMode) {
320+
queryParams.status = "draft";
321+
}
322+
323+
const url = `${baseURL}/${contentType}?${qs.stringify(queryParams)}`;
324+
const response = await fetch(url);
325+
if (!response.ok) {
326+
throw new Error(
327+
`Failed to fetch data from Strapi (url=${url}, status=${response.status})`
328+
);
329+
}
330+
return await response.json();
331+
} catch (error) {
332+
console.error("Error fetching content:", error);
333+
throw error;
334+
}
335+
}
336+
```
337+
338+
This utility method can then be used in your page components to fetch either draft or published content based on the preview state:
339+
340+
```typescript
341+
// In your page component:
342+
const pageData = await fetchContentType('api::page.page', {
343+
// Your other query parameters
344+
});
345+
```
346+
347+
### Live Preview implementation <GrowthBadge/> <EnterpriseBadge />
348+
349+
After setting up the basic Preview feature, you can enhance the experience by implementing Live Preview.
350+
351+
#### Window messages
299352

300-
To track this, within your front-end application, add an event listener to listen to events posted through [the `postMessage()` API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) on the `window` object. The listener needs to filter through messages and react only to Strapi-initiated messages, then refresh the iframe content.
353+
Live Preview creates a more interactive experience by communicating between the admin and your frontend. It relies on events posted through [the `postMessage()` API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) on the `window` object.
354+
355+
You need to add an event listener in your application. It should be present on all pages, ideally in a layout component that wraps your entire application. The listener needs to filter through messages and react only to Strapi-initiated messages.
356+
357+
There are 2 messages to listen to:
358+
359+
- `strapiUpdate`: sent by Strapi when a content update has been saved to the database. It's an opportunity to fetch the updated version of the content and refresh the preview. With Next.js, the recommended way to refresh the iframe content is with <ExternalLink to="https://nextjs.org/docs/app/building-your-application/caching#routerrefresh" text="the `router.refresh()` method" />.
360+
- `previewScript`: sent by Strapi to give you a script that powers the Live Preview functionality. This script should be injected into the page's `<head>` tag. It handles highlighting editable areas in the preview and sending messages back to Strapi when an area is double-clicked for editing.
361+
362+
In order to receive the `previewScript` message, you need to let Strapi know that your frontend is ready to receive it. This is done by posting a `previewReady` message to the parent window.
363+
364+
When putting it all together, a component ready to be added to your global layout could look like:
301365

302-
With Next.js, the recommended way to refresh the iframe content is with <ExternalLink to="https://nextjs.org/docs/app/building-your-application/caching#routerrefresh" text="the `router.refresh()` method" />.
303366

304367
<Tabs groupId="js-ts">
305-
<TabItem value="js" label="JavaScript" >
368+
<TabItem label="JavaScript" value="js">
369+
370+
```jsx title="next/app/path/to/your/front/end/logic.jsx"
371+
'use client';
306372

307-
```tsx title="next/app/path/to/your/front/end/logic.jsx" {6-17}
308-
export default function MyClientComponent({...props}) {
373+
export default function LivePreview() {
309374
//
310375
const router = useRouter();
311376

312377
useEffect(() => {
313378
const handleMessage = async (message) => {
314-
if (
315-
// Filters events emitted through the postMessage() API
316-
message.origin === process.env.NEXT_PUBLIC_API_URL &&
317-
message.data.type === "strapiUpdate"
318-
) { // Recommended way to refresh with Next.js
379+
const { origin, data } = message;
380+
381+
if (origin !== process.env.NEXT_PUBLIC_API_URL) {
382+
return;
383+
}
384+
385+
if (data.type === 'strapiUpdate') {
319386
router.refresh();
387+
} else if (data.type === 'strapiScript') {
388+
const script = window.document.createElement('script');
389+
script.textContent = data.payload.script;
390+
window.document.head.appendChild(script);
320391
}
321392
};
322393

323394
// Add the event listener
324-
window.addEventListener("message", handleMessage);
395+
window.addEventListener('message', handleMessage);
396+
397+
// Let Strapi know we're ready to receive the script
398+
window.parent?.postMessage({ type: 'previewReady' }, '*');
325399

326-
// Cleanup the event listener on unmount
400+
// Remove the event listener on unmount
327401
return () => {
328-
window.removeEventListener("message", handleMessage);
402+
window.removeEventListener('message', handleMessage);
329403
};
330404
}, [router]);
331405

332-
// ...
406+
return null;
333407
}
334408
```
335-
336409
</TabItem>
337-
<TabItem value="ts" label="TypeScript" >
338410
339-
```tsx title="next/app/path/to/your/front/end/logic.tsx" {6-17}
340-
export default function MyClientComponent({
341-
//
411+
<TabItem label="TypeScript" value="ts">
412+
413+
```tsx title="next/app/path/to/your/front/end/logic.tsx"
414+
'use client';
415+
416+
export default function LivePreview() {
417+
//
342418
const router = useRouter();
343419

344420
useEffect(() => {
345421
const handleMessage = async (message: MessageEvent<any>) => {
346-
if (
347-
// Filters events emitted through the postMessage() API
348-
message.origin === process.env.NEXT_PUBLIC_API_URL &&
349-
message.data.type === "strapiUpdate"
350-
) { // Recommended way to refresh with Next.js
422+
const { origin, data } = message;
423+
424+
if (origin !== process.env.NEXT_PUBLIC_API_URL) {
425+
return;
426+
}
427+
428+
if (data.type === 'strapiUpdate') {
351429
router.refresh();
430+
} else if (data.type === 'strapiScript') {
431+
const script = window.document.createElement('script');
432+
script.textContent = data.payload.script;
433+
window.document.head.appendChild(script);
352434
}
353435
};
354436

355437
// Add the event listener
356-
window.addEventListener("message", handleMessage);
438+
window.addEventListener('message', handleMessage);
439+
440+
// Let Strapi know we're ready to receive the script
441+
window.parent?.postMessage({ type: 'previewReady' }, '*');
357442

358-
// Cleanup the event listener on unmount
443+
// Remove the event listener on unmount
359444
return () => {
360-
window.removeEventListener("message", handleMessage);
445+
window.removeEventListener('message', handleMessage);
361446
};
362447
}, [router]);
363448

364-
//
365-
})
449+
return null;
450+
}
366451
```
367452
368453
</TabItem>
369-
370454
</Tabs>
371455
372456
<details>
@@ -377,16 +461,15 @@ In Next.js, [cache persistence](https://nextjs.org/docs/app/building-your-applic
377461
378462
</details>
379463
380-
#### [Front end] Next steps
464+
#### Content source maps
381465
382-
Once the preview system is set up, you need to adapt your data fetching logic to handle draft content appropriately. This involves the following steps:
466+
Live Preview is able to identify the parts of your frontend that correspond to fields in Strapi. This is done through content source maps, which are metadata encoded as hidden characters in your string-based content (e.g., text fields). It uses the <ExternalLink to="https://www.npmjs.com/package/@vercel/stega" text="@vercel/stega"/> library to encode and decode this metadata.
383467
384-
1. Create or adapt your data fetching utility to check if draft mode is enabled
385-
2. Update your API calls to include the draft status parameter when appropriate
468+
Metadatas will only be added in your Content API responses when the `strapi-encode-source-maps` header is set to `true`. You can set this header in your data fetching utility. Make sure to only pass the header when you detect that your site is rendered in a preview context.
386469
387-
The following, taken from the <ExternalLink to="https://github.com/strapi/LaunchPad/tree/feat/preview" text="Launchpad" /> Strapi demo application, is an example of how to implement draft-aware data fetching in your Next.js front-end application:
470+
For a Next.js application, you may use the `draftMode()` method from `next/headers` to detect if draft mode is enabled, and set the header accordingly in all your API calls:
388471
389-
```typescript {8-18}
472+
```typescript {20-23}
390473
import { draftMode } from "next/headers";
391474
import qs from "qs";
392475

@@ -395,7 +478,7 @@ export default async function fetchContentType(
395478
params: Record = {}
396479
): Promise {
397480
// Check if Next.js draft mode is enabled
398-
const { isEnabled: isDraftMode } = draftMode();
481+
const { isEnabled: isDraftMode } = await draftMode();
399482

400483
try {
401484
const queryParams = { ...params };
@@ -405,7 +488,12 @@ export default async function fetchContentType(
405488
}
406489

407490
const url = `${baseURL}/${contentType}?${qs.stringify(queryParams)}`;
408-
const response = await fetch(url);
491+
const response = await fetch(url, {
492+
headers: {
493+
// Enable content source maps in preview mode
494+
"strapi-encode-source-maps": isDraftMode ? "true" : "false",
495+
},
496+
});
409497
if (!response.ok) {
410498
throw new Error(
411499
`Failed to fetch data from Strapi (url=${url}, status=${response.status})`
@@ -419,23 +507,14 @@ export default async function fetchContentType(
419507
}
420508
```
421509
422-
This utility method can then be used in your page components to fetch either draft or published content based on the preview state:
423-
424-
```typescript
425-
// In your page component:
426-
const pageData = await fetchContentType('api::page.page', {
427-
// Your other query parameters
428-
});
429-
```
430-
431510
## Usage
432511
433512
**Path to use the feature:** <Icon name="feather" /> Content Manager, edit view of your content type
434513
435514
:::strapi Preview vs. Live Preview
436515
Based on your CMS plan, your experience with Preview will be different:
437516
- With the Free plan, Preview will be full screen only.
438-
- With the <GrowthBadge /> and <EnterpriseBadge /> plans, you get access to Live Preview. With Live Preview, you can see the Preview alongside the Edit view of the Content Manager, allowing you to edit your content and previewing it simultaneously.
517+
- With the <GrowthBadge /> and <EnterpriseBadge /> plans, you get access to an enhanced experience called Live Preview. With Live Preview, you can see the Preview alongside the Edit view of the Content Manager, and you can also edit the content directly within the preview itself by double-clicking on any content.
439518
:::
440519
441520
Once the Preview feature is properly set up, an **Open preview** button is visible on the right side of the [Content Manager's edit view](/cms/features/content-manager#overview). 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.
@@ -444,25 +523,45 @@ Once the Preview feature is properly set up, an **Open preview** button is visib
444523
<ThemedImage
445524
alt="Previewing content"
446525
sources={{
447-
light: '/img/assets/content-manager/previewing-content2.gif',
448-
dark: '/img/assets/content-manager/previewing-content2.gif',
526+
light: '/img/assets/content-manager/previewing-content3.gif',
527+
dark: '/img/assets/content-manager/previewing-content3.gif',
449528
}}
450529
/>
451530
452531
Once the Preview is open, you can:
453532
454533
- click the close button <Icon name="x" classes="ph-bold" /> in the upper left corner to go back to the Edit View of the Content Manager,
534+
- switch between the Desktop and Mobile preview using the dropdown above the previewed content,
455535
- switch between previewing the draft and the published version (if [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type),
456536
- and click the link icon <Icon name="link" classes="ph-bold"/> 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.
457537
458-
Additionally, with Live Preview, you can:
459-
- with <GrowthBadge /> and <EnterpriseBadge /> plans, expand the side panel by clicking on the <Icon name="arrow-line-left" classes="ph-bold" /> button to make the Preview full screen,
460-
- and, with the <EnterpriseBadge /> plan, use buttons at the top right of the editor to define the assignee and stage for the [Review Workflows feature](/cms/features/review-workflows) if enabled.
461-
462538
:::note
463539
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.
464540
:::
465541
466-
:::tip
467-
Switch between Desktop and Mobile preview mode using the dropdown at the top to check how the page is displayed on different viewports.
468-
:::
542+
### Live Preview
543+
<GrowthBadge /> <EnterpriseBadge />
544+
545+
Live Preview is the enhanced Preview experience available with Strapi’s paid CMS plans.
546+
547+
With Live Preview, in addition to what’s included in the Free plan, you can:
548+
549+
* Use the Side Editor to view both the entry’s Edit view in the Content Manager and the front-end preview side by side. You can also switch between full-screen and side-by-side preview using the <Icon name="arrow-line-left" classes="ph-bold" /> and <Icon name="arrow-line-right" classes="ph-bold" /> buttons.
550+
* Double-click any content in the preview pane to edit it in place. This opens a popover that syncs the front-end content with the corresponding field in Strapi.
551+
552+
<!-- TODO: add dark mode GIF -->
553+
<ThemedImage
554+
alt="Previewing content"
555+
sources={{
556+
light: '/img/assets/content-manager/previewing-content-live.gif',
557+
dark: '/img/assets/content-manager/previewing-content-live.gif',
558+
}}
559+
/>
560+
561+
:::caution Experimental feature
562+
This feature is currently experimental. Feel free to share <ExternalLink to="https://feedback.strapi.io/" text="feedback"/> or <ExternalLink to="https://github.com/strapi/strapi/issues" text="issues" /> with the Strapi team.
563+
564+
The current version of Live Preview comes with the following limitations:
565+
* Blocks fields are not detected, and changing them in the Side Editor won’t be reflected in the preview. Clicking on Save after updates should however still work.
566+
* Media assets and fields in dynamic zones are not handled.
567+
:::

docusaurus/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,6 @@
6868
},
6969
"engines": {
7070
"node": ">=18.0"
71-
}
71+
},
72+
"packageManager": "[email protected]+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
7273
}

docusaurus/src/components/Badge.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default function Badge({
1414
version,
1515
tooltip,
1616
inline = false,
17+
noTooltip = false,
1718
...rest
1819
}) {
1920
const variantNormalized = variant.toLowerCase().replace(/\W/g, '');
@@ -28,7 +29,8 @@ export default function Badge({
2829
(feature && `badge--featureflag`),
2930
((variant === "Updated" || variant === "New") && `badge--content`),
3031
(inline && 'badge--inline'),
31-
className
32+
className,
33+
(noTooltip && 'badge--no-tooltip')
3234
)}
3335
{...rest}
3436
>
@@ -43,15 +45,14 @@ export default function Badge({
4345
/>
4446
<span className="badge__text">{variant}</span>
4547
</span>
46-
<span className="badge__tooltip">{tooltip}</span>
48+
{!noTooltip && tooltip && <span className="badge__tooltip">{tooltip}</span>}
4749
</>
4850
) : (
4951
<>
50-
{variant}
51-
<span className="badge__tooltip">{tooltip}</span>
52+
{variant}
53+
{!noTooltip && tooltip && <span className="badge__tooltip">{tooltip}</span>}
5254
</>
53-
)
54-
}
55+
)}
5556
</>
5657
) : (
5758
<a className="badge__link" href={link}>
@@ -62,7 +63,7 @@ export default function Badge({
6263
/>
6364
)}
6465
{variant}
65-
<span className="badge__tooltip">{tooltip}</span>
66+
{!noTooltip && tooltip && <span className="badge__tooltip">{tooltip}</span>}
6667
</a>
6768
)}
6869
{children}

0 commit comments

Comments
 (0)