Skip to content

Commit e354110

Browse files
authored
feat: setup live preview inline editing (#69)
* feat: setup preview inline editing (cherry picked from commit f611284) * chore: more preview url building to strapi config (cherry picked from commit 0015f26) * fix: only encode source maps in draft mode (cherry picked from commit d58c5b2)
1 parent b8ca475 commit e354110

File tree

6 files changed

+122
-84
lines changed

6 files changed

+122
-84
lines changed

next/app/[locale]/(marketing)/ClientSlugHandler.tsx

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,5 @@ export default function ClientSlugHandler({
1818
}
1919
}, [localizedSlugs, dispatch]);
2020

21-
const router = useRouter();
22-
23-
useEffect(() => {
24-
const handleMessage = async (message: MessageEvent<any>) => {
25-
if (
26-
message.origin === process.env.NEXT_PUBLIC_API_URL &&
27-
message.data.type === 'strapiUpdate'
28-
) {
29-
router.refresh();
30-
}
31-
};
32-
33-
// Add the event listener
34-
window.addEventListener('message', handleMessage);
35-
36-
// Cleanup the event listener on unmount
37-
return () => {
38-
window.removeEventListener('message', handleMessage);
39-
};
40-
}, [router]);
41-
4221
return null; // This component only handles the state and doesn't render anything.
4322
}

next/app/api/preview/route.ts

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,24 @@ import { redirect } from 'next/navigation';
44
export const GET = async (request: Request) => {
55
const { searchParams } = new URL(request.url);
66
const secret = searchParams.get('secret');
7-
const slug = searchParams.get('slug');
8-
const locale = searchParams.get('locale');
9-
const uid = searchParams.get('uid');
7+
const url = searchParams.get('url') ?? '/';
108
const status = searchParams.get('status');
119

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

16-
const contentType = uid?.split('.').pop();
17-
18-
// Specific for the application
19-
let slugToReturn = `/${locale}/${contentType}`;
20-
21-
if (contentType === 'page' || contentType === 'global') {
22-
if (slug && slug !== 'homepage') {
23-
slugToReturn = `/${locale}/${slug}`;
24-
} else {
25-
slugToReturn = `/${locale}`;
26-
}
27-
} else if (contentType === 'article' || contentType?.includes('blog')) {
28-
slugToReturn = `/${locale}/blog${slug ? `/${slug}` : ''}`;
29-
} else if (contentType?.includes('product')) {
30-
slugToReturn = `/en/products${slug ? `/${slug}` : ''}`;
31-
}
32-
3316
const draft = await draftMode();
34-
if (status === 'draft') {
35-
draft.enable();
36-
} else {
17+
18+
if (status === 'published') {
19+
// Make sure draft mode is disabled so we only query published content
3720
draft.disable();
21+
} else {
22+
// Enable draft mode so we can query draft content
23+
draft.enable();
3824
}
39-
redirect(slugToReturn);
25+
26+
redirect(url);
4027
};

next/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Locale, i18n } from '@/i18n.config';
55
import './globals.css';
66

77
import { SlugProvider } from './context/SlugContext';
8+
import { Preview } from '@/components/preview';
89

910
export const viewport: Viewport = {
1011
themeColor: [
@@ -25,6 +26,7 @@ export default function RootLayout({
2526
return (
2627
<html lang="en" suppressHydrationWarning>
2728
<body suppressHydrationWarning>
29+
<Preview />
2830
<SlugProvider>{children}</SlugProvider>
2931
</body>
3032
</html>

next/components/preview.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use client';
2+
3+
import { useRouter } from 'next/navigation';
4+
import { useEffect } from 'react';
5+
6+
export const Preview = () => {
7+
const router = useRouter();
8+
9+
useEffect(() => {
10+
const handleMessage = async (message: MessageEvent<any>) => {
11+
const { origin, data } = message;
12+
13+
if (origin !== process.env.NEXT_PUBLIC_API_URL) {
14+
return;
15+
}
16+
17+
if (data.type === 'strapiUpdate') {
18+
router.refresh();
19+
} else if (data.type === 'strapiScript') {
20+
const script = window.document.createElement('script');
21+
script.textContent = data.payload.script;
22+
window.document.head.appendChild(script);
23+
}
24+
};
25+
26+
// Add the event listener
27+
window.addEventListener('message', handleMessage);
28+
29+
// Let Strapi know we're ready to receive the script
30+
window.parent?.postMessage({ type: 'previewReady' }, '*');
31+
32+
// Remove the event listener on unmount
33+
return () => {
34+
window.removeEventListener('message', handleMessage);
35+
};
36+
}, [router]);
37+
38+
return null;
39+
};

next/lib/strapi/fetchContentType.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ export default async function fetchContentType(
3333
params: Record<string, unknown> = {},
3434
spreadData?: boolean
3535
): Promise<any> {
36-
const { isEnabled } = await draftMode();
36+
const { isEnabled: isDraftMode } = await draftMode();
3737

3838
try {
3939
const queryParams = { ...params };
4040

41-
if (isEnabled) {
41+
if (isDraftMode) {
4242
queryParams.status = 'draft';
4343
}
4444

@@ -49,6 +49,9 @@ export default async function fetchContentType(
4949
const response = await fetch(`${url.href}?${qs.stringify(queryParams)}`, {
5050
method: 'GET',
5151
cache: 'no-store',
52+
headers: {
53+
'strapi-encode-source-maps': isDraftMode ? 'true' : 'false',
54+
},
5255
});
5356

5457
if (!response.ok) {

strapi/config/admin.ts

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,71 @@
1-
export default ({ env }) => ({
2-
auth: {
3-
secret: env('ADMIN_JWT_SECRET'),
4-
},
5-
apiToken: {
6-
salt: env('API_TOKEN_SALT'),
7-
},
8-
transfer: {
9-
token: {
10-
salt: env('TRANSFER_TOKEN_SALT'),
1+
const getPreviewPathname = (uid, { locale, document }): string | null => {
2+
const { slug } = document;
3+
4+
switch (uid) {
5+
case 'api::page.page': {
6+
if (slug === 'homepage') {
7+
return '/';
8+
}
9+
return `/${slug}`;
10+
}
11+
case 'api::product.product':
12+
return `/products/${slug}`;
13+
case 'api::product-page.product-page':
14+
return '/products';
15+
case 'api::article.article':
16+
return `/blog/${slug}`;
17+
case 'api::blog-page.blog-page':
18+
return '/blog';
19+
default:
20+
return null;
21+
}
22+
};
23+
24+
export default ({ env }) => {
25+
const clientUrl = env('CLIENT_URL');
26+
const previewSecret = env('PREVIEW_SECRET');
27+
28+
return {
29+
auth: {
30+
secret: env('ADMIN_JWT_SECRET'),
31+
},
32+
apiToken: {
33+
salt: env('API_TOKEN_SALT'),
34+
},
35+
transfer: {
36+
token: {
37+
salt: env('TRANSFER_TOKEN_SALT'),
38+
},
39+
},
40+
flags: {
41+
nps: env.bool('FLAG_NPS', true),
42+
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
1143
},
12-
},
13-
flags: {
14-
nps: env.bool('FLAG_NPS', true),
15-
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
16-
},
17-
preview: {
18-
enabled: true,
19-
config: {
20-
allowedOrigins: [env('CLIENT_URL')],
21-
async handler(uid, { documentId, locale, status }) {
22-
const document = await strapi.documents(uid).findOne({
23-
documentId,
24-
populate: null,
25-
fields: ['slug'],
26-
});
27-
const { slug } = document;
44+
preview: {
45+
enabled: true,
46+
config: {
47+
allowedOrigins: [clientUrl],
48+
async handler(uid, { documentId, locale, status }) {
49+
const document = await strapi
50+
.documents(uid)
51+
.findOne({ documentId, locale, status });
52+
const pathname = getPreviewPathname(uid, { locale, document });
2853

29-
const urlSearchParams = new URLSearchParams({
30-
secret: env('PREVIEW_SECRET'),
31-
...(slug && { slug }),
32-
locale,
33-
uid,
34-
status,
35-
});
54+
// Disable preview if the pathname is not found
55+
if (!pathname) {
56+
return null;
57+
}
3658

37-
const previewURL = `${env('CLIENT_URL')}/api/preview?${urlSearchParams}`;
59+
// Use Next.js draft mode
60+
const urlSearchParams = new URLSearchParams({
61+
url: `/${locale ?? 'en'}${pathname}`,
62+
secret: previewSecret,
63+
status,
64+
});
3865

39-
return previewURL;
66+
return `${clientUrl}/api/preview?${urlSearchParams}`;
67+
},
4068
},
4169
},
42-
},
43-
});
70+
};
71+
};

0 commit comments

Comments
 (0)