@ingram-tech/nk-seo 0.4.0 → 0.5.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 +123 -19
- package/dist/alternates.d.ts +41 -0
- package/dist/alternates.d.ts.map +1 -0
- package/dist/alternates.js +30 -0
- package/dist/alternates.js.map +1 -0
- package/dist/components.d.ts +1 -1
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +1 -1
- package/dist/components.js.map +1 -1
- package/dist/hreflang.d.ts +10 -18
- package/dist/hreflang.d.ts.map +1 -1
- package/dist/hreflang.js +14 -14
- package/dist/hreflang.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/json-ld.d.ts +1 -3
- package/dist/json-ld.d.ts.map +1 -1
- package/dist/json-ld.js +17 -5
- package/dist/json-ld.js.map +1 -1
- package/dist/metadata.d.ts +25 -2
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +24 -3
- package/dist/metadata.js.map +1 -1
- package/dist/og.d.ts +68 -0
- package/dist/og.d.ts.map +1 -0
- package/dist/og.js +62 -0
- package/dist/og.js.map +1 -0
- package/dist/routes.d.ts +91 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +78 -0
- package/dist/routes.js.map +1 -0
- package/dist/schema.d.ts +182 -27
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +32 -11
- package/dist/schema.js.map +1 -1
- package/dist/url.d.ts +8 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +8 -0
- package/dist/url.js.map +1 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @ingram-tech/nk-seo
|
|
2
2
|
|
|
3
|
-
SEO primitives for Next.js sites, factored out of the patterns
|
|
4
|
-
|
|
3
|
+
SEO primitives for Next.js sites, factored out of the patterns Next.js sites
|
|
4
|
+
keep re-implementing:
|
|
5
5
|
|
|
6
6
|
1. **`<JsonLd>`** + **typed schema.org builders** — `faqPage`, `breadcrumbList`,
|
|
7
7
|
`article`, `softwareApplication`, `organization`, `website`, `person`,
|
|
@@ -9,7 +9,12 @@ kept re-implementing:
|
|
|
9
9
|
paths and injects your publisher.
|
|
10
10
|
2. **`createMetadata`** — a Next `Metadata` factory: canonical + OpenGraph +
|
|
11
11
|
Twitter card from one title/description/path.
|
|
12
|
-
3.
|
|
12
|
+
3. **`createSitemap` / `createRobots`** — `app/sitemap.ts` and `app/robots.ts`
|
|
13
|
+
route helpers; `createRobots` blanket-disallows non-production hosts so
|
|
14
|
+
Vercel preview / branch URLs never get indexed.
|
|
15
|
+
4. **`ogImageResponse`** (`@ingram-tech/nk-seo/og`) — a branded `next/og` share
|
|
16
|
+
card that sidesteps the Satori multi-child pitfall.
|
|
17
|
+
5. **`<HreflangLinks>`** — self-referencing canonical plus per-locale `hreflang`
|
|
13
18
|
alternates (query-param or path-prefix strategy).
|
|
14
19
|
|
|
15
20
|
The package root (`@ingram-tech/nk-seo`) is **pure** — builders and the metadata
|
|
@@ -33,17 +38,17 @@ import { createSeo } from "@ingram-tech/nk-seo";
|
|
|
33
38
|
const seo = createSeo({
|
|
34
39
|
baseUrl: getServerUrl(),
|
|
35
40
|
organization: {
|
|
36
|
-
name: "
|
|
37
|
-
url: "https://
|
|
38
|
-
logo: "https://
|
|
39
|
-
sameAs: ["https://linkedin.com/company/
|
|
41
|
+
name: "Acme",
|
|
42
|
+
url: "https://example.com",
|
|
43
|
+
logo: "https://example.com/logo.png",
|
|
44
|
+
sameAs: ["https://www.linkedin.com/company/acme"],
|
|
40
45
|
},
|
|
41
46
|
});
|
|
42
47
|
|
|
43
48
|
// Homepage:
|
|
44
49
|
<JsonLd
|
|
45
50
|
data={seo.softwareApplication({
|
|
46
|
-
name: "
|
|
51
|
+
name: "Acme",
|
|
47
52
|
applicationCategory: "BusinessApplication",
|
|
48
53
|
operatingSystem: "Web",
|
|
49
54
|
offers: { priceCurrency: "EUR", lowPrice: 59, highPrice: 299, offerCount: 2, url: "/pricing" },
|
|
@@ -56,11 +61,17 @@ const seo = createSeo({
|
|
|
56
61
|
<JsonLd data={seo.breadcrumbs([{ name: "Home", path: "/" }, { name: "Blog", path: "/blog" }, { name: title, path: `/blog/${slug}` }])} />
|
|
57
62
|
```
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
`createSeo` resolves **nested** URL fields too — the organization's `url`/`logo`
|
|
65
|
+
and `offers.url` may be site-relative, as in the example above. Already have
|
|
66
|
+
absolute URLs and don't want the factory? The standalone builders (`faqPage`,
|
|
67
|
+
`article`, `breadcrumbList`, …) take absolute URLs directly (they resolve
|
|
68
|
+
nothing).
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
Every builder returns a typed node (`FaqPageNode`, `ArticleNode`, …), so the
|
|
71
|
+
shape survives past the call site. `<JsonLd data={...} />` accepts a single node
|
|
72
|
+
or an array (e.g. `[organization(org), website(site)]` on the homepage), and
|
|
73
|
+
escapes `<` on serialization so CMS-sourced strings can't break out of the
|
|
74
|
+
`<script>` tag.
|
|
64
75
|
|
|
65
76
|
## Page metadata
|
|
66
77
|
|
|
@@ -69,13 +80,17 @@ Already have absolute URLs and don't want the factory? The standalone builders
|
|
|
69
80
|
import { createMetadata } from "@ingram-tech/nk-seo";
|
|
70
81
|
|
|
71
82
|
export const pageMetadata = createMetadata({
|
|
72
|
-
baseUrl: "https://
|
|
73
|
-
siteName: "
|
|
83
|
+
baseUrl: "https://example.com",
|
|
84
|
+
siteName: "Acme",
|
|
85
|
+
titleTemplate: "%s | Acme",
|
|
74
86
|
defaultImage: "/images/og.png",
|
|
75
87
|
locale: "en_US",
|
|
76
|
-
twitterSite: "@
|
|
88
|
+
twitterSite: "@acme",
|
|
77
89
|
});
|
|
78
90
|
|
|
91
|
+
// app/layout.tsx — metadataBase + default title (+ template when configured):
|
|
92
|
+
export const metadata = pageMetadata.root({ description: "The Acme platform." });
|
|
93
|
+
|
|
79
94
|
// app/services/page.tsx
|
|
80
95
|
export const metadata = pageMetadata({
|
|
81
96
|
title: "Services",
|
|
@@ -86,7 +101,85 @@ export const metadata = pageMetadata({
|
|
|
86
101
|
|
|
87
102
|
Produces `title`, `description`, a self-referencing `alternates.canonical`,
|
|
88
103
|
`openGraph`, and a `summary_large_image` Twitter card. Pass `noIndex`, `keywords`,
|
|
89
|
-
`type: "article"`, or per-page `openGraph`/`twitter` overrides as needed.
|
|
104
|
+
`type: "article"`, or per-page `openGraph`/`twitter` overrides as needed. With
|
|
105
|
+
`titleTemplate` set, `pageMetadata.root()` emits `title.template`, so plain page
|
|
106
|
+
titles render as "Services | Acme" without every page appending the suffix.
|
|
107
|
+
|
|
108
|
+
## Sitemap & robots
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// app/sitemap.ts
|
|
112
|
+
import { createSitemap } from "@ingram-tech/nk-seo";
|
|
113
|
+
|
|
114
|
+
export default () =>
|
|
115
|
+
createSitemap({
|
|
116
|
+
baseUrl: getServerUrl(), // your deployment's own origin
|
|
117
|
+
routes: ["/", "/pricing", "/docs", "/faq", "/support"],
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`"/"` defaults to priority 1, every other route to 0.7; pass objects
|
|
122
|
+
(`{ path, lastModified, changeFrequency, priority, languages }`) to override, or
|
|
123
|
+
set `lastModified` / `defaultChangeFrequency` / `defaultPriority` site-wide.
|
|
124
|
+
Absolute URLs pass through untouched; a `priority` outside 0–1 throws (Google
|
|
125
|
+
would silently reject the whole entry). Localized routes can declare their
|
|
126
|
+
alternates — `languages: { en: "/about", fr: "/fr/about" }` — mirroring what
|
|
127
|
+
`<HreflangLinks>` emits on the page itself.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// app/robots.ts
|
|
131
|
+
import { createRobots } from "@ingram-tech/nk-seo";
|
|
132
|
+
|
|
133
|
+
export default () =>
|
|
134
|
+
createRobots({
|
|
135
|
+
baseUrl: getServerUrl(),
|
|
136
|
+
isProduction: process.env.VERCEL_ENV === "production",
|
|
137
|
+
disallow: ["/api/", "/internal/", "/login"],
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
When `isProduction` is false the whole site is disallowed — the one SEO
|
|
142
|
+
safeguard everyone forgets on Vercel, where preview and branch deployments are
|
|
143
|
+
otherwise crawlable and compete with the production domain for the same content.
|
|
144
|
+
|
|
145
|
+
## Open Graph image
|
|
146
|
+
|
|
147
|
+
`@ingram-tech/nk-seo/og` is a separate entry (it pulls in `next/og`), so the
|
|
148
|
+
package root and `/components` never carry the renderer.
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
// app/opengraph-image.tsx (and re-export from app/twitter-image.tsx)
|
|
152
|
+
import { ogImageResponse } from "@ingram-tech/nk-seo/og";
|
|
153
|
+
|
|
154
|
+
export const size = { width: 1200, height: 630 };
|
|
155
|
+
export const contentType = "image/png";
|
|
156
|
+
export const alt = "Acme — Ship faster";
|
|
157
|
+
|
|
158
|
+
export default () =>
|
|
159
|
+
ogImageResponse({
|
|
160
|
+
title: "Ship faster with Acme",
|
|
161
|
+
subtitle: "The all-in-one platform for modern teams.",
|
|
162
|
+
wordmark: "Acme",
|
|
163
|
+
footer: "example.com",
|
|
164
|
+
accent: "#565ac9",
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Pass `logo` (absolute URL or data URI) to replace the accent-square mark with
|
|
169
|
+
your logo, and `fonts` + `fontFamily` to render with the brand typeface —
|
|
170
|
+
`fonts` is forwarded to `ImageResponse`, which takes raw TTF/OTF/WOFF data.
|
|
171
|
+
|
|
172
|
+
The template encodes the Satori rule that trips everyone up: every node with
|
|
173
|
+
more than one child sets `display: flex`, and text nodes are never mixed with
|
|
174
|
+
sibling elements — so the headline stays a plain string and the accent rides on
|
|
175
|
+
the mark, not a coloured `<span>` inside the title.
|
|
176
|
+
|
|
177
|
+
No linter or type-check validates Satori-supported CSS (`ImageResponse` accepts
|
|
178
|
+
all of `React.CSSProperties`; Satori silently drops what it doesn't know), so
|
|
179
|
+
the only real validator is rendering. This package renders its template through
|
|
180
|
+
the real satori + resvg pipeline in its own tests; if a site hand-rolls extra
|
|
181
|
+
cards, give it the same guard — a vitest file (node environment) that renders
|
|
182
|
+
each `opengraph-image.tsx` and asserts a valid PNG comes out.
|
|
90
183
|
|
|
91
184
|
## Hreflang & canonical
|
|
92
185
|
|
|
@@ -103,11 +196,11 @@ res.headers.set("x-pathname", req.nextUrl.pathname);
|
|
|
103
196
|
import { HreflangLinks } from "@ingram-tech/nk-seo/components";
|
|
104
197
|
|
|
105
198
|
// Query-param locales (?hl=fr):
|
|
106
|
-
<HreflangLinks baseUrl="https://
|
|
199
|
+
<HreflangLinks baseUrl="https://example.com" locales={["en", "fr", "nl"]} />
|
|
107
200
|
|
|
108
201
|
// Path-prefix locales (/fr/about), default locale bare, regional hreflang tags:
|
|
109
202
|
<HreflangLinks
|
|
110
|
-
baseUrl="https://
|
|
203
|
+
baseUrl="https://example.com"
|
|
111
204
|
locales={["en", "fr", "nl"]}
|
|
112
205
|
strategy="prefix"
|
|
113
206
|
defaultLocale="en"
|
|
@@ -116,4 +209,15 @@ import { HreflangLinks } from "@ingram-tech/nk-seo/components";
|
|
|
116
209
|
/>
|
|
117
210
|
```
|
|
118
211
|
|
|
119
|
-
Pass `pathname` explicitly if you don't use the `x-pathname` header.
|
|
212
|
+
Pass `pathname` explicitly if you don't use the `x-pathname` header. When
|
|
213
|
+
neither is available the component **throws** instead of guessing — a silent
|
|
214
|
+
fallback would canonicalize every page to the homepage, the kind of site-wide
|
|
215
|
+
SEO bug nobody notices for months.
|
|
216
|
+
|
|
217
|
+
Two rules of the road:
|
|
218
|
+
|
|
219
|
+
- **One canonical per page.** If your pages already set `alternates.canonical`
|
|
220
|
+
(e.g. via `createMetadata`), render `<HreflangLinks canonical={false} />`.
|
|
221
|
+
- Building metadata instead of rendering links? The pure `hreflangAlternates`
|
|
222
|
+
(package root) returns the same `{ canonical, links }` for use in
|
|
223
|
+
`generateMetadata`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure hreflang computation behind `<HreflangLinks>` (from
|
|
3
|
+
* "@ingram-tech/nk-seo/components"). Usable directly wherever a component
|
|
4
|
+
* doesn't fit (e.g. building `Metadata.alternates` in `generateMetadata`).
|
|
5
|
+
*/
|
|
6
|
+
export interface HreflangConfig {
|
|
7
|
+
/** Absolute site origin, e.g. "https://acme.example". */
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
/** Locales to emit alternates for, e.g. ["en", "fr", "nl"]. */
|
|
10
|
+
locales: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* How locale is encoded in the alternate URLs:
|
|
13
|
+
* - `"query"` (default): `${baseUrl}${path}?${param}=${locale}` for every locale.
|
|
14
|
+
* - `"prefix"`: the default locale stays at the bare path; others get
|
|
15
|
+
* `/${locale}${path}` (matches a localized-rewrite setup).
|
|
16
|
+
*/
|
|
17
|
+
strategy?: "query" | "prefix";
|
|
18
|
+
/** Query-param name for the `"query"` strategy. Default `"hl"`. */
|
|
19
|
+
param?: string;
|
|
20
|
+
/** Default (unprefixed) locale for the `"prefix"` strategy. */
|
|
21
|
+
defaultLocale?: string;
|
|
22
|
+
/** Optional locale → hreflang tag map, e.g. `{ en: "en-BE", fr: "fr-BE" }`. */
|
|
23
|
+
hrefLangTags?: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
export interface HreflangLink {
|
|
26
|
+
/** The `hreflang` attribute value ("fr-BE", "x-default", …). */
|
|
27
|
+
hrefLang: string;
|
|
28
|
+
href: string;
|
|
29
|
+
}
|
|
30
|
+
export interface HreflangAlternates {
|
|
31
|
+
/** Self-referencing canonical URL of `pathname`. */
|
|
32
|
+
canonical: string;
|
|
33
|
+
/** One link per locale, plus the trailing `x-default` (→ canonical). */
|
|
34
|
+
links: HreflangLink[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Computes the self-referencing canonical plus per-locale hreflang alternate
|
|
38
|
+
* URLs (and an `x-default`) for `pathname`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function hreflangAlternates(config: HreflangConfig, pathname: string): HreflangAlternates;
|
|
41
|
+
//# sourceMappingURL=alternates.d.ts.map
|
|
@@ -0,0 +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,+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,CAyBpB"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { absoluteUrl } from "./url.js";
|
|
2
|
+
/**
|
|
3
|
+
* Computes the self-referencing canonical plus per-locale hreflang alternate
|
|
4
|
+
* URLs (and an `x-default`) for `pathname`.
|
|
5
|
+
*/
|
|
6
|
+
export function hreflangAlternates(config, pathname) {
|
|
7
|
+
const { strategy = "query", param = "hl", defaultLocale, hrefLangTags } = config;
|
|
8
|
+
const canonical = absoluteUrl(pathname, config.baseUrl);
|
|
9
|
+
const hrefFor = (locale) => {
|
|
10
|
+
if (strategy === "prefix") {
|
|
11
|
+
if (defaultLocale && locale === defaultLocale) {
|
|
12
|
+
return canonical;
|
|
13
|
+
}
|
|
14
|
+
const clean = pathname === "/" ? "" : pathname;
|
|
15
|
+
return absoluteUrl(`/${locale}${clean}`, config.baseUrl);
|
|
16
|
+
}
|
|
17
|
+
return `${canonical}?${param}=${locale}`;
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
canonical,
|
|
21
|
+
links: [
|
|
22
|
+
...config.locales.map((locale) => ({
|
|
23
|
+
hrefLang: hrefLangTags?.[locale] ?? locale,
|
|
24
|
+
href: hrefFor(locale),
|
|
25
|
+
})),
|
|
26
|
+
{ hrefLang: "x-default", href: canonical },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=alternates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alternates.js","sourceRoot":"","sources":["../src/alternates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAwCvC;;;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,MAAM,SAAS,GAAG,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAExD,MAAM,OAAO,GAAG,CAAC,MAAc,EAAU,EAAE;QAC1C,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC3B,IAAI,aAAa,IAAI,MAAM,KAAK,aAAa,EAAE,CAAC;gBAC/C,OAAO,SAAS,CAAC;YAClB,CAAC;YACD,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,SAAS,IAAI,KAAK,IAAI,MAAM,EAAE,CAAC;IAC1C,CAAC,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,SAAS,EAAE;SAC1C;KACD,CAAC;AACH,CAAC"}
|
package/dist/components.d.ts
CHANGED
package/dist/components.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/components.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// React entry: the rendering components. Kept out of the package root so server
|
|
2
2
|
// code that only needs the builders/metadata never imports React or next/headers.
|
|
3
|
-
export { JsonLd } from "./json-ld.js";
|
|
3
|
+
export { JsonLd, serializeJsonLd } from "./json-ld.js";
|
|
4
4
|
export { HreflangLinks } from "./hreflang.js";
|
|
5
5
|
//# sourceMappingURL=components.js.map
|
package/dist/components.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components.js","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,kFAAkF;AAClF,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"components.js","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,kFAAkF;AAClF,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,aAAa,EAA2B,MAAM,eAAe,CAAC"}
|
package/dist/hreflang.d.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
baseUrl: string;
|
|
4
|
-
/** Locales to emit `<link rel="alternate" hreflang>` for, e.g. ["en", "fr", "nl"]. */
|
|
5
|
-
locales: readonly string[];
|
|
1
|
+
import { type HreflangConfig } from "./alternates.js";
|
|
2
|
+
export interface HreflangLinksProps extends HreflangConfig {
|
|
6
3
|
/**
|
|
7
4
|
* Path being rendered. Defaults to the `x-pathname` request header (set it in
|
|
8
5
|
* middleware: `headers.set("x-pathname", req.nextUrl.pathname)`). Pass
|
|
@@ -10,25 +7,20 @@ export interface HreflangLinksProps {
|
|
|
10
7
|
*/
|
|
11
8
|
pathname?: string;
|
|
12
9
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `/${locale}${path}` (matches a localized-rewrite setup).
|
|
10
|
+
* Emit a self-referencing `<link rel="canonical">`. Default `true`.
|
|
11
|
+
* Disable it if the page's metadata already sets `alternates.canonical`
|
|
12
|
+
* (e.g. via `createMetadata`) — a page must not declare two canonicals.
|
|
17
13
|
*/
|
|
18
|
-
strategy?: "query" | "prefix";
|
|
19
|
-
/** Query-param name for the `"query"` strategy. Default `"hl"`. */
|
|
20
|
-
param?: string;
|
|
21
|
-
/** Default (unprefixed) locale for the `"prefix"` strategy. */
|
|
22
|
-
defaultLocale?: string;
|
|
23
|
-
/** Optional locale → hreflang tag map, e.g. `{ en: "en-BE", fr: "fr-BE" }`. */
|
|
24
|
-
hrefLangTags?: Record<string, string>;
|
|
25
|
-
/** Emit a self-referencing `<link rel="canonical">`. Default `true`. */
|
|
26
14
|
canonical?: boolean;
|
|
27
15
|
}
|
|
28
16
|
/**
|
|
29
17
|
* Emits `<link rel="canonical">` plus per-locale `<link rel="alternate" hreflang>`
|
|
30
18
|
* (and an `x-default`) for the current path. Render inside `<head>` (e.g. from
|
|
31
19
|
* the root layout). Server component — reads the `x-pathname` header by default.
|
|
20
|
+
*
|
|
21
|
+
* Throws when neither `pathname` nor the header is available: silently falling
|
|
22
|
+
* back would canonicalize every page to the homepage, a site-wide
|
|
23
|
+
* duplicate-content bug that is otherwise invisible.
|
|
32
24
|
*/
|
|
33
|
-
export declare function HreflangLinks({
|
|
25
|
+
export declare function HreflangLinks({ pathname, canonical, ...config }: HreflangLinksProps): Promise<import("react").JSX.Element>;
|
|
34
26
|
//# sourceMappingURL=hreflang.d.ts.map
|
package/dist/hreflang.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hreflang.d.ts","sourceRoot":"","sources":["../src/hreflang.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hreflang.d.ts","sourceRoot":"","sources":["../src/hreflang.tsx"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE1E,MAAM,WAAW,kBAAmB,SAAQ,cAAc;IACzD;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,EACnC,QAAQ,EACR,SAAgB,EAChB,GAAG,MAAM,EACT,EAAE,kBAAkB,wCAwBpB"}
|
package/dist/hreflang.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { headers } from "next/headers";
|
|
3
|
+
import { hreflangAlternates } from "./alternates.js";
|
|
3
4
|
/**
|
|
4
5
|
* Emits `<link rel="canonical">` plus per-locale `<link rel="alternate" hreflang>`
|
|
5
6
|
* (and an `x-default`) for the current path. Render inside `<head>` (e.g. from
|
|
6
7
|
* the root layout). Server component — reads the `x-pathname` header by default.
|
|
8
|
+
*
|
|
9
|
+
* Throws when neither `pathname` nor the header is available: silently falling
|
|
10
|
+
* back would canonicalize every page to the homepage, a site-wide
|
|
11
|
+
* duplicate-content bug that is otherwise invisible.
|
|
7
12
|
*/
|
|
8
|
-
export async function HreflangLinks({
|
|
9
|
-
const path = pathname ?? (await headers()).get("x-pathname")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return `${baseUrl}/${locale}${clean}`;
|
|
18
|
-
}
|
|
19
|
-
return `${canonicalUrl}?${param}=${locale}`;
|
|
20
|
-
};
|
|
21
|
-
return (_jsxs(_Fragment, { children: [canonical ? _jsx("link", { rel: "canonical", href: canonicalUrl }) : null, locales.map((locale) => (_jsx("link", { rel: "alternate", hrefLang: hrefLangTags?.[locale] ?? locale, href: hrefFor(locale) }, locale))), _jsx("link", { rel: "alternate", hrefLang: "x-default", href: canonicalUrl })] }));
|
|
13
|
+
export async function HreflangLinks({ pathname, canonical = true, ...config }) {
|
|
14
|
+
const path = pathname ?? (await headers()).get("x-pathname");
|
|
15
|
+
if (!path) {
|
|
16
|
+
throw new Error("HreflangLinks: no `pathname` prop and no `x-pathname` request header. " +
|
|
17
|
+
'Set the header in middleware (`res.headers.set("x-pathname", req.nextUrl.pathname)`) ' +
|
|
18
|
+
"or pass `pathname` explicitly.");
|
|
19
|
+
}
|
|
20
|
+
const { canonical: canonicalUrl, links } = hreflangAlternates(config, path);
|
|
21
|
+
return (_jsxs(_Fragment, { children: [canonical ? _jsx("link", { rel: "canonical", href: canonicalUrl }) : null, links.map((link) => (_jsx("link", { rel: "alternate", hrefLang: link.hrefLang, href: link.href }, link.hrefLang)))] }));
|
|
22
22
|
}
|
|
23
23
|
//# sourceMappingURL=hreflang.js.map
|
package/dist/hreflang.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hreflang.js","sourceRoot":"","sources":["../src/hreflang.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"hreflang.js","sourceRoot":"","sources":["../src/hreflang.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAAuB,MAAM,iBAAiB,CAAC;AAiB1E;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EACnC,QAAQ,EACR,SAAS,GAAG,IAAI,EAChB,GAAG,MAAM,EACW;IACpB,MAAM,IAAI,GAAG,QAAQ,IAAI,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC7D,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACd,wEAAwE;YACvE,uFAAuF;YACvF,gCAAgC,CACjC,CAAC;IACH,CAAC;IACD,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAE5E,OAAO,CACN,8BACE,SAAS,CAAC,CAAC,CAAC,eAAM,GAAG,EAAC,WAAW,EAAC,IAAI,EAAE,YAAY,GAAI,CAAC,CAAC,CAAC,IAAI,EAC/D,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACpB,eAEC,GAAG,EAAC,WAAW,EACf,QAAQ,EAAE,IAAI,CAAC,QAAQ,EACvB,IAAI,EAAE,IAAI,CAAC,IAAI,IAHV,IAAI,CAAC,QAAQ,CAIjB,CACF,CAAC,IACA,CACH,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export { type AggregateRatingInput, article, type ArticleAuthor, type ArticleInput, breadcrumbList, type BreadcrumbItem, type BreadcrumbPath, createSeo, event, type EventInput, type EventLocationInput, faqPage, type FaqItem, type GeoInput, type JsonLdNode, localBusiness, type LocalBusinessInput, type OfferInput, organization, type OrganizationInput, person, type PersonInput, type PostalAddressInput, type Seo, type SeoConfig, softwareApplication, type SoftwareApplicationInput, website, type WebsiteInput, } from "./schema.js";
|
|
2
|
-
export { createMetadata, type MetadataSiteConfig, type PageMetadata, type PageMetadataInput, } from "./metadata.js";
|
|
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";
|
|
3
|
+
export { createRobots, createSitemap, type RobotsConfig, type SitemapConfig, type SitemapLanguages, type SitemapRoute, } from "./routes.js";
|
|
4
|
+
export { hreflangAlternates, type HreflangAlternates, type HreflangConfig, type HreflangLink, } from "./alternates.js";
|
|
3
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACN,KAAK,oBAAoB,EACzB,OAAO,EACP,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,SAAS,EACT,KAAK,EACL,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,OAAO,EACP,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,YAAY,EACZ,KAAK,iBAAiB,EACtB,MAAM,EACN,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,GAAG,EACR,KAAK,SAAS,EACd,mBAAmB,EACnB,KAAK,wBAAwB,EAC7B,OAAO,EACP,KAAK,YAAY,
|
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -4,4 +4,6 @@
|
|
|
4
4
|
// "@ingram-tech/nk-seo/components".
|
|
5
5
|
export { article, breadcrumbList, createSeo, event, faqPage, localBusiness, organization, person, softwareApplication, website, } from "./schema.js";
|
|
6
6
|
export { createMetadata, } from "./metadata.js";
|
|
7
|
+
export { createRobots, createSitemap, } from "./routes.js";
|
|
8
|
+
export { hreflangAlternates, } from "./alternates.js";
|
|
7
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,
|
|
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"}
|
package/dist/json-ld.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
export declare const serializeJsonLd: (data: object | object[]) => string;
|
|
1
2
|
/**
|
|
2
3
|
* Renders one or more schema.org JSON-LD nodes into a
|
|
3
4
|
* `<script type="application/ld+json">`. Works in server and client components.
|
|
4
5
|
*
|
|
5
|
-
* The payload is trusted, server-built structured data (never user input), so
|
|
6
|
-
* serialising it into the script body is safe — that is how JSON-LD ships.
|
|
7
|
-
*
|
|
8
6
|
* @example
|
|
9
7
|
* <JsonLd data={faqPage(items)} />
|
|
10
8
|
* <JsonLd data={[organization(org), website(site)]} />
|
package/dist/json-ld.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-ld.d.ts","sourceRoot":"","sources":["../src/json-ld.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"json-ld.d.ts","sourceRoot":"","sources":["../src/json-ld.tsx"],"names":[],"mappings":"AAUA,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,GAAG,MAAM,EAAE,KAAG,MAMtD,CAAC;AAEL;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAAE,+BAQ3D"}
|
package/dist/json-ld.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* JSON.stringify hardened for inline `<script>` bodies. `<` is escaped so a
|
|
4
|
+
* string value containing `</script>` (FAQ answers and article headlines often
|
|
5
|
+
* come from a CMS) cannot terminate the tag and inject markup; U+2028/2029 are
|
|
6
|
+
* escaped because they are valid JSON but not valid JS source.
|
|
7
|
+
*/
|
|
8
|
+
// U+2028/U+2029 via RegExp() so the source stays ASCII (a literal invisible
|
|
9
|
+
// character would be one editor "normalization" away from silently vanishing).
|
|
10
|
+
const LINE_SEPARATORS = new RegExp("[\\u2028\\u2029]", "g");
|
|
11
|
+
export const serializeJsonLd = (data) => JSON.stringify(data)
|
|
12
|
+
.replace(/</g, "\\u003c")
|
|
13
|
+
.replace(LINE_SEPARATORS, (char) => {
|
|
14
|
+
const hex = char.codePointAt(0)?.toString(16) ?? "";
|
|
15
|
+
return "\\u" + hex;
|
|
16
|
+
});
|
|
2
17
|
/**
|
|
3
18
|
* Renders one or more schema.org JSON-LD nodes into a
|
|
4
19
|
* `<script type="application/ld+json">`. Works in server and client components.
|
|
5
20
|
*
|
|
6
|
-
* The payload is trusted, server-built structured data (never user input), so
|
|
7
|
-
* serialising it into the script body is safe — that is how JSON-LD ships.
|
|
8
|
-
*
|
|
9
21
|
* @example
|
|
10
22
|
* <JsonLd data={faqPage(items)} />
|
|
11
23
|
* <JsonLd data={[organization(org), website(site)]} />
|
|
12
24
|
*/
|
|
13
25
|
export function JsonLd({ data }) {
|
|
14
26
|
return (_jsx("script", { type: "application/ld+json",
|
|
15
|
-
// oxlint-disable-next-line no-danger --
|
|
16
|
-
dangerouslySetInnerHTML: { __html:
|
|
27
|
+
// oxlint-disable-next-line no-danger -- serializeJsonLd escapes `<`, so content strings cannot break out of the script tag
|
|
28
|
+
dangerouslySetInnerHTML: { __html: serializeJsonLd(data) } }));
|
|
17
29
|
}
|
|
18
30
|
//# sourceMappingURL=json-ld.js.map
|
package/dist/json-ld.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-ld.js","sourceRoot":"","sources":["../src/json-ld.tsx"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"json-ld.js","sourceRoot":"","sources":["../src/json-ld.tsx"],"names":[],"mappings":";AAAA;;;;;GAKG;AACH,4EAA4E;AAC5E,+EAA+E;AAC/E,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;AAE5D,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAuB,EAAU,EAAE,CAClE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAClB,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC;KACxB,OAAO,CAAC,eAAe,EAAE,CAAC,IAAI,EAAE,EAAE;IAClC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;IACpD,OAAO,KAAK,GAAG,GAAG,CAAC;AACpB,CAAC,CAAC,CAAC;AAEL;;;;;;;GAOG;AACH,MAAM,UAAU,MAAM,CAAC,EAAE,IAAI,EAA+B;IAC3D,OAAO,CACN,iBACC,IAAI,EAAC,qBAAqB;QAC1B,2HAA2H;QAC3H,uBAAuB,EAAE,EAAE,MAAM,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,GACzD,CACF,CAAC;AACH,CAAC"}
|
package/dist/metadata.d.ts
CHANGED
|
@@ -7,6 +7,11 @@ export interface MetadataSiteConfig {
|
|
|
7
7
|
baseUrl: string;
|
|
8
8
|
/** OpenGraph `siteName`. */
|
|
9
9
|
siteName: string;
|
|
10
|
+
/**
|
|
11
|
+
* `title.template` emitted by `pageMetadata.root()`, e.g. "%s | Acme".
|
|
12
|
+
* Applies to every child page whose title is a plain string.
|
|
13
|
+
*/
|
|
14
|
+
titleTemplate?: string;
|
|
10
15
|
/** Site-relative or absolute default OG/Twitter image. */
|
|
11
16
|
defaultImage?: string;
|
|
12
17
|
defaultImageWidth?: number;
|
|
@@ -34,21 +39,39 @@ export interface PageMetadataInput {
|
|
|
34
39
|
/** Merged into the generated `twitter` (wins on conflict). */
|
|
35
40
|
twitter?: Partial<NonNullable<Metadata["twitter"]>>;
|
|
36
41
|
}
|
|
42
|
+
/** Inputs for `pageMetadata.root()` — the root-layout metadata. */
|
|
43
|
+
export interface RootMetadataInput {
|
|
44
|
+
/** `title.default` for pages without their own. Defaults to `siteName`. */
|
|
45
|
+
title?: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
/** Extra `Metadata` merged over the generated object (wins on conflict). */
|
|
48
|
+
overrides?: Metadata;
|
|
49
|
+
}
|
|
37
50
|
/**
|
|
38
51
|
* Returns a `pageMetadata(input)` builder that produces a Next `Metadata`
|
|
39
52
|
* object with a self-referencing canonical, OpenGraph, and Twitter card from
|
|
40
53
|
* one title/description/path. Collapses the boilerplate every site repeats.
|
|
41
54
|
*
|
|
55
|
+
* `pageMetadata.root(input?)` builds the root-layout metadata: `metadataBase`,
|
|
56
|
+
* the default title (plus `titleTemplate` when configured), and description —
|
|
57
|
+
* pages then only set their own title/description/path.
|
|
58
|
+
*
|
|
42
59
|
* @example
|
|
43
|
-
* export const
|
|
60
|
+
* export const pageMetadata = createMetadata({
|
|
44
61
|
* baseUrl: "https://ingram.tech",
|
|
45
62
|
* siteName: "Ingram Technologies",
|
|
63
|
+
* titleTemplate: "%s | Ingram Technologies",
|
|
46
64
|
* defaultImage: "/images/og.png",
|
|
47
65
|
* twitterSite: "@IngramTech",
|
|
48
66
|
* });
|
|
67
|
+
* // app/layout.tsx:
|
|
68
|
+
* export const metadata = pageMetadata.root({ description: "AI deployment." });
|
|
49
69
|
* // in a page:
|
|
50
70
|
* export const metadata = pageMetadata({ title, description, path: "/services" });
|
|
51
71
|
*/
|
|
52
|
-
export declare function createMetadata(site: MetadataSiteConfig):
|
|
72
|
+
export declare function createMetadata(site: MetadataSiteConfig): {
|
|
73
|
+
(input: PageMetadataInput): Metadata;
|
|
74
|
+
root(input?: RootMetadataInput): Metadata;
|
|
75
|
+
};
|
|
53
76
|
export type PageMetadata = ReturnType<typeof createMetadata>;
|
|
54
77
|
//# sourceMappingURL=metadata.d.ts.map
|
package/dist/metadata.d.ts.map
CHANGED
|
@@ -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;
|
|
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;iBA6CR,iBAAiB,GAAQ,QAAQ;EAalF;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC"}
|
package/dist/metadata.js
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
|
+
import { absoluteUrl } from "./url.js";
|
|
1
2
|
/**
|
|
2
3
|
* Returns a `pageMetadata(input)` builder that produces a Next `Metadata`
|
|
3
4
|
* object with a self-referencing canonical, OpenGraph, and Twitter card from
|
|
4
5
|
* one title/description/path. Collapses the boilerplate every site repeats.
|
|
5
6
|
*
|
|
7
|
+
* `pageMetadata.root(input?)` builds the root-layout metadata: `metadataBase`,
|
|
8
|
+
* the default title (plus `titleTemplate` when configured), and description —
|
|
9
|
+
* pages then only set their own title/description/path.
|
|
10
|
+
*
|
|
6
11
|
* @example
|
|
7
|
-
* export const
|
|
12
|
+
* export const pageMetadata = createMetadata({
|
|
8
13
|
* baseUrl: "https://ingram.tech",
|
|
9
14
|
* siteName: "Ingram Technologies",
|
|
15
|
+
* titleTemplate: "%s | Ingram Technologies",
|
|
10
16
|
* defaultImage: "/images/og.png",
|
|
11
17
|
* twitterSite: "@IngramTech",
|
|
12
18
|
* });
|
|
19
|
+
* // app/layout.tsx:
|
|
20
|
+
* export const metadata = pageMetadata.root({ description: "AI deployment." });
|
|
13
21
|
* // in a page:
|
|
14
22
|
* export const metadata = pageMetadata({ title, description, path: "/services" });
|
|
15
23
|
*/
|
|
16
24
|
export function createMetadata(site) {
|
|
17
|
-
const absolute = (path) =>
|
|
18
|
-
|
|
25
|
+
const absolute = (path) => absoluteUrl(path, site.baseUrl);
|
|
26
|
+
function pageMetadata(input) {
|
|
19
27
|
const imagePath = input.image ?? site.defaultImage;
|
|
20
28
|
const imageUrl = imagePath ? absolute(imagePath) : undefined;
|
|
21
29
|
const url = absolute(input.path);
|
|
22
30
|
return {
|
|
31
|
+
metadataBase: new URL(site.baseUrl),
|
|
23
32
|
title: input.title,
|
|
24
33
|
description: input.description,
|
|
25
34
|
...(input.keywords ? { keywords: input.keywords } : {}),
|
|
@@ -56,6 +65,18 @@ export function createMetadata(site) {
|
|
|
56
65
|
...input.twitter,
|
|
57
66
|
},
|
|
58
67
|
};
|
|
68
|
+
}
|
|
69
|
+
pageMetadata.root = function rootMetadata(input = {}) {
|
|
70
|
+
const title = input.title ?? site.siteName;
|
|
71
|
+
return {
|
|
72
|
+
metadataBase: new URL(site.baseUrl),
|
|
73
|
+
title: site.titleTemplate
|
|
74
|
+
? { default: title, template: site.titleTemplate }
|
|
75
|
+
: title,
|
|
76
|
+
...(input.description ? { description: input.description } : {}),
|
|
77
|
+
...input.overrides,
|
|
78
|
+
};
|
|
59
79
|
};
|
|
80
|
+
return pageMetadata;
|
|
60
81
|
}
|
|
61
82
|
//# sourceMappingURL=metadata.js.map
|