@ingram-tech/nk-seo 0.4.0 → 0.6.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 +142 -23
- package/dist/alternates.d.ts +49 -0
- package/dist/alternates.d.ts.map +1 -0
- package/dist/alternates.js +59 -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 +23 -21
- package/dist/hreflang.d.ts.map +1 -1
- package/dist/hreflang.js +16 -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 +27 -4
- 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 +14 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +23 -0
- package/dist/url.js.map +1 -0
- package/package.json +7 -2
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
|
|
@@ -22,7 +27,9 @@ components live at `@ingram-tech/nk-seo/components`.
|
|
|
22
27
|
bun add @ingram-tech/nk-seo
|
|
23
28
|
```
|
|
24
29
|
|
|
25
|
-
`next` and `react` are optional peers
|
|
30
|
+
`next` and `react` are optional peers — the package root is runtime-free of
|
|
31
|
+
both; the `/components` and `/og` entries need them (`/og` uses `next/og` and
|
|
32
|
+
the React JSX runtime).
|
|
26
33
|
|
|
27
34
|
## Structured data
|
|
28
35
|
|
|
@@ -33,17 +40,17 @@ import { createSeo } from "@ingram-tech/nk-seo";
|
|
|
33
40
|
const seo = createSeo({
|
|
34
41
|
baseUrl: getServerUrl(),
|
|
35
42
|
organization: {
|
|
36
|
-
name: "
|
|
37
|
-
url: "https://
|
|
38
|
-
logo: "https://
|
|
39
|
-
sameAs: ["https://linkedin.com/company/
|
|
43
|
+
name: "Acme",
|
|
44
|
+
url: "https://example.com",
|
|
45
|
+
logo: "https://example.com/logo.png",
|
|
46
|
+
sameAs: ["https://www.linkedin.com/company/acme"],
|
|
40
47
|
},
|
|
41
48
|
});
|
|
42
49
|
|
|
43
50
|
// Homepage:
|
|
44
51
|
<JsonLd
|
|
45
52
|
data={seo.softwareApplication({
|
|
46
|
-
name: "
|
|
53
|
+
name: "Acme",
|
|
47
54
|
applicationCategory: "BusinessApplication",
|
|
48
55
|
operatingSystem: "Web",
|
|
49
56
|
offers: { priceCurrency: "EUR", lowPrice: 59, highPrice: 299, offerCount: 2, url: "/pricing" },
|
|
@@ -56,11 +63,17 @@ const seo = createSeo({
|
|
|
56
63
|
<JsonLd data={seo.breadcrumbs([{ name: "Home", path: "/" }, { name: "Blog", path: "/blog" }, { name: title, path: `/blog/${slug}` }])} />
|
|
57
64
|
```
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
`createSeo` resolves **nested** URL fields too — the organization's `url`/`logo`
|
|
67
|
+
and `offers.url` may be site-relative, as in the example above. Already have
|
|
68
|
+
absolute URLs and don't want the factory? The standalone builders (`faqPage`,
|
|
69
|
+
`article`, `breadcrumbList`, …) take absolute URLs directly (they resolve
|
|
70
|
+
nothing).
|
|
61
71
|
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
Every builder returns a typed node (`FaqPageNode`, `ArticleNode`, …), so the
|
|
73
|
+
shape survives past the call site. `<JsonLd data={...} />` accepts a single node
|
|
74
|
+
or an array (e.g. `[organization(org), website(site)]` on the homepage), and
|
|
75
|
+
escapes `<` on serialization so CMS-sourced strings can't break out of the
|
|
76
|
+
`<script>` tag.
|
|
64
77
|
|
|
65
78
|
## Page metadata
|
|
66
79
|
|
|
@@ -69,13 +82,17 @@ Already have absolute URLs and don't want the factory? The standalone builders
|
|
|
69
82
|
import { createMetadata } from "@ingram-tech/nk-seo";
|
|
70
83
|
|
|
71
84
|
export const pageMetadata = createMetadata({
|
|
72
|
-
baseUrl: "https://
|
|
73
|
-
siteName: "
|
|
85
|
+
baseUrl: "https://example.com",
|
|
86
|
+
siteName: "Acme",
|
|
87
|
+
titleTemplate: "%s | Acme",
|
|
74
88
|
defaultImage: "/images/og.png",
|
|
75
89
|
locale: "en_US",
|
|
76
|
-
twitterSite: "@
|
|
90
|
+
twitterSite: "@acme",
|
|
77
91
|
});
|
|
78
92
|
|
|
93
|
+
// app/layout.tsx — metadataBase + default title (+ template when configured):
|
|
94
|
+
export const metadata = pageMetadata.root({ description: "The Acme platform." });
|
|
95
|
+
|
|
79
96
|
// app/services/page.tsx
|
|
80
97
|
export const metadata = pageMetadata({
|
|
81
98
|
title: "Services",
|
|
@@ -86,28 +103,114 @@ export const metadata = pageMetadata({
|
|
|
86
103
|
|
|
87
104
|
Produces `title`, `description`, a self-referencing `alternates.canonical`,
|
|
88
105
|
`openGraph`, and a `summary_large_image` Twitter card. Pass `noIndex`, `keywords`,
|
|
89
|
-
`type: "article"`, or per-page `openGraph`/`twitter` overrides as needed.
|
|
106
|
+
`type: "article"`, or per-page `openGraph`/`twitter` overrides as needed. With
|
|
107
|
+
`titleTemplate` set, `pageMetadata.root()` emits `title.template`, so plain page
|
|
108
|
+
titles render as "Services | Acme" without every page appending the suffix.
|
|
109
|
+
|
|
110
|
+
## Sitemap & robots
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// app/sitemap.ts
|
|
114
|
+
import { createSitemap } from "@ingram-tech/nk-seo";
|
|
115
|
+
|
|
116
|
+
export default () =>
|
|
117
|
+
createSitemap({
|
|
118
|
+
baseUrl: getServerUrl(), // your deployment's own origin
|
|
119
|
+
routes: ["/", "/pricing", "/docs", "/faq", "/support"],
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`"/"` defaults to priority 1, every other route to 0.7; pass objects
|
|
124
|
+
(`{ path, lastModified, changeFrequency, priority, languages }`) to override, or
|
|
125
|
+
set `lastModified` / `defaultChangeFrequency` / `defaultPriority` site-wide.
|
|
126
|
+
Absolute URLs pass through untouched; a `priority` outside 0–1 throws (Google
|
|
127
|
+
would silently reject the whole entry). Localized routes can declare their
|
|
128
|
+
alternates — `languages: { en: "/about", fr: "/fr/about" }` — mirroring what
|
|
129
|
+
`<HreflangLinks>` emits on the page itself.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// app/robots.ts
|
|
133
|
+
import { createRobots } from "@ingram-tech/nk-seo";
|
|
134
|
+
|
|
135
|
+
export default () =>
|
|
136
|
+
createRobots({
|
|
137
|
+
baseUrl: getServerUrl(),
|
|
138
|
+
isProduction: process.env.VERCEL_ENV === "production",
|
|
139
|
+
disallow: ["/api/", "/internal/", "/login"],
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
When `isProduction` is false the whole site is disallowed — the one SEO
|
|
144
|
+
safeguard everyone forgets on Vercel, where preview and branch deployments are
|
|
145
|
+
otherwise crawlable and compete with the production domain for the same content.
|
|
146
|
+
|
|
147
|
+
## Open Graph image
|
|
148
|
+
|
|
149
|
+
`@ingram-tech/nk-seo/og` is a separate entry (it pulls in `next/og`), so the
|
|
150
|
+
package root and `/components` never carry the renderer.
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
// app/opengraph-image.tsx (and re-export from app/twitter-image.tsx)
|
|
154
|
+
import { ogImageResponse } from "@ingram-tech/nk-seo/og";
|
|
155
|
+
|
|
156
|
+
export const size = { width: 1200, height: 630 };
|
|
157
|
+
export const contentType = "image/png";
|
|
158
|
+
export const alt = "Acme — Ship faster";
|
|
159
|
+
|
|
160
|
+
export default () =>
|
|
161
|
+
ogImageResponse({
|
|
162
|
+
title: "Ship faster with Acme",
|
|
163
|
+
subtitle: "The all-in-one platform for modern teams.",
|
|
164
|
+
wordmark: "Acme",
|
|
165
|
+
footer: "example.com",
|
|
166
|
+
accent: "#565ac9",
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Pass `logo` (absolute URL or data URI) to replace the accent-square mark with
|
|
171
|
+
your logo, and `fonts` + `fontFamily` to render with the brand typeface —
|
|
172
|
+
`fonts` is forwarded to `ImageResponse`, which takes raw TTF/OTF/WOFF data.
|
|
173
|
+
|
|
174
|
+
The template encodes the Satori rule that trips everyone up: every node with
|
|
175
|
+
more than one child sets `display: flex`, and text nodes are never mixed with
|
|
176
|
+
sibling elements — so the headline stays a plain string and the accent rides on
|
|
177
|
+
the mark, not a coloured `<span>` inside the title.
|
|
178
|
+
|
|
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.
|
|
90
185
|
|
|
91
186
|
## Hreflang & canonical
|
|
92
187
|
|
|
93
188
|
Render in `<head>` from the root layout. By default it reads the `x-pathname`
|
|
94
|
-
request header — set it in middleware
|
|
189
|
+
request header — set it on the **forwarded request** in middleware (setting it
|
|
190
|
+
on the response does nothing: `headers()` in a server component reads incoming
|
|
191
|
+
request headers):
|
|
95
192
|
|
|
96
193
|
```ts
|
|
97
194
|
// middleware.ts
|
|
98
|
-
const
|
|
99
|
-
|
|
195
|
+
const requestHeaders = new Headers(req.headers);
|
|
196
|
+
requestHeaders.set("x-pathname", req.nextUrl.pathname);
|
|
197
|
+
return NextResponse.next({ request: { headers: requestHeaders } });
|
|
100
198
|
```
|
|
101
199
|
|
|
200
|
+
Copying `req.headers` first also overwrites any client-spoofed `x-pathname`.
|
|
201
|
+
Note that reading the header (`headers()`) opts the page into dynamic
|
|
202
|
+
rendering — pass `pathname` explicitly (e.g. from route params) on pages that
|
|
203
|
+
must stay static.
|
|
204
|
+
|
|
102
205
|
```tsx
|
|
103
206
|
import { HreflangLinks } from "@ingram-tech/nk-seo/components";
|
|
104
207
|
|
|
105
208
|
// Query-param locales (?hl=fr):
|
|
106
|
-
<HreflangLinks baseUrl="https://
|
|
209
|
+
<HreflangLinks baseUrl="https://example.com" locales={["en", "fr", "nl"]} />
|
|
107
210
|
|
|
108
211
|
// Path-prefix locales (/fr/about), default locale bare, regional hreflang tags:
|
|
109
212
|
<HreflangLinks
|
|
110
|
-
baseUrl="https://
|
|
213
|
+
baseUrl="https://example.com"
|
|
111
214
|
locales={["en", "fr", "nl"]}
|
|
112
215
|
strategy="prefix"
|
|
113
216
|
defaultLocale="en"
|
|
@@ -116,4 +219,20 @@ import { HreflangLinks } from "@ingram-tech/nk-seo/components";
|
|
|
116
219
|
/>
|
|
117
220
|
```
|
|
118
221
|
|
|
119
|
-
Pass `pathname` explicitly if you don't use the `x-pathname` header.
|
|
222
|
+
Pass `pathname` explicitly if you don't use the `x-pathname` header. When
|
|
223
|
+
neither is available the component **throws** instead of guessing — a silent
|
|
224
|
+
fallback would canonicalize every page to the homepage, the kind of site-wide
|
|
225
|
+
SEO bug nobody notices for months.
|
|
226
|
+
|
|
227
|
+
Two rules of the road:
|
|
228
|
+
|
|
229
|
+
- **One canonical per page.** If your pages already set `alternates.canonical`
|
|
230
|
+
(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.
|
|
236
|
+
- Building metadata instead of rendering links? The pure `hreflangAlternates`
|
|
237
|
+
(package root) returns the same `{ canonical, links }` for use in
|
|
238
|
+
`generateMetadata`.
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
/**
|
|
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).
|
|
28
|
+
*/
|
|
29
|
+
currentLocale?: string;
|
|
30
|
+
/** Optional locale → hreflang tag map, e.g. `{ en: "en-BE", fr: "fr-BE" }`. */
|
|
31
|
+
hrefLangTags?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
export interface HreflangLink {
|
|
34
|
+
/** The `hreflang` attribute value ("fr-BE", "x-default", …). */
|
|
35
|
+
hrefLang: string;
|
|
36
|
+
href: string;
|
|
37
|
+
}
|
|
38
|
+
export interface HreflangAlternates {
|
|
39
|
+
/** Self-referencing canonical URL of `pathname`. */
|
|
40
|
+
canonical: string;
|
|
41
|
+
/** One link per locale, plus the trailing `x-default` (→ canonical). */
|
|
42
|
+
links: HreflangLink[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Computes the self-referencing canonical plus per-locale hreflang alternate
|
|
46
|
+
* URLs (and an `x-default`) for `pathname`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function hreflangAlternates(config: HreflangConfig, pathname: string): HreflangAlternates;
|
|
49
|
+
//# 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;;;;;;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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
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.
|
|
11
|
+
throw new Error("hreflangAlternates: `defaultLocale` is required for the prefix strategy.");
|
|
12
|
+
}
|
|
13
|
+
// Prefix strategy: accept both the bare and the locale-prefixed form of the
|
|
14
|
+
// path (middleware's `x-pathname` carries the latter on a real localized
|
|
15
|
+
// route — blindly prepending would emit /fr/fr/about) and detect the
|
|
16
|
+
// current locale from it.
|
|
17
|
+
let basePath = pathname;
|
|
18
|
+
let currentLocale = config.currentLocale;
|
|
19
|
+
if (strategy === "prefix") {
|
|
20
|
+
for (const locale of config.locales) {
|
|
21
|
+
if (pathname === `/${locale}` || pathname.startsWith(`/${locale}/`)) {
|
|
22
|
+
basePath = pathname.slice(locale.length + 1) || "/";
|
|
23
|
+
currentLocale ??= locale;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
currentLocale ??= defaultLocale;
|
|
28
|
+
}
|
|
29
|
+
/** The default-locale URL — the bare path; also serves as x-default. */
|
|
30
|
+
const defaultUrl = absoluteUrl(basePath, config.baseUrl);
|
|
31
|
+
const hrefFor = (locale) => {
|
|
32
|
+
if (strategy === "prefix") {
|
|
33
|
+
if (locale === defaultLocale)
|
|
34
|
+
return defaultUrl;
|
|
35
|
+
const clean = basePath === "/" ? "" : basePath;
|
|
36
|
+
return absoluteUrl(`/${locale}${clean}`, config.baseUrl);
|
|
37
|
+
}
|
|
38
|
+
return `${defaultUrl}${defaultUrl.includes("?") ? "&" : "?"}${param}=${locale}`;
|
|
39
|
+
};
|
|
40
|
+
// Self-referencing canonical: the current variant's own URL. Canonicalizing
|
|
41
|
+
// a localized variant to the bare path makes Google treat the variants as
|
|
42
|
+
// duplicates and ignore the hreflang annotations entirely.
|
|
43
|
+
const canonical = currentLocale &&
|
|
44
|
+
currentLocale !== defaultLocale &&
|
|
45
|
+
config.locales.includes(currentLocale)
|
|
46
|
+
? hrefFor(currentLocale)
|
|
47
|
+
: defaultUrl;
|
|
48
|
+
return {
|
|
49
|
+
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
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# 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;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"}
|
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,34 +1,36 @@
|
|
|
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
|
-
* Path being rendered. Defaults to the `x-pathname`
|
|
8
|
-
* middleware
|
|
9
|
-
*
|
|
4
|
+
* Path being rendered. Defaults to the `x-pathname` REQUEST header — it must
|
|
5
|
+
* be set on the forwarded request in middleware (a response header is
|
|
6
|
+
* invisible to `headers()`):
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const requestHeaders = new Headers(req.headers);
|
|
10
|
+
* requestHeaders.set("x-pathname", req.nextUrl.pathname);
|
|
11
|
+
* return NextResponse.next({ request: { headers: requestHeaders } });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Pass explicitly if you don't use that header (e.g. from route params) —
|
|
15
|
+
* also the only way to keep the page statically renderable, since reading
|
|
16
|
+
* `headers()` opts the page into dynamic rendering.
|
|
10
17
|
*/
|
|
11
18
|
pathname?: string;
|
|
12
19
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `/${locale}${path}` (matches a localized-rewrite setup).
|
|
20
|
+
* Emit a self-referencing `<link rel="canonical">`. Default `true`.
|
|
21
|
+
* Disable it if the page's metadata already sets `alternates.canonical`
|
|
22
|
+
* (e.g. via `createMetadata`) — a page must not declare two canonicals.
|
|
17
23
|
*/
|
|
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
24
|
canonical?: boolean;
|
|
27
25
|
}
|
|
28
26
|
/**
|
|
29
27
|
* Emits `<link rel="canonical">` plus per-locale `<link rel="alternate" hreflang>`
|
|
30
28
|
* (and an `x-default`) for the current path. Render inside `<head>` (e.g. from
|
|
31
29
|
* the root layout). Server component — reads the `x-pathname` header by default.
|
|
30
|
+
*
|
|
31
|
+
* Throws when neither `pathname` nor the header is available: silently falling
|
|
32
|
+
* back would canonicalize every page to the homepage, a site-wide
|
|
33
|
+
* duplicate-content bug that is otherwise invisible.
|
|
32
34
|
*/
|
|
33
|
-
export declare function HreflangLinks({
|
|
35
|
+
export declare function HreflangLinks({ pathname, canonical, ...config }: HreflangLinksProps): Promise<import("react").JSX.Element>;
|
|
34
36
|
//# 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;;;;;;;;;;;;;;OAcG;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,wCA4BpB"}
|
package/dist/hreflang.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
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
|
-
|
|
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 it on the forwarded REQUEST in middleware — " +
|
|
18
|
+
'`const h = new Headers(req.headers); h.set("x-pathname", req.nextUrl.pathname); ' +
|
|
19
|
+
"return NextResponse.next({ request: { headers: h } });` — " +
|
|
20
|
+
"(a response header is invisible to `headers()`), or pass `pathname` explicitly.");
|
|
21
|
+
}
|
|
22
|
+
const { canonical: canonicalUrl, links } = hreflangAlternates(config, path);
|
|
23
|
+
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} ${link.href}`)))] }));
|
|
22
24
|
}
|
|
23
25
|
//# 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;AA2B1E;;;;;;;;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,kDAAkD;YAClD,kFAAkF;YAClF,4DAA4D;YAC5D,iFAAiF,CAClF,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,eAIC,GAAG,EAAC,WAAW,EACf,QAAQ,EAAE,IAAI,CAAC,QAAQ,EACvB,IAAI,EAAE,IAAI,CAAC,IAAI,IAHV,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,CAInC,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"}
|