fix(next): proper 404 handling

This commit is contained in:
Jacob Fletcher
2024-03-29 13:30:00 -04:00
parent f5d9b47177
commit e6b166da7d
8 changed files with 109 additions and 54 deletions

View File

@@ -1,7 +1,9 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import type { Metadata } from 'next'
import config from '@payload-config' import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundView } from '@payloadcms/next/views/NotFound/index.js' import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views/NotFound/index.js'
type Args = { type Args = {
params: { params: {
@@ -12,6 +14,9 @@ type Args = {
} }
} }
const NotFound = ({ params, searchParams }: Args) => NotFoundView({ config, params, searchParams }) export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
export default NotFound export default NotFound

View File

@@ -1,3 +1,3 @@
export { EditView } from '../views/Edit/index.js' export { EditView } from '../views/Edit/index.js'
export { NotFoundView } from '../views/NotFound/index.js' export { NotFoundPage } from '../views/NotFound/index.js'
export { type GenerateViewMetadata, RootPage, generatePageMetadata } from '../views/Root/index.js' export { type GenerateViewMetadata, RootPage, generatePageMetadata } from '../views/Root/index.js'

View File

@@ -22,8 +22,8 @@ import { getRequestLanguage } from './getRequestLanguage.js'
type Args = { type Args = {
config: Promise<SanitizedConfig> | SanitizedConfig config: Promise<SanitizedConfig> | SanitizedConfig
redirectUnauthenticatedUser?: boolean redirectUnauthenticatedUser?: boolean
route?: string route: string
searchParams?: { [key: string]: string | string[] | undefined } searchParams: { [key: string]: string | string[] | undefined }
} }
export const initPage = async ({ export const initPage = async ({
@@ -59,7 +59,7 @@ export const initPage = async ({
const { collections, globals, localization, routes } = payload.config const { collections, globals, localization, routes } = payload.config
if (redirectUnauthenticatedUser && !user && route !== '/login') { if (redirectUnauthenticatedUser && !user && route !== '/login') {
if ('redirect' in searchParams) delete searchParams.redirect if (searchParams && 'redirect' in searchParams) delete searchParams.redirect
const stringifiedSearchParams = Object.keys(searchParams ?? {}).length const stringifiedSearchParams = Object.keys(searchParams ?? {}).length
? `?${qs.stringify(searchParams)}` ? `?${qs.stringify(searchParams)}`
@@ -81,7 +81,7 @@ export const initPage = async ({
translations, translations,
}) })
const queryString = `${qs.stringify(searchParams, { addQueryPrefix: true })}` const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}`
const req = createLocalReq( const req = createLocalReq(
{ {

View File

@@ -7,12 +7,9 @@ import type {
SanitizedGlobalConfig, SanitizedGlobalConfig,
} from 'payload/types' } from 'payload/types'
import React from 'react'
import { APIView as DefaultAPIView } from '../API/index.js' import { APIView as DefaultAPIView } from '../API/index.js'
import { EditView as DefaultEditView } from '../Edit/index.js' import { EditView as DefaultEditView } from '../Edit/index.js'
import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/index.js' import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/index.js'
import { NotFoundClient } from '../NotFound/index.client.js'
import { Unauthorized } from '../Unauthorized/index.js' import { Unauthorized } from '../Unauthorized/index.js'
import { VersionView as DefaultVersionView } from '../Version/index.js' import { VersionView as DefaultVersionView } from '../Version/index.js'
import { VersionsView as DefaultVersionsView } from '../Versions/index.js' import { VersionsView as DefaultVersionsView } from '../Versions/index.js'
@@ -140,9 +137,6 @@ export const getViewsFromConfig = ({
currentRoute, currentRoute,
views, views,
}) })
if (!CustomView) ErrorView = () => <NotFoundClient />
break break
} }
} }
@@ -172,8 +166,6 @@ export const getViewsFromConfig = ({
currentRoute, currentRoute,
views, views,
}) })
if (!CustomView) ErrorView = () => <NotFoundClient />
} }
break break
} }
@@ -266,8 +258,6 @@ export const getViewsFromConfig = ({
currentRoute, currentRoute,
views, views,
}) })
if (!CustomView) ErrorView = () => <NotFoundClient />
} }
break break
} }

View File

@@ -9,13 +9,12 @@ import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomCompo
import { DocumentInfoProvider } from '@payloadcms/ui/providers/DocumentInfo' import { DocumentInfoProvider } from '@payloadcms/ui/providers/DocumentInfo'
import { EditDepthProvider } from '@payloadcms/ui/providers/EditDepth' import { EditDepthProvider } from '@payloadcms/ui/providers/EditDepth'
import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParams' import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParams'
import { notFound } from 'next/navigation.js'
import { docAccessOperation } from 'payload/operations' import { docAccessOperation } from 'payload/operations'
import React from 'react' import React from 'react'
import type { GenerateEditViewMetadata } from './getMetaBySegment.js' import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
import { NotFoundClient } from '../NotFound/index.client.js'
import { NotFoundView } from '../NotFound/index.js'
import { getMetaBySegment } from './getMetaBySegment.js' import { getMetaBySegment } from './getMetaBySegment.js'
import { getViewsFromConfig } from './getViewsFromConfig.js' import { getViewsFromConfig } from './getViewsFromConfig.js'
@@ -57,7 +56,7 @@ export const Document: React.FC<AdminViewProps> = async ({
let ViewOverride: EditViewComponent let ViewOverride: EditViewComponent
let CustomView: EditViewComponent let CustomView: EditViewComponent
let DefaultView: EditViewComponent let DefaultView: EditViewComponent
let ErrorView: AdminViewComponent = NotFoundView let ErrorView: AdminViewComponent
let docPermissions: DocumentPermissions let docPermissions: DocumentPermissions
let hasSavePermission: boolean let hasSavePermission: boolean
@@ -66,7 +65,7 @@ export const Document: React.FC<AdminViewProps> = async ({
if (collectionConfig) { if (collectionConfig) {
if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) { if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) {
return <NotFoundClient /> notFound()
} }
try { try {
@@ -78,7 +77,7 @@ export const Document: React.FC<AdminViewProps> = async ({
req, req,
}) })
} catch (error) { } catch (error) {
return <NotFoundClient /> notFound()
} }
action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}` action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}`
@@ -108,13 +107,17 @@ export const Document: React.FC<AdminViewProps> = async ({
} }
if (!CustomView && !DefaultView && !ViewOverride) { if (!CustomView && !DefaultView && !ViewOverride) {
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} /> if (ErrorView) {
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} />
}
notFound()
} }
} }
if (globalConfig) { if (globalConfig) {
if (!visibleEntities?.globals?.find((visibleSlug) => visibleSlug === globalSlug)) { if (!visibleEntities?.globals?.find((visibleSlug) => visibleSlug === globalSlug)) {
return <NotFoundClient /> notFound()
} }
docPermissions = permissions?.globals?.[globalSlug] docPermissions = permissions?.globals?.[globalSlug]
@@ -141,7 +144,11 @@ export const Document: React.FC<AdminViewProps> = async ({
ErrorView = globalViews?.ErrorView ErrorView = globalViews?.ErrorView
if (!CustomView && !DefaultView && !ViewOverride) { if (!CustomView && !DefaultView && !ViewOverride) {
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} /> if (ErrorView) {
return <ErrorView initPageResult={initPageResult} searchParams={searchParams} />
}
notFound()
} }
} }
} }

View File

@@ -1,14 +1,61 @@
import type { AdminViewComponent } from 'payload/types' import type { I18n } from '@payloadcms/translations'
import type { Metadata } from 'next'
import type { SanitizedConfig } from 'payload/types'
import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
import { DefaultTemplate } from '@payloadcms/ui/templates/Default' import { DefaultTemplate } from '@payloadcms/ui/templates/Default'
import React from 'react' import React, { Fragment } from 'react'
import { initPage } from '../../utilities/initPage.js'
import { NotFoundClient } from './index.client.js' import { NotFoundClient } from './index.client.js'
export const NotFoundView: AdminViewComponent = ({ initPageResult }) => { export const generatePageMetadata = async ({
i18n,
}: {
config: SanitizedConfig
i18n: I18n
params?: { [key: string]: string | string[] }
//eslint-disable-next-line @typescript-eslint/require-await
}): Promise<Metadata> => {
return {
title: i18n.t('general:notFound'),
}
}
export type GenerateViewMetadata = (args: {
config: SanitizedConfig
i18n: I18n
params?: { [key: string]: string | string[] }
}) => Promise<Metadata>
export const NotFoundPage = async ({
config: configPromise,
searchParams,
}: {
config: Promise<SanitizedConfig>
params: {
segments: string[]
}
searchParams: {
[key: string]: string | string[]
}
}) => {
const initPageResult = await initPage({
config: configPromise,
redirectUnauthenticatedUser: true,
route: '/not-found',
searchParams,
})
return ( return (
<DefaultTemplate config={initPageResult?.req?.payload.config}> <Fragment>
<NotFoundClient /> <HydrateClientUser permissions={initPageResult.permissions} user={initPageResult.req.user} />
</DefaultTemplate> <DefaultTemplate
config={initPageResult.req.payload.config}
visibleEntities={initPageResult.visibleEntities}
>
<NotFoundClient />
</DefaultTemplate>
</Fragment>
) )
} }

View File

@@ -2,11 +2,10 @@ import type { I18n } from '@payloadcms/translations'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig } from 'payload/types'
import { EntityVisibilityProvider } from '@payloadcms/ui/providers/EntityVisibility'
import { DefaultTemplate } from '@payloadcms/ui/templates/Default' import { DefaultTemplate } from '@payloadcms/ui/templates/Default'
import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal' import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal'
import { notFound, redirect } from 'next/navigation.js' import { notFound, redirect } from 'next/navigation.js'
import React from 'react' import React, { Fragment } from 'react'
import { initPage } from '../../utilities/initPage.js' import { initPage } from '../../utilities/initPage.js'
import { getViewFromConfig } from './getViewFromConfig.js' import { getViewFromConfig } from './getViewFromConfig.js'
@@ -83,13 +82,15 @@ export const RootPage = async ({
) )
return ( return (
<EntityVisibilityProvider visibleEntities={initPageResult.visibleEntities}> <Fragment>
{templateType === 'minimal' && ( {templateType === 'minimal' && (
<MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate> <MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
)} )}
{templateType === 'default' && ( {templateType === 'default' && (
<DefaultTemplate config={config}>{RenderedView}</DefaultTemplate> <DefaultTemplate config={config} visibleEntities={initPageResult.visibleEntities}>
{RenderedView}
</DefaultTemplate>
)} )}
</EntityVisibilityProvider> </Fragment>
) )
} }

View File

@@ -1,5 +1,6 @@
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig, VisibleEntities } from 'payload/types'
import { EntityVisibilityProvider } from '@payloadcms/ui/providers/EntityVisibility'
import React from 'react' import React from 'react'
import type { NavProps } from '../../elements/Nav/index.js' import type { NavProps } from '../../elements/Nav/index.js'
@@ -18,12 +19,14 @@ export type DefaultTemplateProps = {
children?: React.ReactNode children?: React.ReactNode
className?: string className?: string
config: Promise<SanitizedConfig> | SanitizedConfig config: Promise<SanitizedConfig> | SanitizedConfig
visibleEntities?: VisibleEntities
} }
export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
children, children,
className, className,
config: configPromise, config: configPromise,
visibleEntities,
}) => { }) => {
const config = await configPromise const config = await configPromise
@@ -40,23 +43,25 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
} }
return ( return (
<div> <EntityVisibilityProvider visibleEntities={visibleEntities}>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler"> <div>
<NavToggler className={`${baseClass}__nav-toggler`}> <div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
<NavHamburger /> <NavToggler className={`${baseClass}__nav-toggler`}>
</NavToggler> <NavHamburger />
</div> </NavToggler>
<Wrapper baseClass={baseClass} className={className}>
<RenderCustomComponent
CustomComponent={CustomNav}
DefaultComponent={DefaultNav}
componentProps={navProps}
/>
<div className={`${baseClass}__wrap`}>
<AppHeader />
{children}
</div> </div>
</Wrapper> <Wrapper baseClass={baseClass} className={className}>
</div> <RenderCustomComponent
CustomComponent={CustomNav}
DefaultComponent={DefaultNav}
componentProps={navProps}
/>
<div className={`${baseClass}__wrap`}>
<AppHeader />
{children}
</div>
</Wrapper>
</div>
</EntityVisibilityProvider>
) )
} }