@ingram-tech/nk-seo 0.6.2 → 0.8.0

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 CHANGED
@@ -5,8 +5,9 @@ keep re-implementing:
5
5
 
6
6
  1. **`<JsonLd>`** + **typed schema.org builders** — `faqPage`, `breadcrumbList`,
7
7
  `article`, `softwareApplication`, `organization`, `website`, `person`,
8
- `localBusiness`, `event`, and a `createSeo` factory that resolves site-relative
9
- paths and injects your publisher.
8
+ `localBusiness`, `event`, `dataset`, `definedTerm`/`definedTermSet`, and a
9
+ `createSeo` factory that resolves site-relative paths and injects your
10
+ publisher.
10
11
  2. **`createMetadata`** — a Next `Metadata` factory: canonical + OpenGraph +
11
12
  Twitter card from one title/description/path.
12
13
  3. **`createSitemap` / `createRobots`** — `app/sitemap.ts` and `app/robots.ts`
@@ -75,6 +76,53 @@ or an array (e.g. `[organization(org), website(site)]` on the homepage), and
75
76
  escapes `<` on serialization so CMS-sourced strings can't break out of the
76
77
  `<script>` tag.
77
78
 
79
+ ### Properties the builders don't cover
80
+
81
+ schema.org is much larger than any builder surface, so every builder takes an
82
+ `extra` object shallow-merged over the built payload (it wins on conflict).
83
+ Reach for it rather than dropping the whole node back to a literal:
84
+
85
+ ```ts
86
+ website({
87
+ name: "Acme",
88
+ url: "https://example.com",
89
+ extra: {
90
+ alternateName: "ACME",
91
+ inLanguage: ["en", "fr"],
92
+ potentialAction: {
93
+ "@type": "SearchAction",
94
+ target: "https://example.com/search?q={search_term_string}",
95
+ "query-input": "required name=search_term_string",
96
+ },
97
+ },
98
+ });
99
+ ```
100
+
101
+ `extra` works on nested inputs too — `publisher: { name: "Acme", extra: {…} }`.
102
+ Values passed this way aren't reflected in the returned node type.
103
+
104
+ For a node type no builder covers, `<JsonLd data={…} />` takes **any** object or
105
+ array of objects. Hand-written nodes still get the `<` / U+2028 escaping, which
106
+ is the security-relevant half of the package — worth preferring over
107
+ `dangerouslySetInnerHTML` even with no builder involved.
108
+
109
+ `article` covers `Article`, `BlogPosting`, `NewsArticle`, `ScholarlyArticle`,
110
+ `TechArticle` (the right type for API/developer docs) and `Report` via `type`.
111
+ `dataset` and `definedTerm`/`definedTermSet` mark up data and controlled
112
+ vocabularies:
113
+
114
+ ```ts
115
+ <JsonLd
116
+ data={seo.definedTerm({
117
+ name: "Computer programming",
118
+ termCode: "62.01",
119
+ path: "/code/62.01",
120
+ inDefinedTermSet: { name: "Acme classification", path: "/", version: "2025" },
121
+ extra: { inLanguage: "en" },
122
+ })}
123
+ />
124
+ ```
125
+
78
126
  ## Page metadata
79
127
 
80
128
  ```ts
@@ -103,10 +151,26 @@ export const metadata = pageMetadata({
103
151
 
104
152
  Produces `title`, `description`, a self-referencing `alternates.canonical`,
105
153
  `openGraph`, and a `summary_large_image` Twitter card. Pass `noIndex`, `keywords`,
106
- `type: "article"`, or per-page `openGraph`/`twitter` overrides as needed. With
154
+ `type: "article"`, or per-page `alternates`/`openGraph`/`twitter` overrides as
155
+ needed — each is merged into the generated object and wins on conflict. With
107
156
  `titleTemplate` set, `pageMetadata.root()` emits `title.template`, so plain page
108
157
  titles render as "Services | Acme" without every page appending the suffix.
109
158
 
159
+ On a localized site the per-page `alternates` and `locale` are what make the
160
+ factory usable — `alternates.languages` is what Next renders as
161
+ `<link rel="alternate" hreflang>`, and `og:locale` has to vary per page:
162
+
163
+ ```ts
164
+ import { hreflangAlternates } from "@ingram-tech/nk-seo";
165
+
166
+ const { languages } = hreflangAlternates(HREFLANG_CONFIG, `/${locale}/about`);
167
+ return pageMetadata({
168
+ title, description, path: `/${locale}/about`,
169
+ alternates: { languages },
170
+ locale: "fr_BE", // overrides the site-wide `locale`
171
+ });
172
+ ```
173
+
110
174
  ## Sitemap & robots
111
175
 
112
176
  ```ts
@@ -171,17 +235,50 @@ Pass `logo` (absolute URL or data URI) to replace the accent-square mark with
171
235
  your logo, and `fonts` + `fontFamily` to render with the brand typeface —
172
236
  `fonts` is forwarded to `ImageResponse`, which takes raw TTF/OTF/WOFF data.
173
237
 
238
+ ### Two ways the generated card silently doesn't reach your pages
239
+
240
+ Both fail with a green build and no warning; only fetching each page type shows
241
+ them.
242
+
243
+ **A page's own `openGraph` drops the inherited image.** Next attaches a
244
+ file-convention image to the segment that declares it, and sibling metadata
245
+ objects are *replaced*, not deep-merged — so any page whose `generateMetadata`
246
+ returns an `openGraph` object (i.e. any page setting `og:title`/`og:url`) emits
247
+ no `og:image` at all. The home page, sitting in the same segment as the image
248
+ file, looks perfectly correct throughout. Build the list once and spread it into
249
+ every page:
250
+
251
+ ```ts
252
+ import { ogImageMetadata } from "@ingram-tech/nk-seo";
253
+
254
+ const images = ogImageMetadata({ baseUrl, path: "/opengraph-image", alt: "Acme" });
255
+ return { title, openGraph: { title, url, images } };
256
+ ```
257
+
258
+ `createMetadata`'s `defaultImage` / per-page `image` already do this — point
259
+ either at the image route and every page it builds carries the card.
260
+
261
+ **Locale-negotiating middleware eats the image route.** A site that redirects
262
+ any path lacking a locale prefix also redirects `/opengraph-image`, so the card
263
+ URL never resolves. Put the file at `app/[locale]/opengraph-image.tsx` instead:
264
+ it dodges the redirect and gives per-locale cards from `params`. That is the
265
+ better default for any localized site.
266
+
174
267
  The template encodes the Satori rule that trips everyone up: every node with
175
268
  more than one child sets `display: flex`, and text nodes are never mixed with
176
269
  sibling elements — so the headline stays a plain string and the accent rides on
177
270
  the mark, not a coloured `<span>` inside the title.
178
271
 
179
- No linter or type-check validates Satori-supported CSS (`ImageResponse` accepts
180
- all of `React.CSSProperties`; Satori silently drops what it doesn't know), so
181
- the only real validator is rendering. This package renders its template through
182
- the real satori + resvg pipeline in its own tests; if a site hand-rolls extra
183
- cards, give it the same guard a vitest file (node environment) that renders
184
- each `opengraph-image.tsx` and asserts a valid PNG comes out.
272
+ Type-check won't help here: `ImageResponse` accepts all of
273
+ `React.CSSProperties` and Satori silently drops what it doesn't know. Two guards
274
+ cover that. `@ingram-tech/nk-dev`'s oxlint plugin ships `nextkit/satori-css`,
275
+ which flags unsupported style properties, `calc()`, and the two structural rules
276
+ in files that import `next/og` or are an `opengraph-image`/`twitter-image`
277
+ convention. And rendering remains the final validator: this package renders its
278
+ template through the real satori + resvg pipeline in its own tests, so a site
279
+ hand-rolling extra cards should add the same guard — a vitest file (node
280
+ environment) that renders each `opengraph-image.tsx` and asserts a valid PNG
281
+ comes out.
185
282
 
186
283
  ## Hreflang & canonical
187
284
 
@@ -219,6 +316,22 @@ import { HreflangLinks } from "@ingram-tech/nk-seo/components";
219
316
  />
220
317
  ```
221
318
 
319
+ Sites that prefix **every** locale — where `/about` redirects to `/en/about`
320
+ and no bare path exists — pass `prefixDefaultLocale`, which prefixes the default
321
+ locale too and points `x-default` at that prefixed URL. Without it the bare path
322
+ is emitted for both `en` and `x-default`, annotating URLs that redirect (Google
323
+ wants every hreflang target to answer 200 directly).
324
+
325
+ ```tsx
326
+ <HreflangLinks
327
+ baseUrl="https://example.com"
328
+ locales={["en", "fr", "nl"]}
329
+ strategy="prefix"
330
+ defaultLocale="en"
331
+ prefixDefaultLocale
332
+ />
333
+ ```
334
+
222
335
  Pass `pathname` explicitly if you don't use the `x-pathname` header. When
223
336
  neither is available the component **throws** instead of guessing — a silent
224
337
  fallback would canonicalize every page to the homepage, the kind of site-wide
@@ -228,11 +341,42 @@ Two rules of the road:
228
341
 
229
342
  - **One canonical per page.** If your pages already set `alternates.canonical`
230
343
  (e.g. via `createMetadata`), render `<HreflangLinks canonical={false} />`.
231
- - **The canonical must self-reference.** A localized variant that
232
- canonicalizes to a different URL makes Google discard the whole hreflang
233
- cluster. The prefix strategy auto-detects the current locale from the
234
- (possibly `/fr/…`-prefixed) pathname; the query strategy can't see the query
235
- string server-side, so pass `currentLocale` from your locale negotiation.
344
+ - **The canonical must self-reference, and it follows the ADDRESS, not the
345
+ rendered language.** A localized variant that canonicalizes to a different URL
346
+ makes Google discard the whole hreflang cluster. Under `"query"` the bare path
347
+ is the negotiating `x-default` and belongs to no locale, so `/pricing`
348
+ canonicalizes to `/pricing` even while negotiation renders it in French — pass
349
+ `currentLocale` only when the URL itself names a locale. The prefix strategy
350
+ auto-detects it from the (possibly `/fr/…`-prefixed) pathname; the query
351
+ strategy can't see the query string server-side, so
352
+ [`hreflangConfigFor(routing)`](../nk-i18n/README.md) from
353
+ `@ingram-tech/nk-i18n/next` is the wiring that gets this right for you.
236
354
  - Building metadata instead of rendering links? The pure `hreflangAlternates`
237
- (package root) returns the same `{ canonical, links }` for use in
238
- `generateMetadata`.
355
+ (package root) returns the same links for use in `generateMetadata`:
356
+ `{ canonical, links, languages }`, where `languages` is already keyed by
357
+ hreflang for `Metadata.alternates.languages` (or `createMetadata`'s
358
+ per-page `alternates`).
359
+
360
+ ## Verifying the cluster (`/verify`)
361
+
362
+ hreflang is a set of promises about **other** URLs, and nothing local can tell
363
+ you whether they hold. A site can emit a flawless cluster while its middleware
364
+ redirects every URL in it away, or while each variant quietly canonicalizes to
365
+ the default language. Both delete the non-default languages from search, neither
366
+ raises an error, and Search Console reports them as ordinary redirects and
367
+ duplicates months later. Fetch them:
368
+
369
+ ```ts
370
+ import { assertHreflangCluster } from "@ingram-tech/nk-seo/verify";
371
+ import { routing } from "@/lib/i18n/routing";
372
+
373
+ await assertHreflangCluster(routing, ["/", "/pricing", "/docs/getting-started"]);
374
+ ```
375
+
376
+ It fails on an advertised URL that redirects or isn't 200, a variant that
377
+ canonicalizes elsewhere, a missing or duplicated canonical, a non-reciprocal
378
+ cluster, and an `<html lang>` contradicting the `hreflang` it's advertised under.
379
+ `verifyHreflangCluster` returns the problems instead of throwing.
380
+
381
+ Run it against a real deployment — it is the only check that sees what a crawler
382
+ sees. A `LocaleRouting` from `@ingram-tech/nk-i18n` is a valid config for it.
@@ -12,19 +12,36 @@ export interface HreflangConfig {
12
12
  * How locale is encoded in the alternate URLs:
13
13
  * - `"query"` (default): `${baseUrl}${path}?${param}=${locale}` for every locale.
14
14
  * - `"prefix"`: the default locale stays at the bare path; others get
15
- * `/${locale}${path}` (matches a localized-rewrite setup).
15
+ * `/${locale}${path}` (matches a localized-rewrite setup). Set
16
+ * {@link HreflangConfig.prefixDefaultLocale} for sites that prefix every
17
+ * locale instead.
16
18
  */
17
19
  strategy?: "query" | "prefix";
18
20
  /** Query-param name for the `"query"` strategy. Default `"hl"`. */
19
21
  param?: string;
20
- /** Default (unprefixed) locale for the `"prefix"` strategy. */
22
+ /** Default locale. Required for `"prefix"`; it is what `x-default` resolves to. */
21
23
  defaultLocale?: string;
22
24
  /**
23
- * Locale of the page being rendered. Determines the self-referencing
24
- * canonical: a localized variant that canonicalizes to another URL makes
25
- * Google discard the entire hreflang cluster. For the `"prefix"` strategy it
26
- * is auto-detected from the pathname; for `"query"` the server can't see the
27
- * query string, so pass it (e.g. from your locale negotiation).
25
+ * `"prefix"` only: prefix the default locale too (`/en/about`, never
26
+ * `/about`), and point `x-default` at that prefixed URL. For sites whose
27
+ * locale negotiation redirects every bare path emitting the bare path as
28
+ * an alternate would annotate a URL that 3xx-redirects, which is the bug the
29
+ * mandatory `defaultLocale` exists to prevent.
30
+ */
31
+ prefixDefaultLocale?: boolean;
32
+ /**
33
+ * The locale **the URL names**, which is not always the locale that rendered.
34
+ * It determines the self-referencing canonical, and a canonical is a claim
35
+ * about an address: under `"query"`, `/pricing` canonicalizes to `/pricing`
36
+ * even while content negotiation renders it in French for a French visitor,
37
+ * because `/pricing` is the negotiating entry point and belongs to no locale.
38
+ * Passing the negotiated locale here instead makes the bare path claim to be
39
+ * the French URL, and the real French URL then looks like a duplicate.
40
+ *
41
+ * Leave it unset on a negotiating bare path. Under `"prefix"` it is detected
42
+ * from the pathname; under `"query"` the server cannot see the query string,
43
+ * so pass it — `hreflangConfigFor(routing)` from
44
+ * "@ingram-tech/nk-i18n/next" fills it in correctly.
28
45
  */
29
46
  currentLocale?: string;
30
47
  /** Optional locale → hreflang tag map, e.g. `{ en: "en-BE", fr: "fr-BE" }`. */
@@ -38,8 +55,14 @@ export interface HreflangLink {
38
55
  export interface HreflangAlternates {
39
56
  /** Self-referencing canonical URL of `pathname`. */
40
57
  canonical: string;
41
- /** One link per locale, plus the trailing `x-default` (→ canonical). */
58
+ /** One link per locale, plus the trailing `x-default`. */
42
59
  links: HreflangLink[];
60
+ /**
61
+ * The same links keyed by `hrefLang` (`x-default` included) — the shape Next
62
+ * wants for `Metadata.alternates.languages` / `createMetadata`'s
63
+ * `alternates`.
64
+ */
65
+ languages: Record<string, string>;
43
66
  }
44
67
  /**
45
68
  * Computes the self-referencing canonical plus per-locale hreflang alternate
@@ -1 +1 @@
1
- {"version":3,"file":"alternates.d.ts","sourceRoot":"","sources":["../src/alternates.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC9B,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,YAAY;IAC5B,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IAClC,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,KAAK,EAAE,YAAY,EAAE,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,GACd,kBAAkB,CA2DpB"}
1
+ {"version":3,"file":"alternates.d.ts","sourceRoot":"","sources":["../src/alternates.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC9B,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,YAAY;IAC5B,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IAClC,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,GACd,kBAAkB,CAuFpB"}
@@ -4,10 +4,12 @@ import { absoluteUrl } from "./url.js";
4
4
  * URLs (and an `x-default`) for `pathname`.
5
5
  */
6
6
  export function hreflangAlternates(config, pathname) {
7
- const { strategy = "query", param = "hl", defaultLocale, hrefLangTags } = config;
7
+ const { strategy = "query", param = "hl", defaultLocale, prefixDefaultLocale, hrefLangTags, } = config;
8
8
  if (strategy === "prefix" && !defaultLocale) {
9
- // Without it every locale gets a prefix while canonical/x-default point
10
- // at a bare path that is no locale's URL — a silent SEO bug.
9
+ // Without it canonical/x-default point at a bare path that is no
10
+ // locale's URL — a silent SEO bug. (With `prefixDefaultLocale` no bare
11
+ // path is emitted at all, but x-default still has to resolve to *some*
12
+ // locale, so the requirement stands.)
11
13
  throw new Error("hreflangAlternates: `defaultLocale` is required for the prefix strategy.");
12
14
  }
13
15
  // Prefix strategy: accept both the bare and the locale-prefixed form of the
@@ -26,34 +28,49 @@ export function hreflangAlternates(config, pathname) {
26
28
  }
27
29
  currentLocale ??= defaultLocale;
28
30
  }
29
- /** The default-locale URL the bare path; also serves as x-default. */
30
- const defaultUrl = absoluteUrl(basePath, config.baseUrl);
31
+ /** The bare (locale-free) URL of the path. */
32
+ const bareUrl = absoluteUrl(basePath, config.baseUrl);
33
+ const prefixedUrl = (locale) => absoluteUrl(`/${locale}${basePath === "/" ? "" : basePath}`, config.baseUrl);
31
34
  const hrefFor = (locale) => {
32
35
  if (strategy === "prefix") {
33
- if (locale === defaultLocale)
34
- return defaultUrl;
35
- const clean = basePath === "/" ? "" : basePath;
36
- return absoluteUrl(`/${locale}${clean}`, config.baseUrl);
36
+ if (locale === defaultLocale && !prefixDefaultLocale)
37
+ return bareUrl;
38
+ return prefixedUrl(locale);
37
39
  }
38
- return `${defaultUrl}${defaultUrl.includes("?") ? "&" : "?"}${param}=${locale}`;
40
+ return `${bareUrl}${bareUrl.includes("?") ? "&" : "?"}${param}=${locale}`;
39
41
  };
42
+ /** x-default: the default locale's own URL, prefixed or not. */
43
+ const defaultUrl = strategy === "prefix" && prefixDefaultLocale && defaultLocale
44
+ ? prefixedUrl(defaultLocale)
45
+ : bareUrl;
40
46
  // Self-referencing canonical: the current variant's own URL. Canonicalizing
41
47
  // a localized variant to the bare path makes Google treat the variants as
42
48
  // duplicates and ignore the hreflang annotations entirely.
49
+ //
50
+ // Whether the default locale has a URL of its own is strategy-dependent, and
51
+ // conflating the two is a silent way to delete the default locale from the
52
+ // cluster. Under `"prefix"` it shares the bare path (unless every locale is
53
+ // prefixed), so its canonical IS the bare path. Under `"query"` every locale
54
+ // gets its own `?param=` address and the bare path belongs to none of them:
55
+ // it is the negotiating entry point that `x-default` names. So `?hl=en` must
56
+ // canonicalize to `?hl=en`, never to the bare path.
57
+ const defaultLocaleOwnsBarePath = strategy === "prefix" && !prefixDefaultLocale;
43
58
  const canonical = currentLocale &&
44
- currentLocale !== defaultLocale &&
45
- config.locales.includes(currentLocale)
59
+ config.locales.includes(currentLocale) &&
60
+ !(defaultLocaleOwnsBarePath && currentLocale === defaultLocale)
46
61
  ? hrefFor(currentLocale)
47
62
  : defaultUrl;
63
+ const links = [
64
+ ...config.locales.map((locale) => ({
65
+ hrefLang: hrefLangTags?.[locale] ?? locale,
66
+ href: hrefFor(locale),
67
+ })),
68
+ { hrefLang: "x-default", href: defaultUrl },
69
+ ];
48
70
  return {
49
71
  canonical,
50
- links: [
51
- ...config.locales.map((locale) => ({
52
- hrefLang: hrefLangTags?.[locale] ?? locale,
53
- href: hrefFor(locale),
54
- })),
55
- { hrefLang: "x-default", href: defaultUrl },
56
- ],
72
+ links,
73
+ languages: Object.fromEntries(links.map((link) => [link.hrefLang, link.href])),
57
74
  };
58
75
  }
59
76
  //# sourceMappingURL=alternates.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"alternates.js","sourceRoot":"","sources":["../src/alternates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAgDvC;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CACjC,MAAsB,EACtB,QAAgB;IAEhB,MAAM,EAAE,QAAQ,GAAG,OAAO,EAAE,KAAK,GAAG,IAAI,EAAE,aAAa,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC;IACjF,IAAI,QAAQ,KAAK,QAAQ,IAAI,CAAC,aAAa,EAAE,CAAC;QAC7C,wEAAwE;QACxE,6DAA6D;QAC7D,MAAM,IAAI,KAAK,CACd,0EAA0E,CAC1E,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,yEAAyE;IACzE,qEAAqE;IACrE,0BAA0B;IAC1B,IAAI,QAAQ,GAAG,QAAQ,CAAC;IACxB,IAAI,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC;IACzC,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC3B,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACrC,IAAI,QAAQ,KAAK,IAAI,MAAM,EAAE,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrE,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC;gBACpD,aAAa,KAAK,MAAM,CAAC;gBACzB,MAAM;YACP,CAAC;QACF,CAAC;QACD,aAAa,KAAK,aAAa,CAAC;IACjC,CAAC;IAED,wEAAwE;IACxE,MAAM,UAAU,GAAG,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAEzD,MAAM,OAAO,GAAG,CAAC,MAAc,EAAU,EAAE;QAC1C,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC3B,IAAI,MAAM,KAAK,aAAa;gBAAE,OAAO,UAAU,CAAC;YAChD,MAAM,KAAK,GAAG,QAAQ,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;YAC/C,OAAO,WAAW,CAAC,IAAI,MAAM,GAAG,KAAK,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QAC1D,CAAC;QACD,OAAO,GAAG,UAAU,GAAG,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,KAAK,IAAI,MAAM,EAAE,CAAC;IACjF,CAAC,CAAC;IAEF,4EAA4E;IAC5E,0EAA0E;IAC1E,2DAA2D;IAC3D,MAAM,SAAS,GACd,aAAa;QACb,aAAa,KAAK,aAAa;QAC/B,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;QACrC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC;QACxB,CAAC,CAAC,UAAU,CAAC;IAEf,OAAO;QACN,SAAS;QACT,KAAK,EAAE;YACN,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;gBAClC,QAAQ,EAAE,YAAY,EAAE,CAAC,MAAM,CAAC,IAAI,MAAM;gBAC1C,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC;aACrB,CAAC,CAAC;YACH,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE;SAC3C;KACD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"alternates.js","sourceRoot":"","sources":["../src/alternates.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { type AggregateOfferNode, type AggregateRatingInput, type AggregateRatingNode, article, type ArticleAuthor, type ArticleAuthorNode, type ArticleInput, type ArticleNode, breadcrumbList, type BreadcrumbItem, type BreadcrumbListNode, type BreadcrumbPath, createSeo, event, type EventInput, type EventLocationInput, type EventNode, faqPage, type FaqItem, type FaqPageNode, type GeoCoordinatesNode, type GeoInput, type ImageObjectNode, type JsonLdNode, type ListItemNode, localBusiness, type LocalBusinessInput, type LocalBusinessNode, type OfferInput, type OfferNode, organization, type OrganizationInput, type OrganizationNode, person, type PersonInput, type PersonNode, type PlaceNode, type PostalAddressInput, type PostalAddressNode, type QuestionNode, type Seo, type SeoConfig, softwareApplication, type SoftwareApplicationInput, type SoftwareApplicationNode, type VirtualLocationNode, website, type WebsiteInput, type WebsiteNode, type WithContext, } from "./schema.js";
2
- export { createMetadata, type MetadataSiteConfig, type PageMetadata, type PageMetadataInput, type RootMetadataInput, } from "./metadata.js";
1
+ export { type AggregateOfferNode, type AggregateRatingInput, type AggregateRatingNode, article, type ArticleAuthor, type ArticleAuthorNode, type ArticleInput, type ArticleNode, type ArticleType, breadcrumbList, type BreadcrumbItem, type BreadcrumbListNode, type BreadcrumbPath, createSeo, type DataDownloadInput, type DataDownloadNode, dataset, type DatasetInput, type DatasetNode, definedTerm, type DefinedTermInput, type DefinedTermNode, definedTermSet, type DefinedTermSetInput, type DefinedTermSetNode, event, type EventInput, type EventLocationInput, type EventNode, faqPage, type FaqItem, type FaqPageNode, type GeoCoordinatesNode, type GeoInput, type ImageObjectNode, type JsonLdNode, type ListItemNode, localBusiness, type LocalBusinessInput, type LocalBusinessNode, type OfferInput, type OfferNode, organization, type OrganizationInput, type OrganizationNode, person, type PersonInput, type PersonNode, type PlaceNode, type PostalAddressInput, type PostalAddressNode, type QuestionNode, type Seo, type SeoConfig, softwareApplication, type SoftwareApplicationInput, type SoftwareApplicationNode, type VirtualLocationNode, website, type WebsiteInput, type WebsiteNode, type WithContext, type WithExtra, } from "./schema.js";
2
+ export { createMetadata, type MetadataSiteConfig, ogImageMetadata, type OgImageMetadataInput, type PageMetadata, type PageMetadataInput, type RootMetadataInput, } from "./metadata.js";
3
3
  export { createRobots, createSitemap, type RobotsConfig, type SitemapConfig, type SitemapLanguages, type SitemapRoute, } from "./routes.js";
4
4
  export { hreflangAlternates, type HreflangAlternates, type HreflangConfig, type HreflangLink, } from "./alternates.js";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,OAAO,EACP,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,SAAS,EACT,KAAK,EACL,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,OAAO,EACP,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,MAAM,EACN,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,GAAG,EACR,KAAK,SAAS,EACd,mBAAmB,EACnB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,WAAW,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,cAAc,EACd,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,YAAY,EACZ,aAAa,EACb,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,YAAY,GACjB,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,OAAO,EACP,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,SAAS,EACT,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,cAAc,EACd,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,EACL,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,OAAO,EACP,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,MAAM,EACN,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,GAAG,EACR,KAAK,SAAS,EACd,mBAAmB,EACnB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,SAAS,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,cAAc,EACd,KAAK,kBAAkB,EACvB,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,YAAY,EACZ,aAAa,EACb,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,YAAY,GACjB,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
  // route handlers / sitemap.ts can import these without pulling in the renderer.
3
3
  // The <JsonLd> and <HreflangLinks> components live at
4
4
  // "@ingram-tech/nk-seo/components".
5
- export { article, breadcrumbList, createSeo, event, faqPage, localBusiness, organization, person, softwareApplication, website, } from "./schema.js";
6
- export { createMetadata, } from "./metadata.js";
5
+ export { article, breadcrumbList, createSeo, dataset, definedTerm, definedTermSet, event, faqPage, localBusiness, organization, person, softwareApplication, website, } from "./schema.js";
6
+ export { createMetadata, ogImageMetadata, } from "./metadata.js";
7
7
  export { createRobots, createSitemap, } from "./routes.js";
8
8
  export { hreflangAlternates, } from "./alternates.js";
9
9
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,sDAAsD;AACtD,oCAAoC;AACpC,OAAO,EAIN,OAAO,EAKP,cAAc,EAId,SAAS,EACT,KAAK,EAIL,OAAO,EAQP,aAAa,EAKb,YAAY,EAGZ,MAAM,EASN,mBAAmB,EAInB,OAAO,GAIP,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,cAAc,GAKd,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,YAAY,EACZ,aAAa,GAKb,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,kBAAkB,GAIlB,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,sDAAsD;AACtD,oCAAoC;AACpC,OAAO,EAIN,OAAO,EAMP,cAAc,EAId,SAAS,EAGT,OAAO,EAGP,WAAW,EAGX,cAAc,EAGd,KAAK,EAIL,OAAO,EAQP,aAAa,EAKb,YAAY,EAGZ,MAAM,EASN,mBAAmB,EAInB,OAAO,GAKP,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,cAAc,EAEd,eAAe,GAKf,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,YAAY,EACZ,aAAa,GAKb,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,kBAAkB,GAIlB,MAAM,iBAAiB,CAAC"}
@@ -16,7 +16,10 @@ export interface MetadataSiteConfig {
16
16
  defaultImage?: string;
17
17
  defaultImageWidth?: number;
18
18
  defaultImageHeight?: number;
19
- /** OpenGraph locale, e.g. "en_US". */
19
+ /**
20
+ * Default OpenGraph locale, e.g. "en_US". On a multilingual site set the
21
+ * per-page `locale` instead (or as well, as the fallback).
22
+ */
20
23
  locale?: string;
21
24
  /** Twitter `@handle` for `site`. */
22
25
  twitterSite?: string;
@@ -34,6 +37,18 @@ export interface PageMetadataInput {
34
37
  type?: "website" | "article";
35
38
  keywords?: Metadata["keywords"];
36
39
  noIndex?: boolean;
40
+ /**
41
+ * OpenGraph locale for this page, e.g. "fr_BE". Overrides `site.locale` —
42
+ * on a multilingual site `og:locale` has to vary per page.
43
+ */
44
+ locale?: string;
45
+ /**
46
+ * Merged into the generated `alternates` (wins on conflict). The reason it
47
+ * exists: `alternates.languages` is per-page on a localized site, and
48
+ * spreading the result to bolt it on drops the rest of the object.
49
+ * `hreflangAlternates()` returns a ready-made `languages` map.
50
+ */
51
+ alternates?: Partial<NonNullable<Metadata["alternates"]>>;
37
52
  /** Merged into the generated `openGraph` (wins on conflict). */
38
53
  openGraph?: Partial<NonNullable<Metadata["openGraph"]>>;
39
54
  /** Merged into the generated `twitter` (wins on conflict). */
@@ -74,4 +89,43 @@ export declare function createMetadata(site: MetadataSiteConfig): {
74
89
  root: (input?: RootMetadataInput) => Metadata;
75
90
  };
76
91
  export type PageMetadata = ReturnType<typeof createMetadata>;
92
+ /** Inputs for {@link ogImageMetadata}. */
93
+ export interface OgImageMetadataInput {
94
+ /** Absolute site origin, e.g. "https://acme.example". */
95
+ baseUrl: string;
96
+ /**
97
+ * Path of the image route — the route the `opengraph-image.tsx` file
98
+ * convention serves, e.g. "/opengraph-image" or "/en/opengraph-image" when
99
+ * the file sits under `app/[locale]/`.
100
+ */
101
+ path: string;
102
+ alt: string;
103
+ /** Defaults to the 1200×630 the `/og` template renders. */
104
+ width?: number;
105
+ height?: number;
106
+ }
107
+ /**
108
+ * Builds the `openGraph.images` entry for a card served by Next's
109
+ * `opengraph-image` file convention.
110
+ *
111
+ * Next attaches a file-convention image only to the segment that declares it,
112
+ * and a page's own `openGraph` object *replaces* the inherited one rather than
113
+ * merging into it — so any page returning `openGraph` from `generateMetadata`
114
+ * silently loses the card. Build the list once and spread it into every page.
115
+ *
116
+ * `createMetadata`'s `defaultImage` / per-page `image` already do this; reach
117
+ * for the standalone helper when you assemble `Metadata` by hand.
118
+ *
119
+ * @example
120
+ * const images = ogImageMetadata({
121
+ * baseUrl, path: `/${locale}/opengraph-image`, alt: "Acme",
122
+ * });
123
+ * return { title, openGraph: { title, url, images } };
124
+ */
125
+ export declare function ogImageMetadata(input: OgImageMetadataInput): {
126
+ url: string;
127
+ width: number;
128
+ height: number;
129
+ alt: string;
130
+ }[];
77
131
  //# sourceMappingURL=metadata.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAGrC;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,QAAQ,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IACxD,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;CACpD;AAED,mEAAmE;AACnE,MAAM,WAAW,iBAAiB;IACjC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,kBAAkB;YAGzB,iBAAiB,GAAG,QAAQ;mBA+CR,iBAAiB,KAAQ,QAAQ;EAalF;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC"}
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAGrC;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,QAAQ,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAC1D,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IACxD,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;CACpD;AAED,mEAAmE;AACnE,MAAM,WAAW,iBAAiB;IACjC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,kBAAkB;YAGzB,iBAAiB,GAAG,QAAQ;mBAgDR,iBAAiB,KAAQ,QAAQ;EAalF;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AAE7D,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB;;;;;IAS1D"}
package/dist/metadata.js CHANGED
@@ -27,12 +27,13 @@ export function createMetadata(site) {
27
27
  const imagePath = input.image ?? site.defaultImage;
28
28
  const imageUrl = imagePath ? absolute(imagePath) : undefined;
29
29
  const url = absolute(input.path);
30
+ const locale = input.locale ?? site.locale;
30
31
  return {
31
32
  metadataBase: new URL(site.baseUrl),
32
33
  title: input.title,
33
34
  description: input.description,
34
35
  ...(input.keywords ? { keywords: input.keywords } : {}),
35
- alternates: { canonical: url },
36
+ alternates: { canonical: url, ...input.alternates },
36
37
  // noindex but follow: keep link equity flowing through the page. Use a
37
38
  // full robots override in the page's own metadata for nofollow too.
38
39
  ...(input.noIndex ? { robots: { index: false, follow: true } } : {}),
@@ -42,7 +43,7 @@ export function createMetadata(site) {
42
43
  type: input.type ?? "website",
43
44
  url,
44
45
  siteName: site.siteName,
45
- ...(site.locale ? { locale: site.locale } : {}),
46
+ ...(locale ? { locale } : {}),
46
47
  ...(imageUrl
47
48
  ? {
48
49
  images: [
@@ -81,4 +82,32 @@ export function createMetadata(site) {
81
82
  };
82
83
  return pageMetadata;
83
84
  }
85
+ /**
86
+ * Builds the `openGraph.images` entry for a card served by Next's
87
+ * `opengraph-image` file convention.
88
+ *
89
+ * Next attaches a file-convention image only to the segment that declares it,
90
+ * and a page's own `openGraph` object *replaces* the inherited one rather than
91
+ * merging into it — so any page returning `openGraph` from `generateMetadata`
92
+ * silently loses the card. Build the list once and spread it into every page.
93
+ *
94
+ * `createMetadata`'s `defaultImage` / per-page `image` already do this; reach
95
+ * for the standalone helper when you assemble `Metadata` by hand.
96
+ *
97
+ * @example
98
+ * const images = ogImageMetadata({
99
+ * baseUrl, path: `/${locale}/opengraph-image`, alt: "Acme",
100
+ * });
101
+ * return { title, openGraph: { title, url, images } };
102
+ */
103
+ export function ogImageMetadata(input) {
104
+ return [
105
+ {
106
+ url: absoluteUrl(input.path, input.baseUrl),
107
+ width: input.width ?? 1200,
108
+ height: input.height ?? 630,
109
+ alt: input.alt,
110
+ },
111
+ ];
112
+ }
84
113
  //# sourceMappingURL=metadata.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}