feat!: replaces admin.favicon with admin.icons
@@ -26,6 +26,11 @@ const merriweather = Merriweather({
|
||||
weight: ['400', '900'],
|
||||
})
|
||||
|
||||
export const metadata = {
|
||||
description: 'Generated by Next.js',
|
||||
title: 'Next.js',
|
||||
}
|
||||
|
||||
export const RootLayout = async ({
|
||||
children,
|
||||
config: configPromise,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { Icon } from 'next/dist/lib/metadata/types/metadata-types.js'
|
||||
import type { SanitizedConfig } from 'payload/types'
|
||||
|
||||
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 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
|
||||
|
||||
return Promise.resolve({
|
||||
description,
|
||||
icons: [
|
||||
...(customFavicon
|
||||
? [
|
||||
{
|
||||
type: customFaviconMediaType,
|
||||
rel: 'icon',
|
||||
url: favicon,
|
||||
},
|
||||
]
|
||||
: [
|
||||
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({
|
||||
description,
|
||||
icons,
|
||||
keywords,
|
||||
metadataBase: new URL(
|
||||
config?.serverURL ||
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
"get-port": "5.1.1",
|
||||
"graphql-http": "^1.22.0",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
"next": "^14.3.0-canary.7",
|
||||
"nodemon": "3.0.3",
|
||||
"object.assign": "4.1.4",
|
||||
"object.entries": "1.1.6",
|
||||
|
||||
@@ -68,7 +68,13 @@ export default joi.object({
|
||||
}),
|
||||
logoutRoute: joi.string(),
|
||||
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(),
|
||||
titleSuffix: joi.string(),
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { I18nOptions, TFunction } from '@payloadcms/translations'
|
||||
import type { Options as ExpressFileUploadOptions } from 'express-fileupload'
|
||||
import type GraphQL from 'graphql'
|
||||
import type { Metadata as NextMetadata } from 'next'
|
||||
import type { DestinationStream, LoggerOptions, P } from 'pino'
|
||||
import type React from 'react'
|
||||
import type { default as sharp } from 'sharp'
|
||||
@@ -460,14 +461,15 @@ export type Config = {
|
||||
}
|
||||
/** The route for the logout page. */
|
||||
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?: {
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
||||
@@ -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 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:
|
||||
specifier: 1.6.2
|
||||
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:
|
||||
specifier: 3.0.3
|
||||
version: 3.0.3
|
||||
|
||||
@@ -7,6 +7,7 @@ import { devUser } from '../credentials.js'
|
||||
import { MediaCollection } from './collections/Media/index.js'
|
||||
import { PostsCollection, postsSlug } from './collections/Posts/index.js'
|
||||
import { MenuGlobal } from './globals/Menu/index.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
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 { CustomNestedView } from './components/views/CustomViewNested/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 { CustomGlobalViews2 } from './globals/CustomViews2.js'
|
||||
import { Global } from './globals/Global.js'
|
||||
@@ -34,7 +36,6 @@ import { GlobalHidden } from './globals/Hidden.js'
|
||||
import { GlobalNoApiView } from './globals/NoApiView.js'
|
||||
import { seed } from './seed.js'
|
||||
import { customNestedViewPath, customParamViewPath, customViewPath } from './shared.js'
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
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: [
|
||||
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 })
|
||||
})
|
||||
|
||||
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', () => {
|
||||
test('nav — should navigate to collection', async () => {
|
||||
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
|
||||
}
|
||||