@wular/pnext 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -0
- package/bin/pnext +67 -0
- package/config/lint/base.js +48 -0
- package/config/ts/base.json +26 -0
- package/config/ts/react.json +13 -0
- package/package.json +70 -0
- package/reference/compat.md +63 -0
- package/reference/config.md +120 -0
- package/reference/css.md +58 -0
- package/reference/dev.md +69 -0
- package/reference/env.md +40 -0
- package/reference/metadata.md +86 -0
- package/reference/navigation.md +149 -0
- package/reference/overview.md +35 -0
- package/reference/performance.md +97 -0
- package/reference/rendering.md +127 -0
- package/reference/routing.md +167 -0
- package/reference/typegen.md +64 -0
- package/src/api/cache.ts +80 -0
- package/src/api/client-cache.ts +9 -0
- package/src/api/client-navigation.ts +279 -0
- package/src/api/dynamic.tsx +102 -0
- package/src/api/link.tsx +119 -0
- package/src/api/navigation.ts +198 -0
- package/src/api/router/events.ts +53 -0
- package/src/api/router/history.ts +70 -0
- package/src/api/router/hub.ts +194 -0
- package/src/api/router/policies.ts +107 -0
- package/src/api/router/runtime.ts +5238 -0
- package/src/api/router/types.ts +299 -0
- package/src/api/router.ts +167 -0
- package/src/api/server.ts +323 -0
- package/src/api/suspense.ts +16 -0
- package/src/cache/context.ts +61 -0
- package/src/cli/adapters/vercel-warm.ts +375 -0
- package/src/cli/adapters/vercel.ts +1310 -0
- package/src/cli/analyze-print.ts +181 -0
- package/src/cli/analyze.ts +328 -0
- package/src/cli/boot-trace.ts +29 -0
- package/src/cli/build.ts +3114 -0
- package/src/cli/dev.ts +276 -0
- package/src/cli/index.ts +196 -0
- package/src/cli/named-bin.ts +119 -0
- package/src/cli/serve-ui.ts +160 -0
- package/src/cli/start.ts +1425 -0
- package/src/client/build.ts +2136 -0
- package/src/client/chunk-fold.ts +526 -0
- package/src/client/entry.ts +1525 -0
- package/src/client/paths.ts +22 -0
- package/src/client/prebuilt.ts +621 -0
- package/src/client/profile.ts +75 -0
- package/src/client/react-compiler.ts +94 -0
- package/src/client/reference-stub.ts +145 -0
- package/src/client/reference.ts +47 -0
- package/src/compat/actions/action-client.ts +531 -0
- package/src/compat/actions/action-dispatch.ts +676 -0
- package/src/compat/actions/action-router.ts +40 -0
- package/src/compat/actions/action-shared.ts +95 -0
- package/src/compat/actions/client-plugin.ts +135 -0
- package/src/compat/actions/client-stub.ts +65 -0
- package/src/compat/actions/config.ts +164 -0
- package/src/compat/actions/detect.ts +208 -0
- package/src/compat/actions/discovery.ts +244 -0
- package/src/compat/actions/early-submit.ts +40 -0
- package/src/compat/actions/endpoint.ts +602 -0
- package/src/compat/actions/flight.ts +52 -0
- package/src/compat/actions/form-state.ts +73 -0
- package/src/compat/actions/hoist.ts +485 -0
- package/src/compat/actions/ids.ts +39 -0
- package/src/compat/actions/index.ts +41 -0
- package/src/compat/actions/instances.ts +144 -0
- package/src/compat/actions/origin.ts +109 -0
- package/src/compat/actions/protocol.ts +125 -0
- package/src/compat/actions/registry.ts +74 -0
- package/src/compat/actions/rewrite.ts +282 -0
- package/src/compat/actions/serve.ts +414 -0
- package/src/compat/actions/server-tag.ts +21 -0
- package/src/compat/actions/unrecognized-error.ts +30 -0
- package/src/compat/adapter/build-complete.ts +257 -0
- package/src/compat/bundler/bun-externals.ts +53 -0
- package/src/compat/bundler/cjs-exports.ts +542 -0
- package/src/compat/bundler/config.ts +363 -0
- package/src/compat/bundler/externals.ts +34 -0
- package/src/compat/bundler/import-meta-url.ts +60 -0
- package/src/compat/bundler/modularize-imports.ts +119 -0
- package/src/compat/bundler/new-url-asset.ts +87 -0
- package/src/compat/bundler/optimize-package-imports.ts +273 -0
- package/src/compat/bundler/polyfill.ts +88 -0
- package/src/compat/bundler/react-compiler.ts +61 -0
- package/src/compat/bundler/react-profiler.tsx +25 -0
- package/src/compat/bundler/relay-transform.ts +116 -0
- package/src/compat/bundler/require-context.ts +281 -0
- package/src/compat/bundler/resolve-extensions.ts +75 -0
- package/src/compat/bundler/source-cache.ts +61 -0
- package/src/compat/bundler/static-imports.ts +25 -0
- package/src/compat/bundler/symlink-imports.ts +119 -0
- package/src/compat/bundler/tsconfig-paths.ts +50 -0
- package/src/compat/bundler/wasm.ts +153 -0
- package/src/compat/bundler/webpack-loaders.ts +685 -0
- package/src/compat/bundler/worker.ts +278 -0
- package/src/compat/cache/build-flags.ts +81 -0
- package/src/compat/cache/build-prerender-errors.ts +163 -0
- package/src/compat/cache/custom-handler.ts +159 -0
- package/src/compat/cache/fetch-patch.ts +745 -0
- package/src/compat/cache/handler.ts +100 -0
- package/src/compat/cache/modern-handler.ts +275 -0
- package/src/compat/cache/resume-data-cache.ts +143 -0
- package/src/compat/cache/revalidate.ts +759 -0
- package/src/compat/cache/runtime-error.ts +124 -0
- package/src/compat/cache/use-cache-transform.ts +961 -0
- package/src/compat/cache/use-cache.ts +1695 -0
- package/src/compat/cache-control.ts +269 -0
- package/src/compat/client/base-path.ts +64 -0
- package/src/compat/client/css-order.ts +36 -0
- package/src/compat/client/errors/control-flow.ts +92 -0
- package/src/compat/client/errors/error-boundary.ts +222 -0
- package/src/compat/client/errors/global-error.ts +238 -0
- package/src/compat/client/errors/install.ts +217 -0
- package/src/compat/client/errors/lazy.ts +53 -0
- package/src/compat/client/errors/primitive-throw.ts +126 -0
- package/src/compat/client/errors/soft-refresh.ts +14 -0
- package/src/compat/client/link-status.ts +86 -0
- package/src/compat/client/nav-compat-runtime.ts +57 -0
- package/src/compat/client/nav-compat.ts +42 -0
- package/src/compat/client/navigation-scroll.ts +154 -0
- package/src/compat/client/optimistic-routing.ts +206 -0
- package/src/compat/client/prefetch-cache.ts +111 -0
- package/src/compat/client/route-announcer.ts +72 -0
- package/src/compat/client/segment-cache-policy.ts +159 -0
- package/src/compat/client/segment-cache.ts +1077 -0
- package/src/compat/client/segment-prefetch.ts +375 -0
- package/src/compat/client/trailing-slash.ts +24 -0
- package/src/compat/css/chunking.ts +254 -0
- package/src/compat/css/inline-css.ts +73 -0
- package/src/compat/css/lightningcss.ts +90 -0
- package/src/compat/css/modules.ts +373 -0
- package/src/compat/css/nonce.ts +30 -0
- package/src/compat/css/sass-plugin.ts +65 -0
- package/src/compat/css/sass.ts +392 -0
- package/src/compat/css/styled-jsx-runtime.ts +80 -0
- package/src/compat/css/styled-jsx.ts +49 -0
- package/src/compat/edge-runtime.ts +71 -0
- package/src/compat/export/client.ts +112 -0
- package/src/compat/export/index.ts +272 -0
- package/src/compat/export/standalone.ts +207 -0
- package/src/compat/image-optimizer/cache.ts +119 -0
- package/src/compat/image-optimizer/detect.ts +143 -0
- package/src/compat/image-optimizer/index.ts +601 -0
- package/src/compat/image-optimizer/source.ts +243 -0
- package/src/compat/index.ts +458 -0
- package/src/compat/lifecycle/after-scope.ts +86 -0
- package/src/compat/lifecycle/after.ts +173 -0
- package/src/compat/lifecycle/error-funnel.ts +306 -0
- package/src/compat/lifecycle/error-serialize.ts +87 -0
- package/src/compat/lifecycle/error-ui.ts +167 -0
- package/src/compat/lifecycle/instrumentation-client.ts +138 -0
- package/src/compat/lifecycle/instrumentation.ts +277 -0
- package/src/compat/lifecycle/node-console.ts +19 -0
- package/src/compat/lifecycle/testmode.ts +263 -0
- package/src/compat/mdx/compile.ts +219 -0
- package/src/compat/mdx/next-mdx-stub.ts +46 -0
- package/src/compat/mdx/plugin.ts +37 -0
- package/src/compat/metadata-route-artifacts.ts +458 -0
- package/src/compat/metadata.ts +295 -0
- package/src/compat/middleware/manifest.ts +210 -0
- package/src/compat/misc/action-return.ts +173 -0
- package/src/compat/next/cache.ts +211 -0
- package/src/compat/next/canonical-url.ts +35 -0
- package/src/compat/next/client-cache.ts +57 -0
- package/src/compat/next/client-navigation.ts +313 -0
- package/src/compat/next/client-only.ts +3 -0
- package/src/compat/next/client-script.tsx +215 -0
- package/src/compat/next/client-server.ts +39 -0
- package/src/compat/next/config-loader.ts +569 -0
- package/src/compat/next/config.ts +29 -0
- package/src/compat/next/constants.cjs +6 -0
- package/src/compat/next/constants.ts +6 -0
- package/src/compat/next/custom-server.ts +236 -0
- package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
- package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
- package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
- package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
- package/src/compat/next/dynamic.tsx +46 -0
- package/src/compat/next/error.tsx +148 -0
- package/src/compat/next/font/cache.ts +171 -0
- package/src/compat/next/font/google.ts +2 -0
- package/src/compat/next/font/index.ts +8 -0
- package/src/compat/next/font/local.ts +5 -0
- package/src/compat/next/font/runtime-client.ts +71 -0
- package/src/compat/next/font/runtime.ts +974 -0
- package/src/compat/next/font/shared.ts +281 -0
- package/src/compat/next/form.tsx +156 -0
- package/src/compat/next/head.tsx +10 -0
- package/src/compat/next/headers.ts +247 -0
- package/src/compat/next/image/config.ts +196 -0
- package/src/compat/next/image/optimizer.ts +96 -0
- package/src/compat/next/image/patterns.ts +103 -0
- package/src/compat/next/image/shared.ts +141 -0
- package/src/compat/next/image/static-metadata.ts +283 -0
- package/src/compat/next/image/validate.ts +269 -0
- package/src/compat/next/image-client.tsx +215 -0
- package/src/compat/next/image-props.ts +575 -0
- package/src/compat/next/image-usage.ts +102 -0
- package/src/compat/next/image.tsx +56 -0
- package/src/compat/next/index.ts +1 -0
- package/src/compat/next/legacy-image.tsx +97 -0
- package/src/compat/next/link-usage.ts +29 -0
- package/src/compat/next/link-validation-transform.ts +200 -0
- package/src/compat/next/link.tsx +466 -0
- package/src/compat/next/navigation.cjs +21 -0
- package/src/compat/next/navigation.ts +188 -0
- package/src/compat/next/offline.ts +51 -0
- package/src/compat/next/og.ts +324 -0
- package/src/compat/next/optimistic-route-state.ts +188 -0
- package/src/compat/next/preferred-region.ts +39 -0
- package/src/compat/next/redirects.ts +131 -0
- package/src/compat/next/resource-hints.ts +136 -0
- package/src/compat/next/rewrites.ts +350 -0
- package/src/compat/next/root-params.ts +142 -0
- package/src/compat/next/router.cjs +49 -0
- package/src/compat/next/router.ts +143 -0
- package/src/compat/next/script.tsx +355 -0
- package/src/compat/next/server-only.ts +3 -0
- package/src/compat/next/server.ts +28 -0
- package/src/compat/next/svgr.ts +58 -0
- package/src/compat/next/telemetry.ts +77 -0
- package/src/compat/next/user-agent.ts +100 -0
- package/src/compat/next/web-vitals.ts +56 -0
- package/src/compat/otel/api.ts +95 -0
- package/src/compat/otel/client-trace-metadata.ts +71 -0
- package/src/compat/otel/fetch-span.ts +77 -0
- package/src/compat/otel/tracer.ts +944 -0
- package/src/compat/pages/client-plugin.ts +108 -0
- package/src/compat/pages/index.ts +527 -0
- package/src/compat/pages/router-state.ts +94 -0
- package/src/compat/ppr/io.ts +38 -0
- package/src/compat/ppr/missing-root-params.ts +105 -0
- package/src/compat/ppr/root-params-scan.ts +164 -0
- package/src/compat/ppr/root-params-transform.ts +75 -0
- package/src/compat/ppr/root-params.ts +129 -0
- package/src/compat/ppr/segment-config-incompat.ts +34 -0
- package/src/compat/protocol.ts +202 -0
- package/src/compat/react/client.ts +59 -0
- package/src/compat/react/compiler-runtime.ts +60 -0
- package/src/compat/react/dom-client.ts +115 -0
- package/src/compat/react/dom-react-server.ts +20 -0
- package/src/compat/react/dom-server.ts +40 -0
- package/src/compat/react/dom.ts +154 -0
- package/src/compat/react/preact.ts +522 -0
- package/src/compat/react/react-server.ts +84 -0
- package/src/compat/react/router-shim.ts +26 -0
- package/src/compat/react/server-component-use.ts +48 -0
- package/src/compat/react/server-inserted-html.ts +87 -0
- package/src/compat/react/server.ts +156 -0
- package/src/compat/react/view-transition.ts +60 -0
- package/src/compat/register/actions.ts +875 -0
- package/src/compat/register/boot.ts +141 -0
- package/src/compat/register/build-tier.ts +11 -0
- package/src/compat/register/build.ts +182 -0
- package/src/compat/register/bundler.ts +587 -0
- package/src/compat/register/cache.ts +85 -0
- package/src/compat/register/client-errors.ts +18 -0
- package/src/compat/register/config.ts +17 -0
- package/src/compat/register/css-extras.ts +120 -0
- package/src/compat/register/edge-runtime.ts +6 -0
- package/src/compat/register/errors.ts +46 -0
- package/src/compat/register/export.ts +23 -0
- package/src/compat/register/font.ts +36 -0
- package/src/compat/register/hooks.ts +34 -0
- package/src/compat/register/image.ts +133 -0
- package/src/compat/register/index.ts +111 -0
- package/src/compat/register/instrumentation-client.ts +35 -0
- package/src/compat/register/lifecycle.ts +86 -0
- package/src/compat/register/mdx.ts +48 -0
- package/src/compat/register/middleware.ts +36 -0
- package/src/compat/register/misc.ts +44 -0
- package/src/compat/register/otel.ts +288 -0
- package/src/compat/register/pages-api.ts +473 -0
- package/src/compat/register/ppr.ts +56 -0
- package/src/compat/register/protocol.ts +57 -0
- package/src/compat/register/proxy.ts +127 -0
- package/src/compat/register/render.ts +268 -0
- package/src/compat/register/routing.ts +410 -0
- package/src/compat/register/segment.ts +1903 -0
- package/src/compat/register/static-image.ts +21 -0
- package/src/compat/register/typed-routes.ts +35 -0
- package/src/compat/register/usecache.ts +131 -0
- package/src/compat/register/validation.ts +56 -0
- package/src/compat/segment/loading-boundary.ts +113 -0
- package/src/compat/segment/page-slot.ts +200 -0
- package/src/compat/segment/tree.ts +481 -0
- package/src/compat/segment/vary-key.ts +102 -0
- package/src/compat/segment/vary-params.ts +551 -0
- package/src/compat/static-params.ts +33 -0
- package/src/compat/tsconfig-defaults.ts +301 -0
- package/src/compat/typecheck/index.ts +1481 -0
- package/src/compat/typecheck/worker.ts +26 -0
- package/src/compat/typed-routes/index.ts +92 -0
- package/src/compat/typed-routes/manifest.ts +356 -0
- package/src/compat/typed-routes/typegen.ts +566 -0
- package/src/compat/validation/errors.ts +159 -0
- package/src/compat/validation/index.ts +1770 -0
- package/src/compat/validation/prerender-diagnostics.ts +1508 -0
- package/src/compat-bootstrap.ts +67 -0
- package/src/config.ts +218 -0
- package/src/css/build.ts +697 -0
- package/src/css/index.ts +2 -0
- package/src/css/postcss.ts +236 -0
- package/src/css/worker.ts +34 -0
- package/src/dev/client-actions.ts +35 -0
- package/src/dev/client-chunk-store.ts +92 -0
- package/src/dev/client-key-cache.ts +178 -0
- package/src/dev/global-css-cache.ts +212 -0
- package/src/dev/imports.ts +2430 -0
- package/src/dev/module-cache.ts +721 -0
- package/src/dev/module-generations.ts +38 -0
- package/src/dev/module-transform.ts +188 -0
- package/src/dev/node-module-bundle-cache.ts +63 -0
- package/src/dev/restart-cache.ts +10 -0
- package/src/dev/route-bundle-key-cache.ts +154 -0
- package/src/dev/route-facts-cache.ts +223 -0
- package/src/dev/server.ts +1710 -0
- package/src/dynamic/source.ts +307 -0
- package/src/dynamic/tree-shake.ts +262 -0
- package/src/env.ts +92 -0
- package/src/extensions.ts +1898 -0
- package/src/index.ts +34 -0
- package/src/internal.ts +43 -0
- package/src/islands/boundary-error.ts +8 -0
- package/src/islands/static-children.ts +37 -0
- package/src/islands/static-slots.ts +106 -0
- package/src/ppr-postpone.ts +24 -0
- package/src/ppr.ts +784 -0
- package/src/proxy.ts +752 -0
- package/src/render/hooks.ts +384 -0
- package/src/render/index.ts +1 -0
- package/src/render/island-context.ts +47 -0
- package/src/render/metadata.ts +857 -0
- package/src/render/renderer.ts +7391 -0
- package/src/render/resource-hints.ts +44 -0
- package/src/render/slots.tsx +679 -0
- package/src/request/context.ts +396 -0
- package/src/resolve/engine.ts +219 -0
- package/src/resolve/imports.ts +1104 -0
- package/src/resolve/scan-facts.ts +474 -0
- package/src/resolve/source-text.ts +86 -0
- package/src/routing/forwarded.ts +41 -0
- package/src/routing/handler.ts +271 -0
- package/src/routing/href.ts +203 -0
- package/src/routing/metadata.ts +1018 -0
- package/src/routing/request-runtime.ts +43 -0
- package/src/routing/routes.ts +2560 -0
- package/src/routing/slots.ts +432 -0
- package/src/runtime/server.ts +3453 -0
- package/src/runtime/vendor.ts +1160 -0
- package/src/style-modules.d.ts +9 -0
- package/src/typegen.ts +151 -0
- package/src/types.ts +725 -0
- package/src/utils/ansi.ts +9 -0
- package/src/utils/content-type.ts +31 -0
- package/src/utils/decode.ts +7 -0
- package/src/utils/dev-profile.ts +31 -0
- package/src/utils/error-log.ts +29 -0
- package/src/utils/fs-cache.ts +31 -0
- package/src/utils/fs.ts +119 -0
- package/src/utils/html.ts +46 -0
- package/src/utils/serialize.ts +378 -0
- package/src/utils/source.ts +35 -0
- package/src/utils/verbose.ts +39 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Metadata,
|
|
3
|
+
MetadataAuthor,
|
|
4
|
+
MetadataIcon,
|
|
5
|
+
MetadataIconGroups,
|
|
6
|
+
MetadataImage,
|
|
7
|
+
MetadataLink,
|
|
8
|
+
MetadataOpenGraph,
|
|
9
|
+
MetadataRobots,
|
|
10
|
+
ResourceHint,
|
|
11
|
+
MetadataTwitter,
|
|
12
|
+
Viewport,
|
|
13
|
+
} from '../types';
|
|
14
|
+
import {
|
|
15
|
+
metadataHasOwnImages,
|
|
16
|
+
staticMetadataLink,
|
|
17
|
+
type StaticMetadataForPath,
|
|
18
|
+
} from '../routing/metadata';
|
|
19
|
+
import { escapeHtml, stringifyHtmlValue } from '../utils/html';
|
|
20
|
+
import { getTrailingSlashUrls, withBasePath } from '../routing/href';
|
|
21
|
+
|
|
22
|
+
export type MetadataExport = Metadata | (() => Metadata | Promise<Metadata>);
|
|
23
|
+
|
|
24
|
+
export type ViewportExport = Viewport | (() => Viewport | Promise<Viewport>);
|
|
25
|
+
|
|
26
|
+
export interface MetadataEntry {
|
|
27
|
+
metadata: Metadata;
|
|
28
|
+
dir?: string;
|
|
29
|
+
kind?: 'layout' | 'page';
|
|
30
|
+
root?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MetadataContext {
|
|
34
|
+
url?: URL;
|
|
35
|
+
metadataBase?: URL;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MetadataRenderPage {
|
|
39
|
+
metadata: Metadata;
|
|
40
|
+
metadataUrl?: string;
|
|
41
|
+
fontPreloads: MetadataLink[];
|
|
42
|
+
resourceHints?: ResourceHint[];
|
|
43
|
+
stylesheets: StylesheetLink[];
|
|
44
|
+
modulePreloads?: string[];
|
|
45
|
+
dynamicScriptPreloads?: string[];
|
|
46
|
+
staticMetadata?: StaticMetadataForPath;
|
|
47
|
+
suppressIconLinks?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface StylesheetLink {
|
|
51
|
+
url: string | string[];
|
|
52
|
+
dataPrecedence?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function metadataContext(page: MetadataRenderPage): MetadataContext {
|
|
56
|
+
const url = page.metadataUrl ? new URL(page.metadataUrl) : undefined;
|
|
57
|
+
return {
|
|
58
|
+
url,
|
|
59
|
+
metadataBase: coerceMetadataBase(page.metadata.metadataBase, url),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function headLinks(page: MetadataRenderPage, context: MetadataContext) {
|
|
64
|
+
const canonical = coerceCanonical(page.metadata.alternates, context);
|
|
65
|
+
const links: (MetadataLink | ResourceHint)[] = [
|
|
66
|
+
...page.fontPreloads,
|
|
67
|
+
...(page.resourceHints ?? []),
|
|
68
|
+
...stylesheetLinks(page.stylesheets),
|
|
69
|
+
...(page.modulePreloads ?? []).map(url => ({ rel: 'modulepreload', url })),
|
|
70
|
+
...(page.dynamicScriptPreloads ?? []).map(url => ({
|
|
71
|
+
rel: 'preload',
|
|
72
|
+
url,
|
|
73
|
+
as: 'script',
|
|
74
|
+
// `crossorigin` keeps the hint's fetch mode matching the later
|
|
75
|
+
// `import()` (cors/same-origin), so the chunk isn't downloaded twice.
|
|
76
|
+
crossOrigin: 'anonymous' as const,
|
|
77
|
+
fetchPriority: 'low' as const,
|
|
78
|
+
})),
|
|
79
|
+
...(canonical ? [{ rel: 'canonical', url: canonical }] : []),
|
|
80
|
+
...staticMetadataLinks(page),
|
|
81
|
+
...authorLinks(page.metadata.authors, context),
|
|
82
|
+
...manifestLink(page.metadata.manifest, context),
|
|
83
|
+
...alternateLinks(page.metadata.alternates, context),
|
|
84
|
+
...paginationLinks(page.metadata.pagination, context),
|
|
85
|
+
...appleStartupLinks(page.metadata.appleWebApp, context),
|
|
86
|
+
...coerceIcons(page.metadata.icons, context),
|
|
87
|
+
...(page.metadata.links ?? []),
|
|
88
|
+
];
|
|
89
|
+
return links.map(link => ` <link ${linkAttrs(link)}/>`).join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stylesheetLinks(stylesheets: StylesheetLink[]): MetadataLink[] {
|
|
93
|
+
return stylesheets.flatMap(({ url, dataPrecedence }) =>
|
|
94
|
+
(Array.isArray(url) ? url : [url]).map(href => ({
|
|
95
|
+
rel: 'stylesheet',
|
|
96
|
+
url: href,
|
|
97
|
+
...(dataPrecedence ? { dataPrecedence } : {}),
|
|
98
|
+
})),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function metadataIconLinks(metadata: Metadata, context: MetadataContext, indent = ' ') {
|
|
103
|
+
return coerceIcons(metadata.icons, context)
|
|
104
|
+
.map(link => `${indent}<link ${linkAttrs(link)}/>`)
|
|
105
|
+
.join('\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function staticMetadataLinks(page: MetadataRenderPage): MetadataLink[] {
|
|
109
|
+
const metadata = page.staticMetadata;
|
|
110
|
+
if (!metadata) return [];
|
|
111
|
+
const hasExplicitIcons = page.metadata.icons !== undefined;
|
|
112
|
+
const hasExplicitManifest = page.metadata.manifest !== undefined;
|
|
113
|
+
if (page.suppressIconLinks) {
|
|
114
|
+
return prefixLinks([
|
|
115
|
+
...(!hasExplicitManifest && metadata.manifestLink ? [metadata.manifestLink] : []),
|
|
116
|
+
...(!hasExplicitManifest && metadata.manifest ? [staticMetadataLink(metadata.manifest)] : []),
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
return prefixLinks([
|
|
120
|
+
...(metadata.favicon ? [staticMetadataLink(metadata.favicon)] : []),
|
|
121
|
+
...(!hasExplicitManifest && metadata.manifestLink ? [metadata.manifestLink] : []),
|
|
122
|
+
...(!hasExplicitManifest && metadata.manifest ? [staticMetadataLink(metadata.manifest)] : []),
|
|
123
|
+
...metadata.icons.map(staticMetadataLink),
|
|
124
|
+
...(metadata.dynamicIcons ?? []),
|
|
125
|
+
...metadata.appleIcons.map(staticMetadataLink),
|
|
126
|
+
...(metadata.dynamicAppleIcons ?? []),
|
|
127
|
+
...(!hasExplicitIcons && metadata.icons.length === 0
|
|
128
|
+
? metadata.rootIcons.map(staticMetadataLink)
|
|
129
|
+
: []),
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// File-convention metadata assets (favicon, icons, manifest, ...) are served
|
|
134
|
+
// under the configured basePath, so their hrefs carry the prefix. A no-basePath
|
|
135
|
+
// app leaves every href untouched (withBasePath is a no-op).
|
|
136
|
+
function prefixLinks(links: MetadataLink[]): MetadataLink[] {
|
|
137
|
+
return links.map(link => ({ ...link, url: withBasePath(link.url) }));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function applyStaticMetadata(
|
|
141
|
+
metadata: Metadata,
|
|
142
|
+
staticMetadata: StaticMetadataForPath,
|
|
143
|
+
): Metadata {
|
|
144
|
+
let next = metadata;
|
|
145
|
+
const openGraphImages = [
|
|
146
|
+
...(staticMetadata.openGraphImage ? [staticMetadata.openGraphImage] : []),
|
|
147
|
+
...(staticMetadata.dynamicOpenGraphImages ?? []),
|
|
148
|
+
].map(prefixImageBasePath);
|
|
149
|
+
if (openGraphImages.length > 0 && !metadataHasOwnImages(metadata, 'openGraph')) {
|
|
150
|
+
next = {
|
|
151
|
+
...next,
|
|
152
|
+
openGraph: {
|
|
153
|
+
...(next.openGraph ?? {}),
|
|
154
|
+
images: openGraphImages.length === 1 ? openGraphImages[0] : openGraphImages,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const twitterImages = [
|
|
159
|
+
...(staticMetadata.twitterImage ? [staticMetadata.twitterImage] : []),
|
|
160
|
+
...(staticMetadata.dynamicTwitterImages ?? []),
|
|
161
|
+
].map(prefixImageBasePath);
|
|
162
|
+
if (twitterImages.length > 0 && !metadataHasOwnImages(metadata, 'twitter')) {
|
|
163
|
+
next = {
|
|
164
|
+
...next,
|
|
165
|
+
twitter: {
|
|
166
|
+
...(next.twitter ?? {}),
|
|
167
|
+
images: twitterImages.length === 1 ? twitterImages[0] : twitterImages,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return next;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// File-convention og/twitter images resolve to a root-relative asset path
|
|
175
|
+
// (e.g. `/dir/opengraph-image.png?metadata`); under a basePath the asset is
|
|
176
|
+
// served with that prefix, so the href carries it. String/URL images and a
|
|
177
|
+
// no-basePath app are left untouched.
|
|
178
|
+
function prefixImageBasePath(image: MetadataImage): MetadataImage {
|
|
179
|
+
if (typeof image === 'string') return withBasePath(image);
|
|
180
|
+
if (image instanceof URL) return image;
|
|
181
|
+
if (typeof image === 'object' && image !== null && typeof image.url === 'string') {
|
|
182
|
+
return { ...image, url: withBasePath(image.url) };
|
|
183
|
+
}
|
|
184
|
+
return image;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Emits the common metadata <meta> tags. Every value passes through
|
|
188
|
+
// escapeHtml (which itself coerces via String()), so no value shape can throw.
|
|
189
|
+
export function metadataTags(metadata: Metadata, context: MetadataContext): string {
|
|
190
|
+
const meta = metadata;
|
|
191
|
+
const tags: string[] = [];
|
|
192
|
+
const push = (name: string, content: string | undefined) => {
|
|
193
|
+
if (content) tags.push(`<meta name="${escapeHtml(name)}" content="${escapeHtml(content)}"/>`);
|
|
194
|
+
};
|
|
195
|
+
const pushProperty = (property: string, content: string | undefined) => {
|
|
196
|
+
if (content)
|
|
197
|
+
tags.push(`<meta property="${escapeHtml(property)}" content="${escapeHtml(content)}"/>`);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
push('description', meta.description);
|
|
201
|
+
push('keywords', coerceKeywords(meta.keywords));
|
|
202
|
+
for (const author of coerceAuthors(meta.authors)) push('author', author);
|
|
203
|
+
push('generator', meta.generator);
|
|
204
|
+
push('application-name', meta.applicationName);
|
|
205
|
+
push('referrer', meta.referrer);
|
|
206
|
+
push('creator', meta.creator);
|
|
207
|
+
push('publisher', meta.publisher);
|
|
208
|
+
push('robots', coerceRobots(meta.robots));
|
|
209
|
+
push(
|
|
210
|
+
'googlebot',
|
|
211
|
+
coerceRobots(isRecord(meta.robots) ? (meta.robots.googleBot as MetadataRobots) : undefined),
|
|
212
|
+
);
|
|
213
|
+
push('format-detection', coerceFormatDetection(meta.formatDetection));
|
|
214
|
+
for (const [name, value] of verificationTags(meta.verification)) push(name, value);
|
|
215
|
+
for (const value of list(meta.verification?.other?.me)) push('me', value);
|
|
216
|
+
for (const [name, values] of Object.entries(meta.other ?? {})) {
|
|
217
|
+
for (const value of list(values)) push(name, value);
|
|
218
|
+
}
|
|
219
|
+
const itunes = itunesContent(meta.itunes);
|
|
220
|
+
push('apple-itunes-app', itunes);
|
|
221
|
+
const apple = meta.appleWebApp;
|
|
222
|
+
if (apple) {
|
|
223
|
+
push('mobile-web-app-capable', appleWebAppCapable(apple) ? 'yes' : undefined);
|
|
224
|
+
if (typeof apple === 'object') {
|
|
225
|
+
push('apple-mobile-web-app-title', apple.title);
|
|
226
|
+
push('apple-mobile-web-app-status-bar-style', apple.statusBarStyle);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (meta.facebook?.appId) pushProperty('fb:app_id', meta.facebook.appId);
|
|
230
|
+
for (const admin of list(meta.facebook?.admins)) pushProperty('fb:admins', admin);
|
|
231
|
+
if (meta.pinterest?.richPin !== undefined) {
|
|
232
|
+
pushProperty('pinterest-rich-pin', meta.pinterest.richPin ? 'true' : 'false');
|
|
233
|
+
}
|
|
234
|
+
for (const [property, value] of appLinkTags(meta.appLinks)) pushProperty(property, value);
|
|
235
|
+
|
|
236
|
+
tags.push(...openGraphTags(meta.openGraph, context));
|
|
237
|
+
tags.push(...twitterTags(meta.twitter ?? twitterFromOpenGraph(meta.openGraph), context));
|
|
238
|
+
|
|
239
|
+
return tags.join('\n ');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function openGraphTags(og: MetadataOpenGraph | undefined, context: MetadataContext): string[] {
|
|
243
|
+
if (!og) return [];
|
|
244
|
+
const tags: string[] = [];
|
|
245
|
+
const push = (property: string, content: string | undefined) => {
|
|
246
|
+
if (content)
|
|
247
|
+
tags.push(`<meta property="${escapeHtml(property)}" content="${escapeHtml(content)}"/>`);
|
|
248
|
+
};
|
|
249
|
+
push('og:title', og.title === undefined ? undefined : resolveTitleChain([{ title: og.title }]));
|
|
250
|
+
push('og:description', og.description);
|
|
251
|
+
push('og:url', og.url === undefined ? undefined : resolveMetadataUrl(og.url, context));
|
|
252
|
+
push('og:site_name', og.siteName);
|
|
253
|
+
push('og:locale', og.locale);
|
|
254
|
+
push('og:type', og.type);
|
|
255
|
+
for (const image of coerceImages(og.images, context)) {
|
|
256
|
+
push('og:image', image.url);
|
|
257
|
+
push('og:image:secure_url', image.secureUrl);
|
|
258
|
+
push('og:image:width', image.width);
|
|
259
|
+
push('og:image:height', image.height);
|
|
260
|
+
push('og:image:alt', image.alt);
|
|
261
|
+
push('og:image:type', image.type);
|
|
262
|
+
}
|
|
263
|
+
for (const video of coerceImages(og.videos, context)) {
|
|
264
|
+
push('og:video', video.url);
|
|
265
|
+
push('og:video:width', video.width);
|
|
266
|
+
push('og:video:height', video.height);
|
|
267
|
+
}
|
|
268
|
+
for (const audio of coerceImages(og.audio, context)) push('og:audio', audio.url);
|
|
269
|
+
push('article:published_time', og.publishedTime);
|
|
270
|
+
for (const author of list(og.authors)) push('article:author', author);
|
|
271
|
+
for (const email of list(og.emails)) push('og:email', email);
|
|
272
|
+
for (const phone of list(og.phoneNumbers)) push('og:phone_number', phone);
|
|
273
|
+
for (const fax of list(og.faxNumbers)) push('og:fax_number', fax);
|
|
274
|
+
return tags;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function twitterTags(twitter: MetadataTwitter | undefined, context: MetadataContext): string[] {
|
|
278
|
+
if (!twitter) return [];
|
|
279
|
+
const tags: string[] = [];
|
|
280
|
+
const push = (name: string, content: string | undefined) => {
|
|
281
|
+
if (content) tags.push(`<meta name="${escapeHtml(name)}" content="${escapeHtml(content)}"/>`);
|
|
282
|
+
};
|
|
283
|
+
push('twitter:card', twitterCard(twitter));
|
|
284
|
+
push('twitter:site', twitter.site);
|
|
285
|
+
push('twitter:site:id', twitter.siteId);
|
|
286
|
+
push('twitter:creator', twitter.creator);
|
|
287
|
+
push('twitter:creator:id', twitter.creatorId);
|
|
288
|
+
push(
|
|
289
|
+
'twitter:title',
|
|
290
|
+
twitter.title === undefined ? undefined : resolveTitleChain([{ title: twitter.title }]),
|
|
291
|
+
);
|
|
292
|
+
push('twitter:description', twitter.description);
|
|
293
|
+
for (const image of coerceImages(twitter.images, context)) {
|
|
294
|
+
push('twitter:image', image.url);
|
|
295
|
+
push('twitter:image:secure_url', image.secureUrl);
|
|
296
|
+
push('twitter:image:width', image.width);
|
|
297
|
+
push('twitter:image:height', image.height);
|
|
298
|
+
push('twitter:image:alt', image.alt);
|
|
299
|
+
push('twitter:image:type', image.type);
|
|
300
|
+
}
|
|
301
|
+
push('twitter:player', resolveMetadataUrl(twitter.players?.playerUrl, context));
|
|
302
|
+
push('twitter:player:stream', resolveMetadataUrl(twitter.players?.streamUrl, context));
|
|
303
|
+
push('twitter:player:width', coerceString(twitter.players?.width) || undefined);
|
|
304
|
+
push('twitter:player:height', coerceString(twitter.players?.height) || undefined);
|
|
305
|
+
for (const [device, id] of Object.entries(twitter.app?.id ?? {})) {
|
|
306
|
+
push(`twitter:app:id:${device}`, id);
|
|
307
|
+
}
|
|
308
|
+
for (const [device, url] of Object.entries(twitter.app?.url ?? {})) {
|
|
309
|
+
push(`twitter:app:url:${device}`, resolveMetadataUrl(url, context));
|
|
310
|
+
}
|
|
311
|
+
return tags;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// theme-color: string | { color, media } | array of those.
|
|
315
|
+
export function themeColorTags(value: Viewport['themeColor']): string {
|
|
316
|
+
if (value === undefined) return '';
|
|
317
|
+
const list = Array.isArray(value) ? value : [value];
|
|
318
|
+
return list
|
|
319
|
+
.map(entry => {
|
|
320
|
+
if (typeof entry === 'string')
|
|
321
|
+
return `<meta name="theme-color" content="${escapeHtml(entry)}"/>`;
|
|
322
|
+
const media = entry.media ? ` media="${escapeHtml(entry.media)}"` : '';
|
|
323
|
+
return `<meta name="theme-color"${media} content="${escapeHtml(entry.color)}"/>`;
|
|
324
|
+
})
|
|
325
|
+
.join('\n ');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function linkAttrs(link: MetadataLink | ResourceHint) {
|
|
329
|
+
const attrs = [
|
|
330
|
+
['rel', link.rel],
|
|
331
|
+
['href', link.url],
|
|
332
|
+
['data-precedence', link.dataPrecedence],
|
|
333
|
+
['type', link.type],
|
|
334
|
+
['media', link.media],
|
|
335
|
+
['sizes', link.sizes],
|
|
336
|
+
['as', link.as],
|
|
337
|
+
['title', link.title],
|
|
338
|
+
['hreflang', link.hrefLang],
|
|
339
|
+
['crossorigin', link.crossOrigin],
|
|
340
|
+
['fetchpriority', link.fetchPriority],
|
|
341
|
+
['nonce', link.nonce],
|
|
342
|
+
]
|
|
343
|
+
.filter((attr): attr is [string, string] => typeof attr[1] === 'string')
|
|
344
|
+
.map(([key, value]) => `${key}="${escapeHtml(value)}"`);
|
|
345
|
+
const attributes = 'attributes' in link ? link.attributes : undefined;
|
|
346
|
+
for (const [key, value] of Object.entries(attributes ?? {})) {
|
|
347
|
+
attrs.push(`${escapeHtml(key)}="${escapeHtml(value)}"`);
|
|
348
|
+
}
|
|
349
|
+
return attrs.join(' ');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function viewportContent(viewport: Viewport) {
|
|
353
|
+
const entries: [string, string | number | boolean | undefined][] = [
|
|
354
|
+
['width', viewport.width ?? 'device-width'],
|
|
355
|
+
[
|
|
356
|
+
'initial-scale',
|
|
357
|
+
Object.prototype.hasOwnProperty.call(viewport, 'initialScale') ? viewport.initialScale : 1,
|
|
358
|
+
],
|
|
359
|
+
['minimum-scale', viewport.minimumScale],
|
|
360
|
+
['maximum-scale', viewport.maximumScale],
|
|
361
|
+
[
|
|
362
|
+
'user-scalable',
|
|
363
|
+
viewport.userScalable === undefined ? undefined : viewport.userScalable ? 'yes' : 'no',
|
|
364
|
+
],
|
|
365
|
+
['viewport-fit', viewport.viewportFit],
|
|
366
|
+
['interactive-widget', viewport.interactiveWidget],
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
return entries
|
|
370
|
+
.filter((entry): entry is [string, string | number | boolean] => entry[1] !== undefined)
|
|
371
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
372
|
+
.join(', ');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Core metadata is static for a route render: object exports are read as-is and
|
|
376
|
+
// function exports run without request data. Compat registers the Next
|
|
377
|
+
// generateMetadata/generateViewport resolver separately.
|
|
378
|
+
export async function readModuleMetadata(module: {
|
|
379
|
+
metadata?: MetadataExport;
|
|
380
|
+
}): Promise<Metadata | undefined> {
|
|
381
|
+
return typeof module.metadata === 'function' ? module.metadata() : module.metadata;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function readModuleViewport(module: {
|
|
385
|
+
viewport?: ViewportExport;
|
|
386
|
+
}): Promise<Viewport | undefined> {
|
|
387
|
+
return typeof module.viewport === 'function' ? module.viewport() : module.viewport;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function mergeMetadataEntries(
|
|
391
|
+
...entryGroups: (MetadataEntry | MetadataEntry[])[]
|
|
392
|
+
): Metadata {
|
|
393
|
+
const entries = entryGroups.flat();
|
|
394
|
+
const items = entries.map(entry => entry.metadata);
|
|
395
|
+
const merged = items.reduce<Metadata>(
|
|
396
|
+
(metadata, item) => ({
|
|
397
|
+
...metadata,
|
|
398
|
+
...item,
|
|
399
|
+
icons: mergeIcons(metadata.icons, item.icons),
|
|
400
|
+
links: [...(metadata.links ?? []), ...(item.links ?? [])],
|
|
401
|
+
}),
|
|
402
|
+
{},
|
|
403
|
+
);
|
|
404
|
+
merged.title = resolveTitleEntries(entries);
|
|
405
|
+
return merged;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function mergeIcons(base: Metadata['icons'], next: Metadata['icons']): Metadata['icons'] {
|
|
409
|
+
if (next === undefined) return base;
|
|
410
|
+
if (Array.isArray(base) && Array.isArray(next)) return [...base, ...next];
|
|
411
|
+
return next;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
interface TitleTemplateState {
|
|
415
|
+
template: string | null;
|
|
416
|
+
previous: string | null;
|
|
417
|
+
dir?: string;
|
|
418
|
+
root?: boolean;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function resolveTitleEntries(entries: MetadataEntry[]): string | undefined {
|
|
422
|
+
let template: TitleTemplateState = { template: null, previous: null };
|
|
423
|
+
let resolved: string | undefined;
|
|
424
|
+
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
const title = entry.metadata.title;
|
|
427
|
+
if (title === undefined) continue;
|
|
428
|
+
const activeTemplate =
|
|
429
|
+
entry.kind === 'page' && entry.dir && entry.dir === template.dir && !template.root
|
|
430
|
+
? template.previous
|
|
431
|
+
: template.template;
|
|
432
|
+
|
|
433
|
+
if (typeof title === 'object' && title !== null) {
|
|
434
|
+
if (typeof title.absolute === 'string') {
|
|
435
|
+
resolved = title.absolute;
|
|
436
|
+
} else if (typeof title.default === 'string') {
|
|
437
|
+
resolved = applyTitleTemplate(activeTemplate, title.default);
|
|
438
|
+
}
|
|
439
|
+
if ('template' in title) {
|
|
440
|
+
template = {
|
|
441
|
+
template: title.template ?? null,
|
|
442
|
+
previous: activeTemplate,
|
|
443
|
+
dir: entry.dir,
|
|
444
|
+
root: entry.root,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
resolved = applyTitleTemplate(activeTemplate, coerceString(title));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return resolved;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Walks the chain root->page tracking the last-seen template. A child's plain
|
|
455
|
+
// string title is wrapped by the nearest ancestor template; an `absolute`
|
|
456
|
+
// bypasses the template; a `default` supplies a title when the child has none.
|
|
457
|
+
function resolveTitleChain(items: Metadata[]): string | undefined {
|
|
458
|
+
return resolveTitleEntries(items.map(metadata => ({ metadata })));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function applyTitleTemplate(template: string | null, value: string): string {
|
|
462
|
+
return template ? template.replace('%s', value) : value;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Coerces any metadata scalar (string | number | URL | unknown) into a string.
|
|
466
|
+
// Numbers go through String(); undefined/null collapse to ''. This is the guard
|
|
467
|
+
// that keeps emission from ever crashing on an unexpected value shape.
|
|
468
|
+
export function coerceString(value: unknown): string {
|
|
469
|
+
return stringifyHtmlValue(value);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// keywords: string | number | Array -> comma-joined string.
|
|
473
|
+
function coerceKeywords(value: Metadata['keywords']): string | undefined {
|
|
474
|
+
const list = normalizeKeywordList(value);
|
|
475
|
+
if (list === undefined) return undefined;
|
|
476
|
+
const joined = list.map(coerceString).filter(Boolean).join(',');
|
|
477
|
+
return joined || undefined;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function normalizeKeywordList(value: Metadata['keywords']): (string | number)[] | undefined {
|
|
481
|
+
if (value === undefined) return undefined;
|
|
482
|
+
return Array.isArray(value) ? value : [value];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// authors -> list of display names for meta[name=author].
|
|
486
|
+
function coerceAuthors(value: Metadata['authors']): string[] {
|
|
487
|
+
if (value === undefined) return [];
|
|
488
|
+
const list = Array.isArray(value) ? value : [value];
|
|
489
|
+
return list
|
|
490
|
+
.map(author => (typeof author === 'string' ? author : author.name))
|
|
491
|
+
.map(name => coerceString(name))
|
|
492
|
+
.filter(Boolean);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// robots: string passthrough, or object -> "index, follow, noarchive" style.
|
|
496
|
+
function coerceRobots(value: Metadata['robots']): string | undefined {
|
|
497
|
+
if (value === undefined) return undefined;
|
|
498
|
+
if (typeof value === 'string') return value;
|
|
499
|
+
const flags: string[] = [];
|
|
500
|
+
const r = value;
|
|
501
|
+
if (r.index !== undefined) flags.push(r.index ? 'index' : 'noindex');
|
|
502
|
+
if (r.follow !== undefined) flags.push(r.follow ? 'follow' : 'nofollow');
|
|
503
|
+
if (r.noarchive) flags.push('noarchive');
|
|
504
|
+
if (r.nosnippet) flags.push('nosnippet');
|
|
505
|
+
if (r.noimageindex) flags.push('noimageindex');
|
|
506
|
+
if (r.nocache) flags.push('nocache');
|
|
507
|
+
for (const [key, item] of Object.entries(r)) {
|
|
508
|
+
if (
|
|
509
|
+
item === undefined ||
|
|
510
|
+
item === false ||
|
|
511
|
+
key === 'index' ||
|
|
512
|
+
key === 'follow' ||
|
|
513
|
+
key === 'noarchive' ||
|
|
514
|
+
key === 'nosnippet' ||
|
|
515
|
+
key === 'noimageindex' ||
|
|
516
|
+
key === 'nocache' ||
|
|
517
|
+
key === 'googleBot'
|
|
518
|
+
) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (item === true) flags.push(key);
|
|
522
|
+
else flags.push(`${key}:${String(item)}`);
|
|
523
|
+
}
|
|
524
|
+
return flags.length ? flags.join(', ') : undefined;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
interface ResolvedImage {
|
|
528
|
+
url: string;
|
|
529
|
+
secureUrl?: string;
|
|
530
|
+
width?: string;
|
|
531
|
+
height?: string;
|
|
532
|
+
alt?: string;
|
|
533
|
+
type?: string;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function coerceImages(
|
|
537
|
+
value: MetadataImage | MetadataImage[] | undefined,
|
|
538
|
+
context: MetadataContext,
|
|
539
|
+
): ResolvedImage[] {
|
|
540
|
+
if (value === undefined) return [];
|
|
541
|
+
const list = Array.isArray(value) ? value : [value];
|
|
542
|
+
return list
|
|
543
|
+
.map((image): ResolvedImage | undefined => {
|
|
544
|
+
if (typeof image === 'string')
|
|
545
|
+
return { url: resolveMetadataUrl(image, context, true) ?? image };
|
|
546
|
+
if (image instanceof URL) return { url: image.href };
|
|
547
|
+
if (typeof image === 'object' && image !== null && 'url' in image) {
|
|
548
|
+
const img = image;
|
|
549
|
+
return {
|
|
550
|
+
url: resolveMetadataUrl(img.url, context, true) ?? coerceString(img.url),
|
|
551
|
+
secureUrl: resolveMetadataUrl(img.secureUrl, context, true),
|
|
552
|
+
width: img.width === undefined ? undefined : coerceString(img.width),
|
|
553
|
+
height: img.height === undefined ? undefined : coerceString(img.height),
|
|
554
|
+
alt: img.alt,
|
|
555
|
+
type: img.type,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
return undefined;
|
|
559
|
+
})
|
|
560
|
+
.filter((image): image is ResolvedImage => image !== undefined && Boolean(image.url));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function coerceCanonical(
|
|
564
|
+
value: Metadata['alternates'],
|
|
565
|
+
context: MetadataContext,
|
|
566
|
+
): string | undefined {
|
|
567
|
+
const canonical = value?.canonical;
|
|
568
|
+
if (canonical === undefined) return undefined;
|
|
569
|
+
if (typeof canonical === 'string') return resolveMetadataUrl(canonical, context);
|
|
570
|
+
if (canonical instanceof URL) return canonical.href;
|
|
571
|
+
if (typeof canonical === 'object' && 'url' in canonical)
|
|
572
|
+
return resolveMetadataUrl(canonical.url, context);
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function coerceMetadataBase(value: Metadata['metadataBase'], url?: URL): URL | undefined {
|
|
577
|
+
if (value instanceof URL) return value;
|
|
578
|
+
if (typeof value === 'string' && value) {
|
|
579
|
+
try {
|
|
580
|
+
return new URL(value);
|
|
581
|
+
} catch {
|
|
582
|
+
return url ? new URL(value, url) : undefined;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function resolveMetadataUrl(
|
|
589
|
+
value: unknown,
|
|
590
|
+
context: MetadataContext,
|
|
591
|
+
fallbackToRequestUrl = false,
|
|
592
|
+
): string | undefined {
|
|
593
|
+
if (value === undefined || value === null) return undefined;
|
|
594
|
+
if (value instanceof URL) return value.href;
|
|
595
|
+
const raw = coerceString(value);
|
|
596
|
+
if (!raw) return undefined;
|
|
597
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(raw)) return raw;
|
|
598
|
+
const base =
|
|
599
|
+
context.metadataBase ?? (fallbackToRequestUrl ? requestFallbackBase(context.url) : undefined);
|
|
600
|
+
if (!base) return raw;
|
|
601
|
+
const origin = base.origin;
|
|
602
|
+
const prefix = base.pathname === '/' ? '' : `/${base.pathname.replace(/^\/+|\/+$/g, '')}`;
|
|
603
|
+
if (raw.startsWith('./') && context.url) {
|
|
604
|
+
const requestPath = joinUrlPath(prefix, context.url.pathname);
|
|
605
|
+
const directory = requestPath.endsWith('/') ? requestPath : `${requestPath}/`;
|
|
606
|
+
const resolved = new URL(raw, `${origin}${directory}`).pathname;
|
|
607
|
+
const pathname =
|
|
608
|
+
resolved.length > 1 && resolved.endsWith('/') ? resolved.slice(0, -1) : resolved;
|
|
609
|
+
return joinMetadataUrl(origin, pathname);
|
|
610
|
+
}
|
|
611
|
+
const relative = metadataRelativePath(raw);
|
|
612
|
+
const resolvedPath = joinUrlPath(prefix, relative);
|
|
613
|
+
return joinMetadataUrl(origin, resolvedPath);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function requestFallbackBase(url?: URL) {
|
|
617
|
+
if (!url) return undefined;
|
|
618
|
+
if (url.hostname !== 'pnext.local') return new URL(url.origin);
|
|
619
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
620
|
+
const port = process.env.PORT;
|
|
621
|
+
return port && port !== '0' ? new URL(`http://localhost:${port}`) : undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Join a resolved origin + pathname into an absolute metadata URL, honoring the
|
|
625
|
+
// app's trailingSlash config (canonical/alternate URLs mirror the page URL). A
|
|
626
|
+
// path carrying a query or hash is left untouched — Next only normalizes the
|
|
627
|
+
// bare path, and `/metadata?query=string` must not become `/metadata/?...`.
|
|
628
|
+
function joinMetadataUrl(origin: string, pathname: string) {
|
|
629
|
+
if (/[?#]/.test(pathname)) {
|
|
630
|
+
return pathname === '/' ? origin : `${origin}${pathname}`;
|
|
631
|
+
}
|
|
632
|
+
if (getTrailingSlashUrls()) {
|
|
633
|
+
if (pathname === '/') return `${origin}/`;
|
|
634
|
+
return pathname.endsWith('/') ? `${origin}${pathname}` : `${origin}${pathname}/`;
|
|
635
|
+
}
|
|
636
|
+
return pathname === '/' ? origin : `${origin}${pathname}`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function metadataRelativePath(value: string) {
|
|
640
|
+
let next = value;
|
|
641
|
+
while (next.startsWith('./')) next = next.slice(2);
|
|
642
|
+
while (next.startsWith('../')) next = next.slice(3);
|
|
643
|
+
while (next.startsWith('/')) next = next.slice(1);
|
|
644
|
+
return next;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function joinUrlPath(...parts: string[]) {
|
|
648
|
+
const path = parts.filter(Boolean).join('/').replace(/\/+/g, '/');
|
|
649
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Normalizes every icon form (string | URL | object | group) into <link> descriptors.
|
|
653
|
+
function coerceIcons(value: Metadata['icons'], context: MetadataContext): MetadataLink[] {
|
|
654
|
+
if (value === undefined) return [];
|
|
655
|
+
if (isIconGroups(value)) {
|
|
656
|
+
return [
|
|
657
|
+
...iconList(value.icon, 'icon', context),
|
|
658
|
+
...iconList(value.shortcut, 'shortcut icon', context),
|
|
659
|
+
...iconList(value.apple, 'apple-touch-icon', context),
|
|
660
|
+
...iconList(value.other, 'icon', context),
|
|
661
|
+
];
|
|
662
|
+
}
|
|
663
|
+
return iconList(value, 'icon', context);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function isIconGroups(value: NonNullable<Metadata['icons']>): value is MetadataIconGroups {
|
|
667
|
+
if (Array.isArray(value) || typeof value === 'string' || value instanceof URL) return false;
|
|
668
|
+
if (typeof value !== 'object') return false;
|
|
669
|
+
return 'icon' in value || 'shortcut' in value || 'apple' in value || 'other' in value;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function iconList(
|
|
673
|
+
value: MetadataIcon | MetadataIcon[] | undefined,
|
|
674
|
+
rel: string,
|
|
675
|
+
context: MetadataContext,
|
|
676
|
+
): MetadataLink[] {
|
|
677
|
+
if (value === undefined) return [];
|
|
678
|
+
const list = Array.isArray(value) ? value : [value];
|
|
679
|
+
return list.map((icon): MetadataLink => {
|
|
680
|
+
if (typeof icon === 'string') return { rel, url: resolveMetadataUrl(icon, context) ?? icon };
|
|
681
|
+
if (icon instanceof URL) return { rel, url: icon.href };
|
|
682
|
+
const obj = icon;
|
|
683
|
+
return {
|
|
684
|
+
...obj,
|
|
685
|
+
rel: obj.rel ?? rel,
|
|
686
|
+
url: resolveMetadataUrl(obj.url, context) ?? coerceString(obj.url),
|
|
687
|
+
};
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function authorLinks(value: Metadata['authors'], context: MetadataContext): MetadataLink[] {
|
|
692
|
+
if (value === undefined) return [];
|
|
693
|
+
return (Array.isArray(value) ? value : [value])
|
|
694
|
+
.filter((author): author is MetadataAuthor => typeof author === 'object' && author !== null)
|
|
695
|
+
.flatMap(author => {
|
|
696
|
+
const url = resolveMetadataUrl(author.url, context);
|
|
697
|
+
return url ? [{ rel: 'author', url }] : [];
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function manifestLink(value: Metadata['manifest'], context: MetadataContext): MetadataLink[] {
|
|
702
|
+
const url = resolveMetadataUrl(value, context);
|
|
703
|
+
return url ? [{ rel: 'manifest', url }] : [];
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function paginationLinks(value: Metadata['pagination'], context: MetadataContext): MetadataLink[] {
|
|
707
|
+
return [
|
|
708
|
+
...(value?.previous
|
|
709
|
+
? [
|
|
710
|
+
{
|
|
711
|
+
rel: 'prev',
|
|
712
|
+
url: resolveMetadataUrl(value.previous, context) ?? coerceString(value.previous),
|
|
713
|
+
},
|
|
714
|
+
]
|
|
715
|
+
: []),
|
|
716
|
+
...(value?.next
|
|
717
|
+
? [{ rel: 'next', url: resolveMetadataUrl(value.next, context) ?? coerceString(value.next) }]
|
|
718
|
+
: []),
|
|
719
|
+
];
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function alternateLinks(value: Metadata['alternates'], context: MetadataContext): MetadataLink[] {
|
|
723
|
+
if (!value) return [];
|
|
724
|
+
const links: MetadataLink[] = [];
|
|
725
|
+
for (const [hrefLang, url] of Object.entries(value.languages ?? {})) {
|
|
726
|
+
links.push({
|
|
727
|
+
rel: 'alternate',
|
|
728
|
+
url: resolveMetadataUrl(url, context) ?? coerceString(url),
|
|
729
|
+
hrefLang,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
for (const [media, url] of Object.entries(value.media ?? {})) {
|
|
733
|
+
links.push({
|
|
734
|
+
rel: 'alternate',
|
|
735
|
+
media,
|
|
736
|
+
url: resolveMetadataUrl(url, context) ?? coerceString(url),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
for (const [type, entries] of Object.entries(value.types ?? {})) {
|
|
740
|
+
const listEntries = Array.isArray(entries) ? entries : [entries];
|
|
741
|
+
for (const entry of listEntries) {
|
|
742
|
+
const record =
|
|
743
|
+
typeof entry === 'object' && !(entry instanceof URL) && entry !== null && 'url' in entry
|
|
744
|
+
? entry
|
|
745
|
+
: { url: entry };
|
|
746
|
+
links.push({
|
|
747
|
+
rel: 'alternate',
|
|
748
|
+
type,
|
|
749
|
+
...(record.title ? { title: record.title } : {}),
|
|
750
|
+
url: resolveMetadataUrl(record.url, context) ?? coerceString(record.url),
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return links;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function appleStartupLinks(
|
|
758
|
+
value: Metadata['appleWebApp'],
|
|
759
|
+
context: MetadataContext,
|
|
760
|
+
): MetadataLink[] {
|
|
761
|
+
if (!value || typeof value !== 'object' || value.startupImage === undefined) return [];
|
|
762
|
+
const images = Array.isArray(value.startupImage) ? value.startupImage : [value.startupImage];
|
|
763
|
+
return images.map(image => {
|
|
764
|
+
if (typeof image === 'string' || image instanceof URL) {
|
|
765
|
+
return {
|
|
766
|
+
rel: 'apple-touch-startup-image',
|
|
767
|
+
url: resolveMetadataUrl(image, context) ?? coerceString(image),
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
rel: 'apple-touch-startup-image',
|
|
772
|
+
url: resolveMetadataUrl(image.url, context) ?? coerceString(image.url),
|
|
773
|
+
media: image.media,
|
|
774
|
+
};
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function list(value: unknown): string[] {
|
|
779
|
+
if (value === undefined) return [];
|
|
780
|
+
return Array.isArray(value) ? value.map(coerceString) : [coerceString(value)];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function coerceFormatDetection(value: Metadata['formatDetection']): string | undefined {
|
|
784
|
+
if (!value) return undefined;
|
|
785
|
+
const entries: string[] = [];
|
|
786
|
+
if (value.telephone === false) entries.push('telephone=no');
|
|
787
|
+
if (value.address === false) entries.push('address=no');
|
|
788
|
+
if (value.email === false) entries.push('email=no');
|
|
789
|
+
if (value.url === false) entries.push('url=no');
|
|
790
|
+
return entries.length ? entries.join(', ') : undefined;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function verificationTags(value: Metadata['verification']): [string, string][] {
|
|
794
|
+
if (!value) return [];
|
|
795
|
+
return [
|
|
796
|
+
...list(value.google).map(item => ['google-site-verification', item] as [string, string]),
|
|
797
|
+
...list(value.yahoo).map(item => ['y_key', item] as [string, string]),
|
|
798
|
+
...list(value.yandex).map(item => ['yandex-verification', item] as [string, string]),
|
|
799
|
+
];
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function itunesContent(value: Metadata['itunes']): string | undefined {
|
|
803
|
+
if (!value?.appId) return undefined;
|
|
804
|
+
return [
|
|
805
|
+
`app-id=${value.appId}`,
|
|
806
|
+
value.appArgument ? `app-argument=${value.appArgument}` : undefined,
|
|
807
|
+
]
|
|
808
|
+
.filter(Boolean)
|
|
809
|
+
.join(', ');
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function appleWebAppCapable(value: NonNullable<Metadata['appleWebApp']>) {
|
|
813
|
+
return typeof value === 'boolean' ? value : (value.capable ?? true);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function appLinkTags(value: Metadata['appLinks']): [string, string][] {
|
|
817
|
+
if (!value) return [];
|
|
818
|
+
const tags: [string, string][] = [];
|
|
819
|
+
for (const [platform, entries] of Object.entries(value)) {
|
|
820
|
+
for (const [name, item] of Object.entries(entries)) {
|
|
821
|
+
if (item !== undefined) tags.push([`al:${platform}:${name}`, String(item)]);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return tags;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function twitterCard(twitter: MetadataTwitter) {
|
|
828
|
+
if (twitter.card) return twitter.card;
|
|
829
|
+
if (twitter.players) return 'player';
|
|
830
|
+
if (twitter.app) return 'app';
|
|
831
|
+
return twitter.images === undefined ? 'summary' : 'summary_large_image';
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function twitterFromOpenGraph(
|
|
835
|
+
openGraph: MetadataOpenGraph | undefined,
|
|
836
|
+
): MetadataTwitter | undefined {
|
|
837
|
+
if (!openGraph) return undefined;
|
|
838
|
+
return {
|
|
839
|
+
title: openGraph.title,
|
|
840
|
+
description: openGraph.description,
|
|
841
|
+
images: openGraph.images,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export function mergeViewport(...items: Viewport[]) {
|
|
846
|
+
return items.reduce<Viewport>(
|
|
847
|
+
(viewport, item) => ({
|
|
848
|
+
...viewport,
|
|
849
|
+
...item,
|
|
850
|
+
}),
|
|
851
|
+
{},
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
856
|
+
return typeof value === 'object' && value !== null;
|
|
857
|
+
}
|