chore: safely handles bad network requests
This commit is contained in:
@@ -2,6 +2,11 @@ MONGODB_URI=mongodb://127.0.0.1/payload-example-custom-server
|
||||
PAYLOAD_SECRET=PAYLOAD_CUSTOM_SERVER_EXAMPLE_SECRET_KEY
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
|
||||
COOKIE_DOMAIN=localhost
|
||||
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
|
||||
ENABLE_PAYLOAD_CLOUD=false
|
||||
COOKIE_DOMAIN=localhost
|
||||
|
||||
# Next.js vars
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_IS_LIVE=
|
||||
NEXT_PRIVATE_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
|
||||
NEXT_PRIVATE_REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
require('dotenv').config()
|
||||
|
||||
const imageDomains = ['localhost']
|
||||
if (process.env.PAYLOAD_PUBLIC_SERVER_URL) {
|
||||
const { hostname } = new URL(process.env.PAYLOAD_PUBLIC_SERVER_URL)
|
||||
imageDomains.push(hostname)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('allowed image domains:', imageDomains.join(', '))
|
||||
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
images: {
|
||||
domains: imageDomains,
|
||||
domains: ['localhost', process.env.NEXT_PUBLIC_SERVER_URL]
|
||||
.filter(Boolean)
|
||||
.map(url => url.replace(/https?:\/\//, '')),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { Metadata, ResolvingMetadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
import { Media } from '../../payload/payload-types'
|
||||
import { Media, Page } from '../../payload/payload-types'
|
||||
import { staticHome } from '../../payload/seed/home-static'
|
||||
import { ContentLayout } from '../_components/content/contentLayout'
|
||||
import { fetchPage } from '../_utils/api'
|
||||
import { fetchPage, fetchPages } from '../_utils/api'
|
||||
import { parsePreviewOptions } from '../_utils/preview'
|
||||
|
||||
// Payload Cloud caches all files through Cloudflare, so we don't need Next.js to cache them as well
|
||||
// This means that we can turn off Next.js data caching and instead rely solely on the Cloudflare CDN
|
||||
// To do this, we include the `no-cache` header on the fetch requests used to get the data for this page
|
||||
// But we also need to force Next.js to dynamically render this page on each request for preview mode to work
|
||||
// See https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
|
||||
// If you are not using Payload Cloud then this line can be removed, see `../../../README.md#cache`
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface LandingPageProps {
|
||||
params: {
|
||||
slug: string
|
||||
@@ -13,21 +22,75 @@ interface LandingPageProps {
|
||||
searchParams: Record<string, string>
|
||||
}
|
||||
|
||||
export default async function LandingPage({ params, searchParams }: LandingPageProps) {
|
||||
const { slug } = params
|
||||
const options = parsePreviewOptions(searchParams)
|
||||
|
||||
let page: Page | null = null
|
||||
|
||||
try {
|
||||
page = await fetchPage(slug, options)
|
||||
} catch (error) {
|
||||
// when deploying this template on Payload Cloud, this page needs to build before the APIs are live
|
||||
// so swallow the error here and simply render the page with fallback data where necessary
|
||||
// in production you may want to redirect to a 404 page or at least log the error somewhere
|
||||
// console.error(error)
|
||||
}
|
||||
|
||||
// if no `home` page exists, render a static one using dummy content
|
||||
// you should delete this code once you have a home page in the CMS
|
||||
// this is really only useful for those who are demoing this template
|
||||
if (!page) {
|
||||
page = staticHome
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return <ContentLayout layout={page.layout} className="mt-16" />
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const pages = await fetchPages()
|
||||
return pages.map(({ slug }) => ({ params: { slug } }))
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
{ searchParams, params }: LandingPageProps,
|
||||
{ params, searchParams }: LandingPageProps,
|
||||
parent?: ResolvingMetadata,
|
||||
): Promise<Metadata> {
|
||||
const defaultTitle = (await parent)?.title?.absolute
|
||||
const { slug } = params
|
||||
const options = parsePreviewOptions(searchParams)
|
||||
const page = await fetchPage(params.slug, options)
|
||||
|
||||
let page: Page | null = null
|
||||
|
||||
try {
|
||||
page = await fetchPage(slug, options)
|
||||
} catch (error) {
|
||||
// don't throw an error if the fetch fails
|
||||
// this is so that we can render a static home page for the demo
|
||||
// when deploying this template on Payload Cloud, this page needs to build before the APIs are live
|
||||
// in production you may want to redirect to a 404 page or at least log the error somewhere
|
||||
}
|
||||
|
||||
const defaultTitle = (await parent)?.title?.absolute
|
||||
const title = page?.meta?.title || defaultTitle
|
||||
const description = page?.meta?.description || 'A portfolio of work by a digital professional.'
|
||||
const images = []
|
||||
|
||||
if (page?.meta?.image) {
|
||||
images.push((page.meta.image as Media).url)
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
page = staticHome
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
@@ -38,17 +101,3 @@ export async function generateMetadata(
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const LandingPage = async ({ params, searchParams }: LandingPageProps) => {
|
||||
const { slug } = params
|
||||
const options = parsePreviewOptions(searchParams)
|
||||
const page = await fetchPage(slug, options)
|
||||
|
||||
if (!page?.layout) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return <ContentLayout layout={page.layout} className="mt-16" />
|
||||
}
|
||||
|
||||
export default LandingPage
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useForm } from 'react-hook-form'
|
||||
import { Data } from 'payload/dist/admin/components/forms/Form/types'
|
||||
|
||||
import { Form as FormTypes } from '../../../payload/payload-types'
|
||||
import { serverUrl } from '../../_utils/api'
|
||||
import { Block } from '../ui/block'
|
||||
import { Button } from '../ui/button'
|
||||
import { Dialog, DialogContent } from '../ui/dialog'
|
||||
@@ -92,7 +91,7 @@ export const FormBlock: FC<FormBlockProps> = props => {
|
||||
}))
|
||||
|
||||
try {
|
||||
const req = await fetch(`${serverUrl}/api/form-submissions`, {
|
||||
const req = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/form-submissions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json', // Correct typo: "Content-Types" to "Content-Type"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { Header, Page, Profile, Project } from '../../payload/payload-types'
|
||||
import type { DraftOptions } from './preview'
|
||||
|
||||
export const serverUrl = process.env.PAYLOAD_PUBLIC_SERVER_URL ?? 'http://localhost:3000'
|
||||
|
||||
const initPreviewRequest = (init: RequestInit, qs: URLSearchParams, token: string): void => {
|
||||
if (!token) {
|
||||
throw new Error('No token provided when attempting to preview content')
|
||||
@@ -16,12 +14,15 @@ const initPreviewRequest = (init: RequestInit, qs: URLSearchParams, token: strin
|
||||
}
|
||||
|
||||
export const fetchProfile = async (): Promise<Profile> => {
|
||||
const url = `${serverUrl}/api/globals/profile?locale=en`
|
||||
if (!process.env.PAYLOAD_PUBLIC_SERVER_URL) throw new Error('PAYLOAD_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const url = `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/globals/profile?locale=en`
|
||||
|
||||
const profile: Profile = await fetch(url, {
|
||||
cache: 'force-cache',
|
||||
next: { tags: ['global.profile'] },
|
||||
}).then(res => {
|
||||
if (!res.ok) throw new Error('Error fetching profile doc')
|
||||
return res.json()
|
||||
})
|
||||
|
||||
@@ -29,11 +30,16 @@ export const fetchProfile = async (): Promise<Profile> => {
|
||||
}
|
||||
|
||||
export const fetchHeader = async (): Promise<Header> => {
|
||||
const url = `${serverUrl}/api/globals/header?locale=en`
|
||||
if (!process.env.PAYLOAD_PUBLIC_SERVER_URL) throw new Error('PAYLOAD_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const url = `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/globals/header?locale=en`
|
||||
const header: Header = await fetch(url, {
|
||||
cache: 'force-cache',
|
||||
next: { tags: ['global.header'] },
|
||||
}).then(res => res.json())
|
||||
}).then(res => {
|
||||
if (!res.ok) throw new Error('Error fetching header doc')
|
||||
return res.json()
|
||||
})
|
||||
|
||||
return header
|
||||
}
|
||||
@@ -44,15 +50,20 @@ export const fetchPage = async (
|
||||
slug: string,
|
||||
options: FetchPageOptions,
|
||||
): Promise<Page | undefined> => {
|
||||
if (!process.env.PAYLOAD_PUBLIC_SERVER_URL) throw new Error('PAYLOAD_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const qs = new URLSearchParams({ 'where[slug][equals]': slug })
|
||||
const init: RequestInit = { next: { tags: [`pages/${slug}`] } }
|
||||
if (options.draft) {
|
||||
initPreviewRequest(init, qs, options.payloadToken)
|
||||
}
|
||||
|
||||
const url = `${serverUrl}/api/pages?${qs.toString()}`
|
||||
const url = `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/pages?${qs.toString()}`
|
||||
const page: Page = await fetch(url, init)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Error fetching page doc')
|
||||
return res.json()
|
||||
})
|
||||
.then(res => res?.docs?.[0])
|
||||
|
||||
return page
|
||||
@@ -64,6 +75,8 @@ export const fetchProject = async (
|
||||
slug: string,
|
||||
options: FetchProjectOptions,
|
||||
): Promise<Project> => {
|
||||
if (!process.env.PAYLOAD_PUBLIC_SERVER_URL) throw new Error('PAYLOAD_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const qs = new URLSearchParams(`where[slug][equals]=${slug}`)
|
||||
const init: RequestInit = {}
|
||||
|
||||
@@ -73,10 +86,56 @@ export const fetchProject = async (
|
||||
init.next = { tags: [`projects/${slug}`] }
|
||||
}
|
||||
|
||||
const url = `${serverUrl}/api/projects?${qs.toString()}`
|
||||
const url = `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/projects?${qs.toString()}`
|
||||
const project: Project = await fetch(url, init)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Error fetching project doc')
|
||||
return res.json()
|
||||
})
|
||||
.then(res => res?.docs?.[0])
|
||||
|
||||
return project
|
||||
}
|
||||
|
||||
export const fetchPages = async (options?: FetchPageOptions): Promise<Page[]> => {
|
||||
if (!process.env.PAYLOAD_PUBLIC_SERVER_URL) throw new Error('PAYLOAD_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const qs = new URLSearchParams()
|
||||
|
||||
const init: RequestInit = { next: { tags: ['pages'] } }
|
||||
if (options?.draft) {
|
||||
initPreviewRequest(init, qs, options.payloadToken)
|
||||
}
|
||||
|
||||
const url = `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/pages?${qs.toString()}`
|
||||
const pages: Page[] = await fetch(url, init)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Error fetching pages doc')
|
||||
return res.json()
|
||||
})
|
||||
.then(res => res?.docs || [])
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
export const fetchProjects = async (options?: FetchProjectOptions): Promise<Project[]> => {
|
||||
if (!process.env.PAYLOAD_PUBLIC_SERVER_URL) throw new Error('PAYLOAD_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const init: RequestInit = {}
|
||||
|
||||
if (options.draft) {
|
||||
initPreviewRequest(init, undefined, options.payloadToken)
|
||||
} else {
|
||||
init.next = { tags: ['projects'] }
|
||||
}
|
||||
|
||||
const url = `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/projects`
|
||||
const projects: Project[] = await fetch(url, init)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Error fetching projects doc')
|
||||
return res.json()
|
||||
})
|
||||
.then(res => res?.docs || [])
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
@@ -3,26 +3,36 @@ import { Inter } from 'next/font/google'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
import { Header, Profile } from '../payload/payload-types'
|
||||
import { Footer } from './_components/siteLayout/footer'
|
||||
import { NavBar } from './_components/siteLayout/navBar'
|
||||
import { Backdrop } from './_components/ui/backdrop/backdrop'
|
||||
import { ThemeProvider } from './_provider/themeProvider'
|
||||
import { fetchHeader, fetchProfile, serverUrl } from './_utils/api'
|
||||
import { fetchHeader, fetchProfile } from './_utils/api'
|
||||
|
||||
import './globals.css'
|
||||
|
||||
export async function generateMetadata() {
|
||||
const profile = await fetchProfile()
|
||||
let profile: Profile | null = null
|
||||
|
||||
try {
|
||||
profile = await fetchProfile()
|
||||
} catch (error) {}
|
||||
|
||||
return {
|
||||
metadataBase: new URL(serverUrl),
|
||||
metadataBase: new URL(process.env.NEXT_PUBLIC_SERVER_URL || 'https://payloadcms.com'),
|
||||
title: `Portfolio | ${profile.name}`,
|
||||
description: 'My professional portfolio featuring past projects and contact info.',
|
||||
}
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const [profile, header] = await Promise.all([fetchProfile(), fetchHeader()])
|
||||
let profile: Profile | null = null
|
||||
let header: Header | null = null
|
||||
|
||||
try {
|
||||
;[profile, header] = await Promise.all([fetchProfile(), fetchHeader()])
|
||||
} catch (error) {}
|
||||
|
||||
return (
|
||||
<html lang="en" className={`${inter.className} dark`}>
|
||||
|
||||
@@ -1,30 +1,98 @@
|
||||
import React from 'react'
|
||||
import { Metadata, ResolvingMetadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
import { Media } from '../payload/payload-types'
|
||||
import { Media, Page, Profile } from '../payload/payload-types'
|
||||
import { staticHome } from '../payload/seed/home-static'
|
||||
import { ContentLayout } from './_components/content/contentLayout'
|
||||
import { fetchPage, fetchProfile } from './_utils/api'
|
||||
import { fetchPage, fetchPages, fetchProfile } from './_utils/api'
|
||||
import { parsePreviewOptions } from './_utils/preview'
|
||||
|
||||
// Payload Cloud caches all files through Cloudflare, so we don't need Next.js to cache them as well
|
||||
// This means that we can turn off Next.js data caching and instead rely solely on the Cloudflare CDN
|
||||
// To do this, we include the `no-cache` header on the fetch requests used to get the data for this page
|
||||
// But we also need to force Next.js to dynamically render this page on each request for preview mode to work
|
||||
// See https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
|
||||
// If you are not using Payload Cloud then this line can be removed, see `../../../README.md#cache`
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface LandingPageProps {
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
searchParams: Record<string, string>
|
||||
}
|
||||
|
||||
export default async function LandingPage({ params, searchParams }: LandingPageProps) {
|
||||
const { slug } = params
|
||||
const options = parsePreviewOptions(searchParams)
|
||||
|
||||
let page: Page | null = null
|
||||
let profile: Profile | null = null
|
||||
|
||||
try {
|
||||
;[page, profile] = await Promise.all([fetchPage('home', options), fetchProfile()])
|
||||
} catch (error) {
|
||||
// when deploying this template on Payload Cloud, this page needs to build before the APIs are live
|
||||
// so swallow the error here and simply render the page with fallback data where necessary
|
||||
// in production you may want to redirect to a 404 page or at least log the error somewhere
|
||||
// console.error(error)
|
||||
}
|
||||
|
||||
// if no `home` page exists, render a static one using dummy content
|
||||
// you should delete this code once you have a home page in the CMS
|
||||
// this is really only useful for those who are demoing this template
|
||||
if (!page && !profile && slug === 'home') {
|
||||
page = staticHome
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return <ContentLayout profile={profile} layout={page.layout} />
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const pages = await fetchPages()
|
||||
return pages.map(({ slug }) => ({ params: { slug } }))
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
{ searchParams }: LandingPageProps,
|
||||
{ params, searchParams }: LandingPageProps,
|
||||
parent?: ResolvingMetadata,
|
||||
): Promise<Metadata> {
|
||||
const defaultTitle = (await parent)?.title?.absolute
|
||||
const { slug } = params
|
||||
const options = parsePreviewOptions(searchParams)
|
||||
const page = await fetchPage('home', options)
|
||||
|
||||
let page: Page | null = null
|
||||
|
||||
try {
|
||||
page = await fetchPage('home', options)
|
||||
} catch (error) {
|
||||
// don't throw an error if the fetch fails
|
||||
// this is so that we can render a static home page for the demo
|
||||
// when deploying this template on Payload Cloud, this page needs to build before the APIs are live
|
||||
// in production you may want to redirect to a 404 page or at least log the error somewhere
|
||||
}
|
||||
|
||||
const defaultTitle = (await parent)?.title?.absolute
|
||||
const title = page?.meta?.title || defaultTitle
|
||||
const description = page?.meta?.description || 'A portfolio of work by a digital professional.'
|
||||
const images = []
|
||||
|
||||
if (page?.meta?.image) {
|
||||
images.push((page.meta.image as Media).url)
|
||||
}
|
||||
|
||||
if (!page && slug === 'home') {
|
||||
page = staticHome
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
@@ -36,10 +104,3 @@ export async function generateMetadata(
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function LandingPage({ searchParams }: LandingPageProps) {
|
||||
const options = parsePreviewOptions(searchParams)
|
||||
const [page, profile] = await Promise.all([fetchPage('home', options), fetchProfile()])
|
||||
|
||||
return <ContentLayout profile={profile} layout={page.layout} />
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Metadata, ResolvingMetadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
import { Media } from '../../../payload/payload-types'
|
||||
import { Media, Profile, Project } from '../../../payload/payload-types'
|
||||
import { ProjectDetails } from '../../_components/content/projectDetails/projectDetails'
|
||||
import { fetchProfile, fetchProject } from '../../_utils/api'
|
||||
import { fetchProfile, fetchProject, fetchProjects } from '../../_utils/api'
|
||||
import { parsePreviewOptions } from '../../_utils/preview'
|
||||
|
||||
// Force this page to be dynamic so that Next.js does not cache it
|
||||
// See the note in '../../[slug]/page.tsx' about this
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface ProjectPageProps {
|
||||
params: {
|
||||
slug: string
|
||||
@@ -13,14 +17,48 @@ interface ProjectPageProps {
|
||||
searchParams: Record<string, string>
|
||||
}
|
||||
|
||||
export default async function ProjectPage({ params, searchParams }: ProjectPageProps) {
|
||||
let project: Project | null = null
|
||||
let profile: Profile | null = null
|
||||
|
||||
try {
|
||||
;[project, profile] = await Promise.all([
|
||||
fetchProject(params.slug, parsePreviewOptions(searchParams)),
|
||||
fetchProfile(),
|
||||
])
|
||||
} catch (error) {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return <ProjectDetails project={project} profile={profile} />
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const pages = await fetchProjects()
|
||||
return pages.map(({ slug }) => ({ params: { slug } }))
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params, searchParams }: ProjectPageProps,
|
||||
parent?: ResolvingMetadata,
|
||||
): Promise<Metadata> {
|
||||
const [project, previousTitle] = await Promise.all([
|
||||
fetchProject(params.slug, parsePreviewOptions(searchParams)),
|
||||
(await parent)?.title.absolute,
|
||||
])
|
||||
let project: Project | null = null
|
||||
let previousTitle: string | null = null
|
||||
|
||||
try {
|
||||
;[project, previousTitle] = await Promise.all([
|
||||
fetchProject(params.slug, parsePreviewOptions(searchParams)),
|
||||
(await parent)?.title.absolute,
|
||||
])
|
||||
} catch (error) {}
|
||||
|
||||
const images: string[] = []
|
||||
if (project?.meta?.image) {
|
||||
@@ -44,16 +82,3 @@ export async function generateMetadata(
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ProjectPage({ params, searchParams }: ProjectPageProps) {
|
||||
const [project, profile] = await Promise.all([
|
||||
fetchProject(params.slug, parsePreviewOptions(searchParams)),
|
||||
fetchProfile(),
|
||||
])
|
||||
|
||||
if (!project) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return <ProjectDetails project={project} profile={profile} />
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const staticHome: Page = {
|
||||
id: '',
|
||||
title: 'Profile Landing Page',
|
||||
slug: 'home',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
meta: {
|
||||
title: 'Payload Developer Portfolio Template',
|
||||
description: 'An open-source developer portfolio built with Payload and Next.js.',
|
||||
},
|
||||
layout: [],
|
||||
}
|
||||
Reference in New Issue
Block a user