@better-seo/next 0.0.1

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,65 @@
1
+ # `@better-seo/next`
2
+
3
+ [![npm](https://img.shields.io/npm/v/@better-seo/next?style=flat-square)](https://www.npmjs.com/package/@better-seo/next)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](../../LICENSE)
5
+ [![CI](https://github.com/0xMilord/better-seo-js/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/0xMilord/better-seo-js/actions/workflows/ci.yml)
6
+
7
+ Next.js integration for [**`@better-seo/core`**](../core/README.md): App Router **`Metadata`**, **`generateMetadata`** patterns, shorthand **`seo()`**, **`prepareNextSeo`**, layout merge via **`withSEO`**, and **`NextJsonLd`** so JSON-LD uses the same **`serializeJSONLD`** path as the rest of the stack.
8
+
9
+ **Peers:** `next` **>= 14.2**, `react` **>= 18.2** (see **`package.json`**).
10
+
11
+ **Docs:** [Monorepo README](../../README.md) · [Usage](../../internal-docs/USAGE.md) · [Recipes](../../docs/recipes/)
12
+
13
+ ---
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @better-seo/core @better-seo/next
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Entry points
24
+
25
+ | Import | Role |
26
+ | ------------------------------ | --------------------------------------------------------------------------------------- |
27
+ | **`@better-seo/next`** | `seo`, `prepareNextSeo`, `withSEO`, `toNextMetadata`, adapter registration for `"next"` |
28
+ | **`@better-seo/next/json-ld`** | **`NextJsonLd`** — renders `<script type="application/ld+json">` from a **`SEO`** |
29
+
30
+ ---
31
+
32
+ ## Scripts (monorepo)
33
+
34
+ From **`packages/next`**:
35
+
36
+ ```bash
37
+ npm run build
38
+ npm run test
39
+ npm run test:coverage
40
+ npm run lint
41
+ npm run typecheck
42
+ ```
43
+
44
+ **E2E:** Playwright tests in **`examples/nextjs-app`** exercise real HTML head output (runs in root **`npm run ci`**).
45
+
46
+ ---
47
+
48
+ ## Coverage
49
+
50
+ [`vitest.config.ts`](./vitest.config.ts) scopes coverage to **`src/to-next-metadata.ts`** (mapping logic); wiring in `surface` / `register` is covered by E2E.
51
+
52
+ | Metric | Minimum |
53
+ | -------------- | ------- |
54
+ | **Lines** | 82% |
55
+ | **Statements** | 82% |
56
+ | **Functions** | 80% |
57
+ | **Branches** | 72% |
58
+
59
+ LCov: **`coverage/lcov.info`** (uploaded from CI when present).
60
+
61
+ ---
62
+
63
+ ## License
64
+
65
+ MIT — see [**LICENSE**](../../LICENSE).
package/dist/index.cjs ADDED
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ var core = require('@better-seo/core');
4
+
5
+ // src/register.ts
6
+
7
+ // src/to-next-metadata.ts
8
+ function robotsFromString(robots) {
9
+ const lower = robots.toLowerCase();
10
+ const noindex = lower.includes("noindex");
11
+ const nofollow = lower.includes("nofollow");
12
+ if (!noindex && !nofollow) {
13
+ return { index: true, follow: true };
14
+ }
15
+ return {
16
+ index: !noindex,
17
+ follow: !nofollow
18
+ };
19
+ }
20
+ function omitUndefined(o) {
21
+ return Object.fromEntries(
22
+ Object.entries(o).filter(([, v]) => v !== void 0)
23
+ );
24
+ }
25
+ function toNextMetadata(seo2) {
26
+ const m = {
27
+ title: seo2.meta.title
28
+ };
29
+ if (seo2.meta.description !== void 0) {
30
+ m.description = seo2.meta.description;
31
+ }
32
+ const langs = seo2.meta.alternates?.languages;
33
+ const hasLang = langs !== void 0 && Object.keys(langs).length > 0;
34
+ if (seo2.meta.canonical || hasLang) {
35
+ const alt = {};
36
+ if (seo2.meta.canonical) alt.canonical = seo2.meta.canonical;
37
+ if (hasLang && langs) alt.languages = { ...langs };
38
+ m.alternates = alt;
39
+ }
40
+ if (seo2.meta.robots) {
41
+ m.robots = robotsFromString(seo2.meta.robots);
42
+ }
43
+ if (seo2.openGraph) {
44
+ const og = omitUndefined({
45
+ title: seo2.openGraph.title,
46
+ description: seo2.openGraph.description,
47
+ url: seo2.openGraph.url,
48
+ type: seo2.openGraph.type,
49
+ images: seo2.openGraph.images?.map(
50
+ (img) => omitUndefined({
51
+ url: img.url,
52
+ width: img.width,
53
+ height: img.height,
54
+ alt: img.alt
55
+ })
56
+ )
57
+ });
58
+ if (Object.keys(og).length > 0) {
59
+ m.openGraph = og;
60
+ }
61
+ }
62
+ if (seo2.twitter) {
63
+ const tw = omitUndefined({
64
+ card: seo2.twitter.card,
65
+ title: seo2.twitter.title,
66
+ description: seo2.twitter.description,
67
+ images: seo2.twitter.image ? [seo2.twitter.image] : void 0
68
+ });
69
+ if (Object.keys(tw).length > 0) {
70
+ m.twitter = tw;
71
+ }
72
+ }
73
+ return m;
74
+ }
75
+
76
+ // src/register.ts
77
+ core.registerAdapter({
78
+ id: "next",
79
+ toFramework: toNextMetadata
80
+ });
81
+ function seo(input, config) {
82
+ return toNextMetadata(core.createSEO(input, config));
83
+ }
84
+ function withSEO(parent, child, config) {
85
+ return toNextMetadata(core.withSEO(parent, child, config));
86
+ }
87
+ function mergeNextMetadataSource(parent, child, config) {
88
+ return toNextMetadata(core.mergeSEO(parent, child, config));
89
+ }
90
+ function prepareNextSeo(input, config) {
91
+ const doc = core.createSEO(input, config);
92
+ return { metadata: toNextMetadata(doc), seo: doc };
93
+ }
94
+
95
+ exports.mergeNextMetadataSource = mergeNextMetadataSource;
96
+ exports.prepareNextSeo = prepareNextSeo;
97
+ exports.seo = seo;
98
+ exports.toNextMetadata = toNextMetadata;
99
+ exports.withSEO = withSEO;
100
+ //# sourceMappingURL=index.cjs.map
101
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/to-next-metadata.ts","../src/register.ts","../src/surface.ts"],"names":["seo","registerAdapter","createSEO","withSeoCore","mergeSEO"],"mappings":";;;;;;;AAGA,SAAS,iBAAiB,MAAA,EAAoC;AAC5D,EAAA,MAAM,KAAA,GAAQ,OAAO,WAAA,EAAY;AACjC,EAAA,MAAM,OAAA,GAAU,KAAA,CAAM,QAAA,CAAS,SAAS,CAAA;AACxC,EAAA,MAAM,QAAA,GAAW,KAAA,CAAM,QAAA,CAAS,UAAU,CAAA;AAC1C,EAAA,IAAI,CAAC,OAAA,IAAW,CAAC,QAAA,EAAU;AACzB,IAAA,OAAO,EAAE,KAAA,EAAO,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAK;AAAA,EACrC;AACA,EAAA,OAAO;AAAA,IACL,OAAO,CAAC,OAAA;AAAA,IACR,QAAQ,CAAC;AAAA,GACX;AACF;AAEA,SAAS,cAAiD,CAAA,EAAkB;AAC1E,EAAA,OAAO,MAAA,CAAO,WAAA;AAAA,IACX,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAA,CAA0B,MAAA,CAAO,CAAC,GAAG,CAAC,CAAA,KAAM,CAAA,KAAM,MAAS;AAAA,GAC9E;AACF;AAGO,SAAS,eAAeA,IAAAA,EAAoB;AACjD,EAAA,MAAM,CAAA,GAAc;AAAA,IAClB,KAAA,EAAOA,KAAI,IAAA,CAAK;AAAA,GAClB;AACA,EAAA,IAAIA,IAAAA,CAAI,IAAA,CAAK,WAAA,KAAgB,MAAA,EAAW;AACtC,IAAA,CAAA,CAAE,WAAA,GAAcA,KAAI,IAAA,CAAK,WAAA;AAAA,EAC3B;AACA,EAAA,MAAM,KAAA,GAAQA,IAAAA,CAAI,IAAA,CAAK,UAAA,EAAY,SAAA;AACnC,EAAA,MAAM,UAAU,KAAA,KAAU,MAAA,IAAa,OAAO,IAAA,CAAK,KAAK,EAAE,MAAA,GAAS,CAAA;AACnE,EAAA,IAAIA,IAAAA,CAAI,IAAA,CAAK,SAAA,IAAa,OAAA,EAAS;AACjC,IAAA,MAAM,MAA2C,EAAC;AAClD,IAAA,IAAIA,KAAI,IAAA,CAAK,SAAA,EAAW,GAAA,CAAI,SAAA,GAAYA,KAAI,IAAA,CAAK,SAAA;AACjD,IAAA,IAAI,WAAW,KAAA,EAAO,GAAA,CAAI,SAAA,GAAY,EAAE,GAAG,KAAA,EAAM;AACjD,IAAA,CAAA,CAAE,UAAA,GAAa,GAAA;AAAA,EACjB;AACA,EAAA,IAAIA,IAAAA,CAAI,KAAK,MAAA,EAAQ;AACnB,IAAA,CAAA,CAAE,MAAA,GAAS,gBAAA,CAAiBA,IAAAA,CAAI,IAAA,CAAK,MAAM,CAAA;AAAA,EAC7C;AAEA,EAAA,IAAIA,KAAI,SAAA,EAAW;AACjB,IAAA,MAAM,KAAK,aAAA,CAAc;AAAA,MACvB,KAAA,EAAOA,KAAI,SAAA,CAAU,KAAA;AAAA,MACrB,WAAA,EAAaA,KAAI,SAAA,CAAU,WAAA;AAAA,MAC3B,GAAA,EAAKA,KAAI,SAAA,CAAU,GAAA;AAAA,MACnB,IAAA,EAAMA,KAAI,SAAA,CAAU,IAAA;AAAA,MACpB,MAAA,EAAQA,IAAAA,CAAI,SAAA,CAAU,MAAA,EAAQ,GAAA;AAAA,QAAI,CAAC,QACjC,aAAA,CAAc;AAAA,UACZ,KAAK,GAAA,CAAI,GAAA;AAAA,UACT,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,QAAQ,GAAA,CAAI,MAAA;AAAA,UACZ,KAAK,GAAA,CAAI;AAAA,SACV;AAAA;AACH,KACD,CAAA;AACD,IAAA,IAAI,MAAA,CAAO,IAAA,CAAK,EAAE,CAAA,CAAE,SAAS,CAAA,EAAG;AAC9B,MAAA,CAAA,CAAE,SAAA,GAAY,EAAA;AAAA,IAChB;AAAA,EACF;AAEA,EAAA,IAAIA,KAAI,OAAA,EAAS;AACf,IAAA,MAAM,KAAK,aAAA,CAAc;AAAA,MACvB,IAAA,EAAMA,KAAI,OAAA,CAAQ,IAAA;AAAA,MAClB,KAAA,EAAOA,KAAI,OAAA,CAAQ,KAAA;AAAA,MACnB,WAAA,EAAaA,KAAI,OAAA,CAAQ,WAAA;AAAA,MACzB,MAAA,EAAQA,KAAI,OAAA,CAAQ,KAAA,GAAQ,CAACA,IAAAA,CAAI,OAAA,CAAQ,KAAK,CAAA,GAAI;AAAA,KACnD,CAAA;AACD,IAAA,IAAI,MAAA,CAAO,IAAA,CAAK,EAAE,CAAA,CAAE,SAAS,CAAA,EAAG;AAC9B,MAAA,CAAA,CAAE,OAAA,GAAU,EAAA;AAAA,IACd;AAAA,EACF;AAEA,EAAA,OAAO,CAAA;AACT;;;ACvEAC,oBAAA,CAA0B;AAAA,EACxB,EAAA,EAAI,MAAA;AAAA,EACJ,WAAA,EAAa;AACf,CAAC,CAAA;ACKM,SAAS,GAAA,CAAI,OAAiB,MAAA,EAA8B;AACjE,EAAA,OAAO,cAAA,CAAeC,cAAA,CAAU,KAAA,EAAO,MAAM,CAAC,CAAA;AAChD;AAKO,SAAS,OAAA,CAAQ,MAAA,EAAa,KAAA,EAAiB,MAAA,EAA8B;AAClF,EAAA,OAAO,cAAA,CAAeC,YAAA,CAAY,MAAA,EAAQ,KAAA,EAAO,MAAM,CAAC,CAAA;AAC1D;AAGO,SAAS,uBAAA,CACd,MAAA,EACA,KAAA,EACA,MAAA,EACU;AACV,EAAA,OAAO,cAAA,CAAeC,aAAA,CAAS,MAAA,EAAQ,KAAA,EAAO,MAAM,CAAC,CAAA;AACvD;AAGO,SAAS,cAAA,CACd,OACA,MAAA,EACoD;AACpD,EAAA,MAAM,GAAA,GAAMF,cAAA,CAAU,KAAA,EAAO,MAAM,CAAA;AACnC,EAAA,OAAO,EAAE,QAAA,EAAU,cAAA,CAAe,GAAG,CAAA,EAAG,KAAK,GAAA,EAAI;AACnD","file":"index.cjs","sourcesContent":["import type { Metadata } from \"next\"\nimport type { SEO } from \"@better-seo/core\"\n\nfunction robotsFromString(robots: string): Metadata[\"robots\"] {\n const lower = robots.toLowerCase()\n const noindex = lower.includes(\"noindex\")\n const nofollow = lower.includes(\"nofollow\")\n if (!noindex && !nofollow) {\n return { index: true, follow: true }\n }\n return {\n index: !noindex,\n follow: !nofollow,\n }\n}\n\nfunction omitUndefined<T extends Record<string, unknown>>(o: T): Partial<T> {\n return Object.fromEntries(\n (Object.entries(o) as [string, unknown][]).filter(([, v]) => v !== undefined),\n ) as Partial<T>\n}\n\n/** Map normalized `SEO` → Next.js `Metadata` (App Router). JSON-LD is not included — use `@better-seo/next/json-ld`. */\nexport function toNextMetadata(seo: SEO): Metadata {\n const m: Metadata = {\n title: seo.meta.title,\n }\n if (seo.meta.description !== undefined) {\n m.description = seo.meta.description\n }\n const langs = seo.meta.alternates?.languages\n const hasLang = langs !== undefined && Object.keys(langs).length > 0\n if (seo.meta.canonical || hasLang) {\n const alt: NonNullable<Metadata[\"alternates\"]> = {}\n if (seo.meta.canonical) alt.canonical = seo.meta.canonical\n if (hasLang && langs) alt.languages = { ...langs }\n m.alternates = alt\n }\n if (seo.meta.robots) {\n m.robots = robotsFromString(seo.meta.robots)\n }\n\n if (seo.openGraph) {\n const og = omitUndefined({\n title: seo.openGraph.title,\n description: seo.openGraph.description,\n url: seo.openGraph.url,\n type: seo.openGraph.type,\n images: seo.openGraph.images?.map((img) =>\n omitUndefined({\n url: img.url,\n width: img.width,\n height: img.height,\n alt: img.alt,\n }),\n ),\n })\n if (Object.keys(og).length > 0) {\n m.openGraph = og as Metadata[\"openGraph\"]\n }\n }\n\n if (seo.twitter) {\n const tw = omitUndefined({\n card: seo.twitter.card,\n title: seo.twitter.title,\n description: seo.twitter.description,\n images: seo.twitter.image ? [seo.twitter.image] : undefined,\n })\n if (Object.keys(tw).length > 0) {\n m.twitter = tw as Metadata[\"twitter\"]\n }\n }\n\n return m\n}\n","import type { Metadata } from \"next\"\nimport { registerAdapter } from \"@better-seo/core\"\nimport { toNextMetadata } from \"./to-next-metadata.js\"\n\nregisterAdapter<Metadata>({\n id: \"next\",\n toFramework: toNextMetadata,\n})\n","import type { Metadata } from \"next\"\nimport {\n createSEO,\n mergeSEO,\n type SEO,\n type SEOConfig,\n type SEOInput,\n withSEO as withSeoCore,\n} from \"@better-seo/core\"\nimport { toNextMetadata } from \"./to-next-metadata.js\"\n\n/** Voilà: `export const metadata = seo({ title: \"...\" })` (FEATURES N1). */\nexport function seo(input: SEOInput, config?: SEOConfig): Metadata {\n return toNextMetadata(createSEO(input, config))\n}\n\n/**\n * Layer child metadata on a parent `SEO` document (layout → page), then emit Next `Metadata`.\n */\nexport function withSEO(parent: SEO, child: SEOInput, config?: SEOConfig): Metadata {\n return toNextMetadata(withSeoCore(parent, child, config))\n}\n\n/** Explicit merge without `withSEO` naming (FEATURES N2). */\nexport function mergeNextMetadataSource(\n parent: SEO,\n child: SEOInput,\n config?: SEOConfig,\n): Metadata {\n return toNextMetadata(mergeSEO(parent, child, config))\n}\n\n/** One call → Next metadata + full `SEO` (for `NextJsonLd` in the same route). */\nexport function prepareNextSeo(\n input: SEOInput,\n config?: SEOConfig,\n): { readonly metadata: Metadata; readonly seo: SEO } {\n const doc = createSEO(input, config)\n return { metadata: toNextMetadata(doc), seo: doc }\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import { Metadata } from 'next';
2
+ import { SEO, SEOInput, SEOConfig } from '@better-seo/core';
3
+
4
+ /** Voilà: `export const metadata = seo({ title: "..." })` (FEATURES N1). */
5
+ declare function seo(input: SEOInput, config?: SEOConfig): Metadata;
6
+ /**
7
+ * Layer child metadata on a parent `SEO` document (layout → page), then emit Next `Metadata`.
8
+ */
9
+ declare function withSEO(parent: SEO, child: SEOInput, config?: SEOConfig): Metadata;
10
+ /** Explicit merge without `withSEO` naming (FEATURES N2). */
11
+ declare function mergeNextMetadataSource(parent: SEO, child: SEOInput, config?: SEOConfig): Metadata;
12
+ /** One call → Next metadata + full `SEO` (for `NextJsonLd` in the same route). */
13
+ declare function prepareNextSeo(input: SEOInput, config?: SEOConfig): {
14
+ readonly metadata: Metadata;
15
+ readonly seo: SEO;
16
+ };
17
+
18
+ /** Map normalized `SEO` → Next.js `Metadata` (App Router). JSON-LD is not included — use `@better-seo/next/json-ld`. */
19
+ declare function toNextMetadata(seo: SEO): Metadata;
20
+
21
+ export { mergeNextMetadataSource, prepareNextSeo, seo, toNextMetadata, withSEO };
@@ -0,0 +1,21 @@
1
+ import { Metadata } from 'next';
2
+ import { SEO, SEOInput, SEOConfig } from '@better-seo/core';
3
+
4
+ /** Voilà: `export const metadata = seo({ title: "..." })` (FEATURES N1). */
5
+ declare function seo(input: SEOInput, config?: SEOConfig): Metadata;
6
+ /**
7
+ * Layer child metadata on a parent `SEO` document (layout → page), then emit Next `Metadata`.
8
+ */
9
+ declare function withSEO(parent: SEO, child: SEOInput, config?: SEOConfig): Metadata;
10
+ /** Explicit merge without `withSEO` naming (FEATURES N2). */
11
+ declare function mergeNextMetadataSource(parent: SEO, child: SEOInput, config?: SEOConfig): Metadata;
12
+ /** One call → Next metadata + full `SEO` (for `NextJsonLd` in the same route). */
13
+ declare function prepareNextSeo(input: SEOInput, config?: SEOConfig): {
14
+ readonly metadata: Metadata;
15
+ readonly seo: SEO;
16
+ };
17
+
18
+ /** Map normalized `SEO` → Next.js `Metadata` (App Router). JSON-LD is not included — use `@better-seo/next/json-ld`. */
19
+ declare function toNextMetadata(seo: SEO): Metadata;
20
+
21
+ export { mergeNextMetadataSource, prepareNextSeo, seo, toNextMetadata, withSEO };
package/dist/index.js ADDED
@@ -0,0 +1,95 @@
1
+ import { registerAdapter, createSEO, withSEO as withSEO$1, mergeSEO } from '@better-seo/core';
2
+
3
+ // src/register.ts
4
+
5
+ // src/to-next-metadata.ts
6
+ function robotsFromString(robots) {
7
+ const lower = robots.toLowerCase();
8
+ const noindex = lower.includes("noindex");
9
+ const nofollow = lower.includes("nofollow");
10
+ if (!noindex && !nofollow) {
11
+ return { index: true, follow: true };
12
+ }
13
+ return {
14
+ index: !noindex,
15
+ follow: !nofollow
16
+ };
17
+ }
18
+ function omitUndefined(o) {
19
+ return Object.fromEntries(
20
+ Object.entries(o).filter(([, v]) => v !== void 0)
21
+ );
22
+ }
23
+ function toNextMetadata(seo2) {
24
+ const m = {
25
+ title: seo2.meta.title
26
+ };
27
+ if (seo2.meta.description !== void 0) {
28
+ m.description = seo2.meta.description;
29
+ }
30
+ const langs = seo2.meta.alternates?.languages;
31
+ const hasLang = langs !== void 0 && Object.keys(langs).length > 0;
32
+ if (seo2.meta.canonical || hasLang) {
33
+ const alt = {};
34
+ if (seo2.meta.canonical) alt.canonical = seo2.meta.canonical;
35
+ if (hasLang && langs) alt.languages = { ...langs };
36
+ m.alternates = alt;
37
+ }
38
+ if (seo2.meta.robots) {
39
+ m.robots = robotsFromString(seo2.meta.robots);
40
+ }
41
+ if (seo2.openGraph) {
42
+ const og = omitUndefined({
43
+ title: seo2.openGraph.title,
44
+ description: seo2.openGraph.description,
45
+ url: seo2.openGraph.url,
46
+ type: seo2.openGraph.type,
47
+ images: seo2.openGraph.images?.map(
48
+ (img) => omitUndefined({
49
+ url: img.url,
50
+ width: img.width,
51
+ height: img.height,
52
+ alt: img.alt
53
+ })
54
+ )
55
+ });
56
+ if (Object.keys(og).length > 0) {
57
+ m.openGraph = og;
58
+ }
59
+ }
60
+ if (seo2.twitter) {
61
+ const tw = omitUndefined({
62
+ card: seo2.twitter.card,
63
+ title: seo2.twitter.title,
64
+ description: seo2.twitter.description,
65
+ images: seo2.twitter.image ? [seo2.twitter.image] : void 0
66
+ });
67
+ if (Object.keys(tw).length > 0) {
68
+ m.twitter = tw;
69
+ }
70
+ }
71
+ return m;
72
+ }
73
+
74
+ // src/register.ts
75
+ registerAdapter({
76
+ id: "next",
77
+ toFramework: toNextMetadata
78
+ });
79
+ function seo(input, config) {
80
+ return toNextMetadata(createSEO(input, config));
81
+ }
82
+ function withSEO(parent, child, config) {
83
+ return toNextMetadata(withSEO$1(parent, child, config));
84
+ }
85
+ function mergeNextMetadataSource(parent, child, config) {
86
+ return toNextMetadata(mergeSEO(parent, child, config));
87
+ }
88
+ function prepareNextSeo(input, config) {
89
+ const doc = createSEO(input, config);
90
+ return { metadata: toNextMetadata(doc), seo: doc };
91
+ }
92
+
93
+ export { mergeNextMetadataSource, prepareNextSeo, seo, toNextMetadata, withSEO };
94
+ //# sourceMappingURL=index.js.map
95
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/to-next-metadata.ts","../src/register.ts","../src/surface.ts"],"names":["seo","withSeoCore"],"mappings":"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","file":"index.js","sourcesContent":["import type { Metadata } from \"next\"\nimport type { SEO } from \"@better-seo/core\"\n\nfunction robotsFromString(robots: string): Metadata[\"robots\"] {\n const lower = robots.toLowerCase()\n const noindex = lower.includes(\"noindex\")\n const nofollow = lower.includes(\"nofollow\")\n if (!noindex && !nofollow) {\n return { index: true, follow: true }\n }\n return {\n index: !noindex,\n follow: !nofollow,\n }\n}\n\nfunction omitUndefined<T extends Record<string, unknown>>(o: T): Partial<T> {\n return Object.fromEntries(\n (Object.entries(o) as [string, unknown][]).filter(([, v]) => v !== undefined),\n ) as Partial<T>\n}\n\n/** Map normalized `SEO` → Next.js `Metadata` (App Router). JSON-LD is not included — use `@better-seo/next/json-ld`. */\nexport function toNextMetadata(seo: SEO): Metadata {\n const m: Metadata = {\n title: seo.meta.title,\n }\n if (seo.meta.description !== undefined) {\n m.description = seo.meta.description\n }\n const langs = seo.meta.alternates?.languages\n const hasLang = langs !== undefined && Object.keys(langs).length > 0\n if (seo.meta.canonical || hasLang) {\n const alt: NonNullable<Metadata[\"alternates\"]> = {}\n if (seo.meta.canonical) alt.canonical = seo.meta.canonical\n if (hasLang && langs) alt.languages = { ...langs }\n m.alternates = alt\n }\n if (seo.meta.robots) {\n m.robots = robotsFromString(seo.meta.robots)\n }\n\n if (seo.openGraph) {\n const og = omitUndefined({\n title: seo.openGraph.title,\n description: seo.openGraph.description,\n url: seo.openGraph.url,\n type: seo.openGraph.type,\n images: seo.openGraph.images?.map((img) =>\n omitUndefined({\n url: img.url,\n width: img.width,\n height: img.height,\n alt: img.alt,\n }),\n ),\n })\n if (Object.keys(og).length > 0) {\n m.openGraph = og as Metadata[\"openGraph\"]\n }\n }\n\n if (seo.twitter) {\n const tw = omitUndefined({\n card: seo.twitter.card,\n title: seo.twitter.title,\n description: seo.twitter.description,\n images: seo.twitter.image ? [seo.twitter.image] : undefined,\n })\n if (Object.keys(tw).length > 0) {\n m.twitter = tw as Metadata[\"twitter\"]\n }\n }\n\n return m\n}\n","import type { Metadata } from \"next\"\nimport { registerAdapter } from \"@better-seo/core\"\nimport { toNextMetadata } from \"./to-next-metadata.js\"\n\nregisterAdapter<Metadata>({\n id: \"next\",\n toFramework: toNextMetadata,\n})\n","import type { Metadata } from \"next\"\nimport {\n createSEO,\n mergeSEO,\n type SEO,\n type SEOConfig,\n type SEOInput,\n withSEO as withSeoCore,\n} from \"@better-seo/core\"\nimport { toNextMetadata } from \"./to-next-metadata.js\"\n\n/** Voilà: `export const metadata = seo({ title: \"...\" })` (FEATURES N1). */\nexport function seo(input: SEOInput, config?: SEOConfig): Metadata {\n return toNextMetadata(createSEO(input, config))\n}\n\n/**\n * Layer child metadata on a parent `SEO` document (layout → page), then emit Next `Metadata`.\n */\nexport function withSEO(parent: SEO, child: SEOInput, config?: SEOConfig): Metadata {\n return toNextMetadata(withSeoCore(parent, child, config))\n}\n\n/** Explicit merge without `withSEO` naming (FEATURES N2). */\nexport function mergeNextMetadataSource(\n parent: SEO,\n child: SEOInput,\n config?: SEOConfig,\n): Metadata {\n return toNextMetadata(mergeSEO(parent, child, config))\n}\n\n/** One call → Next metadata + full `SEO` (for `NextJsonLd` in the same route). */\nexport function prepareNextSeo(\n input: SEOInput,\n config?: SEOConfig,\n): { readonly metadata: Metadata; readonly seo: SEO } {\n const doc = createSEO(input, config)\n return { metadata: toNextMetadata(doc), seo: doc }\n}\n"]}
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ var core = require('@better-seo/core');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/next-json-ld.tsx
7
+ function NextJsonLd(props) {
8
+ const { seo } = props;
9
+ if (!seo.schema.length) return null;
10
+ const [first] = seo.schema;
11
+ if (!first) return null;
12
+ const payload = seo.schema.length === 1 ? first : [...seo.schema];
13
+ const json = core.serializeJSONLD(payload);
14
+ return /* @__PURE__ */ jsxRuntime.jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: json } });
15
+ }
16
+
17
+ exports.NextJsonLd = NextJsonLd;
18
+ //# sourceMappingURL=json-ld.cjs.map
19
+ //# sourceMappingURL=json-ld.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next-json-ld.tsx"],"names":["serializeJSONLD","jsx"],"mappings":";;;;;;AAIO,SAAS,WAAW,KAAA,EAAwD;AACjF,EAAA,MAAM,EAAE,KAAI,GAAI,KAAA;AAChB,EAAA,IAAI,CAAC,GAAA,CAAI,MAAA,CAAO,MAAA,EAAQ,OAAO,IAAA;AAC/B,EAAA,MAAM,CAAC,KAAK,CAAA,GAAI,GAAA,CAAI,MAAA;AACpB,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,OAAA,GAAU,IAAI,MAAA,CAAO,MAAA,KAAW,IAAI,KAAA,GAAQ,CAAC,GAAG,GAAA,CAAI,MAAM,CAAA;AAChE,EAAA,MAAM,IAAA,GAAOA,qBAAgB,OAAO,CAAA;AACpC,EAAA,uBAAOC,cAAA,CAAC,YAAO,IAAA,EAAK,qBAAA,EAAsB,yBAAyB,EAAE,MAAA,EAAQ,MAAK,EAAG,CAAA;AACvF","file":"json-ld.cjs","sourcesContent":["import { serializeJSONLD } from \"@better-seo/core\"\nimport type { SEO } from \"@better-seo/core\"\n\n/** Renders JSON-LD using core `serializeJSONLD` only (FEATURES N4, ARCHITECTURE §7). */\nexport function NextJsonLd(props: { readonly seo: SEO }): React.JSX.Element | null {\n const { seo } = props\n if (!seo.schema.length) return null\n const [first] = seo.schema\n if (!first) return null\n const payload = seo.schema.length === 1 ? first : [...seo.schema]\n const json = serializeJSONLD(payload)\n return <script type=\"application/ld+json\" dangerouslySetInnerHTML={{ __html: json }} />\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import { SEO } from '@better-seo/core';
2
+
3
+ /** Renders JSON-LD using core `serializeJSONLD` only (FEATURES N4, ARCHITECTURE §7). */
4
+ declare function NextJsonLd(props: {
5
+ readonly seo: SEO;
6
+ }): React.JSX.Element | null;
7
+
8
+ export { NextJsonLd };
@@ -0,0 +1,8 @@
1
+ import { SEO } from '@better-seo/core';
2
+
3
+ /** Renders JSON-LD using core `serializeJSONLD` only (FEATURES N4, ARCHITECTURE §7). */
4
+ declare function NextJsonLd(props: {
5
+ readonly seo: SEO;
6
+ }): React.JSX.Element | null;
7
+
8
+ export { NextJsonLd };
@@ -0,0 +1,17 @@
1
+ import { serializeJSONLD } from '@better-seo/core';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/next-json-ld.tsx
5
+ function NextJsonLd(props) {
6
+ const { seo } = props;
7
+ if (!seo.schema.length) return null;
8
+ const [first] = seo.schema;
9
+ if (!first) return null;
10
+ const payload = seo.schema.length === 1 ? first : [...seo.schema];
11
+ const json = serializeJSONLD(payload);
12
+ return /* @__PURE__ */ jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: json } });
13
+ }
14
+
15
+ export { NextJsonLd };
16
+ //# sourceMappingURL=json-ld.js.map
17
+ //# sourceMappingURL=json-ld.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next-json-ld.tsx"],"names":[],"mappings":";;;;AAIO,SAAS,WAAW,KAAA,EAAwD;AACjF,EAAA,MAAM,EAAE,KAAI,GAAI,KAAA;AAChB,EAAA,IAAI,CAAC,GAAA,CAAI,MAAA,CAAO,MAAA,EAAQ,OAAO,IAAA;AAC/B,EAAA,MAAM,CAAC,KAAK,CAAA,GAAI,GAAA,CAAI,MAAA;AACpB,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,OAAA,GAAU,IAAI,MAAA,CAAO,MAAA,KAAW,IAAI,KAAA,GAAQ,CAAC,GAAG,GAAA,CAAI,MAAM,CAAA;AAChE,EAAA,MAAM,IAAA,GAAO,gBAAgB,OAAO,CAAA;AACpC,EAAA,uBAAO,GAAA,CAAC,YAAO,IAAA,EAAK,qBAAA,EAAsB,yBAAyB,EAAE,MAAA,EAAQ,MAAK,EAAG,CAAA;AACvF","file":"json-ld.js","sourcesContent":["import { serializeJSONLD } from \"@better-seo/core\"\nimport type { SEO } from \"@better-seo/core\"\n\n/** Renders JSON-LD using core `serializeJSONLD` only (FEATURES N4, ARCHITECTURE §7). */\nexport function NextJsonLd(props: { readonly seo: SEO }): React.JSX.Element | null {\n const { seo } = props\n if (!seo.schema.length) return null\n const [first] = seo.schema\n if (!first) return null\n const payload = seo.schema.length === 1 ? first : [...seo.schema]\n const json = serializeJSONLD(payload)\n return <script type=\"application/ld+json\" dangerouslySetInnerHTML={{ __html: json }} />\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@better-seo/next",
3
+ "version": "0.0.1",
4
+ "description": "Next.js integration for @better-seo/core on the App Router (and documented Pages Router patterns): turn your SEO model into Metadata and generateMetadata, use shorthand helpers like seo() and prepareNextSeo, merge layout and page SEO without duplicating logic, and stream JSON-LD through NextJsonLd from @better-seo/next/json-ld so structured data uses the same serializeJSONLD path as everywhere else. Peer dependencies: next >= 14.2 and react >= 18.2.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": true,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ },
17
+ "./json-ld": {
18
+ "types": "./dist/json-ld.d.ts",
19
+ "import": "./dist/json-ld.js",
20
+ "require": "./dist/json-ld.cjs"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "dependencies": {
27
+ "@better-seo/core": "0.0.1"
28
+ },
29
+ "peerDependencies": {
30
+ "next": ">=14.2.0",
31
+ "react": ">=18.2.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "next": {
35
+ "optional": false
36
+ }
37
+ },
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "test": "vitest run",
41
+ "test:coverage": "vitest run --coverage",
42
+ "lint": "eslint .",
43
+ "typecheck": "tsc -p tsconfig.json --noEmit",
44
+ "audit": "npm audit --audit-level=high"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "engines": {
50
+ "node": ">=20"
51
+ },
52
+ "devDependencies": {
53
+ "@types/react": "^19.0.8",
54
+ "@vitest/coverage-v8": "^3.0.6",
55
+ "tsup": "^8.3.6",
56
+ "typescript": "^5.7.3",
57
+ "vitest": "^3.0.6"
58
+ }
59
+ }