Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support metadata icons field #45105

Merged
merged 8 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/next/src/lib/metadata/generate/basic.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ResolvedMetadata } from '../types/metadata-interface'

import React from 'react'
import { Meta } from './utils'
import { Meta } from './meta'

export function ResolvedBasicMetadata({
metadata,
Expand Down
69 changes: 69 additions & 0 deletions packages/next/src/lib/metadata/generate/icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { ResolvedMetadata } from '../types/metadata-interface'
import type { Icon, IconDescriptor } from '../types/metadata-types'

import React from 'react'

const resolveUrl = (url: string | URL) =>
typeof url === 'string' ? url : url.toString()

function IconDescriptorLink({ icon }: { icon: IconDescriptor }) {
const { url, rel = 'icon', ...props } = icon

return <link rel={rel} href={resolveUrl(url)} {...props} />
}

function IconLink({ rel, icon }: { rel?: string; icon: Icon }) {
if (typeof icon === 'object' && !(icon instanceof URL)) {
if (rel) icon.rel = rel
return <IconDescriptorLink icon={icon} />
} else {
const href = resolveUrl(icon)
return <link rel={rel} href={href} />
}
}

export function ResolvedIconsMetadata({
icons,
}: {
icons: ResolvedMetadata['icons']
}) {
if (!icons) return null

const shortcutList = icons.shortcut
const iconList = icons.icon
const appleList = icons.apple
const otherList = icons.other

return (
<>
{shortcutList
? shortcutList.map((icon, index) => (
<IconLink
key={`shortcut-${index}`}
rel="shortcut icon"
icon={icon}
/>
))
: null}
{iconList
? iconList.map((icon, index) => (
<IconLink key={`shortcut-${index}`} rel="icon" icon={icon} />
))
: null}
{appleList
? appleList.map((icon, index) => (
<IconLink
key={`apple-${index}`}
rel="apple-touch-icon"
icon={icon}
/>
))
: null}
{otherList
? otherList.map((icon, index) => (
<IconDescriptorLink key={`other-${index}`} icon={icon} />
))
: null}
</>
)
}
2 changes: 1 addition & 1 deletion packages/next/src/lib/metadata/generate/opengraph.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ResolvedMetadata } from '../types/metadata-interface'

import React from 'react'
import { Meta, MultiMeta } from './utils'
import { Meta, MultiMeta } from './meta'

export function ResolvedOpenGraphMetadata({
openGraph,
Expand Down
11 changes: 11 additions & 0 deletions packages/next/src/lib/metadata/generate/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function resolveAsArrayOrUndefined<T = any>(
value: T | T[] | undefined | null
): undefined | T[] {
if (typeof value === 'undefined' || value === null) {
return undefined
}
if (Array.isArray(value)) {
return value
}
return [value]
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import React from 'react'

import type { ResolvedMetadata } from './types/metadata-interface'

import React from 'react'
import { ResolvedBasicMetadata } from './generate/basic'
import { ResolvedAlternatesMetadata } from './generate/alternate'
import { ResolvedOpenGraphMetadata } from './generate/opengraph'
import { resolveMetadata } from './resolve-metadata'
import { ResolvedIconsMetadata } from './generate/icons'

// Generate the actual React elements from the resolved metadata.
export async function Metadata({ metadata }: { metadata: any }) {
if (!metadata) return null

const resolved: ResolvedMetadata = await resolveMetadata(metadata)
return (
<>
<ResolvedBasicMetadata metadata={resolved} />
<ResolvedAlternatesMetadata metadata={resolved} />
<ResolvedOpenGraphMetadata openGraph={resolved.openGraph} />
<ResolvedIconsMetadata icons={resolved.icons} />
</>
)
}
79 changes: 63 additions & 16 deletions packages/next/src/lib/metadata/resolve-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import type {
} from './types/metadata-interface'
import type { Viewport } from './types/extra-types'
import type { ResolvedTwitterMetadata } from './types/twitter-types'
import type { AbsoluteTemplateString } from './types/metadata-types'
import type {
AbsoluteTemplateString,
Icon,
IconDescriptor,
Icons,
} from './types/metadata-types'
import { createDefaultMetadata } from './default-metadata'
import { resolveOpenGraph } from './resolve-opengraph'
import { mergeTitle } from './resolve-title'
import { resolveAsArrayOrUndefined } from './generate/utils'

const viewPortKeys = {
width: 'width',
Expand Down Expand Up @@ -48,6 +54,57 @@ type Item =
path?: string
}

function resolveViewport(
viewport: Metadata['viewport']
): ResolvedMetadata['viewport'] {
let resolved: ResolvedMetadata['viewport'] = null

if (typeof viewport === 'string') {
resolved = viewport
} else if (viewport) {
resolved = ''
for (const viewportKey_ in viewPortKeys) {
const viewportKey = viewportKey_ as keyof Viewport
if (viewport[viewportKey]) {
if (resolved) resolved += ', '
resolved += `${viewPortKeys[viewportKey]}=${viewport[viewportKey]}`
}
}
}
return resolved
}

function isUrlIcon(icon: any): icon is string | URL {
return typeof icon === 'string' || icon instanceof URL
}

function resolveIcon(icon: Icon): IconDescriptor {
if (isUrlIcon(icon)) return { url: icon }
else if (Array.isArray(icon)) return icon
return icon
}

const IconKeys = ['icon', 'shortcut', 'apple', 'other'] as (keyof Icons)[]

function resolveIcons(icons: Metadata['icons']): ResolvedMetadata['icons'] {
if (!icons) {
return null
}

const resolved: ResolvedMetadata['icons'] = {}
if (Array.isArray(icons)) {
resolved.icon = icons.map(resolveIcon).filter(Boolean)
} else if (isUrlIcon(icons)) {
resolved.icon = [resolveIcon(icons)]
} else {
for (const key of IconKeys) {
const values = resolveAsArrayOrUndefined(icons[key])
if (values) resolved[key] = values.map(resolveIcon)
}
}
return resolved
}

// Merge the source metadata into the resolved target metadata.
function merge(
target: ResolvedMetadata,
Expand Down Expand Up @@ -94,21 +151,11 @@ function merge(
break
}
case 'viewport': {
let content: string | null = null
const { viewport } = source
if (typeof viewport === 'string') {
content = viewport
} else if (viewport) {
content = ''
for (const viewportKey_ in viewPortKeys) {
const viewportKey = viewportKey_ as keyof Viewport
if (viewport[viewportKey]) {
if (content) content += ', '
content += `${viewPortKeys[viewportKey]}=${viewport[viewportKey]}`
}
}
}
target.viewport = content
target.viewport = resolveViewport(source.viewport)
break
}
case 'icons': {
target.icons = resolveIcons(source.icons)
break
}
default: {
Expand Down
11 changes: 1 addition & 10 deletions packages/next/src/lib/metadata/resolve-opengraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
OpenGraph,
ResolvedOpenGraph,
} from './types/opengraph-types'
import { resolveAsArrayOrUndefined } from './generate/utils'

const OgTypFields = {
article: ['authors', 'tags'],
Expand All @@ -22,16 +23,6 @@ const OgTypFields = {
],
} as const

function resolveAsArrayOrUndefined<T = any>(value: T): undefined | any[] {
if (typeof value === 'undefined' || value === null) {
return undefined
}
if (Array.isArray(value)) {
return value
}
return [value]
}

function getFieldsByOgType(ogType: OpenGraphType | undefined) {
switch (ogType) {
case 'article':
Expand Down
6 changes: 4 additions & 2 deletions packages/next/src/lib/metadata/types/metadata-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import type {
ColorSchemeEnum,
Icon,
Icons,
IconURL,
ReferrerEnum,
ResolvedIcons,
Robots,
TemplateString,
Verification,
Expand Down Expand Up @@ -56,7 +58,7 @@ export interface Metadata {

// Defaults to rel="icon" but the Icons type can be used
// to get more specific about rel types
icons?: null | Array<Icon> | Icons
icons?: null | IconURL | Array<Icon> | Icons

openGraph?: null | OpenGraph

Expand Down Expand Up @@ -145,7 +147,7 @@ export interface ResolvedMetadata {

// Defaults to rel="icon" but the Icons type can be used
// to get more specific about rel types
icons: null | Icons
icons: null | ResolvedIcons

openGraph: null | ResolvedOpenGraph

Expand Down
24 changes: 16 additions & 8 deletions packages/next/src/lib/metadata/types/metadata-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ export type Robots = {
googleBot?: string | Robots
}

export type Icon = string | IconDescriptor | URL
export type IconURL = string | URL
export type Icon = IconURL | IconDescriptor
export type IconDescriptor = {
url: string | URL
type?: string
Expand All @@ -74,20 +75,27 @@ export type IconDescriptor = {
}
export type Icons = {
// rel="icon"
icon?: Icon | Array<Icon>
icon?: Icon | Icon[]
// rel="shortcut icon"
shortcut?: Icon | Array<Icon>
shortcut?: Icon | Icon[]
// rel="apple-touch-icon"
apple?: Icon | Array<Icon>
apple?: Icon | Icon[]
// rel inferred from descriptor, defaults to "icon"
other?: Icon | Array<Icon>
other?: IconDescriptor | IconDescriptor[]
}

export type Verification = {
google?: null | string | number | Array<string | number>
yahoo?: null | string | number | Array<string | number>
google?: null | string | number | (string | number)[]
yahoo?: null | string | number | (string | number)[]
// if you ad-hoc additional verification
other?: {
[name: string]: string | number | Array<string | number>
[name: string]: string | number | (string | number)[]
}
}

export type ResolvedIcons = {
icon?: IconDescriptor[]
shortcut?: IconDescriptor[]
apple?: IconDescriptor[]
other?: IconDescriptor[]
}
2 changes: 1 addition & 1 deletion packages/next/src/server/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
} from '../client/components/app-router-headers'
import type { StaticGenerationAsyncStorage } from '../client/components/static-generation-async-storage'
import { formatServerError } from '../lib/format-server-error'
import { Metadata } from '../lib/metadata/ui'
import { Metadata } from '../lib/metadata/metadata'
import type { RequestAsyncStorage } from '../client/components/request-async-storage'
import { runWithRequestAsyncStorage } from './run-with-request-async-storage'
import { runWithStaticGenerationAsyncStorage } from './run-with-static-generation-async-storage'
Expand Down
20 changes: 20 additions & 0 deletions test/e2e/app-dir/metadata/app/icons/descriptor/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default function page() {
return 'icons'
}

export const metadata = {
icons: {
icon: [{ url: '/icon.png' }, new URL('/icon.png', 'https://example.com')],
shortcut: ['/shortcut-icon.png'],
apple: [
{ url: '/apple-icon.png' },
{ url: '/apple-icon-x3.png', sizes: '180x180', type: 'image/png' },
],
other: [
{
rel: 'apple-touch-icon-precomposed',
url: '/apple-touch-icon-precomposed.png',
},
],
},
}
15 changes: 15 additions & 0 deletions test/e2e/app-dir/metadata/app/icons/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default function page() {
return 'icons'
}

export const metadata = {
icons: {
icon: '/icon.png',
shortcut: '/shortcut-icon.png',
apple: '/apple-icon.png',
other: {
rel: 'apple-touch-icon-precomposed',
url: '/apple-touch-icon-precomposed.png',
},
},
}
7 changes: 7 additions & 0 deletions test/e2e/app-dir/metadata/app/icons/string/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function page() {
return 'icons'
}

export const metadata = {
icons: '/icon.png',
}