fix(next): 404 handling

This commit is contained in:
Jacob Fletcher
2024-03-06 12:57:37 -05:00
parent b70bf81d6c
commit e9abe63b47
9 changed files with 209 additions and 162 deletions

View File

@@ -0,0 +1,17 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundView } from '@payloadcms/next/views/NotFound/index'
type Args = {
params: {
segments: string[]
}
searchParams: {
[key: string]: string | string[]
}
}
const NotFound = ({ params, searchParams }: Args) => NotFoundView({ config, params, searchParams })
export default NotFound

View File

@@ -10,7 +10,7 @@ import { initI18n } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/client'
import { findLocaleFromCode } from '@payloadcms/ui'
import { headers as getHeaders } from 'next/headers'
import { redirect } from 'next/navigation'
import { notFound, redirect } from 'next/navigation'
import { createLocalReq } from 'payload/utilities'
import qs from 'qs'
@@ -88,10 +88,18 @@ export const initPage = async ({
if (collectionSlug) {
collectionConfig = collections.find((collection) => collection.slug === collectionSlug)
if (!collectionConfig) {
notFound()
}
}
if (globalSlug) {
globalConfig = globals.find((global) => global.slug === globalSlug)
if (!globalConfig) {
notFound()
}
}
return {

View File

@@ -2,13 +2,13 @@ import type { Metadata } from 'next'
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload/types'
import { getNextI18n } from '../../utilities/getNextI18n'
import { meta } from '../../utilities/meta'
import { generateMetadata as apiMeta } from '../API/meta'
import { generateMetadata as editMeta } from '../Edit/meta'
import { generateMetadata as livePreviewMeta } from '../LivePreview/meta'
import { generateMetadata as versionMeta } from '../Version/meta'
import { generateMetadata as versionsMeta } from '../Versions/meta'
import { GenerateViewMetadata } from '../Root'
import { generateNotFoundMeta } from '../NotFound/meta'
export type GenerateEditViewMetadata = (
args: Parameters<GenerateViewMetadata>[0] & {
@@ -102,10 +102,5 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
})
}
return meta({
config,
description: '',
keywords: '',
title: '',
})
return generateNotFoundMeta({ i18n })
}

View File

@@ -18,7 +18,6 @@ import {
buildStateFromSchema,
formatFields,
} from '@payloadcms/ui'
import { notFound } from 'next/navigation'
import queryString from 'qs'
import React, { Fragment } from 'react'
@@ -27,6 +26,7 @@ import type { GenerateEditViewMetadata } from './getMetaBySegment'
import { getMetaBySegment } from './getMetaBySegment'
import { getViewsFromConfig } from './getViewsFromConfig'
import { NotFoundClient } from '../NotFound/index.client'
export const generateMetadata: GenerateEditViewMetadata = async (args) => getMetaBySegment(args)
@@ -97,20 +97,24 @@ export const Document: React.FC<AdminViewProps> = async ({
DefaultView = collectionViews?.DefaultView
if (!CustomView && !DefaultView) {
return notFound()
return <NotFoundClient />
}
try {
data = await payload.findByID({
id,
collection: collectionSlug,
depth: 0,
locale: locale.code,
user,
})
} catch (error) {} // eslint-disable-line no-empty
if (id) {
try {
data = await payload.findByID({
id,
collection: collectionSlug,
depth: 0,
locale: locale.code,
user,
})
} catch (error) {} // eslint-disable-line no-empty
if (!data) {
return <NotFoundClient />
}
preferencesKey = `collection-${collectionSlug}-${id}`
}
}
@@ -137,15 +141,21 @@ export const Document: React.FC<AdminViewProps> = async ({
DefaultView = globalViews?.DefaultView
if (!CustomView && !DefaultView) {
return notFound()
return <NotFoundClient />
}
data = await payload.findGlobal({
slug: globalSlug,
depth: 0,
locale: locale.code,
user,
})
try {
data = await payload.findGlobal({
slug: globalSlug,
depth: 0,
locale: locale.code,
user,
})
} catch (error) {} // eslint-disable-line no-empty
if (!data) {
return <NotFoundClient />
}
preferencesKey = `global-${globalSlug}`
}

View File

@@ -0,0 +1,45 @@
'use client'
import { Button, Gutter, useConfig, useStepNav, useTranslation } from '@payloadcms/ui'
import Link from 'next/link'
import React, { useEffect } from 'react'
import './index.scss'
const baseClass = 'not-found'
export const NotFoundClient: React.FC<{
marginTop?: 'large'
}> = (props) => {
const { marginTop = 'large' } = props
const { setStepNav } = useStepNav()
const { t } = useTranslation()
const {
routes: { admin },
} = useConfig()
useEffect(() => {
setStepNav([
{
label: t('general:notFound'),
},
])
}, [setStepNav, t])
return (
<div
className={[baseClass, marginTop && `${baseClass}--margin-top-${marginTop}`]
.filter(Boolean)
.join(' ')}
>
<Gutter className={`${baseClass}__wrap`}>
<h1>{t('general:nothingFound')}</h1>
<p>{t('general:sorryNotFound')}</p>
<Button Link={Link} className={`${baseClass}__button`} el="link" to={`${admin}`}>
{t('general:backToDashboard')}
</Button>
</Gutter>
</div>
)
}

View File

@@ -1,53 +1,39 @@
'use client'
import { Button, Gutter, useConfig, useStepNav, useTranslation } from '@payloadcms/ui'
import Link from 'next/link'
import React from 'react'
// import Meta from '../../utilities/Meta'
import './index.scss'
import { NotFoundClient } from './index.client'
import { DefaultTemplate } from '@payloadcms/ui'
import { SanitizedConfig } from 'payload/types'
import { initPage } from '../../utilities/initPage'
const baseClass = 'not-found'
export const NotFoundView = async ({
config: configPromise,
params,
searchParams,
}: {
config: Promise<SanitizedConfig>
params: {
[key: string]: string | string[]
}
searchParams: {
[key: string]: string | string[]
}
}) => {
const config = await configPromise
const NotFound: React.FC<{
marginTop?: 'large'
}> = (props) => {
const { marginTop } = props
const { setStepNav } = useStepNav()
const { t } = useTranslation()
const {
routes: { admin },
} = useConfig()
// useEffect(() => {
// setStepNav([
// {
// label: t('general:notFound'),
// },
// ])
// }, [setStepNav, t])
const initPageResult = await initPage({
config,
searchParams,
route: '',
})
return (
<div
className={[baseClass, marginTop && `${baseClass}--margin-top-${marginTop}`]
.filter(Boolean)
.join(' ')}
<DefaultTemplate
config={initPageResult.req.payload.config}
i18n={initPageResult.req.i18n}
permissions={initPageResult.permissions}
user={initPageResult.req.user}
>
{/* <Meta
description={t('general:pageNotFound')}
keywords={`404 ${t('general:notFound')}`}
title={t('general:notFound')}
/> */}
<Gutter className={`${baseClass}__wrap`}>
<h1>{t('general:nothingFound')}</h1>
<p>{t('general:sorryNotFound')}</p>
<Button Link={Link} className={`${baseClass}__button`} el="link" to={`${admin}`}>
{t('general:backToDashboard')}
</Button>
</Gutter>
</div>
<NotFoundClient />
</DefaultTemplate>
)
}
export default NotFound

View File

@@ -0,0 +1,9 @@
import { I18n } from '@payloadcms/translations'
export const generateNotFoundMeta = ({ i18n }: { i18n: I18n }) => {
return {
description: i18n.t('general:pageNotFound'),
keywords: `404 ${i18n.t('general:notFound')}`,
title: i18n.t('general:notFound'),
}
}

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next'
import type { InitPageResult, SanitizedConfig } from 'payload/types'
import { DefaultTemplate, MinimalTemplate } from '@payloadcms/ui'
import { redirect } from 'next/navigation'
import { notFound, redirect } from 'next/navigation'
import React from 'react'
import { initPage } from '../../utilities/initPage'
@@ -21,16 +21,6 @@ import { Verify, verifyBaseClass } from '../Verify'
export { generatePageMetadata } from './meta'
type Args = {
config: Promise<SanitizedConfig>
params: {
[key: string]: string | string[]
}
searchParams: {
[key: string]: string | string[]
}
}
export type GenerateViewMetadata = (args: {
config: SanitizedConfig
i18n: I18n
@@ -59,7 +49,19 @@ const oneSegmentViews = {
unauthorized: Unauthorized,
}
export const RootPage = async ({ config: configPromise, params, searchParams }: Args) => {
export const RootPage = async ({
config: configPromise,
params,
searchParams,
}: {
config: Promise<SanitizedConfig>
params: {
[key: string]: string | string[]
}
searchParams: {
[key: string]: string | string[]
}
}) => {
const config = await configPromise
const {
@@ -69,7 +71,6 @@ export const RootPage = async ({ config: configPromise, params, searchParams }:
let ViewToRender: React.FC<AdminViewProps>
let templateClassName
let initPageResult: InitPageResult
let templateType: 'default' | 'minimal' = 'default'
let route = adminRoute
@@ -85,6 +86,12 @@ export const RootPage = async ({ config: configPromise, params, searchParams }:
const isGlobal = segmentOne === 'globals'
const isCollection = segmentOne === 'collections'
let initPageOptions: Parameters<typeof initPage>[0] = {
config,
searchParams,
route,
}
// TODO: handle custom routes
switch (segments.length) {
@@ -92,12 +99,7 @@ export const RootPage = async ({ config: configPromise, params, searchParams }:
ViewToRender = Dashboard
templateClassName = 'dashboard'
templateType = 'default'
initPageResult = await initPage({
config,
redirectUnauthenticatedUser: true,
route,
searchParams,
})
initPageOptions.redirectUnauthenticatedUser = true
break
}
case 1: {
@@ -108,18 +110,12 @@ export const RootPage = async ({ config: configPromise, params, searchParams }:
// --> /logout
// --> /logout-inactivity
// --> /unauthorized
initPageResult = await initPage({ config, route, searchParams })
ViewToRender = oneSegmentViews[segmentOne]
templateClassName = baseClasses[segmentOne]
templateType = 'minimal'
} else if (segmentOne === 'account') {
// --> /account
initPageResult = await initPage({
config,
redirectUnauthenticatedUser: true,
route,
searchParams,
})
initPageOptions.redirectUnauthenticatedUser = true
ViewToRender = Account
templateClassName = 'account'
templateType = 'default'
@@ -129,30 +125,19 @@ export const RootPage = async ({ config: configPromise, params, searchParams }:
case 2: {
if (segmentOne === 'reset') {
// --> /reset/:token
initPageResult = await initPage({ config, route, searchParams })
ViewToRender = ResetPassword
templateClassName = baseClasses[segmentTwo]
templateType = 'minimal'
}
if (isCollection) {
// --> /collections/:collectionSlug
initPageResult = await initPage({
config,
redirectUnauthenticatedUser: true,
route,
searchParams,
})
initPageOptions.redirectUnauthenticatedUser = true
ViewToRender = ListView
templateClassName = `${segmentTwo}-list`
templateType = 'default'
} else if (isGlobal) {
// --> /globals/:globalSlug
initPageResult = await initPage({
config,
redirectUnauthenticatedUser: true,
route,
searchParams,
})
initPageOptions.redirectUnauthenticatedUser = true
ViewToRender = DocumentView
templateClassName = 'global-edit'
templateType = 'default'
@@ -162,7 +147,6 @@ export const RootPage = async ({ config: configPromise, params, searchParams }:
default:
if (segmentTwo === 'verify') {
// --> /:collectionSlug/verify/:token
initPageResult = await initPage({ config, route, searchParams })
ViewToRender = Verify
templateClassName = 'verify'
templateType = 'minimal'
@@ -173,12 +157,7 @@ export const RootPage = async ({ config: configPromise, params, searchParams }:
// --> /collections/:collectionSlug/:id/versions
// --> /collections/:collectionSlug/:id/versions/:versionId
// --> /collections/:collectionSlug/:id/api
initPageResult = await initPage({
config,
redirectUnauthenticatedUser: true,
route,
searchParams,
})
initPageOptions.redirectUnauthenticatedUser = true
ViewToRender = DocumentView
templateClassName = `collection-default-edit`
templateType = 'default'
@@ -188,12 +167,7 @@ export const RootPage = async ({ config: configPromise, params, searchParams }:
// --> /globals/:globalSlug/preview
// --> /globals/:globalSlug/versions/:versionId
// --> /globals/:globalSlug/api
initPageResult = await initPage({
config,
redirectUnauthenticatedUser: true,
route,
searchParams,
})
initPageOptions.redirectUnauthenticatedUser = true
ViewToRender = DocumentView
templateClassName = `global-edit`
templateType = 'default'
@@ -201,51 +175,49 @@ export const RootPage = async ({ config: configPromise, params, searchParams }:
break
}
const dbHasUser = await initPageResult.req.payload.db
.findOne({
collection: userSlug,
req: initPageResult.req,
})
?.then((doc) => !!doc)
let dbHasUser = false
const createFirstUserRoute = `${adminRoute}/create-first-user`
if (!dbHasUser && route !== createFirstUserRoute) {
redirect(createFirstUserRoute)
if (!ViewToRender) {
notFound()
}
if (dbHasUser && route === createFirstUserRoute) {
redirect(adminRoute)
}
const initPageResult = await initPage(initPageOptions)
if (initPageResult) {
if (templateType === 'minimal') {
return (
<MinimalTemplate className={templateClassName}>
<ViewToRender
initPageResult={initPageResult}
params={params}
searchParams={searchParams}
/>
</MinimalTemplate>
)
} else {
return (
<DefaultTemplate
config={config}
i18n={initPageResult.req.i18n}
permissions={initPageResult.permissions}
user={initPageResult.req.user}
>
<ViewToRender
initPageResult={initPageResult}
params={params}
searchParams={searchParams}
/>
</DefaultTemplate>
)
dbHasUser = await initPageResult?.req.payload.db
.findOne({
collection: userSlug,
req: initPageResult?.req,
})
?.then((doc) => !!doc)
const createFirstUserRoute = `${adminRoute}/create-first-user`
if (!dbHasUser && route !== createFirstUserRoute) {
redirect(createFirstUserRoute)
}
if (dbHasUser && route === createFirstUserRoute) {
redirect(adminRoute)
}
}
return null
if (templateType === 'minimal') {
return (
<MinimalTemplate className={templateClassName}>
<ViewToRender initPageResult={initPageResult} params={params} searchParams={searchParams} />
</MinimalTemplate>
)
} else {
return (
<DefaultTemplate
config={config}
i18n={initPageResult.req.i18n}
permissions={initPageResult.permissions}
user={initPageResult.req.user}
>
<ViewToRender initPageResult={initPageResult} params={params} searchParams={searchParams} />
</DefaultTemplate>
)
}
}

View File

@@ -11,6 +11,7 @@ import { Metadata } from 'next'
import { generateDocumentMetadata } from '../Document/meta'
import { getNextI18n } from '../../utilities/getNextI18n'
import { SanitizedConfig } from 'payload/types'
import { generateNotFoundMeta } from '../NotFound/meta'
const oneSegmentMeta = {
'create-first-user': generateCreateFirstUserMetadata,
@@ -131,5 +132,9 @@ export const generatePageMetadata = async ({ config: configPromise, params }: Ar
}
}
if (!meta) {
meta = generateNotFoundMeta({ i18n })
}
return meta
}