feat!: replaces admin.favicon with admin.icons
@@ -26,6 +26,11 @@ const merriweather = Merriweather({
|
|||||||
weight: ['400', '900'],
|
weight: ['400', '900'],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
description: 'Generated by Next.js',
|
||||||
|
title: 'Next.js',
|
||||||
|
}
|
||||||
|
|
||||||
export const RootLayout = async ({
|
export const RootLayout = async ({
|
||||||
children,
|
children,
|
||||||
config: configPromise,
|
config: configPromise,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
import type { Icon } from 'next/dist/lib/metadata/types/metadata-types.js'
|
||||||
import type { SanitizedConfig } from 'payload/types'
|
import type { SanitizedConfig } from 'payload/types'
|
||||||
|
|
||||||
import { payloadFaviconDark, payloadFaviconLight, payloadOgImage } from '@payloadcms/ui/assets'
|
import { payloadFaviconDark, payloadFaviconLight, payloadOgImage } from '@payloadcms/ui/assets'
|
||||||
@@ -13,38 +14,35 @@ export const meta = async (args: {
|
|||||||
|
|
||||||
const titleSuffix = config.admin.meta?.titleSuffix ?? '- Payload'
|
const titleSuffix = config.admin.meta?.titleSuffix ?? '- Payload'
|
||||||
|
|
||||||
const customFavicon = config.admin.meta?.favicon
|
|
||||||
const customFaviconFiletype = customFavicon?.split('.').pop()
|
|
||||||
const customFaviconMediaType = `image/${customFaviconFiletype}`
|
|
||||||
|
|
||||||
const favicon = customFavicon ?? payloadFaviconLight?.src
|
|
||||||
const ogImage = config.admin?.meta?.ogImage ?? payloadOgImage?.src
|
const ogImage = config.admin?.meta?.ogImage ?? payloadOgImage?.src
|
||||||
|
|
||||||
|
const customIcons = config.admin.meta.icons as Metadata['icons']
|
||||||
|
|
||||||
|
let icons = customIcons ?? []
|
||||||
|
|
||||||
|
const payloadIcons: Icon[] = [
|
||||||
|
{
|
||||||
|
type: 'image/png',
|
||||||
|
rel: 'icon',
|
||||||
|
sizes: '32x32',
|
||||||
|
url: payloadFaviconDark?.src,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image/png',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
rel: 'icon',
|
||||||
|
sizes: '32x32',
|
||||||
|
url: payloadFaviconLight?.src,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (customIcons && typeof customIcons === 'object' && Array.isArray(customIcons)) {
|
||||||
|
icons = payloadIcons.concat(customIcons)
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
description,
|
description,
|
||||||
icons: [
|
icons,
|
||||||
...(customFavicon
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
type: customFaviconMediaType,
|
|
||||||
rel: 'icon',
|
|
||||||
url: favicon,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
type: 'image/png',
|
|
||||||
rel: 'icon',
|
|
||||||
url: payloadFaviconDark?.src,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'image/png',
|
|
||||||
media: '(prefers-color-scheme: dark)',
|
|
||||||
rel: 'icon',
|
|
||||||
url: payloadFaviconLight?.src,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
keywords,
|
keywords,
|
||||||
metadataBase: new URL(
|
metadataBase: new URL(
|
||||||
config?.serverURL ||
|
config?.serverURL ||
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
"get-port": "5.1.1",
|
"get-port": "5.1.1",
|
||||||
"graphql-http": "^1.22.0",
|
"graphql-http": "^1.22.0",
|
||||||
"mini-css-extract-plugin": "1.6.2",
|
"mini-css-extract-plugin": "1.6.2",
|
||||||
|
"next": "^14.3.0-canary.7",
|
||||||
"nodemon": "3.0.3",
|
"nodemon": "3.0.3",
|
||||||
"object.assign": "4.1.4",
|
"object.assign": "4.1.4",
|
||||||
"object.entries": "1.1.6",
|
"object.entries": "1.1.6",
|
||||||
|
|||||||
@@ -68,7 +68,13 @@ export default joi.object({
|
|||||||
}),
|
}),
|
||||||
logoutRoute: joi.string(),
|
logoutRoute: joi.string(),
|
||||||
meta: joi.object().keys({
|
meta: joi.object().keys({
|
||||||
favicon: joi.string(),
|
icons: joi
|
||||||
|
.alternatives()
|
||||||
|
.try(
|
||||||
|
joi.array().items(joi.alternatives().try(joi.string(), joi.object())),
|
||||||
|
joi.object(),
|
||||||
|
joi.string().allow(null),
|
||||||
|
),
|
||||||
ogImage: joi.string(),
|
ogImage: joi.string(),
|
||||||
titleSuffix: joi.string(),
|
titleSuffix: joi.string(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { I18nOptions, TFunction } from '@payloadcms/translations'
|
import type { I18nOptions, TFunction } from '@payloadcms/translations'
|
||||||
import type { Options as ExpressFileUploadOptions } from 'express-fileupload'
|
import type { Options as ExpressFileUploadOptions } from 'express-fileupload'
|
||||||
import type GraphQL from 'graphql'
|
import type GraphQL from 'graphql'
|
||||||
|
import type { Metadata as NextMetadata } from 'next'
|
||||||
import type { DestinationStream, LoggerOptions, P } from 'pino'
|
import type { DestinationStream, LoggerOptions, P } from 'pino'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import type { default as sharp } from 'sharp'
|
import type { default as sharp } from 'sharp'
|
||||||
@@ -460,14 +461,15 @@ export type Config = {
|
|||||||
}
|
}
|
||||||
/** The route for the logout page. */
|
/** The route for the logout page. */
|
||||||
logoutRoute?: string
|
logoutRoute?: string
|
||||||
/** Base meta data to use for the Admin panel. Included properties are titleSuffix, ogImage, and favicon. */
|
/** Base meta data to use for the Admin Panel. Included properties are titleSuffix, ogImage, and favicon. */
|
||||||
meta?: {
|
meta?: {
|
||||||
/**
|
/**
|
||||||
* Public path to an icon
|
* An array of Next.js metadata objects that represent icons to be used by devices and browsers.
|
||||||
*
|
*
|
||||||
* This image may be displayed in the browser next to the title of the page
|
* For example browser tabs, phone home screens, and search engine results.
|
||||||
|
* @reference https://nextjs.org/docs/app/api-reference/functions/generate-metadata#icons
|
||||||
*/
|
*/
|
||||||
favicon?: string
|
icons?: NextMetadata['icons']
|
||||||
/**
|
/**
|
||||||
* Public path to an image
|
* Public path to an image
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { default as payloadFavicon } from '../assets/favicon.svg'
|
|
||||||
export { default as payloadFaviconDark } from '../assets/favicon-dark.png'
|
|
||||||
export { default as payloadFaviconLight } from '../assets/favicon-light.png'
|
|
||||||
export { default as payloadOgImage } from '../assets/og-image.png'
|
export { default as payloadOgImage } from '../assets/og-image.png'
|
||||||
|
export { default as payloadFavicon } from '../assets/payload-favicon.svg'
|
||||||
|
export { default as payloadFaviconDark } from '../assets/payload-favicon-dark.png'
|
||||||
|
export { default as payloadFaviconLight } from '../assets/payload-favicon-light.png'
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 638 B |
|
Before Width: | Height: | Size: 485 B After Width: | Height: | Size: 485 B |
|
Before Width: | Height: | Size: 437 B After Width: | Height: | Size: 437 B |
3
pnpm-lock.yaml
generated
@@ -912,6 +912,9 @@ importers:
|
|||||||
mini-css-extract-plugin:
|
mini-css-extract-plugin:
|
||||||
specifier: 1.6.2
|
specifier: 1.6.2
|
||||||
version: 1.6.2(webpack@5.91.0)
|
version: 1.6.2(webpack@5.91.0)
|
||||||
|
next:
|
||||||
|
specifier: ^14.3.0-canary.7
|
||||||
|
version: 14.3.0-canary.7(@babel/core@7.24.4)(@playwright/test@1.43.0)(react-dom@18.2.0)(react@18.2.0)(sass@1.74.1)
|
||||||
nodemon:
|
nodemon:
|
||||||
specifier: 3.0.3
|
specifier: 3.0.3
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { devUser } from '../credentials.js'
|
|||||||
import { MediaCollection } from './collections/Media/index.js'
|
import { MediaCollection } from './collections/Media/index.js'
|
||||||
import { PostsCollection, postsSlug } from './collections/Posts/index.js'
|
import { PostsCollection, postsSlug } from './collections/Posts/index.js'
|
||||||
import { MenuGlobal } from './globals/Menu/index.js'
|
import { MenuGlobal } from './globals/Menu/index.js'
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import { CustomMinimalView } from './components/views/CustomMinimal/index.js'
|
|||||||
import { CustomView } from './components/views/CustomView/index.js'
|
import { CustomView } from './components/views/CustomView/index.js'
|
||||||
import { CustomNestedView } from './components/views/CustomViewNested/index.js'
|
import { CustomNestedView } from './components/views/CustomViewNested/index.js'
|
||||||
import { CustomViewWithParam } from './components/views/CustomViewWithParam/index.js'
|
import { CustomViewWithParam } from './components/views/CustomViewWithParam/index.js'
|
||||||
|
import { default as customFaviconDark } from './custom-favicon-dark.png'
|
||||||
|
import { default as customFaviconLight } from './custom-favicon-light.png'
|
||||||
import { CustomGlobalViews1 } from './globals/CustomViews1.js'
|
import { CustomGlobalViews1 } from './globals/CustomViews1.js'
|
||||||
import { CustomGlobalViews2 } from './globals/CustomViews2.js'
|
import { CustomGlobalViews2 } from './globals/CustomViews2.js'
|
||||||
import { Global } from './globals/Global.js'
|
import { Global } from './globals/Global.js'
|
||||||
@@ -34,7 +36,6 @@ import { GlobalHidden } from './globals/Hidden.js'
|
|||||||
import { GlobalNoApiView } from './globals/NoApiView.js'
|
import { GlobalNoApiView } from './globals/NoApiView.js'
|
||||||
import { seed } from './seed.js'
|
import { seed } from './seed.js'
|
||||||
import { customNestedViewPath, customParamViewPath, customViewPath } from './shared.js'
|
import { customNestedViewPath, customParamViewPath, customViewPath } from './shared.js'
|
||||||
|
|
||||||
export default buildConfigWithDefaults({
|
export default buildConfigWithDefaults({
|
||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: {
|
||||||
@@ -74,6 +75,21 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
meta: {
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
type: 'image/png',
|
||||||
|
rel: 'icon',
|
||||||
|
url: customFaviconDark.src,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image/png',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
rel: 'icon',
|
||||||
|
url: customFaviconLight.src,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
collections: [
|
collections: [
|
||||||
UploadCollection,
|
UploadCollection,
|
||||||
|
|||||||
BIN
test/admin/custom-favicon-dark.png
Normal file
|
After Width: | Height: | Size: 477 B |
BIN
test/admin/custom-favicon-light.png
Normal file
|
After Width: | Height: | Size: 496 B |
@@ -106,6 +106,30 @@ describe('admin', () => {
|
|||||||
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
|
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('metadata', () => {
|
||||||
|
test('should set Payload favicons', async () => {
|
||||||
|
await page.goto(postsUrl.admin)
|
||||||
|
const favicons = page.locator('link[rel="icon"]')
|
||||||
|
await expect(favicons).toHaveCount(4)
|
||||||
|
await expect(favicons.nth(0)).toHaveAttribute('sizes', '32x32')
|
||||||
|
await expect(favicons.nth(1)).toHaveAttribute('sizes', '32x32')
|
||||||
|
await expect(favicons.nth(1)).toHaveAttribute('media', '(prefers-color-scheme: dark)')
|
||||||
|
await expect(favicons.nth(1)).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
/\/payload-favicon-light\.[a-z\d]+\.png/,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should inject custom favicons', async () => {
|
||||||
|
await page.goto(postsUrl.admin)
|
||||||
|
const favicons = page.locator('link[rel="icon"]')
|
||||||
|
await expect(favicons).toHaveCount(4)
|
||||||
|
await expect(favicons.nth(2)).toHaveAttribute('href', /\/custom-favicon-dark\.[a-z\d]+\.png/)
|
||||||
|
await expect(favicons.nth(3)).toHaveAttribute('media', '(prefers-color-scheme: dark)')
|
||||||
|
await expect(favicons.nth(3)).toHaveAttribute('href', /\/custom-favicon-light\.[a-z\d]+\.png/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('navigation', () => {
|
describe('navigation', () => {
|
||||||
test('nav — should navigate to collection', async () => {
|
test('nav — should navigate to collection', async () => {
|
||||||
await page.goto(postsUrl.admin)
|
await page.goto(postsUrl.admin)
|
||||||
|
|||||||
4
test/admin/types.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.png' {
|
||||||
|
const value: any
|
||||||
|
export = value
|
||||||
|
}
|
||||||