From c4c8e273deb451b5ff72e461bb118f67bcd71bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Re=C3=A9?= Date: Sat, 4 Jul 2026 20:30:24 +0200 Subject: [PATCH] Add Metadata: full App Router metadata binding Typed subset of the Metadata API shaped to the official recommendations: metadataBase (JS URL), the three title shapes (plain/template/absolute) behind one abstract type with typed constructors, alternates.canonical, openGraph (+images), icons, manifest, robots. Supersedes the minimal Next.Metadata, which stays untouched for compatibility. Extracted from njc-2026 (sc-38). --- src/Metadata.res | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ src/Next.res | 7 ++++- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/Metadata.res diff --git a/src/Metadata.res b/src/Metadata.res new file mode 100644 index 0000000..dac2afb --- /dev/null +++ b/src/Metadata.res @@ -0,0 +1,76 @@ +/** + * Typed subset of the Next.js App Router Metadata API — the `metadata` export + * and the `generateMetadata` return value — following the official + * recommendations: the TITLE TEMPLATE lives in the root layout and pages + * return plain title strings (or opt out with an absolute title); + * `metadataBase` lives in the root layout so per-page canonical and OpenGraph + * URLs can stay relative. Structural: Next consumes the plain object shape. + * + * Supersedes the minimal `Next.Metadata` (kept for compatibility). + * + * @example + * + * // Root layout + * let metadata: Metadata.t = { + * metadataBase: Metadata.makeUrl("https://example.com"), + * title: Metadata.titleTemplate({default: "Site", template: "%s | Site"}), + * description: "Default description", + * openGraph: {siteName: "Site", type_: "website"}, + * icons: [{rel: "icon", url: "/favicon.ico", sizes: "32x32"}], + * } + * + * // Page-level generateMetadata + * let generateMetadata = async (props): Metadata.t => { + * ... + * { + * title: Metadata.title(pageTitle), // templated by the root layout + * alternates: {canonical: path}, // relative, resolved by metadataBase + * } + * } + */ +// metadataBase must be a JS URL instance. +type url +@new external makeUrl: string => url = "URL" + +// `title` is string | {default, template} | {absolute}: three runtime shapes +// behind one abstract type, with typed constructors. +type title +type titleTemplate = {default: string, template: string} +type titleAbsolute = {absolute: string} +external title: string => title = "%identity" +external titleTemplate: titleTemplate => title = "%identity" +external titleAbsolute: titleAbsolute => title = "%identity" + +type alternates = {canonical?: string} + +type ogImage = {url: string, width?: int, height?: int, alt?: string} + +type openGraph = { + title?: string, + description?: string, + url?: string, + siteName?: string, + locale?: string, + @as("type") type_?: string, + images?: array, +} + +type icon = { + rel: string, + url: string, + @as("type") type_?: string, + sizes?: string, +} + +type robots = {index?: bool, follow?: bool} + +type t = { + metadataBase?: url, + title?: title, + description?: string, + alternates?: alternates, + openGraph?: openGraph, + icons?: array, + manifest?: string, + robots?: robots, +} diff --git a/src/Next.res b/src/Next.res index f97a4d6..5a4e2a5 100644 --- a/src/Next.res +++ b/src/Next.res @@ -339,7 +339,12 @@ module Headers = { } /** - * Module for handling Next.js metadata configuration + * Module for handling Next.js metadata configuration. + * + * DEPRECATED: kept only to support older code. New code should use the + * top-level `Metadata` module instead, which covers the full App Router + * Metadata API (metadataBase, title templates, canonical, OpenGraph, robots) + * and is a superset of this one. */ module Metadata = { /**