.
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
export const ViewportMetaKeys = {
|
||||
width: 'width',
|
||||
height: 'height',
|
||||
initialScale: 'initial-scale',
|
||||
minimumScale: 'minimum-scale',
|
||||
maximumScale: 'maximum-scale',
|
||||
viewportFit: 'viewport-fit',
|
||||
userScalable: 'user-scalable',
|
||||
interactiveWidget: 'interactive-widget'
|
||||
};
|
||||
export const IconKeys = [
|
||||
'icon',
|
||||
'shortcut',
|
||||
'apple',
|
||||
'other'
|
||||
];
|
||||
|
||||
//# sourceMappingURL=constants.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/lib/metadata/constants.ts"],"sourcesContent":["import type { ViewportLayout } from './types/extra-types'\nimport type { Icons } from './types/metadata-types'\n\nexport const ViewportMetaKeys: { [k in keyof ViewportLayout]: string } = {\n width: 'width',\n height: 'height',\n initialScale: 'initial-scale',\n minimumScale: 'minimum-scale',\n maximumScale: 'maximum-scale',\n viewportFit: 'viewport-fit',\n userScalable: 'user-scalable',\n interactiveWidget: 'interactive-widget',\n} as const\n\nexport const IconKeys: (keyof Icons)[] = ['icon', 'shortcut', 'apple', 'other']\n"],"names":["ViewportMetaKeys","width","height","initialScale","minimumScale","maximumScale","viewportFit","userScalable","interactiveWidget","IconKeys"],"mappings":"AAGA,OAAO,MAAMA,mBAA4D;IACvEC,OAAO;IACPC,QAAQ;IACRC,cAAc;IACdC,cAAc;IACdC,cAAc;IACdC,aAAa;IACbC,cAAc;IACdC,mBAAmB;AACrB,EAAU;AAEV,OAAO,MAAMC,WAA4B;IAAC;IAAQ;IAAY;IAAS;CAAQ,CAAA","ignoreList":[0]}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
export function createDefaultViewport() {
|
||||
return {
|
||||
// name=viewport
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
// visual metadata
|
||||
themeColor: null,
|
||||
colorScheme: null
|
||||
};
|
||||
}
|
||||
export function createDefaultMetadata() {
|
||||
return {
|
||||
// Deprecated ones
|
||||
viewport: null,
|
||||
themeColor: null,
|
||||
colorScheme: null,
|
||||
metadataBase: null,
|
||||
// Other values are all null
|
||||
title: null,
|
||||
description: null,
|
||||
applicationName: null,
|
||||
authors: null,
|
||||
generator: null,
|
||||
keywords: null,
|
||||
referrer: null,
|
||||
creator: null,
|
||||
publisher: null,
|
||||
robots: null,
|
||||
manifest: null,
|
||||
alternates: {
|
||||
canonical: null,
|
||||
languages: null,
|
||||
media: null,
|
||||
types: null
|
||||
},
|
||||
icons: null,
|
||||
openGraph: null,
|
||||
twitter: null,
|
||||
verification: {},
|
||||
appleWebApp: null,
|
||||
formatDetection: null,
|
||||
itunes: null,
|
||||
facebook: null,
|
||||
pinterest: null,
|
||||
abstract: null,
|
||||
appLinks: null,
|
||||
archives: null,
|
||||
assets: null,
|
||||
bookmarks: null,
|
||||
category: null,
|
||||
classification: null,
|
||||
pagination: {
|
||||
previous: null,
|
||||
next: null
|
||||
},
|
||||
other: {}
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=default-metadata.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/lib/metadata/default-metadata.tsx"],"sourcesContent":["import type {\n ResolvedMetadata,\n ResolvedViewport,\n} from './types/metadata-interface'\n\nexport function createDefaultViewport(): ResolvedViewport {\n return {\n // name=viewport\n width: 'device-width',\n initialScale: 1,\n // visual metadata\n themeColor: null,\n colorScheme: null,\n }\n}\n\nexport function createDefaultMetadata(): ResolvedMetadata {\n return {\n // Deprecated ones\n viewport: null,\n themeColor: null,\n colorScheme: null,\n\n metadataBase: null,\n // Other values are all null\n title: null,\n description: null,\n applicationName: null,\n authors: null,\n generator: null,\n keywords: null,\n referrer: null,\n creator: null,\n publisher: null,\n robots: null,\n manifest: null,\n alternates: {\n canonical: null,\n languages: null,\n media: null,\n types: null,\n },\n icons: null,\n openGraph: null,\n twitter: null,\n verification: {},\n appleWebApp: null,\n formatDetection: null,\n itunes: null,\n facebook: null,\n pinterest: null,\n abstract: null,\n appLinks: null,\n archives: null,\n assets: null,\n bookmarks: null,\n category: null,\n classification: null,\n pagination: {\n previous: null,\n next: null,\n },\n other: {},\n }\n}\n"],"names":["createDefaultViewport","width","initialScale","themeColor","colorScheme","createDefaultMetadata","viewport","metadataBase","title","description","applicationName","authors","generator","keywords","referrer","creator","publisher","robots","manifest","alternates","canonical","languages","media","types","icons","openGraph","twitter","verification","appleWebApp","formatDetection","itunes","facebook","pinterest","abstract","appLinks","archives","assets","bookmarks","category","classification","pagination","previous","next","other"],"mappings":"AAKA,OAAO,SAASA;IACd,OAAO;QACL,gBAAgB;QAChBC,OAAO;QACPC,cAAc;QACd,kBAAkB;QAClBC,YAAY;QACZC,aAAa;IACf;AACF;AAEA,OAAO,SAASC;IACd,OAAO;QACL,kBAAkB;QAClBC,UAAU;QACVH,YAAY;QACZC,aAAa;QAEbG,cAAc;QACd,4BAA4B;QAC5BC,OAAO;QACPC,aAAa;QACbC,iBAAiB;QACjBC,SAAS;QACTC,WAAW;QACXC,UAAU;QACVC,UAAU;QACVC,SAAS;QACTC,WAAW;QACXC,QAAQ;QACRC,UAAU;QACVC,YAAY;YACVC,WAAW;YACXC,WAAW;YACXC,OAAO;YACPC,OAAO;QACT;QACAC,OAAO;QACPC,WAAW;QACXC,SAAS;QACTC,cAAc,CAAC;QACfC,aAAa;QACbC,iBAAiB;QACjBC,QAAQ;QACRC,UAAU;QACVC,WAAW;QACXC,UAAU;QACVC,UAAU;QACVC,UAAU;QACVC,QAAQ;QACRC,WAAW;QACXC,UAAU;QACVC,gBAAgB;QAChBC,YAAY;YACVC,UAAU;YACVC,MAAM;QACR;QACAC,OAAO,CAAC;IACV;AACF","ignoreList":[0]}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
// This is a client component that only renders during SSR,
|
||||
// but will be replaced during streaming with an icon insertion script tag.
|
||||
// We don't want it to be presented anywhere so it's only visible during streaming,
|
||||
// right after the icon meta tags so that browser can pick it up as soon as it's rendered.
|
||||
// Note: we don't just emit the script here because we only need the script if it's not in the head,
|
||||
// and we need it to be hoistable alongside the other metadata but sync scripts are not hoistable.
|
||||
export const IconMark = ()=>{
|
||||
if (typeof window !== 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return /*#__PURE__*/ _jsx("meta", {
|
||||
name: "\xabnxt-icon\xbb"
|
||||
});
|
||||
};
|
||||
|
||||
//# sourceMappingURL=icon-mark.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/lib/metadata/generate/icon-mark.tsx"],"sourcesContent":["'use client'\n\n// This is a client component that only renders during SSR,\n// but will be replaced during streaming with an icon insertion script tag.\n// We don't want it to be presented anywhere so it's only visible during streaming,\n// right after the icon meta tags so that browser can pick it up as soon as it's rendered.\n// Note: we don't just emit the script here because we only need the script if it's not in the head,\n// and we need it to be hoistable alongside the other metadata but sync scripts are not hoistable.\nexport const IconMark = () => {\n if (typeof window !== 'undefined') {\n return null\n }\n return <meta name=\"«nxt-icon»\" />\n}\n"],"names":["IconMark","window","meta","name"],"mappings":"AAAA;;AAEA,2DAA2D;AAC3D,2EAA2E;AAC3E,mFAAmF;AACnF,0FAA0F;AAC1F,oGAAoG;AACpG,kGAAkG;AAClG,OAAO,MAAMA,WAAW;IACtB,IAAI,OAAOC,WAAW,aAAa;QACjC,OAAO;IACT;IACA,qBAAO,KAACC;QAAKC,MAAK;;AACpB,EAAC","ignoreList":[0]}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
function resolveArray(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return [
|
||||
value
|
||||
];
|
||||
}
|
||||
function resolveAsArrayOrUndefined(value) {
|
||||
if (typeof value === 'undefined' || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveArray(value);
|
||||
}
|
||||
function getOrigin(url) {
|
||||
let origin = undefined;
|
||||
if (typeof url === 'string') {
|
||||
try {
|
||||
url = new URL(url);
|
||||
origin = url.origin;
|
||||
} catch {}
|
||||
}
|
||||
return origin;
|
||||
}
|
||||
export { resolveAsArrayOrUndefined, resolveArray, getOrigin };
|
||||
|
||||
//# sourceMappingURL=utils.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/lib/metadata/generate/utils.ts"],"sourcesContent":["function resolveArray<T>(value: T | T[]): T[] {\n if (Array.isArray(value)) {\n return value as any\n }\n return [value] as any\n}\n\nfunction resolveAsArrayOrUndefined<T>(\n value: T | T[] | undefined | null\n): T extends undefined | null ? undefined : T[] {\n if (typeof value === 'undefined' || value === null) {\n return undefined as any\n }\n return resolveArray(value) as any\n}\n\nfunction getOrigin(url: string | URL): string | undefined {\n let origin = undefined\n if (typeof url === 'string') {\n try {\n url = new URL(url)\n origin = url.origin\n } catch {}\n }\n return origin\n}\n\nexport { resolveAsArrayOrUndefined, resolveArray, getOrigin }\n"],"names":["resolveArray","value","Array","isArray","resolveAsArrayOrUndefined","undefined","getOrigin","url","origin","URL"],"mappings":"AAAA,SAASA,aAAgBC,KAAc;IACrC,IAAIC,MAAMC,OAAO,CAACF,QAAQ;QACxB,OAAOA;IACT;IACA,OAAO;QAACA;KAAM;AAChB;AAEA,SAASG,0BACPH,KAAiC;IAEjC,IAAI,OAAOA,UAAU,eAAeA,UAAU,MAAM;QAClD,OAAOI;IACT;IACA,OAAOL,aAAaC;AACtB;AAEA,SAASK,UAAUC,GAAiB;IAClC,IAAIC,SAASH;IACb,IAAI,OAAOE,QAAQ,UAAU;QAC3B,IAAI;YACFA,MAAM,IAAIE,IAAIF;YACdC,SAASD,IAAIC,MAAM;QACrB,EAAE,OAAM,CAAC;IACX;IACA,OAAOA;AACT;AAEA,SAASJ,yBAAyB,EAAEJ,YAAY,EAAEM,SAAS,GAAE","ignoreList":[0]}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import { isMetadataPage } from './is-metadata-route';
|
||||
import path from '../../shared/lib/isomorphic/path';
|
||||
import { interpolateDynamicPath } from '../../server/server-utils';
|
||||
import { getNamedRouteRegex } from '../../shared/lib/router/utils/route-regex';
|
||||
import { djb2Hash } from '../../shared/lib/hash';
|
||||
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths';
|
||||
import { normalizePathSep } from '../../shared/lib/page-path/normalize-path-sep';
|
||||
import { isGroupSegment, isParallelRouteSegment } from '../../shared/lib/segment';
|
||||
/*
|
||||
* If there's special convention like (...) or @ in the page path,
|
||||
* Give it a unique hash suffix to avoid conflicts
|
||||
*
|
||||
* e.g.
|
||||
* /opengraph-image -> /opengraph-image
|
||||
* /(post)/opengraph-image.tsx -> /opengraph-image-[0-9a-z]{6}
|
||||
*
|
||||
* Sitemap is an exception, it should not have a suffix.
|
||||
* Each sitemap contains all the urls of sub routes, we don't have the case of duplicates `/(group)/sitemap.[ext]` and `/sitemap.[ext]` since they should be the same.
|
||||
* Hence we always normalize the urls for sitemap and do not append hash suffix, and ensure user-land only contains one sitemap per pathname.
|
||||
*
|
||||
* /sitemap -> /sitemap
|
||||
* /(post)/sitemap -> /sitemap
|
||||
*/ function getMetadataRouteSuffix(page) {
|
||||
// Remove the last segment and get the parent pathname
|
||||
// e.g. /parent/a/b/c -> /parent/a/b
|
||||
// e.g. /parent/opengraph-image -> /parent
|
||||
const parentPathname = path.dirname(page);
|
||||
// Only apply suffix to metadata routes except for sitemaps
|
||||
if (page.endsWith('/sitemap') || page.endsWith('/sitemap.xml')) {
|
||||
return '';
|
||||
}
|
||||
// Calculate the hash suffix based on the parent path
|
||||
let suffix = '';
|
||||
// Check if there's any special characters in the parent pathname.
|
||||
const segments = parentPathname.split('/');
|
||||
if (segments.some((seg)=>isGroupSegment(seg) || isParallelRouteSegment(seg))) {
|
||||
// Hash the parent path to get a unique suffix
|
||||
suffix = djb2Hash(parentPathname).toString(36).slice(0, 6);
|
||||
}
|
||||
return suffix;
|
||||
}
|
||||
/**
|
||||
* Fill the dynamic segment in the metadata route
|
||||
*
|
||||
* Example:
|
||||
* fillMetadataSegment('/a/[slug]', { params: { slug: 'b' } }, 'open-graph', false) -> '/a/b/open-graph'
|
||||
*
|
||||
* When isStatic is true, all dynamic segments are filled with "-" placeholder
|
||||
* since static metadata files have consistent responses regardless of params.
|
||||
* Example:
|
||||
* fillMetadataSegment('/a/[slug]', {}, 'icon.png', true) -> '/a/-/icon.png'
|
||||
*
|
||||
*/ export function fillMetadataSegment(segment, params, lastSegment, isStatic) {
|
||||
const pathname = normalizeAppPath(segment);
|
||||
const routeRegex = getNamedRouteRegex(pathname, {
|
||||
prefixRouteKeys: false
|
||||
});
|
||||
// For static metadata files, fill all dynamic segments with "-" placeholder
|
||||
const routeParams = isStatic ? Object.keys(routeRegex.groups).reduce((acc, key)=>{
|
||||
const { repeat } = routeRegex.groups[key];
|
||||
// Use array for catch-all segments, string for regular segments
|
||||
acc[key] = repeat ? [
|
||||
'-'
|
||||
] : '-';
|
||||
return acc;
|
||||
}, {}) : params;
|
||||
const route = interpolateDynamicPath(pathname, routeParams, routeRegex);
|
||||
const { name, ext } = path.parse(lastSegment);
|
||||
const pagePath = path.posix.join(segment, name);
|
||||
const suffix = getMetadataRouteSuffix(pagePath);
|
||||
const routeSuffix = suffix ? `-${suffix}` : '';
|
||||
return normalizePathSep(path.join(route, `${name}${routeSuffix}${ext}`));
|
||||
}
|
||||
/**
|
||||
* Map metadata page key to the corresponding route
|
||||
*
|
||||
* static file page key: /app/robots.txt -> /robots.xml -> /robots.txt/route
|
||||
* dynamic route page key: /app/robots.tsx -> /robots -> /robots.txt/route
|
||||
*
|
||||
* @param page
|
||||
* @returns
|
||||
*/ export function normalizeMetadataRoute(page) {
|
||||
if (!isMetadataPage(page)) {
|
||||
return page;
|
||||
}
|
||||
let route = page;
|
||||
let suffix = '';
|
||||
if (page === '/robots') {
|
||||
route += '.txt';
|
||||
} else if (page === '/manifest') {
|
||||
route += '.webmanifest';
|
||||
} else {
|
||||
suffix = getMetadataRouteSuffix(page);
|
||||
}
|
||||
// Support both /<metadata-route.ext> and custom routes /<metadata-route>/route.ts.
|
||||
// If it's a metadata file route, we need to append /[id]/route to the page.
|
||||
if (!route.endsWith('/route')) {
|
||||
const { dir, name: baseName, ext } = path.parse(route);
|
||||
route = path.posix.join(dir, `${baseName}${suffix ? `-${suffix}` : ''}${ext}`, 'route');
|
||||
}
|
||||
return route;
|
||||
}
|
||||
// Normalize metadata route page to either a single route or a dynamic route.
|
||||
// e.g. Input: /sitemap/route
|
||||
// when isDynamic is false, single route -> /sitemap.xml/route
|
||||
// when isDynamic is false, dynamic route -> /sitemap/[__metadata_id__]/route
|
||||
// also works for pathname such as /sitemap -> /sitemap.xml, but will not append /route suffix
|
||||
export function normalizeMetadataPageToRoute(page, isDynamic) {
|
||||
const isRoute = page.endsWith('/route');
|
||||
const routePagePath = isRoute ? page.slice(0, -'/route'.length) : page;
|
||||
const metadataRouteExtension = routePagePath.endsWith('/sitemap') ? '.xml' : '';
|
||||
const mapped = isDynamic ? `${routePagePath}/[__metadata_id__]` : `${routePagePath}${metadataRouteExtension}`;
|
||||
return mapped + (isRoute ? '/route' : '');
|
||||
}
|
||||
|
||||
//# sourceMappingURL=get-metadata-route.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+214
@@ -0,0 +1,214 @@
|
||||
import { normalizePathSep } from '../../shared/lib/page-path/normalize-path-sep';
|
||||
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths';
|
||||
import { isAppRouteRoute } from '../is-app-route-route';
|
||||
export const STATIC_METADATA_IMAGES = {
|
||||
icon: {
|
||||
filename: 'icon',
|
||||
extensions: [
|
||||
'ico',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'svg'
|
||||
]
|
||||
},
|
||||
apple: {
|
||||
filename: 'apple-icon',
|
||||
extensions: [
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png'
|
||||
]
|
||||
},
|
||||
favicon: {
|
||||
filename: 'favicon',
|
||||
extensions: [
|
||||
'ico'
|
||||
]
|
||||
},
|
||||
openGraph: {
|
||||
filename: 'opengraph-image',
|
||||
extensions: [
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif'
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
filename: 'twitter-image',
|
||||
extensions: [
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif'
|
||||
]
|
||||
}
|
||||
};
|
||||
// Match routes that are metadata routes, e.g. /sitemap.xml, /favicon.<ext>, /<icon>.<ext>, etc.
|
||||
// TODO-METADATA: support more metadata routes with more extensions
|
||||
export const DEFAULT_METADATA_ROUTE_EXTENSIONS = [
|
||||
'js',
|
||||
'jsx',
|
||||
'ts',
|
||||
'tsx'
|
||||
];
|
||||
// Match the file extension with the dynamic multi-routes extensions
|
||||
// e.g. ([xml, js], null) -> can match `/sitemap.xml/route`, `sitemap.js/route`
|
||||
// e.g. ([png], [ts]) -> can match `/opengraph-image.png`, `/opengraph-image.ts`
|
||||
export const getExtensionRegexString = (staticExtensions, dynamicExtensions)=>{
|
||||
let result;
|
||||
// If there's no possible multi dynamic routes, will not match any <name>[].<ext> files
|
||||
if (!dynamicExtensions || dynamicExtensions.length === 0) {
|
||||
result = `(\\.(?:${staticExtensions.join('|')}))`;
|
||||
} else {
|
||||
result = `(?:\\.(${staticExtensions.join('|')})|(\\.(${dynamicExtensions.join('|')})))`;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
/**
|
||||
* Matches the static metadata files, e.g. /robots.txt, /sitemap.xml, /favicon.ico, etc.
|
||||
* @param appDirRelativePath the relative file path to app/
|
||||
* @returns if the path is a static metadata file route
|
||||
*/ export function isStaticMetadataFile(appDirRelativePath) {
|
||||
return isMetadataRouteFile(appDirRelativePath, [], true);
|
||||
}
|
||||
// Pre-compiled static regexes for common cases
|
||||
const FAVICON_REGEX = /^[\\/]favicon\.ico$/;
|
||||
const ROBOTS_TXT_REGEX = /^[\\/]robots\.txt$/;
|
||||
const MANIFEST_JSON_REGEX = /^[\\/]manifest\.json$/;
|
||||
const MANIFEST_WEBMANIFEST_REGEX = /^[\\/]manifest\.webmanifest$/;
|
||||
const SITEMAP_XML_REGEX = /[\\/]sitemap\.xml$/;
|
||||
// Cache for compiled regex patterns based on parameters
|
||||
const compiledRegexCache = new Map();
|
||||
// Fast path checks for common metadata files
|
||||
function fastPathCheck(normalizedPath) {
|
||||
// Check favicon.ico first (most common)
|
||||
if (FAVICON_REGEX.test(normalizedPath)) return true;
|
||||
// Check other common static files
|
||||
if (ROBOTS_TXT_REGEX.test(normalizedPath)) return true;
|
||||
if (MANIFEST_JSON_REGEX.test(normalizedPath)) return true;
|
||||
if (MANIFEST_WEBMANIFEST_REGEX.test(normalizedPath)) return true;
|
||||
if (SITEMAP_XML_REGEX.test(normalizedPath)) return true;
|
||||
// Quick negative check - if it doesn't contain any metadata keywords, skip
|
||||
if (!normalizedPath.includes('robots') && !normalizedPath.includes('manifest') && !normalizedPath.includes('sitemap') && !normalizedPath.includes('icon') && !normalizedPath.includes('apple-icon') && !normalizedPath.includes('opengraph-image') && !normalizedPath.includes('twitter-image') && !normalizedPath.includes('favicon')) {
|
||||
return false;
|
||||
}
|
||||
return null // Continue with full regex matching
|
||||
;
|
||||
}
|
||||
function getCompiledRegexes(pageExtensions, strictlyMatchExtensions) {
|
||||
// Create cache key
|
||||
const cacheKey = `${pageExtensions.join(',')}|${strictlyMatchExtensions}`;
|
||||
const cached = compiledRegexCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
// Pre-compute common strings
|
||||
const trailingMatcher = strictlyMatchExtensions ? '$' : '?$';
|
||||
const variantsMatcher = '\\d?';
|
||||
const groupSuffix = strictlyMatchExtensions ? '' : '(-\\w{6})?';
|
||||
const suffixMatcher = variantsMatcher + groupSuffix;
|
||||
// Pre-compute extension arrays to avoid repeated concatenation
|
||||
const robotsExts = pageExtensions.length > 0 ? [
|
||||
...pageExtensions,
|
||||
'txt'
|
||||
] : [
|
||||
'txt'
|
||||
];
|
||||
const manifestExts = pageExtensions.length > 0 ? [
|
||||
...pageExtensions,
|
||||
'webmanifest',
|
||||
'json'
|
||||
] : [
|
||||
'webmanifest',
|
||||
'json'
|
||||
];
|
||||
const regexes = [
|
||||
new RegExp(`^[\\\\/]robots${getExtensionRegexString(robotsExts, null)}${trailingMatcher}`),
|
||||
new RegExp(`^[\\\\/]manifest${getExtensionRegexString(manifestExts, null)}${trailingMatcher}`),
|
||||
// FAVICON_REGEX removed - already handled in fastPathCheck
|
||||
new RegExp(`[\\\\/]sitemap${getExtensionRegexString([
|
||||
'xml'
|
||||
], pageExtensions)}${trailingMatcher}`),
|
||||
new RegExp(`[\\\\/]icon${suffixMatcher}${getExtensionRegexString(STATIC_METADATA_IMAGES.icon.extensions, pageExtensions)}${trailingMatcher}`),
|
||||
new RegExp(`[\\\\/]apple-icon${suffixMatcher}${getExtensionRegexString(STATIC_METADATA_IMAGES.apple.extensions, pageExtensions)}${trailingMatcher}`),
|
||||
new RegExp(`[\\\\/]opengraph-image${suffixMatcher}${getExtensionRegexString(STATIC_METADATA_IMAGES.openGraph.extensions, pageExtensions)}${trailingMatcher}`),
|
||||
new RegExp(`[\\\\/]twitter-image${suffixMatcher}${getExtensionRegexString(STATIC_METADATA_IMAGES.twitter.extensions, pageExtensions)}${trailingMatcher}`)
|
||||
];
|
||||
compiledRegexCache.set(cacheKey, regexes);
|
||||
return regexes;
|
||||
}
|
||||
/**
|
||||
* Determine if the file is a metadata route file entry
|
||||
* @param appDirRelativePath the relative file path to app/
|
||||
* @param pageExtensions the js extensions, such as ['js', 'jsx', 'ts', 'tsx']
|
||||
* @param strictlyMatchExtensions if it's true, match the file with page extension, otherwise match the file with default corresponding extension
|
||||
* @returns if the file is a metadata route file
|
||||
*/ export function isMetadataRouteFile(appDirRelativePath, pageExtensions, strictlyMatchExtensions) {
|
||||
// Early exit for empty or obviously non-metadata paths
|
||||
if (!appDirRelativePath || appDirRelativePath.length < 2) {
|
||||
return false;
|
||||
}
|
||||
const normalizedPath = normalizePathSep(appDirRelativePath);
|
||||
// Fast path check for common cases
|
||||
const fastResult = fastPathCheck(normalizedPath);
|
||||
if (fastResult !== null) {
|
||||
return fastResult;
|
||||
}
|
||||
// Get compiled regexes from cache
|
||||
const regexes = getCompiledRegexes(pageExtensions, strictlyMatchExtensions);
|
||||
// Use for loop instead of .some() for better performance
|
||||
for(let i = 0; i < regexes.length; i++){
|
||||
if (regexes[i].test(normalizedPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Check if the route is a static metadata route, with /route suffix
|
||||
// e.g. /favicon.ico/route, /icon.png/route, etc.
|
||||
// But skip the text routes like robots.txt since they might also be dynamic.
|
||||
// Checking route path is not enough to determine if text routes is dynamic.
|
||||
export function isStaticMetadataRoute(route) {
|
||||
// extract ext with regex
|
||||
const pathname = route.replace(/\/route$/, '');
|
||||
const matched = isAppRouteRoute(route) && isMetadataRouteFile(pathname, [], true) && // These routes can either be built by static or dynamic entrypoints,
|
||||
// so we assume they're dynamic
|
||||
pathname !== '/robots.txt' && pathname !== '/manifest.webmanifest' && !pathname.endsWith('/sitemap.xml');
|
||||
return matched;
|
||||
}
|
||||
/**
|
||||
* Determine if a page or pathname is a metadata page.
|
||||
*
|
||||
* The input is a page or pathname, which can be with or without page suffix /foo/page or /foo.
|
||||
* But it will not contain the /route suffix.
|
||||
*
|
||||
* .e.g
|
||||
* /robots -> true
|
||||
* /sitemap -> true
|
||||
* /foo -> false
|
||||
*/ export function isMetadataPage(page) {
|
||||
const matched = !isAppRouteRoute(page) && isMetadataRouteFile(page, [], false);
|
||||
return matched;
|
||||
}
|
||||
/*
|
||||
* Determine if a Next.js route is a metadata route.
|
||||
* `route` will has a route suffix.
|
||||
*
|
||||
* e.g.
|
||||
* /app/robots/route -> true
|
||||
* /robots/route -> true
|
||||
* /sitemap/[__metadata_id__]/route -> true
|
||||
* /app/sitemap/page -> false
|
||||
* /icon-a102f4/route -> true
|
||||
*/ export function isMetadataRoute(route) {
|
||||
let page = normalizeAppPath(route).replace(/^\/?app\//, '')// Remove the dynamic route id
|
||||
.replace('/[__metadata_id__]', '')// Remove the /route suffix
|
||||
.replace(/\/route$/, '');
|
||||
if (page[0] !== '/') page = '/' + page;
|
||||
const matched = isAppRouteRoute(route) && isMetadataRouteFile(page, [], false);
|
||||
return matched;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=is-metadata-route.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+8
@@ -0,0 +1,8 @@
|
||||
export function createMetadataContext(renderOpts) {
|
||||
return {
|
||||
trailingSlash: renderOpts.trailingSlash,
|
||||
isStaticMetadataRouteFile: false
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=metadata-context.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../src/lib/metadata/metadata-context.tsx"],"sourcesContent":["import type { AppRenderContext } from '../../server/app-render/app-render'\nimport type { MetadataContext } from './types/resolvers'\n\nexport function createMetadataContext(\n renderOpts: AppRenderContext['renderOpts']\n): MetadataContext {\n return {\n trailingSlash: renderOpts.trailingSlash,\n isStaticMetadataRouteFile: false,\n }\n}\n"],"names":["createMetadataContext","renderOpts","trailingSlash","isStaticMetadataRouteFile"],"mappings":"AAGA,OAAO,SAASA,sBACdC,UAA0C;IAE1C,OAAO;QACLC,eAAeD,WAAWC,aAAa;QACvCC,2BAA2B;IAC7B;AACF","ignoreList":[0]}
|
||||
+1641
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+818
@@ -0,0 +1,818 @@
|
||||
import { getSegmentParam } from '../../shared/lib/router/utils/get-segment-param';
|
||||
import { workAsyncStorage } from '../../server/app-render/work-async-storage.external';
|
||||
import { InvariantError } from '../../shared/lib/invariant-error';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import 'server-only';
|
||||
import { cache } from 'react';
|
||||
import { createDefaultMetadata, createDefaultViewport } from './default-metadata';
|
||||
import { resolveOpenGraph, resolveTwitter } from './resolvers/resolve-opengraph';
|
||||
import { resolveTitle } from './resolvers/resolve-title';
|
||||
import { resolveAsArrayOrUndefined } from './generate/utils';
|
||||
import { getComponentTypeModule, getLayoutOrPageModule } from '../../server/lib/app-dir-module';
|
||||
import { interopDefault } from '../interop-default';
|
||||
import { resolveAlternates, resolveAppleWebApp, resolveAppLinks, resolveRobots, resolveThemeColor, resolveVerification, resolveItunes, resolveFacebook, resolvePagination } from './resolvers/resolve-basics';
|
||||
import { resolveIcons } from './resolvers/resolve-icons';
|
||||
import { getTracer } from '../../server/lib/trace/tracer';
|
||||
import { ResolveMetadataSpan } from '../../server/lib/trace/constants';
|
||||
import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment';
|
||||
import * as Log from '../../build/output/log';
|
||||
import { createServerParamsForMetadata } from '../../server/request/params';
|
||||
import { getUseCacheFunctionInfo, isUseCacheFunction } from '../client-and-server-references';
|
||||
import { createLazyResult } from '../../server/lib/lazy-result';
|
||||
function isFavicon(icon) {
|
||||
if (!icon) {
|
||||
return false;
|
||||
}
|
||||
// turbopack appends a hash to all images
|
||||
return (icon.url === '/favicon.ico' || icon.url.toString().startsWith('/favicon.ico?')) && icon.type === 'image/x-icon';
|
||||
}
|
||||
function convertUrlsToStrings(input) {
|
||||
if (input instanceof URL) {
|
||||
return input.toString();
|
||||
} else if (Array.isArray(input)) {
|
||||
return input.map((item)=>convertUrlsToStrings(item));
|
||||
} else if (input && typeof input === 'object') {
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(input)){
|
||||
result[key] = convertUrlsToStrings(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
function normalizeMetadataBase(metadataBase) {
|
||||
if (typeof metadataBase === 'string') {
|
||||
try {
|
||||
metadataBase = new URL(metadataBase);
|
||||
} catch {
|
||||
throw Object.defineProperty(new Error(`metadataBase is not a valid URL: ${metadataBase}`), "__NEXT_ERROR_CODE", {
|
||||
value: "E850",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
return metadataBase;
|
||||
}
|
||||
async function mergeStaticMetadata(metadataBase, source, target, staticFilesMetadata, metadataContext, titleTemplates, leafSegmentStaticIcons, pathname) {
|
||||
var _source_twitter, _source_openGraph;
|
||||
if (!staticFilesMetadata) return target;
|
||||
const { icon, apple, openGraph, twitter, manifest } = staticFilesMetadata;
|
||||
// Keep updating the static icons in the most leaf node
|
||||
if (icon) {
|
||||
leafSegmentStaticIcons.icon = icon;
|
||||
}
|
||||
if (apple) {
|
||||
leafSegmentStaticIcons.apple = apple;
|
||||
}
|
||||
// file based metadata is specified and current level metadata twitter.images is not specified
|
||||
if (twitter && !(source == null ? void 0 : (_source_twitter = source.twitter) == null ? void 0 : _source_twitter.hasOwnProperty('images'))) {
|
||||
const resolvedTwitter = resolveTwitter({
|
||||
...target.twitter,
|
||||
images: twitter
|
||||
}, metadataBase, {
|
||||
...metadataContext,
|
||||
isStaticMetadataRouteFile: true
|
||||
}, titleTemplates.twitter);
|
||||
target.twitter = convertUrlsToStrings(resolvedTwitter);
|
||||
}
|
||||
// file based metadata is specified and current level metadata openGraph.images is not specified
|
||||
if (openGraph && !(source == null ? void 0 : (_source_openGraph = source.openGraph) == null ? void 0 : _source_openGraph.hasOwnProperty('images'))) {
|
||||
const resolvedOpenGraph = await resolveOpenGraph({
|
||||
...target.openGraph,
|
||||
images: openGraph
|
||||
}, metadataBase, pathname, {
|
||||
...metadataContext,
|
||||
isStaticMetadataRouteFile: true
|
||||
}, titleTemplates.openGraph);
|
||||
target.openGraph = convertUrlsToStrings(resolvedOpenGraph);
|
||||
}
|
||||
if (manifest) {
|
||||
target.manifest = manifest;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
/**
|
||||
* Merges the given metadata with the resolved metadata. Returns a new object.
|
||||
*/ async function mergeMetadata(route, pathname, { metadata, resolvedMetadata, staticFilesMetadata, titleTemplates, metadataContext, buildState, leafSegmentStaticIcons }) {
|
||||
const newResolvedMetadata = structuredClone(resolvedMetadata);
|
||||
const metadataBase = normalizeMetadataBase((metadata == null ? void 0 : metadata.metadataBase) !== undefined ? metadata.metadataBase : resolvedMetadata.metadataBase);
|
||||
for(const key_ in metadata){
|
||||
const key = key_;
|
||||
switch(key){
|
||||
case 'title':
|
||||
{
|
||||
newResolvedMetadata.title = resolveTitle(metadata.title, titleTemplates.title);
|
||||
break;
|
||||
}
|
||||
case 'alternates':
|
||||
{
|
||||
newResolvedMetadata.alternates = convertUrlsToStrings(await resolveAlternates(metadata.alternates, metadataBase, pathname, metadataContext));
|
||||
break;
|
||||
}
|
||||
case 'openGraph':
|
||||
{
|
||||
newResolvedMetadata.openGraph = convertUrlsToStrings(await resolveOpenGraph(metadata.openGraph, metadataBase, pathname, metadataContext, titleTemplates.openGraph));
|
||||
break;
|
||||
}
|
||||
case 'twitter':
|
||||
{
|
||||
newResolvedMetadata.twitter = convertUrlsToStrings(resolveTwitter(metadata.twitter, metadataBase, metadataContext, titleTemplates.twitter));
|
||||
break;
|
||||
}
|
||||
case 'facebook':
|
||||
newResolvedMetadata.facebook = resolveFacebook(metadata.facebook);
|
||||
break;
|
||||
case 'verification':
|
||||
newResolvedMetadata.verification = resolveVerification(metadata.verification);
|
||||
break;
|
||||
case 'icons':
|
||||
{
|
||||
newResolvedMetadata.icons = convertUrlsToStrings(resolveIcons(metadata.icons));
|
||||
break;
|
||||
}
|
||||
case 'appleWebApp':
|
||||
newResolvedMetadata.appleWebApp = resolveAppleWebApp(metadata.appleWebApp);
|
||||
break;
|
||||
case 'appLinks':
|
||||
newResolvedMetadata.appLinks = convertUrlsToStrings(resolveAppLinks(metadata.appLinks));
|
||||
break;
|
||||
case 'robots':
|
||||
{
|
||||
newResolvedMetadata.robots = resolveRobots(metadata.robots);
|
||||
break;
|
||||
}
|
||||
case 'archives':
|
||||
case 'assets':
|
||||
case 'bookmarks':
|
||||
case 'keywords':
|
||||
{
|
||||
newResolvedMetadata[key] = resolveAsArrayOrUndefined(metadata[key]);
|
||||
break;
|
||||
}
|
||||
case 'authors':
|
||||
{
|
||||
newResolvedMetadata[key] = convertUrlsToStrings(resolveAsArrayOrUndefined(metadata.authors));
|
||||
break;
|
||||
}
|
||||
case 'itunes':
|
||||
{
|
||||
newResolvedMetadata[key] = await resolveItunes(metadata.itunes, metadataBase, pathname, metadataContext);
|
||||
break;
|
||||
}
|
||||
case 'pagination':
|
||||
{
|
||||
newResolvedMetadata.pagination = await resolvePagination(metadata.pagination, metadataBase, pathname, metadataContext);
|
||||
break;
|
||||
}
|
||||
// directly assign fields that fallback to null
|
||||
case 'abstract':
|
||||
newResolvedMetadata[key] = metadata[key] ?? null;
|
||||
break;
|
||||
case 'applicationName':
|
||||
newResolvedMetadata[key] = metadata[key] ?? null;
|
||||
break;
|
||||
case 'description':
|
||||
newResolvedMetadata[key] = metadata[key] ?? null;
|
||||
break;
|
||||
case 'generator':
|
||||
newResolvedMetadata[key] = metadata[key] ?? null;
|
||||
break;
|
||||
case 'creator':
|
||||
newResolvedMetadata[key] = metadata[key] ?? null;
|
||||
break;
|
||||
case 'publisher':
|
||||
newResolvedMetadata[key] = metadata[key] ?? null;
|
||||
break;
|
||||
case 'category':
|
||||
newResolvedMetadata[key] = metadata[key] ?? null;
|
||||
break;
|
||||
case 'classification':
|
||||
newResolvedMetadata[key] = metadata[key] ?? null;
|
||||
break;
|
||||
case 'referrer':
|
||||
newResolvedMetadata[key] = metadata[key] ?? null;
|
||||
break;
|
||||
case 'formatDetection':
|
||||
newResolvedMetadata[key] = metadata[key] ?? null;
|
||||
break;
|
||||
case 'manifest':
|
||||
newResolvedMetadata[key] = convertUrlsToStrings(metadata[key]) ?? null;
|
||||
break;
|
||||
case 'pinterest':
|
||||
newResolvedMetadata[key] = convertUrlsToStrings(metadata[key]) ?? null;
|
||||
break;
|
||||
case 'other':
|
||||
newResolvedMetadata.other = Object.assign({}, newResolvedMetadata.other, metadata.other);
|
||||
if (metadata.other) {
|
||||
if ('apple-touch-fullscreen' in metadata.other) {
|
||||
buildState.warnings.add(`Use appleWebApp instead\nRead more: https://nextjs.org/docs/app/api-reference/functions/generate-metadata`);
|
||||
}
|
||||
if ('apple-touch-icon-precomposed' in metadata.other) {
|
||||
buildState.warnings.add(`Use icons.apple instead\nRead more: https://nextjs.org/docs/app/api-reference/functions/generate-metadata`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'metadataBase':
|
||||
newResolvedMetadata.metadataBase = metadataBase ? metadataBase.toString() : null;
|
||||
break;
|
||||
case 'apple-touch-fullscreen':
|
||||
{
|
||||
buildState.warnings.add(`Use appleWebApp instead\nRead more: https://nextjs.org/docs/app/api-reference/functions/generate-metadata`);
|
||||
break;
|
||||
}
|
||||
case 'apple-touch-icon-precomposed':
|
||||
{
|
||||
buildState.warnings.add(`Use icons.apple instead\nRead more: https://nextjs.org/docs/app/api-reference/functions/generate-metadata`);
|
||||
break;
|
||||
}
|
||||
case 'themeColor':
|
||||
case 'colorScheme':
|
||||
case 'viewport':
|
||||
if (metadata[key] != null) {
|
||||
buildState.warnings.add(`Unsupported metadata ${key} is configured in metadata export in ${route}. Please move it to viewport export instead.\nRead more: https://nextjs.org/docs/app/api-reference/functions/generate-viewport`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
{
|
||||
key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mergeStaticMetadata(metadataBase, metadata, newResolvedMetadata, staticFilesMetadata, metadataContext, titleTemplates, leafSegmentStaticIcons, pathname);
|
||||
}
|
||||
/**
|
||||
* Merges the given viewport with the resolved viewport. Returns a new object.
|
||||
*/ function mergeViewport({ resolvedViewport, viewport }) {
|
||||
const newResolvedViewport = structuredClone(resolvedViewport);
|
||||
if (viewport) {
|
||||
for(const key_ in viewport){
|
||||
const key = key_;
|
||||
switch(key){
|
||||
case 'themeColor':
|
||||
{
|
||||
newResolvedViewport.themeColor = resolveThemeColor(viewport.themeColor);
|
||||
break;
|
||||
}
|
||||
case 'colorScheme':
|
||||
newResolvedViewport.colorScheme = viewport.colorScheme || null;
|
||||
break;
|
||||
case 'width':
|
||||
case 'height':
|
||||
case 'initialScale':
|
||||
case 'minimumScale':
|
||||
case 'maximumScale':
|
||||
case 'userScalable':
|
||||
case 'viewportFit':
|
||||
case 'interactiveWidget':
|
||||
// always override the target with the source
|
||||
// @ts-ignore viewport properties
|
||||
newResolvedViewport[key] = viewport[key];
|
||||
break;
|
||||
default:
|
||||
key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return newResolvedViewport;
|
||||
}
|
||||
function getDefinedViewport(mod, props, tracingProps) {
|
||||
if (typeof mod.generateViewport === 'function') {
|
||||
const { route } = tracingProps;
|
||||
const segmentProps = createSegmentProps(mod.generateViewport, props);
|
||||
return Object.assign((parent)=>getTracer().trace(ResolveMetadataSpan.generateViewport, {
|
||||
spanName: `generateViewport ${route}`,
|
||||
attributes: {
|
||||
'next.page': route
|
||||
}
|
||||
}, ()=>mod.generateViewport(segmentProps, parent)), {
|
||||
$$original: mod.generateViewport
|
||||
});
|
||||
}
|
||||
return mod.viewport || null;
|
||||
}
|
||||
function getDefinedMetadata(mod, props, tracingProps) {
|
||||
if (typeof mod.generateMetadata === 'function') {
|
||||
const { route } = tracingProps;
|
||||
const segmentProps = createSegmentProps(mod.generateMetadata, props);
|
||||
return Object.assign((parent)=>getTracer().trace(ResolveMetadataSpan.generateMetadata, {
|
||||
spanName: `generateMetadata ${route}`,
|
||||
attributes: {
|
||||
'next.page': route
|
||||
}
|
||||
}, ()=>mod.generateMetadata(segmentProps, parent)), {
|
||||
$$original: mod.generateMetadata
|
||||
});
|
||||
}
|
||||
return mod.metadata || null;
|
||||
}
|
||||
/**
|
||||
* If `fn` is a `'use cache'` function, we add special markers to the props,
|
||||
* that the cache wrapper reads and removes, before passing the props to the
|
||||
* user function.
|
||||
*/ function createSegmentProps(fn, props) {
|
||||
return isUseCacheFunction(fn) ? 'searchParams' in props ? {
|
||||
...props,
|
||||
$$isPage: true
|
||||
} : {
|
||||
...props,
|
||||
$$isLayout: true
|
||||
} : props;
|
||||
}
|
||||
async function collectStaticImagesFiles(metadata, props, type) {
|
||||
var _this;
|
||||
if (!(metadata == null ? void 0 : metadata[type])) return undefined;
|
||||
const iconPromises = metadata[type].map(async (imageModule)=>await interopDefault(imageModule)(props));
|
||||
return (iconPromises == null ? void 0 : iconPromises.length) > 0 ? (_this = await Promise.all(iconPromises)) == null ? void 0 : _this.flat() : undefined;
|
||||
}
|
||||
async function resolveStaticMetadata(modules, props) {
|
||||
const { metadata } = modules;
|
||||
if (!metadata) return null;
|
||||
const [icon, apple, openGraph, twitter] = await Promise.all([
|
||||
collectStaticImagesFiles(metadata, props, 'icon'),
|
||||
collectStaticImagesFiles(metadata, props, 'apple'),
|
||||
collectStaticImagesFiles(metadata, props, 'openGraph'),
|
||||
collectStaticImagesFiles(metadata, props, 'twitter')
|
||||
]);
|
||||
const staticMetadata = {
|
||||
icon,
|
||||
apple,
|
||||
openGraph,
|
||||
twitter,
|
||||
manifest: metadata.manifest
|
||||
};
|
||||
return staticMetadata;
|
||||
}
|
||||
// [layout.metadata, static files metadata] -> ... -> [page.metadata, static files metadata]
|
||||
async function collectMetadata({ tree, metadataItems, errorMetadataItem, props, route, errorConvention }) {
|
||||
let mod;
|
||||
let modType;
|
||||
const hasErrorConventionComponent = Boolean(errorConvention && tree[2][errorConvention]);
|
||||
if (errorConvention) {
|
||||
mod = await getComponentTypeModule(tree, 'layout');
|
||||
modType = errorConvention;
|
||||
} else {
|
||||
const { mod: layoutOrPageMod, modType: layoutOrPageModType } = await getLayoutOrPageModule(tree);
|
||||
mod = layoutOrPageMod;
|
||||
modType = layoutOrPageModType;
|
||||
}
|
||||
if (modType) {
|
||||
route += `/${modType}`;
|
||||
}
|
||||
const staticFilesMetadata = await resolveStaticMetadata(tree[2], props);
|
||||
const metadataExport = mod ? getDefinedMetadata(mod, props, {
|
||||
route
|
||||
}) : null;
|
||||
metadataItems.push([
|
||||
metadataExport,
|
||||
staticFilesMetadata
|
||||
]);
|
||||
if (hasErrorConventionComponent && errorConvention) {
|
||||
const errorMod = await getComponentTypeModule(tree, errorConvention);
|
||||
const errorMetadataExport = errorMod ? getDefinedMetadata(errorMod, props, {
|
||||
route
|
||||
}) : null;
|
||||
errorMetadataItem[0] = errorMetadataExport;
|
||||
errorMetadataItem[1] = staticFilesMetadata;
|
||||
}
|
||||
}
|
||||
// [layout.metadata, static files metadata] -> ... -> [page.metadata, static files metadata]
|
||||
async function collectViewport({ tree, viewportItems, errorViewportItemRef, props, route, errorConvention }) {
|
||||
let mod;
|
||||
let modType;
|
||||
const hasErrorConventionComponent = Boolean(errorConvention && tree[2][errorConvention]);
|
||||
if (errorConvention) {
|
||||
mod = await getComponentTypeModule(tree, 'layout');
|
||||
modType = errorConvention;
|
||||
} else {
|
||||
const { mod: layoutOrPageMod, modType: layoutOrPageModType } = await getLayoutOrPageModule(tree);
|
||||
mod = layoutOrPageMod;
|
||||
modType = layoutOrPageModType;
|
||||
}
|
||||
if (modType) {
|
||||
route += `/${modType}`;
|
||||
}
|
||||
const viewportExport = mod ? getDefinedViewport(mod, props, {
|
||||
route
|
||||
}) : null;
|
||||
viewportItems.push(viewportExport);
|
||||
if (hasErrorConventionComponent && errorConvention) {
|
||||
const errorMod = await getComponentTypeModule(tree, errorConvention);
|
||||
const errorViewportExport = errorMod ? getDefinedViewport(errorMod, props, {
|
||||
route
|
||||
}) : null;
|
||||
errorViewportItemRef.current = errorViewportExport;
|
||||
}
|
||||
}
|
||||
const resolveMetadataItems = cache(async function(tree, searchParams, errorConvention, interpolatedParams, isRuntimePrefetchable) {
|
||||
const parentParams = {};
|
||||
const metadataItems = [];
|
||||
const errorMetadataItem = [
|
||||
null,
|
||||
null
|
||||
];
|
||||
const treePrefix = undefined;
|
||||
return resolveMetadataItemsImpl(metadataItems, tree, treePrefix, parentParams, null, searchParams, errorConvention, errorMetadataItem, interpolatedParams, isRuntimePrefetchable);
|
||||
});
|
||||
async function resolveMetadataItemsImpl(metadataItems, tree, /** Provided tree can be nested subtree, this argument says what is the path of such subtree */ treePrefix, parentParams, parentOptionalCatchAllParamName, searchParams, errorConvention, errorMetadataItem, interpolatedParams, isRuntimePrefetchable) {
|
||||
const [segment, parallelRoutes, { page }] = tree;
|
||||
const currentTreePrefix = treePrefix && treePrefix.length ? [
|
||||
...treePrefix,
|
||||
segment
|
||||
] : [
|
||||
segment
|
||||
];
|
||||
const isPage = typeof page !== 'undefined';
|
||||
// Handle dynamic segment params.
|
||||
let currentParams = parentParams;
|
||||
const segmentParam = getSegmentParam(segment);
|
||||
if (segmentParam) {
|
||||
const value = interpolatedParams[segmentParam.paramName];
|
||||
if (value !== null && value !== undefined) {
|
||||
currentParams = {
|
||||
...parentParams,
|
||||
[segmentParam.paramName]: value
|
||||
};
|
||||
}
|
||||
}
|
||||
// Track optional catch-all params with no value (see comment in
|
||||
// create-component-tree.tsx for full explanation).
|
||||
const optionalCatchAllParamName = (segmentParam == null ? void 0 : segmentParam.paramType) === 'optional-catchall' && (interpolatedParams[segmentParam.paramName] === null || interpolatedParams[segmentParam.paramName] === undefined) ? segmentParam.paramName : parentOptionalCatchAllParamName;
|
||||
const params = createServerParamsForMetadata(currentParams, optionalCatchAllParamName, isRuntimePrefetchable);
|
||||
const props = isPage ? {
|
||||
params,
|
||||
searchParams
|
||||
} : {
|
||||
params
|
||||
};
|
||||
await collectMetadata({
|
||||
tree,
|
||||
metadataItems,
|
||||
errorMetadataItem,
|
||||
errorConvention,
|
||||
props,
|
||||
route: currentTreePrefix// __PAGE__ shouldn't be shown in a route
|
||||
.filter((s)=>s !== PAGE_SEGMENT_KEY).join('/')
|
||||
});
|
||||
for(const key in parallelRoutes){
|
||||
const childTree = parallelRoutes[key];
|
||||
await resolveMetadataItemsImpl(metadataItems, childTree, currentTreePrefix, currentParams, optionalCatchAllParamName, searchParams, errorConvention, errorMetadataItem, interpolatedParams, isRuntimePrefetchable);
|
||||
}
|
||||
if (Object.keys(parallelRoutes).length === 0 && errorConvention) {
|
||||
// If there are no parallel routes, place error metadata as the last item.
|
||||
// e.g. layout -> layout -> not-found
|
||||
metadataItems.push(errorMetadataItem);
|
||||
}
|
||||
return metadataItems;
|
||||
}
|
||||
const resolveViewportItems = cache(async function(tree, searchParams, errorConvention, interpolatedParams, isRuntimePrefetchable) {
|
||||
const parentParams = {};
|
||||
const viewportItems = [];
|
||||
const errorViewportItemRef = {
|
||||
current: null
|
||||
};
|
||||
const treePrefix = undefined;
|
||||
return resolveViewportItemsImpl(viewportItems, tree, treePrefix, parentParams, null, searchParams, errorConvention, errorViewportItemRef, interpolatedParams, isRuntimePrefetchable);
|
||||
});
|
||||
async function resolveViewportItemsImpl(viewportItems, tree, /** Provided tree can be nested subtree, this argument says what is the path of such subtree */ treePrefix, parentParams, parentOptionalCatchAllParamName, searchParams, errorConvention, errorViewportItemRef, interpolatedParams, isRuntimePrefetchable) {
|
||||
const [segment, parallelRoutes, { page }] = tree;
|
||||
const currentTreePrefix = treePrefix && treePrefix.length ? [
|
||||
...treePrefix,
|
||||
segment
|
||||
] : [
|
||||
segment
|
||||
];
|
||||
const isPage = typeof page !== 'undefined';
|
||||
// Handle dynamic segment params.
|
||||
let currentParams = parentParams;
|
||||
const segmentParam = getSegmentParam(segment);
|
||||
if (segmentParam) {
|
||||
const value = interpolatedParams[segmentParam.paramName];
|
||||
if (value !== null && value !== undefined) {
|
||||
currentParams = {
|
||||
...parentParams,
|
||||
[segmentParam.paramName]: value
|
||||
};
|
||||
}
|
||||
}
|
||||
// Track optional catch-all params with no value (see comment in
|
||||
// create-component-tree.tsx for full explanation).
|
||||
const optionalCatchAllParamName = (segmentParam == null ? void 0 : segmentParam.paramType) === 'optional-catchall' && (interpolatedParams[segmentParam.paramName] === null || interpolatedParams[segmentParam.paramName] === undefined) ? segmentParam.paramName : parentOptionalCatchAllParamName;
|
||||
const params = createServerParamsForMetadata(currentParams, optionalCatchAllParamName, isRuntimePrefetchable);
|
||||
let layerProps;
|
||||
if (isPage) {
|
||||
layerProps = {
|
||||
params,
|
||||
searchParams
|
||||
};
|
||||
} else {
|
||||
layerProps = {
|
||||
params
|
||||
};
|
||||
}
|
||||
await collectViewport({
|
||||
tree,
|
||||
viewportItems,
|
||||
errorViewportItemRef,
|
||||
errorConvention,
|
||||
props: layerProps,
|
||||
route: currentTreePrefix// __PAGE__ shouldn't be shown in a route
|
||||
.filter((s)=>s !== PAGE_SEGMENT_KEY).join('/')
|
||||
});
|
||||
for(const key in parallelRoutes){
|
||||
const childTree = parallelRoutes[key];
|
||||
await resolveViewportItemsImpl(viewportItems, childTree, currentTreePrefix, currentParams, optionalCatchAllParamName, searchParams, errorConvention, errorViewportItemRef, interpolatedParams, isRuntimePrefetchable);
|
||||
}
|
||||
if (Object.keys(parallelRoutes).length === 0 && errorConvention) {
|
||||
// If there are no parallel routes, place error metadata as the last item.
|
||||
// e.g. layout -> layout -> not-found
|
||||
viewportItems.push(errorViewportItemRef.current);
|
||||
}
|
||||
return viewportItems;
|
||||
}
|
||||
const isTitleTruthy = (title)=>!!(title == null ? void 0 : title.absolute);
|
||||
const hasTitle = (metadata)=>isTitleTruthy(metadata == null ? void 0 : metadata.title);
|
||||
function inheritFromMetadata(target, metadata) {
|
||||
if (target) {
|
||||
if (!hasTitle(target) && hasTitle(metadata)) {
|
||||
target.title = metadata.title;
|
||||
}
|
||||
if (!target.description && metadata.description) {
|
||||
target.description = metadata.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const commonOgKeys = [
|
||||
'title',
|
||||
'description',
|
||||
'images'
|
||||
];
|
||||
function postProcessMetadata(metadata, favicon, titleTemplates, metadataContext) {
|
||||
const { openGraph, twitter } = metadata;
|
||||
if (openGraph) {
|
||||
// If there's openGraph information but not configured in twitter,
|
||||
// inherit them from openGraph metadata.
|
||||
let autoFillProps = {};
|
||||
const hasTwTitle = hasTitle(twitter);
|
||||
const hasTwDescription = twitter == null ? void 0 : twitter.description;
|
||||
const hasTwImages = Boolean((twitter == null ? void 0 : twitter.hasOwnProperty('images')) && twitter.images);
|
||||
if (!hasTwTitle) {
|
||||
if (isTitleTruthy(openGraph.title)) {
|
||||
autoFillProps.title = openGraph.title;
|
||||
} else if (metadata.title && isTitleTruthy(metadata.title)) {
|
||||
autoFillProps.title = metadata.title;
|
||||
}
|
||||
}
|
||||
if (!hasTwDescription) autoFillProps.description = openGraph.description || metadata.description || undefined;
|
||||
if (!hasTwImages) autoFillProps.images = openGraph.images;
|
||||
if (Object.keys(autoFillProps).length > 0) {
|
||||
const partialTwitter = resolveTwitter(autoFillProps, normalizeMetadataBase(metadata.metadataBase), metadataContext, titleTemplates.twitter);
|
||||
if (metadata.twitter) {
|
||||
metadata.twitter = Object.assign({}, metadata.twitter, {
|
||||
...!hasTwTitle && {
|
||||
title: partialTwitter == null ? void 0 : partialTwitter.title
|
||||
},
|
||||
...!hasTwDescription && {
|
||||
description: partialTwitter == null ? void 0 : partialTwitter.description
|
||||
},
|
||||
...!hasTwImages && {
|
||||
images: partialTwitter == null ? void 0 : partialTwitter.images
|
||||
}
|
||||
});
|
||||
} else {
|
||||
metadata.twitter = convertUrlsToStrings(partialTwitter);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If there's no title and description configured in openGraph or twitter,
|
||||
// use the title and description from metadata.
|
||||
inheritFromMetadata(openGraph, metadata);
|
||||
inheritFromMetadata(twitter, metadata);
|
||||
if (favicon) {
|
||||
if (!metadata.icons) {
|
||||
metadata.icons = {
|
||||
icon: [],
|
||||
apple: []
|
||||
};
|
||||
}
|
||||
metadata.icons.icon.unshift(favicon);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
function prerenderMetadata(metadataItems) {
|
||||
// If the index is a function then it is a resolver and the next slot
|
||||
// is the corresponding result. If the index is not a function it is the result
|
||||
// itself.
|
||||
const resolversAndResults = [];
|
||||
for(let i = 0; i < metadataItems.length; i++){
|
||||
const metadataExport = metadataItems[i][0];
|
||||
getResult(resolversAndResults, metadataExport);
|
||||
}
|
||||
return resolversAndResults;
|
||||
}
|
||||
function prerenderViewport(viewportItems) {
|
||||
// If the index is a function then it is a resolver and the next slot
|
||||
// is the corresponding result. If the index is not a function it is the result
|
||||
// itself.
|
||||
const resolversAndResults = [];
|
||||
for(let i = 0; i < viewportItems.length; i++){
|
||||
const viewportExport = viewportItems[i];
|
||||
getResult(resolversAndResults, viewportExport);
|
||||
}
|
||||
return resolversAndResults;
|
||||
}
|
||||
const noop = ()=>{};
|
||||
function getResult(resolversAndResults, exportForResult) {
|
||||
if (typeof exportForResult === 'function') {
|
||||
// If the function is a 'use cache' function that uses the parent data as
|
||||
// the second argument, we don't want to eagerly execute it during
|
||||
// metadata/viewport pre-rendering, as the parent data might also be
|
||||
// computed from another 'use cache' function. To ensure that the hanging
|
||||
// input abort signal handling works in this case (i.e. the depending
|
||||
// function waits for the cached input to resolve while encoding its args),
|
||||
// they must be called sequentially. This can be accomplished by wrapping
|
||||
// the call in a lazy promise, so that the original function is only called
|
||||
// when the result is actually awaited.
|
||||
const useCacheFunctionInfo = getUseCacheFunctionInfo(exportForResult.$$original);
|
||||
if (useCacheFunctionInfo && useCacheFunctionInfo.usedArgs[1]) {
|
||||
const promise = new Promise((resolve)=>resolversAndResults.push(resolve));
|
||||
resolversAndResults.push(createLazyResult(async ()=>exportForResult(promise)));
|
||||
} else {
|
||||
let result;
|
||||
if (useCacheFunctionInfo) {
|
||||
resolversAndResults.push(noop);
|
||||
// @ts-expect-error We intentionally omit the parent argument, because
|
||||
// we know from the check above that the 'use cache' function does not
|
||||
// use it.
|
||||
result = exportForResult();
|
||||
} else {
|
||||
result = exportForResult(new Promise((resolve)=>resolversAndResults.push(resolve)));
|
||||
}
|
||||
resolversAndResults.push(result);
|
||||
if (result instanceof Promise) {
|
||||
// since we eager execute generateMetadata and
|
||||
// they can reject at anytime we need to ensure
|
||||
// we attach the catch handler right away to
|
||||
// prevent unhandled rejections crashing the process
|
||||
result.catch((err)=>{
|
||||
return {
|
||||
__nextError: err
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (typeof exportForResult === 'object') {
|
||||
resolversAndResults.push(exportForResult);
|
||||
} else {
|
||||
resolversAndResults.push(null);
|
||||
}
|
||||
}
|
||||
function freezeInDev(obj) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return require('../../shared/lib/deep-freeze').deepFreeze(obj);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
export async function accumulateMetadata(route, metadataItems, pathname, metadataContext) {
|
||||
let resolvedMetadata = createDefaultMetadata();
|
||||
let titleTemplates = {
|
||||
title: null,
|
||||
twitter: null,
|
||||
openGraph: null
|
||||
};
|
||||
const buildState = {
|
||||
warnings: new Set()
|
||||
};
|
||||
let favicon;
|
||||
// Collect the static icons in the most leaf node,
|
||||
// since we don't collect all the static metadata icons in the parent segments.
|
||||
const leafSegmentStaticIcons = {
|
||||
icon: [],
|
||||
apple: []
|
||||
};
|
||||
const resolversAndResults = prerenderMetadata(metadataItems);
|
||||
let resultIndex = 0;
|
||||
for(let i = 0; i < metadataItems.length; i++){
|
||||
var _staticFilesMetadata_icon;
|
||||
const staticFilesMetadata = metadataItems[i][1];
|
||||
// Treat favicon as special case, it should be the first icon in the list
|
||||
// i <= 1 represents root layout, and if current page is also at root
|
||||
if (i <= 1 && isFavicon(staticFilesMetadata == null ? void 0 : (_staticFilesMetadata_icon = staticFilesMetadata.icon) == null ? void 0 : _staticFilesMetadata_icon[0])) {
|
||||
var _staticFilesMetadata_icon1;
|
||||
const iconMod = staticFilesMetadata == null ? void 0 : (_staticFilesMetadata_icon1 = staticFilesMetadata.icon) == null ? void 0 : _staticFilesMetadata_icon1.shift();
|
||||
if (i === 0) favicon = iconMod;
|
||||
}
|
||||
let pendingMetadata = resolversAndResults[resultIndex++];
|
||||
if (typeof pendingMetadata === 'function') {
|
||||
// This metadata item had a `generateMetadata` and
|
||||
// we need to provide the currently resolved metadata
|
||||
// to it before we continue;
|
||||
const resolveParentMetadata = pendingMetadata;
|
||||
// we know that the next item is a result if this item
|
||||
// was a resolver
|
||||
pendingMetadata = resolversAndResults[resultIndex++];
|
||||
resolveParentMetadata(freezeInDev(resolvedMetadata));
|
||||
}
|
||||
// Otherwise the item was either null or a static export
|
||||
let metadata;
|
||||
if (isPromiseLike(pendingMetadata)) {
|
||||
metadata = await pendingMetadata;
|
||||
} else {
|
||||
metadata = pendingMetadata;
|
||||
}
|
||||
resolvedMetadata = await mergeMetadata(route, pathname, {
|
||||
resolvedMetadata,
|
||||
metadata,
|
||||
metadataContext,
|
||||
staticFilesMetadata,
|
||||
titleTemplates,
|
||||
buildState,
|
||||
leafSegmentStaticIcons
|
||||
});
|
||||
// If the layout is the same layer with page, skip the leaf layout and leaf page
|
||||
// The leaf layout and page are the last two items
|
||||
if (i < metadataItems.length - 2) {
|
||||
var _resolvedMetadata_title, _resolvedMetadata_openGraph, _resolvedMetadata_twitter;
|
||||
titleTemplates = {
|
||||
title: ((_resolvedMetadata_title = resolvedMetadata.title) == null ? void 0 : _resolvedMetadata_title.template) || null,
|
||||
openGraph: ((_resolvedMetadata_openGraph = resolvedMetadata.openGraph) == null ? void 0 : _resolvedMetadata_openGraph.title.template) || null,
|
||||
twitter: ((_resolvedMetadata_twitter = resolvedMetadata.twitter) == null ? void 0 : _resolvedMetadata_twitter.title.template) || null
|
||||
};
|
||||
}
|
||||
}
|
||||
if (leafSegmentStaticIcons.icon.length > 0 || leafSegmentStaticIcons.apple.length > 0) {
|
||||
if (!resolvedMetadata.icons) {
|
||||
resolvedMetadata.icons = {
|
||||
icon: [],
|
||||
apple: []
|
||||
};
|
||||
if (leafSegmentStaticIcons.icon.length > 0) {
|
||||
resolvedMetadata.icons.icon.unshift(...leafSegmentStaticIcons.icon);
|
||||
}
|
||||
if (leafSegmentStaticIcons.apple.length > 0) {
|
||||
resolvedMetadata.icons.apple.unshift(...leafSegmentStaticIcons.apple);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only log warnings if there are any, and only once after the metadata resolving process is finished
|
||||
if (buildState.warnings.size > 0) {
|
||||
for (const warning of buildState.warnings){
|
||||
Log.warn(warning);
|
||||
}
|
||||
}
|
||||
return postProcessMetadata(resolvedMetadata, favicon, titleTemplates, metadataContext);
|
||||
}
|
||||
export async function accumulateViewport(viewportItems) {
|
||||
let resolvedViewport = createDefaultViewport();
|
||||
const resolversAndResults = prerenderViewport(viewportItems);
|
||||
let i = 0;
|
||||
while(i < resolversAndResults.length){
|
||||
let pendingViewport = resolversAndResults[i++];
|
||||
if (typeof pendingViewport === 'function') {
|
||||
// this viewport item had a `generateViewport` and
|
||||
// we need to provide the currently resolved viewport
|
||||
// to it before we continue;
|
||||
const resolveParentViewport = pendingViewport;
|
||||
// we know that the next item is a result if this item
|
||||
// was a resolver
|
||||
pendingViewport = resolversAndResults[i++];
|
||||
resolveParentViewport(freezeInDev(resolvedViewport));
|
||||
}
|
||||
// Otherwise the item was either null or a static export
|
||||
let viewport;
|
||||
if (isPromiseLike(pendingViewport)) {
|
||||
viewport = await pendingViewport;
|
||||
} else {
|
||||
viewport = pendingViewport;
|
||||
}
|
||||
resolvedViewport = mergeViewport({
|
||||
resolvedViewport,
|
||||
viewport
|
||||
});
|
||||
}
|
||||
return resolvedViewport;
|
||||
}
|
||||
// Exposed API for metadata component, that directly resolve the loader tree and related context as resolved metadata.
|
||||
export async function resolveMetadata(tree, pathname, searchParams, errorConvention, interpolatedParams, metadataContext, isRuntimePrefetchable) {
|
||||
const metadataItems = await resolveMetadataItems(tree, searchParams, errorConvention, interpolatedParams, isRuntimePrefetchable);
|
||||
const workStore = workAsyncStorage.getStore();
|
||||
if (!workStore) {
|
||||
throw Object.defineProperty(new InvariantError('Expected workStore to be initialized'), "__NEXT_ERROR_CODE", {
|
||||
value: "E1068",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
return accumulateMetadata(workStore.route, metadataItems, pathname, metadataContext);
|
||||
}
|
||||
// Exposed API for viewport component, that directly resolve the loader tree and related context as resolved viewport.
|
||||
export async function resolveViewport(tree, searchParams, errorConvention, interpolatedParams, isRuntimePrefetchable) {
|
||||
const viewportItems = await resolveViewportItems(tree, searchParams, errorConvention, interpolatedParams, isRuntimePrefetchable);
|
||||
return accumulateViewport(viewportItems);
|
||||
}
|
||||
function isPromiseLike(value) {
|
||||
return typeof value === 'object' && value !== null && typeof value.then === 'function';
|
||||
}
|
||||
|
||||
//# sourceMappingURL=resolve-metadata.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+182
@@ -0,0 +1,182 @@
|
||||
import { resolveAsArrayOrUndefined } from '../generate/utils';
|
||||
import { resolveAbsoluteUrlWithPathname } from './resolve-url';
|
||||
function resolveAlternateUrl(url, metadataBase, pathname, metadataContext) {
|
||||
// If alter native url is an URL instance,
|
||||
// we treat it as a URL base and resolve with current pathname
|
||||
if (url instanceof URL) {
|
||||
const newUrl = new URL(pathname, url);
|
||||
url.searchParams.forEach((value, key)=>newUrl.searchParams.set(key, value));
|
||||
url = newUrl;
|
||||
}
|
||||
return resolveAbsoluteUrlWithPathname(url, metadataBase, pathname, metadataContext);
|
||||
}
|
||||
export const resolveThemeColor = (themeColor)=>{
|
||||
var _resolveAsArrayOrUndefined;
|
||||
if (!themeColor) return null;
|
||||
const themeColorDescriptors = [];
|
||||
(_resolveAsArrayOrUndefined = resolveAsArrayOrUndefined(themeColor)) == null ? void 0 : _resolveAsArrayOrUndefined.forEach((descriptor)=>{
|
||||
if (typeof descriptor === 'string') themeColorDescriptors.push({
|
||||
color: descriptor
|
||||
});
|
||||
else if (typeof descriptor === 'object') themeColorDescriptors.push({
|
||||
color: descriptor.color,
|
||||
media: descriptor.media
|
||||
});
|
||||
});
|
||||
return themeColorDescriptors;
|
||||
};
|
||||
async function resolveUrlValuesOfObject(obj, metadataBase, pathname, metadataContext) {
|
||||
if (!obj) return null;
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(obj)){
|
||||
if (typeof value === 'string' || value instanceof URL) {
|
||||
const pathnameForUrl = await pathname;
|
||||
result[key] = [
|
||||
{
|
||||
url: resolveAlternateUrl(value, metadataBase, pathnameForUrl, metadataContext)
|
||||
}
|
||||
];
|
||||
} else if (value && value.length) {
|
||||
result[key] = [];
|
||||
const pathnameForUrl = await pathname;
|
||||
value.forEach((item, index)=>{
|
||||
const url = resolveAlternateUrl(item.url, metadataBase, pathnameForUrl, metadataContext);
|
||||
result[key][index] = {
|
||||
url,
|
||||
title: item.title
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async function resolveCanonicalUrl(urlOrDescriptor, metadataBase, pathname, metadataContext) {
|
||||
if (!urlOrDescriptor) return null;
|
||||
const url = typeof urlOrDescriptor === 'string' || urlOrDescriptor instanceof URL ? urlOrDescriptor : urlOrDescriptor.url;
|
||||
const pathnameForUrl = await pathname;
|
||||
// Return string url because structureClone can't handle URL instance
|
||||
return {
|
||||
url: resolveAlternateUrl(url, metadataBase, pathnameForUrl, metadataContext)
|
||||
};
|
||||
}
|
||||
export const resolveAlternates = async (alternates, metadataBase, pathname, context)=>{
|
||||
if (!alternates) return null;
|
||||
const canonical = await resolveCanonicalUrl(alternates.canonical, metadataBase, pathname, context);
|
||||
const languages = await resolveUrlValuesOfObject(alternates.languages, metadataBase, pathname, context);
|
||||
const media = await resolveUrlValuesOfObject(alternates.media, metadataBase, pathname, context);
|
||||
const types = await resolveUrlValuesOfObject(alternates.types, metadataBase, pathname, context);
|
||||
return {
|
||||
canonical,
|
||||
languages,
|
||||
media,
|
||||
types
|
||||
};
|
||||
};
|
||||
const robotsKeys = [
|
||||
'noarchive',
|
||||
'nosnippet',
|
||||
'noimageindex',
|
||||
'nocache',
|
||||
'notranslate',
|
||||
'indexifembedded',
|
||||
'nositelinkssearchbox',
|
||||
'unavailable_after',
|
||||
'max-video-preview',
|
||||
'max-image-preview',
|
||||
'max-snippet'
|
||||
];
|
||||
const resolveRobotsValue = (robots)=>{
|
||||
if (!robots) return null;
|
||||
if (typeof robots === 'string') return robots;
|
||||
const values = [];
|
||||
if (robots.index) values.push('index');
|
||||
else if (typeof robots.index === 'boolean') values.push('noindex');
|
||||
if (robots.follow) values.push('follow');
|
||||
else if (typeof robots.follow === 'boolean') values.push('nofollow');
|
||||
for (const key of robotsKeys){
|
||||
const value = robots[key];
|
||||
if (typeof value !== 'undefined' && value !== false) {
|
||||
values.push(typeof value === 'boolean' ? key : `${key}:${value}`);
|
||||
}
|
||||
}
|
||||
return values.join(', ');
|
||||
};
|
||||
export const resolveRobots = (robots)=>{
|
||||
if (!robots) return null;
|
||||
return {
|
||||
basic: resolveRobotsValue(robots),
|
||||
googleBot: typeof robots !== 'string' ? resolveRobotsValue(robots.googleBot) : null
|
||||
};
|
||||
};
|
||||
const VerificationKeys = [
|
||||
'google',
|
||||
'yahoo',
|
||||
'yandex',
|
||||
'me',
|
||||
'other'
|
||||
];
|
||||
export const resolveVerification = (verification)=>{
|
||||
if (!verification) return null;
|
||||
const res = {};
|
||||
for (const key of VerificationKeys){
|
||||
const value = verification[key];
|
||||
if (value) {
|
||||
if (key === 'other') {
|
||||
res.other = {};
|
||||
for(const otherKey in verification.other){
|
||||
const otherValue = resolveAsArrayOrUndefined(verification.other[otherKey]);
|
||||
if (otherValue) res.other[otherKey] = otherValue;
|
||||
}
|
||||
} else res[key] = resolveAsArrayOrUndefined(value);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
export const resolveAppleWebApp = (appWebApp)=>{
|
||||
var _resolveAsArrayOrUndefined;
|
||||
if (!appWebApp) return null;
|
||||
if (appWebApp === true) {
|
||||
return {
|
||||
capable: true
|
||||
};
|
||||
}
|
||||
const startupImages = appWebApp.startupImage ? (_resolveAsArrayOrUndefined = resolveAsArrayOrUndefined(appWebApp.startupImage)) == null ? void 0 : _resolveAsArrayOrUndefined.map((item)=>typeof item === 'string' ? {
|
||||
url: item
|
||||
} : item) : null;
|
||||
return {
|
||||
capable: 'capable' in appWebApp ? !!appWebApp.capable : true,
|
||||
title: appWebApp.title || null,
|
||||
startupImage: startupImages,
|
||||
statusBarStyle: appWebApp.statusBarStyle || 'default'
|
||||
};
|
||||
};
|
||||
export const resolveAppLinks = (appLinks)=>{
|
||||
if (!appLinks) return null;
|
||||
for(const key in appLinks){
|
||||
// @ts-ignore // TODO: type infer
|
||||
appLinks[key] = resolveAsArrayOrUndefined(appLinks[key]);
|
||||
}
|
||||
return appLinks;
|
||||
};
|
||||
export const resolveItunes = async (itunes, metadataBase, pathname, context)=>{
|
||||
if (!itunes) return null;
|
||||
return {
|
||||
appId: itunes.appId,
|
||||
appArgument: itunes.appArgument ? resolveAlternateUrl(itunes.appArgument, metadataBase, await pathname, context) : undefined
|
||||
};
|
||||
};
|
||||
export const resolveFacebook = (facebook)=>{
|
||||
if (!facebook) return null;
|
||||
return {
|
||||
appId: facebook.appId,
|
||||
admins: resolveAsArrayOrUndefined(facebook.admins)
|
||||
};
|
||||
};
|
||||
export const resolvePagination = async (pagination, metadataBase, pathname, context)=>{
|
||||
return {
|
||||
previous: (pagination == null ? void 0 : pagination.previous) ? resolveAlternateUrl(pagination.previous, metadataBase, await pathname, context) : null,
|
||||
next: (pagination == null ? void 0 : pagination.next) ? resolveAlternateUrl(pagination.next, metadataBase, await pathname, context) : null
|
||||
};
|
||||
};
|
||||
|
||||
//# sourceMappingURL=resolve-basics.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+34
@@ -0,0 +1,34 @@
|
||||
import { resolveAsArrayOrUndefined } from '../generate/utils';
|
||||
import { isStringOrURL } from './resolve-url';
|
||||
import { IconKeys } from '../constants';
|
||||
export function resolveIcon(icon) {
|
||||
if (isStringOrURL(icon)) return {
|
||||
url: icon
|
||||
};
|
||||
else if (Array.isArray(icon)) return icon;
|
||||
return icon;
|
||||
}
|
||||
export const resolveIcons = (icons)=>{
|
||||
if (!icons) {
|
||||
return null;
|
||||
}
|
||||
const resolved = {
|
||||
icon: [],
|
||||
apple: []
|
||||
};
|
||||
if (Array.isArray(icons)) {
|
||||
resolved.icon = icons.map(resolveIcon).filter(Boolean);
|
||||
} else if (isStringOrURL(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;
|
||||
};
|
||||
|
||||
//# sourceMappingURL=resolve-icons.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/lib/metadata/resolvers/resolve-icons.ts"],"sourcesContent":["import type { ResolvedMetadataWithURLs } from '../types/metadata-interface'\nimport type { Icon, IconDescriptor } from '../types/metadata-types'\nimport type { FieldResolver } from '../types/resolvers'\nimport { resolveAsArrayOrUndefined } from '../generate/utils'\nimport { isStringOrURL } from './resolve-url'\nimport { IconKeys } from '../constants'\n\nexport function resolveIcon(icon: Icon): IconDescriptor {\n if (isStringOrURL(icon)) return { url: icon }\n else if (Array.isArray(icon)) return icon\n return icon\n}\n\nexport const resolveIcons: FieldResolver<'icons'> = (icons) => {\n if (!icons) {\n return null\n }\n\n const resolved: ResolvedMetadataWithURLs['icons'] = {\n icon: [],\n apple: [],\n }\n if (Array.isArray(icons)) {\n resolved.icon = icons.map(resolveIcon).filter(Boolean)\n } else if (isStringOrURL(icons)) {\n resolved.icon = [resolveIcon(icons)]\n } else {\n for (const key of IconKeys) {\n const values = resolveAsArrayOrUndefined(icons[key])\n if (values) resolved[key] = values.map(resolveIcon)\n }\n }\n return resolved\n}\n"],"names":["resolveAsArrayOrUndefined","isStringOrURL","IconKeys","resolveIcon","icon","url","Array","isArray","resolveIcons","icons","resolved","apple","map","filter","Boolean","key","values"],"mappings":"AAGA,SAASA,yBAAyB,QAAQ,oBAAmB;AAC7D,SAASC,aAAa,QAAQ,gBAAe;AAC7C,SAASC,QAAQ,QAAQ,eAAc;AAEvC,OAAO,SAASC,YAAYC,IAAU;IACpC,IAAIH,cAAcG,OAAO,OAAO;QAAEC,KAAKD;IAAK;SACvC,IAAIE,MAAMC,OAAO,CAACH,OAAO,OAAOA;IACrC,OAAOA;AACT;AAEA,OAAO,MAAMI,eAAuC,CAACC;IACnD,IAAI,CAACA,OAAO;QACV,OAAO;IACT;IAEA,MAAMC,WAA8C;QAClDN,MAAM,EAAE;QACRO,OAAO,EAAE;IACX;IACA,IAAIL,MAAMC,OAAO,CAACE,QAAQ;QACxBC,SAASN,IAAI,GAAGK,MAAMG,GAAG,CAACT,aAAaU,MAAM,CAACC;IAChD,OAAO,IAAIb,cAAcQ,QAAQ;QAC/BC,SAASN,IAAI,GAAG;YAACD,YAAYM;SAAO;IACtC,OAAO;QACL,KAAK,MAAMM,OAAOb,SAAU;YAC1B,MAAMc,SAAShB,0BAA0BS,KAAK,CAACM,IAAI;YACnD,IAAIC,QAAQN,QAAQ,CAACK,IAAI,GAAGC,OAAOJ,GAAG,CAACT;QACzC;IACF;IACA,OAAOO;AACT,EAAC","ignoreList":[0]}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import { resolveArray, resolveAsArrayOrUndefined } from '../generate/utils';
|
||||
import { getSocialImageMetadataBaseFallback, isStringOrURL, resolveUrl, resolveAbsoluteUrlWithPathname } from './resolve-url';
|
||||
import { resolveTitle } from './resolve-title';
|
||||
import { isFullStringUrl } from '../../url';
|
||||
import { warnOnce } from '../../../build/output/log';
|
||||
const OgTypeFields = {
|
||||
article: [
|
||||
'authors',
|
||||
'tags'
|
||||
],
|
||||
song: [
|
||||
'albums',
|
||||
'musicians'
|
||||
],
|
||||
playlist: [
|
||||
'albums',
|
||||
'musicians'
|
||||
],
|
||||
radio: [
|
||||
'creators'
|
||||
],
|
||||
video: [
|
||||
'actors',
|
||||
'directors',
|
||||
'writers',
|
||||
'tags'
|
||||
],
|
||||
basic: [
|
||||
'emails',
|
||||
'phoneNumbers',
|
||||
'faxNumbers',
|
||||
'alternateLocale',
|
||||
'audio',
|
||||
'videos'
|
||||
]
|
||||
};
|
||||
function resolveAndValidateImage(item, metadataBase, isStaticMetadataRouteFile) {
|
||||
if (!item) return undefined;
|
||||
const isItemUrl = isStringOrURL(item);
|
||||
const inputUrl = isItemUrl ? item : item.url;
|
||||
if (!inputUrl) return undefined;
|
||||
// process.env.VERCEL is set to "1" when System Environment Variables are
|
||||
// exposed. When exposed, validation is not necessary since we are falling back to
|
||||
// process.env.VERCEL_PROJECT_PRODUCTION_URL, process.env.VERCEL_BRANCH_URL, or
|
||||
// process.env.VERCEL_URL for the `metadataBase`. process.env.VERCEL is undefined
|
||||
// when System Environment Variables are not exposed. When not exposed, we cannot
|
||||
// detect in the build environment if the deployment is a Vercel deployment or not.
|
||||
//
|
||||
// x-ref: https://vercel.com/docs/projects/environment-variables/system-environment-variables#system-environment-variables
|
||||
const isUsingVercelSystemEnvironmentVariables = Boolean(process.env.VERCEL);
|
||||
const isRelativeUrl = typeof inputUrl === 'string' && !isFullStringUrl(inputUrl);
|
||||
// When no explicit metadataBase is specified by the user, we'll override it with the fallback metadata
|
||||
// under the following conditions:
|
||||
// - The provided URL is relative (ie ./og-image).
|
||||
// - The image is statically generated by Next.js (such as the special `opengraph-image` route)
|
||||
// In both cases, we want to ensure that across all environments, the ogImage is a fully qualified URL.
|
||||
// In the `opengraph-image` case, since the user isn't explicitly passing a relative path, this ensures
|
||||
// the ogImage will be properly discovered across different environments without the user needing to
|
||||
// have a bunch of `process.env` checks when defining their `metadataBase`.
|
||||
if (isRelativeUrl && (!metadataBase || isStaticMetadataRouteFile)) {
|
||||
const fallbackMetadataBase = getSocialImageMetadataBaseFallback(metadataBase);
|
||||
// When not using Vercel environment variables for URL injection, we aren't able to determine
|
||||
// a fallback value for `metadataBase`. For self-hosted setups, we want to warn
|
||||
// about this since the only fallback we'll be able to generate is `localhost`.
|
||||
// In development, we'll only warn for relative metadata that isn't part of the static
|
||||
// metadata conventions (eg `opengraph-image`), as otherwise it's currently very noisy
|
||||
// for common cases. Eventually we should remove this warning all together in favor of
|
||||
// devtools.
|
||||
const shouldWarn = !isUsingVercelSystemEnvironmentVariables && !metadataBase && (process.env.NODE_ENV === 'production' || !isStaticMetadataRouteFile);
|
||||
if (shouldWarn) {
|
||||
warnOnce(`metadataBase property in metadata export is not set for resolving social open graph or twitter images, using "${fallbackMetadataBase.origin}". See https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadatabase`);
|
||||
}
|
||||
metadataBase = fallbackMetadataBase;
|
||||
}
|
||||
return isItemUrl ? {
|
||||
url: resolveUrl(inputUrl, metadataBase)
|
||||
} : {
|
||||
...item,
|
||||
// Update image descriptor url
|
||||
url: resolveUrl(inputUrl, metadataBase)
|
||||
};
|
||||
}
|
||||
export function resolveImages(images, metadataBase, isStaticMetadataRouteFile) {
|
||||
const resolvedImages = resolveAsArrayOrUndefined(images);
|
||||
if (!resolvedImages) return resolvedImages;
|
||||
const nonNullableImages = [];
|
||||
for (const item of resolvedImages){
|
||||
const resolvedItem = resolveAndValidateImage(item, metadataBase, isStaticMetadataRouteFile);
|
||||
if (!resolvedItem) continue;
|
||||
nonNullableImages.push(resolvedItem);
|
||||
}
|
||||
return nonNullableImages;
|
||||
}
|
||||
const ogTypeToFields = {
|
||||
article: OgTypeFields.article,
|
||||
book: OgTypeFields.article,
|
||||
'music.song': OgTypeFields.song,
|
||||
'music.album': OgTypeFields.song,
|
||||
'music.playlist': OgTypeFields.playlist,
|
||||
'music.radio_station': OgTypeFields.radio,
|
||||
'video.movie': OgTypeFields.video,
|
||||
'video.episode': OgTypeFields.video
|
||||
};
|
||||
function getFieldsByOgType(ogType) {
|
||||
if (!ogType || !(ogType in ogTypeToFields)) return OgTypeFields.basic;
|
||||
return ogTypeToFields[ogType].concat(OgTypeFields.basic);
|
||||
}
|
||||
export const resolveOpenGraph = async (openGraph, metadataBase, pathname, metadataContext, titleTemplate)=>{
|
||||
if (!openGraph) return null;
|
||||
function resolveProps(target, og) {
|
||||
const ogType = og && 'type' in og ? og.type : undefined;
|
||||
const keys = getFieldsByOgType(ogType);
|
||||
for (const k of keys){
|
||||
const key = k;
|
||||
if (key in og && key !== 'url') {
|
||||
const value = og[key];
|
||||
target[key] = value ? resolveArray(value) : null;
|
||||
}
|
||||
}
|
||||
target.images = resolveImages(og.images, metadataBase, metadataContext.isStaticMetadataRouteFile);
|
||||
}
|
||||
const resolved = {
|
||||
...openGraph,
|
||||
title: resolveTitle(openGraph.title, titleTemplate)
|
||||
};
|
||||
resolveProps(resolved, openGraph);
|
||||
resolved.url = openGraph.url ? resolveAbsoluteUrlWithPathname(openGraph.url, metadataBase, await pathname, metadataContext) : null;
|
||||
return resolved;
|
||||
};
|
||||
const TwitterBasicInfoKeys = [
|
||||
'site',
|
||||
'siteId',
|
||||
'creator',
|
||||
'creatorId',
|
||||
'description'
|
||||
];
|
||||
export const resolveTwitter = (twitter, metadataBase, metadataContext, titleTemplate)=>{
|
||||
var _resolved_images;
|
||||
if (!twitter) return null;
|
||||
let card = 'card' in twitter ? twitter.card : undefined;
|
||||
const resolved = {
|
||||
...twitter,
|
||||
title: resolveTitle(twitter.title, titleTemplate)
|
||||
};
|
||||
for (const infoKey of TwitterBasicInfoKeys){
|
||||
resolved[infoKey] = twitter[infoKey] || null;
|
||||
}
|
||||
resolved.images = resolveImages(twitter.images, metadataBase, metadataContext.isStaticMetadataRouteFile);
|
||||
card = card || (((_resolved_images = resolved.images) == null ? void 0 : _resolved_images.length) ? 'summary_large_image' : 'summary');
|
||||
resolved.card = card;
|
||||
if ('card' in resolved) {
|
||||
switch(resolved.card){
|
||||
case 'player':
|
||||
{
|
||||
resolved.players = resolveAsArrayOrUndefined(resolved.players) || [];
|
||||
break;
|
||||
}
|
||||
case 'app':
|
||||
{
|
||||
resolved.app = resolved.app || {};
|
||||
break;
|
||||
}
|
||||
case 'summary':
|
||||
case 'summary_large_image':
|
||||
break;
|
||||
default:
|
||||
resolved;
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
};
|
||||
|
||||
//# sourceMappingURL=resolve-opengraph.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+30
@@ -0,0 +1,30 @@
|
||||
function resolveTitleTemplate(template, title) {
|
||||
return template ? template.replace(/%s/g, title) : title;
|
||||
}
|
||||
export function resolveTitle(title, stashedTemplate) {
|
||||
let resolved;
|
||||
const template = typeof title !== 'string' && title && 'template' in title ? title.template : null;
|
||||
if (typeof title === 'string') {
|
||||
resolved = resolveTitleTemplate(stashedTemplate, title);
|
||||
} else if (title) {
|
||||
if ('default' in title) {
|
||||
resolved = resolveTitleTemplate(stashedTemplate, title.default);
|
||||
}
|
||||
if ('absolute' in title && title.absolute) {
|
||||
resolved = title.absolute;
|
||||
}
|
||||
}
|
||||
if (title && typeof title !== 'string') {
|
||||
return {
|
||||
template,
|
||||
absolute: resolved || ''
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
absolute: resolved || title || '',
|
||||
template
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=resolve-title.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/lib/metadata/resolvers/resolve-title.ts"],"sourcesContent":["import type { Metadata } from '../types/metadata-interface'\nimport type { AbsoluteTemplateString } from '../types/metadata-types'\n\nfunction resolveTitleTemplate(\n template: string | null | undefined,\n title: string\n) {\n return template ? template.replace(/%s/g, title) : title\n}\n\nexport function resolveTitle(\n title: Metadata['title'],\n stashedTemplate: string | null | undefined\n): AbsoluteTemplateString {\n let resolved\n const template =\n typeof title !== 'string' && title && 'template' in title\n ? title.template\n : null\n\n if (typeof title === 'string') {\n resolved = resolveTitleTemplate(stashedTemplate, title)\n } else if (title) {\n if ('default' in title) {\n resolved = resolveTitleTemplate(stashedTemplate, title.default)\n }\n if ('absolute' in title && title.absolute) {\n resolved = title.absolute\n }\n }\n\n if (title && typeof title !== 'string') {\n return {\n template,\n absolute: resolved || '',\n }\n } else {\n return { absolute: resolved || title || '', template }\n }\n}\n"],"names":["resolveTitleTemplate","template","title","replace","resolveTitle","stashedTemplate","resolved","default","absolute"],"mappings":"AAGA,SAASA,qBACPC,QAAmC,EACnCC,KAAa;IAEb,OAAOD,WAAWA,SAASE,OAAO,CAAC,OAAOD,SAASA;AACrD;AAEA,OAAO,SAASE,aACdF,KAAwB,EACxBG,eAA0C;IAE1C,IAAIC;IACJ,MAAML,WACJ,OAAOC,UAAU,YAAYA,SAAS,cAAcA,QAChDA,MAAMD,QAAQ,GACd;IAEN,IAAI,OAAOC,UAAU,UAAU;QAC7BI,WAAWN,qBAAqBK,iBAAiBH;IACnD,OAAO,IAAIA,OAAO;QAChB,IAAI,aAAaA,OAAO;YACtBI,WAAWN,qBAAqBK,iBAAiBH,MAAMK,OAAO;QAChE;QACA,IAAI,cAAcL,SAASA,MAAMM,QAAQ,EAAE;YACzCF,WAAWJ,MAAMM,QAAQ;QAC3B;IACF;IAEA,IAAIN,SAAS,OAAOA,UAAU,UAAU;QACtC,OAAO;YACLD;YACAO,UAAUF,YAAY;QACxB;IACF,OAAO;QACL,OAAO;YAAEE,UAAUF,YAAYJ,SAAS;YAAID;QAAS;IACvD;AACF","ignoreList":[0]}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import path from '../../../shared/lib/isomorphic/path';
|
||||
function isStringOrURL(icon) {
|
||||
return typeof icon === 'string' || icon instanceof URL;
|
||||
}
|
||||
function createLocalMetadataBase() {
|
||||
// Check if experimental HTTPS is enabled
|
||||
const isExperimentalHttps = Boolean(process.env.__NEXT_EXPERIMENTAL_HTTPS);
|
||||
const protocol = isExperimentalHttps ? 'https' : 'http';
|
||||
return new URL(`${protocol}://localhost:${process.env.PORT || 3000}`);
|
||||
}
|
||||
function getPreviewDeploymentUrl() {
|
||||
const origin = process.env.VERCEL_BRANCH_URL || process.env.VERCEL_URL;
|
||||
return origin ? new URL(`https://${origin}`) : undefined;
|
||||
}
|
||||
function getProductionDeploymentUrl() {
|
||||
const origin = process.env.VERCEL_PROJECT_PRODUCTION_URL;
|
||||
return origin ? new URL(`https://${origin}`) : undefined;
|
||||
}
|
||||
/**
|
||||
* Given an optional user-provided metadataBase, this determines what the metadataBase should
|
||||
* fallback to. Specifically:
|
||||
* - In dev, it should always be localhost
|
||||
* - In Vercel preview builds, it should be the preview build ID
|
||||
* - In start, it should be the user-provided metadataBase value. Otherwise,
|
||||
* it'll fall back to the Vercel production deployment, and localhost as a last resort.
|
||||
*/ export function getSocialImageMetadataBaseFallback(metadataBase) {
|
||||
const defaultMetadataBase = createLocalMetadataBase();
|
||||
const previewDeploymentUrl = getPreviewDeploymentUrl();
|
||||
const productionDeploymentUrl = getProductionDeploymentUrl();
|
||||
let fallbackMetadataBase;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
fallbackMetadataBase = defaultMetadataBase;
|
||||
} else {
|
||||
fallbackMetadataBase = process.env.NODE_ENV === 'production' && previewDeploymentUrl && process.env.VERCEL_ENV === 'preview' ? previewDeploymentUrl : metadataBase || productionDeploymentUrl || defaultMetadataBase;
|
||||
}
|
||||
return fallbackMetadataBase;
|
||||
}
|
||||
function resolveUrl(url, metadataBase) {
|
||||
if (url instanceof URL) return url;
|
||||
if (!url) return null;
|
||||
try {
|
||||
// If we can construct a URL instance from url, ignore metadataBase
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl;
|
||||
} catch {}
|
||||
if (!metadataBase) {
|
||||
metadataBase = createLocalMetadataBase();
|
||||
}
|
||||
// Handle relative or absolute paths
|
||||
const pathname = metadataBase.pathname || '';
|
||||
const joinedPath = path.posix.join(pathname, url);
|
||||
return new URL(joinedPath, metadataBase);
|
||||
}
|
||||
// Resolve with `pathname` if `url` is a relative path.
|
||||
function resolveRelativeUrl(url, pathname) {
|
||||
if (typeof url === 'string' && url.startsWith('./')) {
|
||||
return path.posix.resolve(pathname, url);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
// The regex is matching logic from packages/next/src/lib/load-custom-routes.ts
|
||||
const FILE_REGEX = /^(?:\/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+))(\/?|$)/i;
|
||||
function isFilePattern(pathname) {
|
||||
return FILE_REGEX.test(pathname);
|
||||
}
|
||||
// Resolve `pathname` if `url` is a relative path the compose with `metadataBase`.
|
||||
function resolveAbsoluteUrlWithPathname(url, metadataBase, pathname, { trailingSlash }) {
|
||||
// Resolve url with pathname that always starts with `/`
|
||||
url = resolveRelativeUrl(url, pathname);
|
||||
// Convert string url or URL instance to absolute url string,
|
||||
// if there's case needs to be resolved with metadataBase
|
||||
let resolvedUrl = '';
|
||||
const result = metadataBase ? resolveUrl(url, metadataBase) : url;
|
||||
if (typeof result === 'string') {
|
||||
resolvedUrl = result;
|
||||
} else {
|
||||
resolvedUrl = result.pathname === '/' && result.searchParams.size === 0 ? result.origin : result.href;
|
||||
}
|
||||
// Add trailing slash if it's enabled for urls matches the condition
|
||||
// - Not external, same origin with metadataBase
|
||||
// - Doesn't have query
|
||||
if (trailingSlash && !resolvedUrl.endsWith('/')) {
|
||||
let isRelative = resolvedUrl.startsWith('/');
|
||||
let hasQuery = resolvedUrl.includes('?');
|
||||
let isExternal = false;
|
||||
let isFileUrl = false;
|
||||
if (!isRelative) {
|
||||
try {
|
||||
const parsedUrl = new URL(resolvedUrl);
|
||||
isExternal = metadataBase != null && parsedUrl.origin !== metadataBase.origin;
|
||||
isFileUrl = isFilePattern(parsedUrl.pathname);
|
||||
} catch {
|
||||
// If it's not a valid URL, treat it as external
|
||||
isExternal = true;
|
||||
}
|
||||
if (// Do not apply trailing slash for file like urls, aligning with the behavior with `trailingSlash`
|
||||
!isFileUrl && !isExternal && !hasQuery) return `${resolvedUrl}/`;
|
||||
}
|
||||
}
|
||||
return resolvedUrl;
|
||||
}
|
||||
export { isStringOrURL, resolveUrl, resolveRelativeUrl, resolveAbsoluteUrlWithPathname, };
|
||||
|
||||
//# sourceMappingURL=resolve-url.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+4
@@ -0,0 +1,4 @@
|
||||
// Reference: https://hreflang.org/what-is-a-valid-hreflang
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=alternative-urls-types.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+13
@@ -0,0 +1,13 @@
|
||||
// When rendering applink meta tags add a namespace tag before each array instance
|
||||
// if more than one member exists.
|
||||
// ref: https://developers.facebook.com/docs/applinks/metadata-reference
|
||||
// Format Detection
|
||||
// This is a poorly specified metadata export type that is supposed to
|
||||
// control whether the device attempts to conver text that matches
|
||||
// certain formats into links for action. The most supported example
|
||||
// is how mobile devices detect phone numbers and make them into links
|
||||
// that can initiate a phone call
|
||||
// https://www.goodemailcode.com/email-code/template.html
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=extra-types.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/lib/metadata/types/extra-types.ts"],"sourcesContent":["// When rendering applink meta tags add a namespace tag before each array instance\n// if more than one member exists.\n// ref: https://developers.facebook.com/docs/applinks/metadata-reference\n\nexport type AppLinks = {\n ios?: AppLinksApple | Array<AppLinksApple> | undefined\n iphone?: AppLinksApple | Array<AppLinksApple> | undefined\n ipad?: AppLinksApple | Array<AppLinksApple> | undefined\n android?: AppLinksAndroid | Array<AppLinksAndroid> | undefined\n windows_phone?: AppLinksWindows | Array<AppLinksWindows> | undefined\n windows?: AppLinksWindows | Array<AppLinksWindows> | undefined\n windows_universal?: AppLinksWindows | Array<AppLinksWindows> | undefined\n web?: AppLinksWeb | Array<AppLinksWeb> | undefined\n}\nexport type ResolvedAppLinks = {\n ios?: Array<AppLinksApple> | undefined\n iphone?: Array<AppLinksApple> | undefined\n ipad?: Array<AppLinksApple> | undefined\n android?: Array<AppLinksAndroid> | undefined\n windows_phone?: Array<AppLinksWindows> | undefined\n windows?: Array<AppLinksWindows> | undefined\n windows_universal?: Array<AppLinksWindows> | undefined\n web?: Array<AppLinksWeb> | undefined\n}\nexport type AppLinksApple = {\n url: string | URL\n app_store_id?: string | number | undefined\n app_name?: string | undefined\n}\nexport type AppLinksAndroid = {\n package: string\n url?: string | URL | undefined\n class?: string | undefined\n app_name?: string | undefined\n}\nexport type AppLinksWindows = {\n url: string | URL\n app_id?: string | undefined\n app_name?: string | undefined\n}\nexport type AppLinksWeb = {\n url: string | URL\n should_fallback?: boolean | undefined\n}\n\n// Apple Itunes APp\n// https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners\nexport type ItunesApp = {\n appId: string\n appArgument?: string | undefined\n}\n\n// Viewport meta structure\n// https://developer.mozilla.org/docs/Web/HTML/Viewport_meta_tag\n// intentionally leaving out user-scalable, use a string if you want that behavior\nexport type ViewportLayout = {\n width?: string | number | undefined\n height?: string | number | undefined\n initialScale?: number | undefined\n minimumScale?: number | undefined\n maximumScale?: number | undefined\n userScalable?: boolean | undefined\n viewportFit?: 'auto' | 'cover' | 'contain' | undefined\n interactiveWidget?:\n | 'resizes-visual'\n | 'resizes-content'\n | 'overlays-content'\n | undefined\n}\n\n// Apple Web App\n// https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html\n// https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html\nexport type AppleWebApp = {\n // default true\n capable?: boolean | undefined\n title?: string | undefined\n startupImage?: AppleImage | Array<AppleImage> | undefined\n // default \"default\"\n statusBarStyle?: 'default' | 'black' | 'black-translucent' | undefined\n}\nexport type AppleImage = string | AppleImageDescriptor\nexport type AppleImageDescriptor = {\n url: string\n media?: string | undefined\n}\n\nexport type ResolvedAppleWebApp = {\n capable: boolean\n title?: string | null | undefined\n startupImage?: AppleImageDescriptor[] | null | undefined\n statusBarStyle?: 'default' | 'black' | 'black-translucent' | undefined\n}\n\nexport type Facebook = FacebookAppId | FacebookAdmins | ResolvedFacebook\nexport type FacebookAppId = {\n appId: string\n admins?: never | undefined\n}\nexport type FacebookAdmins = {\n appId?: never | undefined\n admins: string | string[]\n}\nexport type ResolvedFacebook = {\n appId?: string | undefined\n admins?: string[] | undefined\n}\n\nexport type Pinterest = PinterestRichPin | ResolvedPinterest\nexport type PinterestRichPin = {\n richPin: string | boolean\n}\n\nexport type ResolvedPinterest = {\n richPin?: string | boolean\n}\n\n// Format Detection\n// This is a poorly specified metadata export type that is supposed to\n// control whether the device attempts to conver text that matches\n// certain formats into links for action. The most supported example\n// is how mobile devices detect phone numbers and make them into links\n// that can initiate a phone call\n// https://www.goodemailcode.com/email-code/template.html\nexport type FormatDetection = {\n telephone?: boolean | undefined\n date?: boolean | undefined\n address?: boolean | undefined\n email?: boolean | undefined\n url?: boolean | undefined\n}\n"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,kCAAkC;AAClC,wEAAwE;AAmHxE,mBAAmB;AACnB,sEAAsE;AACtE,kEAAkE;AAClE,oEAAoE;AACpE,sEAAsE;AACtE,iCAAiC;AACjC,yDAAyD;AACzD,WAMC","ignoreList":[0]}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=icons.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/lib/metadata/types/icons.ts"],"sourcesContent":["export type StaticMetadata = {\n icon: any[] | undefined\n apple: any[] | undefined\n openGraph: any[] | undefined\n twitter: any[] | undefined\n manifest: string | undefined\n} | null\n"],"names":[],"mappings":"AAAA,WAMQ","ignoreList":[0]}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=manifest-types.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/lib/metadata/types/manifest-types.ts"],"sourcesContent":["type ClientModeEnum =\n | 'auto'\n | 'focus-existing'\n | 'navigate-existing'\n | 'navigate-new'\n\ntype File = {\n name: string\n accept: string | string[]\n}\n\ntype Icon = {\n src: string\n type?: string | undefined\n sizes?: string | undefined\n purpose?: 'any' | 'maskable' | 'monochrome' | undefined\n}\n\nexport type Manifest = {\n background_color?: string | undefined\n categories?: string[] | undefined\n description?: string | undefined\n dir?: 'ltr' | 'rtl' | 'auto' | undefined\n display?: 'fullscreen' | 'standalone' | 'minimal-ui' | 'browser' | undefined\n display_override?:\n | (\n | 'fullscreen'\n | 'standalone'\n | 'minimal-ui'\n | 'browser'\n | 'window-controls-overlay'\n )[]\n | undefined\n file_handlers?:\n | {\n action: string\n accept: {\n [mimeType: string]: string[]\n }\n }[]\n | undefined\n icons?: Icon[] | undefined\n id?: string | undefined\n lang?: string | undefined\n launch_handler?:\n | {\n client_mode: ClientModeEnum | ClientModeEnum[]\n }\n | undefined\n name?: string | undefined\n orientation?:\n | 'any'\n | 'natural'\n | 'landscape'\n | 'portrait'\n | 'portrait-primary'\n | 'portrait-secondary'\n | 'landscape-primary'\n | 'landscape-secondary'\n | undefined\n prefer_related_applications?: boolean | undefined\n protocol_handlers?:\n | {\n protocol: string\n url: string\n }[]\n | undefined\n related_applications?:\n | {\n platform: string\n url: string\n id?: string | undefined\n }[]\n | undefined\n scope?: string | undefined\n screenshots?:\n | {\n form_factor?: 'narrow' | 'wide' | undefined\n label?: string | undefined\n platform?:\n | 'android'\n | 'chromeos'\n | 'ipados'\n | 'ios'\n | 'kaios'\n | 'macos'\n | 'windows'\n | 'xbox'\n | 'chrome_web_store'\n | 'itunes'\n | 'microsoft-inbox'\n | 'microsoft-store'\n | 'play'\n | undefined\n src: string\n type?: string | undefined\n sizes?: string | undefined\n }[]\n | undefined\n share_target?:\n | {\n action: string\n method?: 'get' | 'post' | 'GET' | 'POST' | undefined\n enctype?:\n | 'application/x-www-form-urlencoded'\n | 'multipart/form-data'\n | undefined\n params: {\n title?: string | undefined\n text?: string | undefined\n url?: string | undefined\n files?: File | File[] | undefined\n }\n }\n | undefined\n short_name?: string | undefined\n shortcuts?:\n | {\n name: string\n short_name?: string | undefined\n description?: string | undefined\n url: string\n icons?: Icon[] | undefined\n }[]\n | undefined\n start_url?: string | undefined\n theme_color?: string | undefined\n}\n"],"names":[],"mappings":"AAkBA,WA6GC","ignoreList":[0]}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Next.js Metadata API
|
||||
*
|
||||
* This file defines the types used by Next.js to configure metadata
|
||||
* through static exports or dynamic `generateMetadata` functions in Server Components.
|
||||
*
|
||||
* @remarks
|
||||
* - The static `metadata` object and `generateMetadata` function are only supported in Server Components.
|
||||
* - Do not export both a `metadata` object and a `generateMetadata` function from the same route segment.
|
||||
* - You can still render metadata in client components directly as part of the component's JSX.
|
||||
*
|
||||
* @see https://nextjs.org/docs/app/api-reference/metadata
|
||||
*/ export { };
|
||||
|
||||
//# sourceMappingURL=metadata-interface.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+7
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
*
|
||||
* Metadata types
|
||||
*
|
||||
*/ export { };
|
||||
|
||||
//# sourceMappingURL=metadata-types.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+3
@@ -0,0 +1,3 @@
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=opengraph-types.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+3
@@ -0,0 +1,3 @@
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=resolvers.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/lib/metadata/types/resolvers.ts"],"sourcesContent":["import type { Metadata, ResolvedMetadataWithURLs } from './metadata-interface'\n\nexport type FieldResolver<\n Key extends keyof Data & keyof ResolvedData,\n Data = Metadata,\n ResolvedData = ResolvedMetadataWithURLs,\n> = (T: Data[Key]) => ResolvedData[Key]\n\nexport type FieldResolverExtraArgs<\n Key extends keyof Data & keyof ResolvedData,\n ExtraArgs extends unknown[] = any[],\n Data = Metadata,\n ResolvedData = ResolvedMetadataWithURLs,\n> = (T: Data[Key], ...args: ExtraArgs) => ResolvedData[Key]\n\nexport type AsyncFieldResolverExtraArgs<\n Key extends keyof Data & keyof ResolvedData,\n ExtraArgs extends unknown[] = any[],\n Data = Metadata,\n ResolvedData = ResolvedMetadataWithURLs,\n> = (T: Data[Key], ...args: ExtraArgs) => Promise<ResolvedData[Key]>\n\nexport type MetadataContext = {\n trailingSlash: boolean\n isStaticMetadataRouteFile: boolean\n}\n"],"names":[],"mappings":"AAsBA,WAGC","ignoreList":[0]}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// Reference: https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=twitter-types.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/lib/metadata/types/twitter-types.ts"],"sourcesContent":["// Reference: https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup\n\nimport type { AbsoluteTemplateString, TemplateString } from './metadata-types'\n\nexport type Twitter =\n | TwitterSummary\n | TwitterSummaryLargeImage\n | TwitterPlayer\n | TwitterApp\n | TwitterMetadata\n\ntype TwitterMetadata = {\n // defaults to card=\"summary\"\n site?: null | string | undefined // username for account associated to the site itself\n siteId?: null | string | undefined // id for account associated to the site itself\n creator?: null | string | undefined // username for the account associated to the creator of the content on the site\n creatorId?: null | string | undefined // id for the account associated to the creator of the content on the site\n description?: null | string | undefined\n title?: string | TemplateString | undefined\n images?: TwitterImage | Array<TwitterImage> | undefined\n}\ntype TwitterSummary = TwitterMetadata & {\n card: 'summary'\n}\ntype TwitterSummaryLargeImage = TwitterMetadata & {\n card: 'summary_large_image'\n}\ntype TwitterPlayer = TwitterMetadata & {\n card: 'player'\n players: TwitterPlayerDescriptor | Array<TwitterPlayerDescriptor>\n}\ntype TwitterApp = TwitterMetadata & {\n card: 'app'\n app: TwitterAppDescriptor\n}\nexport type TwitterAppDescriptor = {\n id: {\n iphone?: string | number | undefined\n ipad?: string | number | undefined\n googleplay?: string | undefined\n }\n url?:\n | {\n iphone?: string | URL | undefined\n ipad?: string | URL | undefined\n googleplay?: string | URL | undefined\n }\n | undefined\n name?: string | undefined\n}\n\ntype TwitterImage = string | TwitterImageDescriptor | URL\ntype TwitterImageDescriptor = {\n url: string | URL\n alt?: string | undefined\n secureUrl?: string | URL | undefined\n type?: string | undefined\n width?: string | number | undefined\n height?: string | number | undefined\n}\ntype TwitterPlayerDescriptor = {\n playerUrl: string | URL\n streamUrl: string | URL\n width: number\n height: number\n}\n\ntype ResolvedTwitterImage = {\n url: string | URL\n alt?: string | undefined\n secureUrl?: string | URL | undefined\n type?: string | undefined\n width?: string | number | undefined\n height?: string | number | undefined\n}\ntype ResolvedTwitterSummary = {\n site: string | null\n siteId: string | null\n creator: string | null\n creatorId: string | null\n description: string | null\n title: AbsoluteTemplateString\n images?: Array<ResolvedTwitterImage> | undefined\n}\ntype ResolvedTwitterPlayer = ResolvedTwitterSummary & {\n players: Array<TwitterPlayerDescriptor>\n}\ntype ResolvedTwitterApp = ResolvedTwitterSummary & { app: TwitterAppDescriptor }\n\nexport type ResolvedTwitterMetadata =\n | ({ card: 'summary' } & ResolvedTwitterSummary)\n | ({ card: 'summary_large_image' } & ResolvedTwitterSummary)\n | ({ card: 'player' } & ResolvedTwitterPlayer)\n | ({ card: 'app' } & ResolvedTwitterApp)\n"],"names":[],"mappings":"AAAA,8FAA8F;AAyF9F,WAI0C","ignoreList":[0]}
|
||||
Reference in New Issue
Block a user