Skip to content

Commit fb60344

Browse files
authored
feat(templates): add search functionality to the website template (#8454)
- adds a /search and search plugin example to website template - adds an additional check for valid paths on /preview - fixes a few bugs around the site
1 parent f50174f commit fb60344

File tree

19 files changed

+10602
-61
lines changed

19 files changed

+10602
-61
lines changed

templates/website/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@payloadcms/plugin-form-builder": "beta",
2626
"@payloadcms/plugin-nested-docs": "beta",
2727
"@payloadcms/plugin-redirects": "beta",
28+
"@payloadcms/plugin-search": "beta",
2829
"@payloadcms/plugin-seo": "beta",
2930
"@payloadcms/richtext-lexical": "beta",
3031
"@payloadcms/ui": "beta",

templates/website/pnpm-lock.yaml

Lines changed: 10116 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

templates/website/src/Header/Nav/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import React from 'react'
55
import type { Header as HeaderType } from '@/payload-types'
66

77
import { CMSLink } from '@/components/Link'
8+
import Link from 'next/link'
9+
import { SearchIcon } from 'lucide-react'
810

911
export const HeaderNav: React.FC<{ header: HeaderType }> = ({ header }) => {
1012
const navItems = header?.navItems || []
@@ -14,6 +16,10 @@ export const HeaderNav: React.FC<{ header: HeaderType }> = ({ header }) => {
1416
{navItems.map(({ link }, i) => {
1517
return <CMSLink key={i} {...link} appearance="link" />
1618
})}
19+
<Link href="/search">
20+
<span className="sr-only">Search</span>
21+
<SearchIcon className="w-5" />
22+
</Link>
1723
</nav>
1824
)
1925
}

templates/website/src/app/(frontend)/[slug]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default async function Page({ params: { slug = 'home' } }) {
3939
})
4040

4141
// Remove this code once your website is seeded
42-
if (!page) {
42+
if (!page && slug === 'home') {
4343
page = homeStatic
4444
}
4545

templates/website/src/app/(frontend)/next/preview/route.ts

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { draftMode } from 'next/headers'
33
import { redirect } from 'next/navigation'
44
import { getPayloadHMR } from '@payloadcms/next/utilities'
55
import configPromise from '@payload-config'
6+
import { CollectionSlug } from 'payload'
67

78
const payloadToken = 'payload-token'
89

@@ -19,29 +20,67 @@ export async function GET(
1920
const token = req.cookies.get(payloadToken)?.value
2021
const { searchParams } = new URL(req.url)
2122
const path = searchParams.get('path')
23+
const collection = searchParams.get('collection') as CollectionSlug
24+
const slug = searchParams.get('slug')
2225

23-
if (!path) {
24-
return new Response('No path provided', { status: 404 })
25-
}
26+
const previewSecret = searchParams.get('previewSecret')
2627

27-
if (!token) {
28-
new Response('You are not allowed to preview this page', { status: 403 })
29-
}
28+
if (previewSecret) {
29+
return new Response('You are not allowed to preview this page', { status: 403 })
30+
} else {
31+
if (!path) {
32+
return new Response('No path provided', { status: 404 })
33+
}
3034

31-
let user
35+
if (!collection) {
36+
return new Response('No path provided', { status: 404 })
37+
}
3238

33-
try {
34-
user = jwt.verify(token, payload.secret)
35-
} catch (error) {
36-
payload.logger.error('Error verifying token for live preview:', error)
37-
}
39+
if (!slug) {
40+
return new Response('No path provided', { status: 404 })
41+
}
3842

39-
// You can add additional checks here to see if the user is allowed to preview this page
40-
if (!user) {
41-
draftMode().disable()
42-
return new Response('You are not allowed to preview this page', { status: 403 })
43-
}
43+
if (!token) {
44+
new Response('You are not allowed to preview this page', { status: 403 })
45+
}
46+
47+
if (!path.startsWith('/')) {
48+
new Response('This endpoint can only be used for internal previews', { status: 500 })
49+
}
50+
51+
let user
52+
53+
try {
54+
user = jwt.verify(token, payload.secret)
55+
} catch (error) {
56+
payload.logger.error('Error verifying token for live preview:', error)
57+
}
58+
59+
// You can add additional checks here to see if the user is allowed to preview this page
60+
if (!user) {
61+
draftMode().disable()
62+
return new Response('You are not allowed to preview this page', { status: 403 })
63+
}
4464

45-
draftMode().enable()
46-
redirect(path)
65+
// Verify the given slug exists
66+
try {
67+
const docs = await payload.find({
68+
collection: collection,
69+
where: {
70+
slug: {
71+
equals: slug,
72+
},
73+
},
74+
})
75+
76+
if (!docs.docs.length) {
77+
return new Response('Document not found', { status: 404 })
78+
}
79+
} catch (error) {
80+
payload.logger.error('Error verifying token for live preview:', error)
81+
}
82+
83+
draftMode().enable()
84+
redirect(path)
85+
}
4786
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Metadata } from 'next/types'
2+
3+
import { CollectionArchive } from '@/components/CollectionArchive'
4+
import configPromise from '@payload-config'
5+
import { getPayloadHMR } from '@payloadcms/next/utilities'
6+
import React from 'react'
7+
import { Post } from '@/payload-types'
8+
import { Search } from '@/search/Component'
9+
10+
export default async function Page({ searchParams }: { searchParams: { q: string } }) {
11+
const query = searchParams.q
12+
const payload = await getPayloadHMR({ config: configPromise })
13+
14+
const posts = await payload.find({
15+
collection: 'search',
16+
depth: 1,
17+
limit: 12,
18+
...(query
19+
? {
20+
where: {
21+
or: [
22+
{
23+
title: {
24+
like: query,
25+
},
26+
},
27+
{
28+
'meta.description': {
29+
like: query,
30+
},
31+
},
32+
{
33+
'meta.title': {
34+
like: query,
35+
},
36+
},
37+
{
38+
slug: {
39+
like: query,
40+
},
41+
},
42+
],
43+
},
44+
}
45+
: {}),
46+
})
47+
48+
return (
49+
<div className="pt-24 pb-24">
50+
<div className="container mb-16">
51+
<div className="prose dark:prose-invert max-w-none">
52+
<h1 className="sr-only">Search</h1>
53+
<Search />
54+
</div>
55+
</div>
56+
57+
<CollectionArchive posts={posts.docs as unknown as Post[]} />
58+
</div>
59+
)
60+
}
61+
62+
export function generateMetadata(): Metadata {
63+
return {
64+
title: `Payload Website Template Search`,
65+
}
66+
}

templates/website/src/app/(payload)/admin/importMap.js

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,30 @@ import { PreviewComponent as PreviewComponent_14 } from '@payloadcms/plugin-seo/
1616
import { SlugComponent as SlugComponent_15 } from '@/fields/slug/SlugComponent'
1717
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_16 } from '@payloadcms/richtext-lexical/client'
1818
import { BlocksFeatureClient as BlocksFeatureClient_17 } from '@payloadcms/richtext-lexical/client'
19-
import { default as default_18 } from '@/components/BeforeDashboard'
20-
import { default as default_19 } from '@/components/BeforeLogin'
19+
import { LinkToDoc as LinkToDoc_18 } from '@payloadcms/plugin-search/client'
20+
import { default as default_19 } from '@/components/BeforeDashboard'
21+
import { default as default_20 } from '@/components/BeforeLogin'
2122

2223
export const importMap = {
23-
'@payloadcms/richtext-lexical/client#RichTextCell': RichTextCell_0,
24-
'@payloadcms/richtext-lexical/client#RichTextField': RichTextField_1,
25-
'@payloadcms/richtext-lexical/generateComponentMap#getGenerateComponentMap':
26-
getGenerateComponentMap_2,
27-
'@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient': InlineToolbarFeatureClient_3,
28-
'@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient': FixedToolbarFeatureClient_4,
29-
'@payloadcms/richtext-lexical/client#HeadingFeatureClient': HeadingFeatureClient_5,
30-
'@payloadcms/richtext-lexical/client#UnderlineFeatureClient': UnderlineFeatureClient_6,
31-
'@payloadcms/richtext-lexical/client#BoldFeatureClient': BoldFeatureClient_7,
32-
'@payloadcms/richtext-lexical/client#ItalicFeatureClient': ItalicFeatureClient_8,
33-
'@payloadcms/richtext-lexical/client#LinkFeatureClient': LinkFeatureClient_9,
34-
'@payloadcms/plugin-seo/client#OverviewComponent': OverviewComponent_10,
35-
'@payloadcms/plugin-seo/client#MetaTitleComponent': MetaTitleComponent_11,
36-
'@payloadcms/plugin-seo/client#MetaImageComponent': MetaImageComponent_12,
37-
'@payloadcms/plugin-seo/client#MetaDescriptionComponent': MetaDescriptionComponent_13,
38-
'@payloadcms/plugin-seo/client#PreviewComponent': PreviewComponent_14,
39-
'@/fields/slug/SlugComponent#SlugComponent': SlugComponent_15,
40-
'@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient': HorizontalRuleFeatureClient_16,
41-
'@payloadcms/richtext-lexical/client#BlocksFeatureClient': BlocksFeatureClient_17,
42-
'@/components/BeforeDashboard#default': default_18,
43-
'@/components/BeforeLogin#default': default_19,
24+
"@payloadcms/richtext-lexical/client#RichTextCell": RichTextCell_0,
25+
"@payloadcms/richtext-lexical/client#RichTextField": RichTextField_1,
26+
"@payloadcms/richtext-lexical/generateComponentMap#getGenerateComponentMap": getGenerateComponentMap_2,
27+
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_3,
28+
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_4,
29+
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_5,
30+
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_6,
31+
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_7,
32+
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_8,
33+
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_9,
34+
"@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_10,
35+
"@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_11,
36+
"@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_12,
37+
"@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_13,
38+
"@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_14,
39+
"@/fields/slug/SlugComponent#SlugComponent": SlugComponent_15,
40+
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_16,
41+
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_17,
42+
"@payloadcms/plugin-search/client#LinkToDoc": LinkToDoc_18,
43+
"@/components/BeforeDashboard#default": default_19,
44+
"@/components/BeforeLogin#default": default_20
4445
}

templates/website/src/collections/Pages/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,21 @@ export const Pages: CollectionConfig = {
3333
livePreview: {
3434
url: ({ data }) => {
3535
const path = generatePreviewPath({
36-
path: `/${typeof data?.slug === 'string' ? data.slug : ''}`,
36+
slug: typeof data?.slug === 'string' ? data.slug : '',
37+
collection: 'pages',
3738
})
39+
3840
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
3941
},
4042
},
41-
preview: (doc) =>
42-
generatePreviewPath({ path: `/${typeof doc?.slug === 'string' ? doc.slug : ''}` }),
43+
preview: (data) => {
44+
const path = generatePreviewPath({
45+
slug: typeof data?.slug === 'string' ? data.slug : '',
46+
collection: 'pages',
47+
})
48+
49+
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
50+
},
4351
useAsTitle: 'title',
4452
},
4553
fields: [

templates/website/src/collections/Posts/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,21 @@ export const Posts: CollectionConfig = {
4040
livePreview: {
4141
url: ({ data }) => {
4242
const path = generatePreviewPath({
43-
path: `/posts/${typeof data?.slug === 'string' ? data.slug : ''}`,
43+
slug: typeof data?.slug === 'string' ? data.slug : '',
44+
collection: 'posts',
4445
})
46+
4547
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
4648
},
4749
},
48-
preview: (doc) =>
49-
generatePreviewPath({ path: `/posts/${typeof doc?.slug === 'string' ? doc.slug : ''}` }),
50+
preview: (data) => {
51+
const path = generatePreviewPath({
52+
slug: typeof data?.slug === 'string' ? data.slug : '',
53+
collection: 'posts',
54+
})
55+
56+
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
57+
},
5058
useAsTitle: 'title',
5159
},
5260
fields: [

templates/website/src/components/PayloadRedirects/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ export const PayloadRedirects: React.FC<Props> = async ({ disableNotFound, url }
4545
}
4646

4747
if (disableNotFound) return null
48-
return notFound()
48+
49+
notFound()
4950
}

0 commit comments

Comments
 (0)