UNPKG

next

Version:

The React Framework

214 lines (213 loc) • 8.76 kB
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