@ingram-tech/nk-seo 0.2.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 ADDED
@@ -0,0 +1,119 @@
1
+ # @ingram-tech/nk-seo
2
+
3
+ SEO primitives for Next.js sites, factored out of the patterns every Ingram site
4
+ kept re-implementing:
5
+
6
+ 1. **`<JsonLd>`** + **typed schema.org builders** — `faqPage`, `breadcrumbList`,
7
+ `article`, `softwareApplication`, `organization`, `website`, and a
8
+ `createSeo` factory that resolves site-relative paths and injects your
9
+ publisher.
10
+ 2. **`createMetadata`** — a Next `Metadata` factory: canonical + OpenGraph +
11
+ Twitter card from one title/description/path.
12
+ 3. **`<HreflangLinks>`** — self-referencing canonical plus per-locale `hreflang`
13
+ alternates (query-param or path-prefix strategy).
14
+
15
+ The package root (`@ingram-tech/nk-seo`) is **pure** — builders and the metadata
16
+ factory, no React — so `sitemap.ts` and route handlers can import it freely. The
17
+ components live at `@ingram-tech/nk-seo/components`.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ bun add @ingram-tech/nk-seo
23
+ ```
24
+
25
+ `next` and `react` are optional peers (only the components need them).
26
+
27
+ ## Structured data
28
+
29
+ ```tsx
30
+ import { JsonLd } from "@ingram-tech/nk-seo/components";
31
+ import { createSeo } from "@ingram-tech/nk-seo";
32
+
33
+ const seo = createSeo({
34
+ baseUrl: getServerUrl(),
35
+ organization: {
36
+ name: "Financica",
37
+ url: "https://financica.app",
38
+ logo: "https://financica.app/logo.png",
39
+ sameAs: ["https://linkedin.com/company/financica"],
40
+ },
41
+ });
42
+
43
+ // Homepage:
44
+ <JsonLd
45
+ data={seo.softwareApplication({
46
+ name: "Financica",
47
+ applicationCategory: "BusinessApplication",
48
+ operatingSystem: "Web",
49
+ offers: { priceCurrency: "EUR", lowPrice: 59, highPrice: 299, offerCount: 2, url: "/pricing" },
50
+ })}
51
+ />
52
+ <JsonLd data={seo.faqPage(faqs)} />
53
+
54
+ // Blog post (path + image resolved, configured org used as publisher):
55
+ <JsonLd data={seo.article({ path: `/blog/${slug}`, headline, datePublished, authors: [{ name: author }] })} />
56
+ <JsonLd data={seo.breadcrumbs([{ name: "Home", path: "/" }, { name: "Blog", path: "/blog" }, { name: title, path: `/blog/${slug}` }])} />
57
+ ```
58
+
59
+ Already have absolute URLs and don't want the factory? The standalone builders
60
+ (`faqPage`, `article`, `breadcrumbList`, …) take absolute URLs directly.
61
+
62
+ `<JsonLd data={...} />` accepts a single node or an array (e.g.
63
+ `[organization(org), website(site)]` on the homepage).
64
+
65
+ ## Page metadata
66
+
67
+ ```ts
68
+ // lib/metadata.ts
69
+ import { createMetadata } from "@ingram-tech/nk-seo";
70
+
71
+ export const pageMetadata = createMetadata({
72
+ baseUrl: "https://ingram.tech",
73
+ siteName: "Ingram Technologies",
74
+ defaultImage: "/images/og.png",
75
+ locale: "en_US",
76
+ twitterSite: "@IngramTech",
77
+ });
78
+
79
+ // app/services/page.tsx
80
+ export const metadata = pageMetadata({
81
+ title: "Services",
82
+ description: "AI deployment and custom agents.",
83
+ path: "/services",
84
+ });
85
+ ```
86
+
87
+ Produces `title`, `description`, a self-referencing `alternates.canonical`,
88
+ `openGraph`, and a `summary_large_image` Twitter card. Pass `noIndex`, `keywords`,
89
+ `type: "article"`, or per-page `openGraph`/`twitter` overrides as needed.
90
+
91
+ ## Hreflang & canonical
92
+
93
+ Render in `<head>` from the root layout. By default it reads the `x-pathname`
94
+ request header — set it in middleware:
95
+
96
+ ```ts
97
+ // middleware.ts
98
+ const res = NextResponse.next();
99
+ res.headers.set("x-pathname", req.nextUrl.pathname);
100
+ ```
101
+
102
+ ```tsx
103
+ import { HreflangLinks } from "@ingram-tech/nk-seo/components";
104
+
105
+ // Query-param locales (?hl=fr):
106
+ <HreflangLinks baseUrl="https://financica.app" locales={["en", "fr", "nl"]} />
107
+
108
+ // Path-prefix locales (/fr/about), default locale bare, regional hreflang tags:
109
+ <HreflangLinks
110
+ baseUrl="https://malinamore.studio"
111
+ locales={["en", "fr", "nl"]}
112
+ strategy="prefix"
113
+ defaultLocale="en"
114
+ hrefLangTags={{ en: "en-BE", fr: "fr-BE", nl: "nl-BE" }}
115
+ canonical={false}
116
+ />
117
+ ```
118
+
119
+ Pass `pathname` explicitly if you don't use the `x-pathname` header.
@@ -0,0 +1,3 @@
1
+ export { JsonLd } from "./json-ld.js";
2
+ export { HreflangLinks, type HreflangLinksProps } from "./hreflang.js";
3
+ //# sourceMappingURL=components.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,5 @@
1
+ // React entry: the rendering components. Kept out of the package root so server
2
+ // code that only needs the builders/metadata never imports React or next/headers.
3
+ export { JsonLd } from "./json-ld.js";
4
+ export { HreflangLinks } from "./hreflang.js";
5
+ //# sourceMappingURL=components.js.map
@@ -0,0 +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;AACtC,OAAO,EAAE,aAAa,EAA2B,MAAM,eAAe,CAAC"}
@@ -0,0 +1,34 @@
1
+ export interface HreflangLinksProps {
2
+ /** Absolute site origin, e.g. "https://financica.app". */
3
+ baseUrl: string;
4
+ /** Locales to emit `<link rel="alternate" hreflang>` for, e.g. ["en", "fr", "nl"]. */
5
+ locales: readonly string[];
6
+ /**
7
+ * Path being rendered. Defaults to the `x-pathname` request header (set it in
8
+ * middleware: `headers.set("x-pathname", req.nextUrl.pathname)`). Pass
9
+ * explicitly if you don't use that header (e.g. from route params).
10
+ */
11
+ pathname?: string;
12
+ /**
13
+ * How locale is encoded in the alternate URLs:
14
+ * - `"query"` (default): `${baseUrl}${path}?${param}=${locale}` for every locale.
15
+ * - `"prefix"`: the default locale stays at the bare path; others get
16
+ * `/${locale}${path}` (matches a localized-rewrite setup).
17
+ */
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
+ canonical?: boolean;
27
+ }
28
+ /**
29
+ * Emits `<link rel="canonical">` plus per-locale `<link rel="alternate" hreflang>`
30
+ * (and an `x-default`) for the current path. Render inside `<head>` (e.g. from
31
+ * the root layout). Server component — reads the `x-pathname` header by default.
32
+ */
33
+ export declare function HreflangLinks({ baseUrl, locales, pathname, strategy, param, defaultLocale, hrefLangTags, canonical, }: HreflangLinksProps): Promise<import("react").JSX.Element>;
34
+ //# sourceMappingURL=hreflang.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hreflang.d.ts","sourceRoot":"","sources":["../src/hreflang.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IAClC,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;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;IACtC,wEAAwE;IACxE,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,EACnC,OAAO,EACP,OAAO,EACP,QAAQ,EACR,QAAkB,EAClB,KAAY,EACZ,aAAa,EACb,YAAY,EACZ,SAAgB,GAChB,EAAE,kBAAkB,wCA6BpB"}
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { headers } from "next/headers";
3
+ /**
4
+ * Emits `<link rel="canonical">` plus per-locale `<link rel="alternate" hreflang>`
5
+ * (and an `x-default`) for the current path. Render inside `<head>` (e.g. from
6
+ * the root layout). Server component — reads the `x-pathname` header by default.
7
+ */
8
+ export async function HreflangLinks({ baseUrl, locales, pathname, strategy = "query", param = "hl", defaultLocale, hrefLangTags, canonical = true, }) {
9
+ const path = pathname ?? (await headers()).get("x-pathname") ?? "/";
10
+ const canonicalUrl = `${baseUrl}${path}`;
11
+ const hrefFor = (locale) => {
12
+ if (strategy === "prefix") {
13
+ if (defaultLocale && locale === defaultLocale) {
14
+ return canonicalUrl;
15
+ }
16
+ const clean = path === "/" ? "" : path;
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 })] }));
22
+ }
23
+ //# sourceMappingURL=hreflang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hreflang.js","sourceRoot":"","sources":["../src/hreflang.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AA8BvC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EACnC,OAAO,EACP,OAAO,EACP,QAAQ,EACR,QAAQ,GAAG,OAAO,EAClB,KAAK,GAAG,IAAI,EACZ,aAAa,EACb,YAAY,EACZ,SAAS,GAAG,IAAI,GACI;IACpB,MAAM,IAAI,GAAG,QAAQ,IAAI,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC;IACpE,MAAM,YAAY,GAAG,GAAG,OAAO,GAAG,IAAI,EAAE,CAAC;IAEzC,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,YAAY,CAAC;YACrB,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YACvC,OAAO,GAAG,OAAO,IAAI,MAAM,GAAG,KAAK,EAAE,CAAC;QACvC,CAAC;QACD,OAAO,GAAG,YAAY,IAAI,KAAK,IAAI,MAAM,EAAE,CAAC;IAC7C,CAAC,CAAC;IAEF,OAAO,CACN,8BACE,SAAS,CAAC,CAAC,CAAC,eAAM,GAAG,EAAC,WAAW,EAAC,IAAI,EAAE,YAAY,GAAI,CAAC,CAAC,CAAC,IAAI,EAC/D,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CACxB,eAEC,GAAG,EAAC,WAAW,EACf,QAAQ,EAAE,YAAY,EAAE,CAAC,MAAM,CAAC,IAAI,MAAM,EAC1C,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAHhB,MAAM,CAIV,CACF,CAAC,EACF,eAAM,GAAG,EAAC,WAAW,EAAC,QAAQ,EAAC,WAAW,EAAC,IAAI,EAAE,YAAY,GAAI,IAC/D,CACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { article, type ArticleAuthor, type ArticleInput, breadcrumbList, type BreadcrumbItem, type BreadcrumbPath, createSeo, faqPage, type FaqItem, type JsonLdNode, type OfferInput, organization, type OrganizationInput, 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";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACN,OAAO,EACP,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,SAAS,EACT,OAAO,EACP,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,UAAU,EACf,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,GAAG,EACR,KAAK,SAAS,EACd,mBAAmB,EACnB,KAAK,wBAAwB,EAC7B,OAAO,EACP,KAAK,YAAY,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,cAAc,EACd,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,iBAAiB,GACtB,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // Server entry: pure schema.org builders + the Metadata factory. No React, so
2
+ // route handlers / sitemap.ts can import these without pulling in the renderer.
3
+ // The <JsonLd> and <HreflangLinks> components live at
4
+ // "@ingram-tech/nk-seo/components".
5
+ export { article, breadcrumbList, createSeo, faqPage, organization, softwareApplication, website, } from "./schema.js";
6
+ export { createMetadata, } from "./metadata.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,sDAAsD;AACtD,oCAAoC;AACpC,OAAO,EACN,OAAO,EAGP,cAAc,EAGd,SAAS,EACT,OAAO,EAIP,YAAY,EAIZ,mBAAmB,EAEnB,OAAO,GAEP,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,cAAc,GAId,MAAM,eAAe,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Renders one or more schema.org JSON-LD nodes into a
3
+ * `<script type="application/ld+json">`. Works in server and client components.
4
+ *
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
+ * @example
9
+ * <JsonLd data={faqPage(items)} />
10
+ * <JsonLd data={[organization(org), website(site)]} />
11
+ */
12
+ export declare function JsonLd({ data }: {
13
+ data: object | object[];
14
+ }): import("react").JSX.Element;
15
+ //# sourceMappingURL=json-ld.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-ld.d.ts","sourceRoot":"","sources":["../src/json-ld.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAAE,+BAQ3D"}
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Renders one or more schema.org JSON-LD nodes into a
4
+ * `<script type="application/ld+json">`. Works in server and client components.
5
+ *
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
+ * @example
10
+ * <JsonLd data={faqPage(items)} />
11
+ * <JsonLd data={[organization(org), website(site)]} />
12
+ */
13
+ export function JsonLd({ data }) {
14
+ return (_jsx("script", { type: "application/ld+json",
15
+ // oxlint-disable-next-line no-danger -- trusted, server-built structured data, not user input
16
+ dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
17
+ }
18
+ //# sourceMappingURL=json-ld.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-ld.js","sourceRoot":"","sources":["../src/json-ld.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,MAAM,CAAC,EAAE,IAAI,EAA+B;IAC3D,OAAO,CACN,iBACC,IAAI,EAAC,qBAAqB;QAC1B,8FAA8F;QAC9F,uBAAuB,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,GACxD,CACF,CAAC;AACH,CAAC"}
@@ -0,0 +1,54 @@
1
+ import type { Metadata } from "next";
2
+ /**
3
+ * Per-site defaults for {@link createMetadata}. Set once, reuse on every page.
4
+ */
5
+ export interface MetadataSiteConfig {
6
+ /** Absolute site origin, e.g. "https://ingram.tech". */
7
+ baseUrl: string;
8
+ /** OpenGraph `siteName`. */
9
+ siteName: string;
10
+ /** Site-relative or absolute default OG/Twitter image. */
11
+ defaultImage?: string;
12
+ defaultImageWidth?: number;
13
+ defaultImageHeight?: number;
14
+ /** OpenGraph locale, e.g. "en_US". */
15
+ locale?: string;
16
+ /** Twitter `@handle` for `site`. */
17
+ twitterSite?: string;
18
+ /** Twitter `@handle` for `creator`. */
19
+ twitterCreator?: string;
20
+ }
21
+ /** Per-page inputs for the bound `pageMetadata` function. */
22
+ export interface PageMetadataInput {
23
+ title: string;
24
+ description: string;
25
+ /** Site-relative path of the page, e.g. "/blog/post". Used for canonical + OG url. */
26
+ path: string;
27
+ /** Overrides the site default image (site-relative or absolute). */
28
+ image?: string;
29
+ type?: "website" | "article";
30
+ keywords?: Metadata["keywords"];
31
+ noIndex?: boolean;
32
+ /** Merged into the generated `openGraph` (wins on conflict). */
33
+ openGraph?: Partial<NonNullable<Metadata["openGraph"]>>;
34
+ /** Merged into the generated `twitter` (wins on conflict). */
35
+ twitter?: Partial<NonNullable<Metadata["twitter"]>>;
36
+ }
37
+ /**
38
+ * Returns a `pageMetadata(input)` builder that produces a Next `Metadata`
39
+ * object with a self-referencing canonical, OpenGraph, and Twitter card from
40
+ * one title/description/path. Collapses the boilerplate every site repeats.
41
+ *
42
+ * @example
43
+ * export const metadata = createMetadata({
44
+ * baseUrl: "https://ingram.tech",
45
+ * siteName: "Ingram Technologies",
46
+ * defaultImage: "/images/og.png",
47
+ * twitterSite: "@IngramTech",
48
+ * });
49
+ * // in a page:
50
+ * export const metadata = pageMetadata({ title, description, path: "/services" });
51
+ */
52
+ export declare function createMetadata(site: MetadataSiteConfig): (input: PageMetadataInput) => Metadata;
53
+ export type PageMetadata = ReturnType<typeof createMetadata>;
54
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,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;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,kBAAkB,IAIzB,OAAO,iBAAiB,KAAG,QAAQ,CA2ChE;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Returns a `pageMetadata(input)` builder that produces a Next `Metadata`
3
+ * object with a self-referencing canonical, OpenGraph, and Twitter card from
4
+ * one title/description/path. Collapses the boilerplate every site repeats.
5
+ *
6
+ * @example
7
+ * export const metadata = createMetadata({
8
+ * baseUrl: "https://ingram.tech",
9
+ * siteName: "Ingram Technologies",
10
+ * defaultImage: "/images/og.png",
11
+ * twitterSite: "@IngramTech",
12
+ * });
13
+ * // in a page:
14
+ * export const metadata = pageMetadata({ title, description, path: "/services" });
15
+ */
16
+ export function createMetadata(site) {
17
+ const absolute = (path) => /^https?:\/\//.test(path) ? path : new URL(path, site.baseUrl).toString();
18
+ return function pageMetadata(input) {
19
+ const imagePath = input.image ?? site.defaultImage;
20
+ const imageUrl = imagePath ? absolute(imagePath) : undefined;
21
+ const url = absolute(input.path);
22
+ return {
23
+ title: input.title,
24
+ description: input.description,
25
+ ...(input.keywords ? { keywords: input.keywords } : {}),
26
+ alternates: { canonical: url },
27
+ ...(input.noIndex ? { robots: { index: false, follow: false } } : {}),
28
+ openGraph: {
29
+ title: input.title,
30
+ description: input.description,
31
+ type: input.type ?? "website",
32
+ url,
33
+ siteName: site.siteName,
34
+ ...(site.locale ? { locale: site.locale } : {}),
35
+ ...(imageUrl
36
+ ? {
37
+ images: [
38
+ {
39
+ url: imageUrl,
40
+ width: site.defaultImageWidth ?? 1200,
41
+ height: site.defaultImageHeight ?? 630,
42
+ alt: input.title,
43
+ },
44
+ ],
45
+ }
46
+ : {}),
47
+ ...input.openGraph,
48
+ },
49
+ twitter: {
50
+ card: "summary_large_image",
51
+ title: input.title,
52
+ description: input.description,
53
+ ...(site.twitterSite ? { site: site.twitterSite } : {}),
54
+ ...(site.twitterCreator ? { creator: site.twitterCreator } : {}),
55
+ ...(imageUrl ? { images: [imageUrl] } : {}),
56
+ ...input.twitter,
57
+ },
58
+ };
59
+ };
60
+ }
61
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAAC,IAAwB;IACtD,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAU,EAAE,CACzC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IAE3E,OAAO,SAAS,YAAY,CAAC,KAAwB;QACpD,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC;QACnD,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC7D,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAEjC,OAAO;YACN,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvD,UAAU,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE;YAC9B,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrE,SAAS,EAAE;gBACV,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,WAAW,EAAE,KAAK,CAAC,WAAW;gBAC9B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,SAAS;gBAC7B,GAAG;gBACH,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/C,GAAG,CAAC,QAAQ;oBACX,CAAC,CAAC;wBACA,MAAM,EAAE;4BACP;gCACC,GAAG,EAAE,QAAQ;gCACb,KAAK,EAAE,IAAI,CAAC,iBAAiB,IAAI,IAAI;gCACrC,MAAM,EAAE,IAAI,CAAC,kBAAkB,IAAI,GAAG;gCACtC,GAAG,EAAE,KAAK,CAAC,KAAK;6BAChB;yBACD;qBACD;oBACF,CAAC,CAAC,EAAE,CAAC;gBACN,GAAG,KAAK,CAAC,SAAS;aAClB;YACD,OAAO,EAAE;gBACR,IAAI,EAAE,qBAAqB;gBAC3B,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,WAAW,EAAE,KAAK,CAAC,WAAW;gBAC9B,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvD,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC3C,GAAG,KAAK,CAAC,OAAO;aAChB;SACD,CAAC;IACH,CAAC,CAAC;AACH,CAAC"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Typed schema.org JSON-LD builders. Each returns a plain object ready to hand
3
+ * to `<JsonLd>` (from "@ingram-tech/nk-seo/components").
4
+ *
5
+ * The standalone builders take values as-is: pass **absolute** URLs for
6
+ * `url`/`image`/`logo` fields. If your pages work in site-relative paths and
7
+ * share one Organization, prefer {@link createSeo}, which resolves paths against
8
+ * a base URL and injects the publisher/provider for you.
9
+ */
10
+ /** A schema.org node. Hand it to `<JsonLd data={...} />`. */
11
+ export type JsonLdNode = Record<string, unknown>;
12
+ export interface FaqItem {
13
+ question: string;
14
+ answer: string;
15
+ }
16
+ /** FAQPage: marks up a list of question/answer pairs for FAQ rich results. */
17
+ export declare function faqPage(items: FaqItem[]): JsonLdNode;
18
+ export interface BreadcrumbItem {
19
+ name: string;
20
+ /** Absolute URL of the crumb. */
21
+ url: string;
22
+ }
23
+ /** BreadcrumbList: the trail of links Google shows in place of the raw URL. */
24
+ export declare function breadcrumbList(items: BreadcrumbItem[]): JsonLdNode;
25
+ export interface OrganizationInput {
26
+ name: string;
27
+ /** Absolute URL of the organization's site. */
28
+ url?: string;
29
+ /** Absolute URL of the logo image. */
30
+ logo?: string;
31
+ description?: string;
32
+ email?: string;
33
+ /** Profile/social URLs that corroborate the entity (LinkedIn, Crunchbase…). */
34
+ sameAs?: string[];
35
+ }
36
+ /** Organization: the entity behind the site (for the knowledge panel). */
37
+ export declare function organization(input: OrganizationInput): JsonLdNode;
38
+ export interface WebsiteInput {
39
+ name: string;
40
+ /** Absolute URL of the site root. */
41
+ url: string;
42
+ publisher?: OrganizationInput;
43
+ }
44
+ /** WebSite: identifies the site as an entity; pair with {@link organization}. */
45
+ export declare function website(input: WebsiteInput): JsonLdNode;
46
+ export interface OfferInput {
47
+ priceCurrency: string;
48
+ /** Single price. Mutually exclusive with low/high (which emit an AggregateOffer). */
49
+ price?: string | number;
50
+ lowPrice?: string | number;
51
+ highPrice?: string | number;
52
+ offerCount?: string | number;
53
+ /** Absolute URL of the pricing/offer page. */
54
+ url?: string;
55
+ }
56
+ export interface SoftwareApplicationInput {
57
+ name: string;
58
+ description?: string;
59
+ /** Absolute URL of the app/marketing page. */
60
+ url?: string;
61
+ /** e.g. "BusinessApplication", "FinanceApplication". */
62
+ applicationCategory?: string;
63
+ /** e.g. "Web". */
64
+ operatingSystem?: string;
65
+ offers?: OfferInput;
66
+ }
67
+ /** SoftwareApplication: marks a product page as an app for rich results. */
68
+ export declare function softwareApplication(input: SoftwareApplicationInput): JsonLdNode;
69
+ export interface ArticleAuthor {
70
+ name: string;
71
+ /** Absolute URL of the author's profile/page. */
72
+ url?: string;
73
+ }
74
+ export interface ArticleInput {
75
+ headline: string;
76
+ /** Absolute canonical URL of the article. */
77
+ url: string;
78
+ description?: string;
79
+ type?: "Article" | "BlogPosting" | "NewsArticle" | "ScholarlyArticle";
80
+ datePublished?: string;
81
+ /** Defaults to `datePublished` when omitted. */
82
+ dateModified?: string;
83
+ authors?: ArticleAuthor[];
84
+ /** Absolute URL of the lead image. */
85
+ image?: string;
86
+ keywords?: string[];
87
+ publisher?: OrganizationInput;
88
+ }
89
+ /** Article (or BlogPosting/NewsArticle/ScholarlyArticle) for editorial pages. */
90
+ export declare function article(input: ArticleInput): JsonLdNode;
91
+ export interface SeoConfig {
92
+ /** Absolute site origin, e.g. "https://financica.app". */
93
+ baseUrl: string;
94
+ /** Shared publisher/provider, injected into article()/website()/organization(). */
95
+ organization?: OrganizationInput;
96
+ }
97
+ /** A path-aware version of a crumb: `path` is resolved against the base URL. */
98
+ export interface BreadcrumbPath {
99
+ name: string;
100
+ path: string;
101
+ }
102
+ /**
103
+ * Binds the builders to a site so callers pass site-relative paths instead of
104
+ * absolute URLs, and the configured Organization is injected automatically.
105
+ *
106
+ * @example
107
+ * const seo = createSeo({ baseUrl: getServerUrl(), organization: ORG });
108
+ * <JsonLd data={seo.article({ path: `/blog/${slug}`, headline, datePublished })} />
109
+ */
110
+ export declare function createSeo(config: SeoConfig): {
111
+ /** Resolve a site-relative path to an absolute URL. */
112
+ absolute: (path: string) => string;
113
+ /** FAQPage (path-free). */
114
+ faqPage: typeof faqPage;
115
+ /** BreadcrumbList from site-relative paths. */
116
+ breadcrumbs(items: BreadcrumbPath[]): JsonLdNode;
117
+ /** Organization from config (with optional per-call overrides). */
118
+ organization(overrides?: Partial<OrganizationInput>): JsonLdNode;
119
+ /** WebSite from config, defaulting name/url/publisher to the configured org. */
120
+ website(input?: Partial<WebsiteInput>): JsonLdNode;
121
+ /** SoftwareApplication; `path` (default "/") is resolved to an absolute URL. */
122
+ softwareApplication(input: Omit<SoftwareApplicationInput, "url"> & {
123
+ path?: string;
124
+ }): JsonLdNode;
125
+ /** Article; `path`/`image` are resolved and the configured org becomes publisher. */
126
+ article(input: Omit<ArticleInput, "url" | "image" | "publisher"> & {
127
+ path: string;
128
+ image?: string;
129
+ publisher?: OrganizationInput;
130
+ }): JsonLdNode;
131
+ };
132
+ export type Seo = ReturnType<typeof createSeo>;
133
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
package/dist/schema.js ADDED
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Typed schema.org JSON-LD builders. Each returns a plain object ready to hand
3
+ * to `<JsonLd>` (from "@ingram-tech/nk-seo/components").
4
+ *
5
+ * The standalone builders take values as-is: pass **absolute** URLs for
6
+ * `url`/`image`/`logo` fields. If your pages work in site-relative paths and
7
+ * share one Organization, prefer {@link createSeo}, which resolves paths against
8
+ * a base URL and injects the publisher/provider for you.
9
+ */
10
+ const CONTEXT = "https://schema.org";
11
+ /** FAQPage: marks up a list of question/answer pairs for FAQ rich results. */
12
+ export function faqPage(items) {
13
+ return {
14
+ "@context": CONTEXT,
15
+ "@type": "FAQPage",
16
+ mainEntity: items.map((item) => ({
17
+ "@type": "Question",
18
+ name: item.question,
19
+ acceptedAnswer: { "@type": "Answer", text: item.answer },
20
+ })),
21
+ };
22
+ }
23
+ /** BreadcrumbList: the trail of links Google shows in place of the raw URL. */
24
+ export function breadcrumbList(items) {
25
+ return {
26
+ "@context": CONTEXT,
27
+ "@type": "BreadcrumbList",
28
+ itemListElement: items.map((item, index) => ({
29
+ "@type": "ListItem",
30
+ position: index + 1,
31
+ name: item.name,
32
+ item: item.url,
33
+ })),
34
+ };
35
+ }
36
+ /** Organization node without `@context`, for embedding as publisher/provider. */
37
+ function organizationNode(input) {
38
+ return {
39
+ "@type": "Organization",
40
+ name: input.name,
41
+ ...(input.url ? { url: input.url } : {}),
42
+ ...(input.logo ? { logo: { "@type": "ImageObject", url: input.logo } } : {}),
43
+ ...(input.description ? { description: input.description } : {}),
44
+ ...(input.email ? { email: input.email } : {}),
45
+ ...(input.sameAs?.length ? { sameAs: input.sameAs } : {}),
46
+ };
47
+ }
48
+ /** Organization: the entity behind the site (for the knowledge panel). */
49
+ export function organization(input) {
50
+ return { "@context": CONTEXT, ...organizationNode(input) };
51
+ }
52
+ /** WebSite: identifies the site as an entity; pair with {@link organization}. */
53
+ export function website(input) {
54
+ return {
55
+ "@context": CONTEXT,
56
+ "@type": "WebSite",
57
+ name: input.name,
58
+ url: input.url,
59
+ ...(input.publisher ? { publisher: organizationNode(input.publisher) } : {}),
60
+ };
61
+ }
62
+ function offerNode(offer) {
63
+ const isAggregate = offer.lowPrice !== undefined || offer.highPrice !== undefined;
64
+ if (isAggregate) {
65
+ return {
66
+ "@type": "AggregateOffer",
67
+ priceCurrency: offer.priceCurrency,
68
+ ...(offer.lowPrice !== undefined
69
+ ? { lowPrice: String(offer.lowPrice) }
70
+ : {}),
71
+ ...(offer.highPrice !== undefined
72
+ ? { highPrice: String(offer.highPrice) }
73
+ : {}),
74
+ ...(offer.offerCount !== undefined
75
+ ? { offerCount: String(offer.offerCount) }
76
+ : {}),
77
+ ...(offer.url ? { url: offer.url } : {}),
78
+ };
79
+ }
80
+ return {
81
+ "@type": "Offer",
82
+ priceCurrency: offer.priceCurrency,
83
+ ...(offer.price !== undefined ? { price: String(offer.price) } : {}),
84
+ ...(offer.url ? { url: offer.url } : {}),
85
+ };
86
+ }
87
+ /** SoftwareApplication: marks a product page as an app for rich results. */
88
+ export function softwareApplication(input) {
89
+ return {
90
+ "@context": CONTEXT,
91
+ "@type": "SoftwareApplication",
92
+ name: input.name,
93
+ ...(input.applicationCategory
94
+ ? { applicationCategory: input.applicationCategory }
95
+ : {}),
96
+ ...(input.operatingSystem ? { operatingSystem: input.operatingSystem } : {}),
97
+ ...(input.url ? { url: input.url } : {}),
98
+ ...(input.description ? { description: input.description } : {}),
99
+ ...(input.offers ? { offers: offerNode(input.offers) } : {}),
100
+ };
101
+ }
102
+ /** Article (or BlogPosting/NewsArticle/ScholarlyArticle) for editorial pages. */
103
+ export function article(input) {
104
+ const dateModified = input.dateModified ?? input.datePublished;
105
+ return {
106
+ "@context": CONTEXT,
107
+ "@type": input.type ?? "Article",
108
+ headline: input.headline,
109
+ url: input.url,
110
+ mainEntityOfPage: input.url,
111
+ ...(input.description ? { description: input.description } : {}),
112
+ ...(input.authors?.length
113
+ ? {
114
+ author: input.authors.map((author) => ({
115
+ "@type": "Person",
116
+ name: author.name,
117
+ ...(author.url ? { url: author.url } : {}),
118
+ })),
119
+ }
120
+ : {}),
121
+ ...(input.publisher ? { publisher: organizationNode(input.publisher) } : {}),
122
+ ...(input.datePublished ? { datePublished: input.datePublished } : {}),
123
+ ...(dateModified ? { dateModified } : {}),
124
+ ...(input.image ? { image: input.image } : {}),
125
+ ...(input.keywords?.length ? { keywords: input.keywords } : {}),
126
+ };
127
+ }
128
+ /**
129
+ * Binds the builders to a site so callers pass site-relative paths instead of
130
+ * absolute URLs, and the configured Organization is injected automatically.
131
+ *
132
+ * @example
133
+ * const seo = createSeo({ baseUrl: getServerUrl(), organization: ORG });
134
+ * <JsonLd data={seo.article({ path: `/blog/${slug}`, headline, datePublished })} />
135
+ */
136
+ export function createSeo(config) {
137
+ const absolute = (path) => /^https?:\/\//.test(path) ? path : new URL(path, config.baseUrl).toString();
138
+ return {
139
+ /** Resolve a site-relative path to an absolute URL. */
140
+ absolute,
141
+ /** FAQPage (path-free). */
142
+ faqPage,
143
+ /** BreadcrumbList from site-relative paths. */
144
+ breadcrumbs(items) {
145
+ return breadcrumbList(items.map((item) => ({ name: item.name, url: absolute(item.path) })));
146
+ },
147
+ /** Organization from config (with optional per-call overrides). */
148
+ organization(overrides) {
149
+ if (!config.organization) {
150
+ throw new Error("createSeo: organization() needs `organization` in config");
151
+ }
152
+ return organization({ ...config.organization, ...overrides });
153
+ },
154
+ /** WebSite from config, defaulting name/url/publisher to the configured org. */
155
+ website(input) {
156
+ const name = input?.name ?? config.organization?.name;
157
+ if (!name) {
158
+ throw new Error("createSeo: website() needs a name or configured organization");
159
+ }
160
+ return website({
161
+ name,
162
+ url: input?.url ?? config.baseUrl,
163
+ publisher: input?.publisher ?? config.organization,
164
+ });
165
+ },
166
+ /** SoftwareApplication; `path` (default "/") is resolved to an absolute URL. */
167
+ softwareApplication(input) {
168
+ const { path, ...rest } = input;
169
+ return softwareApplication({ ...rest, url: absolute(path ?? "/") });
170
+ },
171
+ /** Article; `path`/`image` are resolved and the configured org becomes publisher. */
172
+ article(input) {
173
+ const { path, image, publisher, ...rest } = input;
174
+ return article({
175
+ ...rest,
176
+ url: absolute(path),
177
+ ...(image ? { image: absolute(image) } : {}),
178
+ publisher: publisher ?? config.organization,
179
+ });
180
+ },
181
+ };
182
+ }
183
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@ingram-tech/nk-seo",
3
+ "version": "0.2.0",
4
+ "description": "SEO primitives for Next.js sites: typed schema.org JSON-LD builders, a <JsonLd> tag, a Metadata factory (canonical + OG + Twitter), and configurable hreflang alternates.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ingram-technologies/nextkit.git",
10
+ "directory": "packages/nk-seo"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./components": {
24
+ "types": "./dist/components.d.ts",
25
+ "import": "./dist/components.js"
26
+ }
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.json",
30
+ "type-check": "tsc -p tsconfig.json --noEmit",
31
+ "test": "vitest run"
32
+ },
33
+ "devDependencies": {
34
+ "@ingram-tech/nk-dev": "0.2.4",
35
+ "@types/node": "^26.0.1",
36
+ "@types/react": "^19.2.17",
37
+ "next": "^16.0.0",
38
+ "react": "^19.2.7",
39
+ "typescript": "^6.0.3",
40
+ "vitest": "^4.1.9"
41
+ },
42
+ "peerDependencies": {
43
+ "next": ">=14.0.0",
44
+ "react": "^18.0.0 || ^19.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "next": {
48
+ "optional": true
49
+ },
50
+ "react": {
51
+ "optional": true
52
+ }
53
+ }
54
+ }