Skip to content

Commit 583561c

Browse files
committed
feat: ✨ implement sanity preview mode
allows for previewing pages from the CMS in real time GH-8
1 parent 0f57693 commit 583561c

File tree

11 files changed

+318
-35
lines changed

11 files changed

+318
-35
lines changed

app/components/app/ExitPreview.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
export const ExitPreview = () => {
3+
return (
4+
<div className="fixed inset-0 z-50 flex items-end justify-center w-screen h-screen pointer-events-none">
5+
<form className="pointer-events-auto" action="/resource/preview" method="POST">
6+
<button className="p-4 font-bold text-white bg-black" type="submit">
7+
Exit Preview Mode
8+
</button>
9+
</form>
10+
</div>
11+
)
12+
}

app/components/app/Page.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react'
2+
import { RouteData } from '~/loaders'
3+
import { Module } from '../modules'
4+
5+
interface PageProps {
6+
page: RouteData['page']
7+
}
8+
9+
export const Page = ({ page }: PageProps) => {
10+
11+
return (
12+
<>
13+
{page?.modules?.map((module, i) => (
14+
<Module key={i} index={i} data={module} />
15+
))}
16+
</>
17+
)
18+
}

app/components/app/PagePreview.tsx

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { PreviewSuspense } from '@sanity/preview-kit'
2+
import { QueryParams } from 'sanity';
3+
import { usePreview } from '~/utils/sanityClient';
4+
import { ExitPreview } from './ExitPreview'
5+
import { Page } from './Page'
6+
7+
interface PagePreviewProps {
8+
query: string;
9+
params: QueryParams;
10+
}
11+
12+
export const PagePreview = ({ query, params }: PagePreviewProps) => {
13+
const page = usePreview(null, query, params)
14+
15+
return (
16+
<>
17+
<ExitPreview />
18+
<Page page={page} />
19+
</>
20+
)
21+
}

app/loaders/index.ts

+14-10
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,23 @@ import type { BlogPost } from './groq-fragments/documents/blog-post'
1010
import type { Page } from './groq-fragments/documents/page'
1111
import type { Site } from './groq-fragments/documents/site'
1212
import type { Modules } from './groq-fragments/objects/modules'
13-
import { Params } from '@remix-run/react'
13+
import type { Params } from '@remix-run/react'
14+
1415
export type { Page, Site, BlogPost, Modules }
1516

17+
export const getPageQueryAndParams = (params: Params) => {
18+
const { lang, slug } = getLangAndSlugFromParams(params)
19+
const query = slug === lang ? pageQueryById.replace('$id', queryHomeID) : pageQueryBySlug
20+
21+
return { query, params: { slug, lang } }
22+
}
23+
1624
export async function getPage(params: Params) {
1725
let page: Page | undefined
18-
const { lang, slug } = getLangAndSlugFromParams(params)
26+
const args = getPageQueryAndParams(params)
1927

2028
try {
21-
if(slug === lang) {
22-
page = await client.fetch<Page>(pageQueryById.replace('$id', queryHomeID), { lang })
23-
} else {
24-
page = await client.fetch<Page>(pageQueryBySlug, { slug, lang })
25-
}
29+
page = await client.fetch<Page>(args.query, args.params)
2630
} catch (error: unknown) {
2731
if(
2832
error &&
@@ -33,23 +37,23 @@ export async function getPage(params: Params) {
3337
}
3438

3539
page && assert(
36-
page.lang === lang,
40+
page.lang === args.params.lang,
3741
`pathname language didn't match the page results language`
3842
)
3943

4044
return {
4145
page: page ? Object.assign(page, {
4246
images: buildPageImages(page)
4347
}) : undefined,
44-
lang,
48+
lang: args.params.lang,
4549
notFound: !page
4650
}
4751
}
4852

4953
export async function getSite(params: Params) {
5054
const { lang } = getLangAndSlugFromParams(params)
5155

52-
const site = await client.fetch<Site>(siteQuery, { lang })
56+
const site: Site = await client.fetch(siteQuery, { lang })
5357
assert(site, 'site was undefined')
5458

5559
return {

app/routes/__app/($lang)/$.tsx

+25-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import { Module } from "~/components/modules";
2-
import { getPage, getSite } from "~/loaders";
1+
import { useLoaderData } from "@remix-run/react";
2+
import { PreviewSuspense } from "@sanity/preview-kit";
3+
4+
import { getPage, getPageQueryAndParams, getSite } from "~/loaders";
35
import { metadata } from "~/loaders/metadata";
46
import { dynamicLinks } from "~/loaders/dynamicLinks";
57
import { useRouteData } from "~/hooks/useRouteData";
68
import { merge } from "~/utils/utils";
9+
import { getSession } from "~/sessions";
10+
import { Page } from "~/components/app/Page";
11+
import { PagePreview } from "~/components/app/PagePreview";
712

813
import type { ActionArgs, LoaderArgs, MetaFunction } from "@remix-run/node";
914

@@ -17,7 +22,15 @@ export const handle = {
1722
dynamicLinks
1823
}
1924

20-
export const loader = async ({ params }: LoaderArgs) => {
25+
export const loader = async ({ request, params }: LoaderArgs) => {
26+
const isPreview = !!(await getSession(request.headers.get('Cookie'))).get('preview')
27+
28+
if(isPreview) return {
29+
...await getSite(params),
30+
isPreview,
31+
...getPageQueryAndParams(params)
32+
}
33+
2134
const data = await merge([
2235
getSite(params),
2336
getPage(params)
@@ -26,7 +39,7 @@ export const loader = async ({ params }: LoaderArgs) => {
2639
if (!data.page)
2740
throw new Response("Not Found", { status: 404 })
2841

29-
return data
42+
return { ...data, isPreview }
3043
}
3144

3245
export const action = async ({ request }: ActionArgs) => {
@@ -52,14 +65,18 @@ export const action = async ({ request }: ActionArgs) => {
5265

5366
export type ActionData = Awaited<ReturnType<typeof action>> | undefined
5467

55-
export default function Page() {
68+
export default function Component() {
5669
const data = useRouteData()
70+
const { isPreview, query, params } = useLoaderData()
5771

5872
return (
5973
<>
60-
{data?.page?.modules?.map((module, i) => (
61-
<Module key={i} index={i} data={module} />
62-
))}
74+
{isPreview ?
75+
<PreviewSuspense fallback="Loading...">
76+
<PagePreview query={query} params={params} />
77+
</PreviewSuspense>
78+
: <Page page={data.page} />
79+
}
6380
</>
6481
);
6582
}

app/routes/resource/preview.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { json, redirect } from '@remix-run/node'
2+
3+
import { getSession, commitSession, destroySession } from '~/sessions'
4+
5+
import type { ActionFunction, LoaderArgs } from '@remix-run/node'
6+
7+
// A `POST` request to this route will exit preview mode
8+
export const action: ActionFunction = async ({request}) => {
9+
if (request.method !== 'POST') {
10+
return json({message: 'Method not allowed'}, 405)
11+
}
12+
13+
const session = await getSession(request.headers.get('Cookie'))
14+
15+
return redirect('/', {
16+
headers: {
17+
'Set-Cookie': await destroySession(session),
18+
},
19+
})
20+
}
21+
22+
// A `GET` request to this route will enter preview mode
23+
export const loader = async ({request}: LoaderArgs) => {
24+
const session = await getSession(request.headers.get('Cookie'))
25+
// For a more advanced use case, you could use this
26+
// to store a read token from sanity.io/manage
27+
session.set(`preview`, `a-random-string`)
28+
29+
return redirect(`/`, {
30+
headers: {
31+
'Set-Cookie': await commitSession(session),
32+
},
33+
})
34+
}

app/sessions.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {createCookieSessionStorage} from '@remix-run/node'
2+
3+
const {getSession, commitSession, destroySession} = createCookieSessionStorage({
4+
cookie: {
5+
name: '__session',
6+
sameSite: 'lax',
7+
secrets: [process.env.SESSION_SECRET!],
8+
secure: process.env.NODE_ENV === "production",
9+
},
10+
})
11+
12+
export {getSession, commitSession, destroySession}

app/utils/sanityClient.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import createSanityClient from "@sanity/client";
1+
import { createClient } from "@sanity/client";
2+
import { definePreview } from "@sanity/preview-kit";
23
import { projectDetails } from "sanity/projectDetails";
34

45
import { IS_PROD } from "~/utils/constants";
@@ -10,4 +11,5 @@ const options = {
1011
useCdn: IS_PROD,
1112
}
1213

13-
export const client = createSanityClient(options)
14+
export const client = createClient(options)
15+
export const usePreview = definePreview(options);

0 commit comments

Comments
 (0)