Skip to content

Commit

Permalink
Support metadata icons field (#45105)
Browse files Browse the repository at this point in the history
NEXT-400

## Feature

- [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [x] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)
  • Loading branch information
huozhi committed Jan 24, 2023
1 parent ff1664b commit 45a9373
Show file tree
Hide file tree
Showing 15 changed files with 295 additions and 54 deletions.
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',
}

0 comments on commit 45a9373

Please sign in to comment.