@hezaerd/seo 0.1.0 → 0.1.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 CHANGED
@@ -48,6 +48,48 @@ Your root layout must render TanStack's `HeadContent` in `<head>` as usual.
48
48
 
49
49
  Both adapters accept the same options, including `image`, `imageAlt`, `siteName`, `locale`, `robots`, and `jsonLd`. Pass absolute URLs (strings or `URL` objects); `url` also supplies the canonical URL unless `canonical` is set. Public types and helpers are available from `@hezaerd/seo/core`.
50
50
 
51
+ ## Structured data
52
+
53
+ Use `defineJsonLd` to add the Schema.org context to a typed node. Schema types
54
+ are re-exported from `schema-dts` through `@hezaerd/seo/core` (and the package root).
55
+
56
+ ```ts
57
+ import { defineJsonLd, type Article } from '@hezaerd/seo/core';
58
+ import { seo } from '@hezaerd/seo/tanstack-router';
59
+
60
+ const article = defineJsonLd<Article>({
61
+ '@type': 'Article',
62
+ headline: 'Introducing my website',
63
+ author: { '@type': 'Person', name: 'Hezaerd' },
64
+ });
65
+
66
+ const head = seo({ jsonLd: article });
67
+ ```
68
+
69
+ In Astro, pass the same object to `<Seo jsonLd={article} />`.
70
+ For interconnected nodes, use a graph:
71
+
72
+ ```ts
73
+ import { defineJsonLdGraph } from '@hezaerd/seo/core';
74
+
75
+ const graph = defineJsonLdGraph([
76
+ { '@type': 'Person', '@id': 'https://example.com/#author', name: 'Hezaerd' },
77
+ {
78
+ '@type': 'Article',
79
+ headline: 'Introducing my website',
80
+ author: { '@id': 'https://example.com/#author' },
81
+ },
82
+ ]);
83
+ ```
84
+
85
+ Pass `graph` as `jsonLd` to render one script, or pass an array of documents to
86
+ render several scripts. Existing custom JSON-LD objects remain supported. You
87
+ can also author documents directly with `WithContext<Article>` or `Graph`.
88
+ Both adapters safely escape JSON-LD for inline HTML scripts.
89
+
90
+ The helpers provide compile-time Schema.org typing; they do not perform runtime
91
+ validation or guarantee eligibility for Google rich results.
92
+
51
93
  ## Development
52
94
 
53
95
  ```sh
package/dist/core.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { Graph, JsonLdObject, Thing, WithContext } from "schema-dts";
2
+ export type * from "schema-dts";
1
3
  //#region src/core.d.ts
2
4
  /** A URL accepted by the SEO helpers. */
3
5
  export type SeoUrl = string | URL;
@@ -5,6 +7,12 @@ export type SeoUrl = string | URL;
5
7
  export type JsonLdValue = string | number | boolean | null | readonly JsonLdValue[] | {
6
8
  readonly [key: string]: JsonLdValue | undefined;
7
9
  };
10
+ /** A custom JSON value or a typed Schema.org document. */
11
+ export type JsonLdDocument = WithContext<JsonLdObject> | Graph | JsonLdValue;
12
+ /** Add the Schema.org context to a typed node without mutating it. */
13
+ export declare function defineJsonLd<T extends Thing>(value: Exclude<T, string>): WithContext<Exclude<T, string>>;
14
+ /** Group linked Schema.org nodes under one context. */
15
+ export declare function defineJsonLdGraph(nodes: Graph["@graph"]): Graph;
8
16
  export type OpenGraphType = "website" | "article" | "profile" | "book" | "music.song" | "music.album" | "music.playlist" | "music.radio_status" | "video.movie" | "video.episode" | "video.tv_show" | "video.other" | (string & {});
9
17
  export type TwitterCard = "summary" | "summary_large_image" | "app" | "player" | (string & {});
10
18
  /**
@@ -41,7 +49,7 @@ export interface SeoOptions {
41
49
  /** Add `nofollow` to the robots directives. */
42
50
  noFollow?: boolean;
43
51
  /** One JSON-LD document, or several documents to render. */
44
- jsonLd?: JsonLdValue | readonly JsonLdValue[];
52
+ jsonLd?: JsonLdDocument | readonly JsonLdDocument[];
45
53
  }
46
54
  /** Normalized SEO data shared by the framework adapters. */
47
55
  export interface BuiltSeo {
@@ -56,7 +64,7 @@ export interface BuiltSeo {
56
64
  locale?: string;
57
65
  twitterCard: TwitterCard;
58
66
  robots?: string;
59
- jsonLd: readonly JsonLdValue[];
67
+ jsonLd: readonly JsonLdDocument[];
60
68
  }
61
69
  /**
62
70
  * Normalize SEO options once before passing them to a framework adapter.
@@ -71,5 +79,5 @@ export declare function buildSeo(options?: SeoOptions): BuiltSeo;
71
79
  * HTML-significant characters are escaped after JSON encoding, preventing a
72
80
  * value such as `</script>` from closing the containing script element.
73
81
  */
74
- export declare function serializeJsonLd(value: JsonLdValue): string;
82
+ export declare function serializeJsonLd(value: JsonLdDocument): string;
75
83
  //#endregion
package/dist/core.js CHANGED
@@ -1,4 +1,18 @@
1
1
  //#region src/core.ts
2
+ /** Add the Schema.org context to a typed node without mutating it. */
3
+ function defineJsonLd(value) {
4
+ return {
5
+ ...value,
6
+ "@context": "https://schema.org"
7
+ };
8
+ }
9
+ /** Group linked Schema.org nodes under one context. */
10
+ function defineJsonLdGraph(nodes) {
11
+ return {
12
+ "@context": "https://schema.org",
13
+ "@graph": [...nodes]
14
+ };
15
+ }
2
16
  function normalizeText(value) {
3
17
  if (value === void 0) return void 0;
4
18
  const normalized = value.trim();
@@ -27,9 +41,12 @@ function normalizeRobots(options) {
27
41
  }
28
42
  return directives.length > 0 ? directives.join(", ") : void 0;
29
43
  }
44
+ function isJsonLdArray(value) {
45
+ return Array.isArray(value);
46
+ }
30
47
  function normalizeJsonLd(value) {
31
48
  if (value === void 0) return [];
32
- return Array.isArray(value) ? value : [value];
49
+ return isJsonLdArray(value) ? value : [value];
33
50
  }
34
51
  /**
35
52
  * Normalize SEO options once before passing them to a framework adapter.
@@ -73,4 +90,4 @@ function serializeJsonLd(value) {
73
90
  }
74
91
  }
75
92
  //#endregion
76
- export { buildSeo, serializeJsonLd };
93
+ export { buildSeo, defineJsonLd, defineJsonLdGraph, serializeJsonLd };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hezaerd/seo",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A small, typed SEO metadata helper for Astro and TanStack Router",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -28,7 +28,7 @@
28
28
  "build": "tsdown",
29
29
  "check": "tsc --noEmit && astro check",
30
30
  "test": "bun test --pass-with-no-tests",
31
- "prepublishOnly": "bun run check && bun run test && bun run build"
31
+ "prepublishOnly": "bun run build && bun run check && bun run test"
32
32
  },
33
33
  "keywords": ["seo", "astro", "tanstack", "tanstack-router"],
34
34
  "license": "MIT",
@@ -43,6 +43,9 @@
43
43
  "@tanstack/react-router": "^1.0.0",
44
44
  "astro": "^7.0.0"
45
45
  },
46
+ "dependencies": {
47
+ "schema-dts": "^2.0.0"
48
+ },
46
49
  "peerDependenciesMeta": {
47
50
  "@tanstack/react-router": { "optional": true },
48
51
  "astro": { "optional": true }
package/src/core.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import type { Graph, JsonLdObject, Thing, WithContext } from "schema-dts";
2
+
3
+ export type * from "schema-dts";
4
+
1
5
  /** A URL accepted by the SEO helpers. */
2
6
  export type SeoUrl = string | URL;
3
7
 
@@ -10,6 +14,21 @@ export type JsonLdValue =
10
14
  | readonly JsonLdValue[]
11
15
  | { readonly [key: string]: JsonLdValue | undefined };
12
16
 
17
+ /** A custom JSON value or a typed Schema.org document. */
18
+ export type JsonLdDocument = WithContext<JsonLdObject> | Graph | JsonLdValue;
19
+
20
+ /** Add the Schema.org context to a typed node without mutating it. */
21
+ export function defineJsonLd<T extends Thing>(
22
+ value: Exclude<T, string>,
23
+ ): WithContext<Exclude<T, string>> {
24
+ return { ...value, "@context": "https://schema.org" };
25
+ }
26
+
27
+ /** Group linked Schema.org nodes under one context. */
28
+ export function defineJsonLdGraph(nodes: Graph["@graph"]): Graph {
29
+ return { "@context": "https://schema.org", "@graph": [...nodes] };
30
+ }
31
+
13
32
  export type OpenGraphType =
14
33
  | "website"
15
34
  | "article"
@@ -66,7 +85,7 @@ export interface SeoOptions {
66
85
  /** Add `nofollow` to the robots directives. */
67
86
  noFollow?: boolean;
68
87
  /** One JSON-LD document, or several documents to render. */
69
- jsonLd?: JsonLdValue | readonly JsonLdValue[];
88
+ jsonLd?: JsonLdDocument | readonly JsonLdDocument[];
70
89
  }
71
90
 
72
91
  /** Normalized SEO data shared by the framework adapters. */
@@ -82,7 +101,7 @@ export interface BuiltSeo {
82
101
  locale?: string;
83
102
  twitterCard: TwitterCard;
84
103
  robots?: string;
85
- jsonLd: readonly JsonLdValue[];
104
+ jsonLd: readonly JsonLdDocument[];
86
105
  }
87
106
 
88
107
  function normalizeText(value: string | undefined): string | undefined {
@@ -128,11 +147,17 @@ function normalizeRobots(options: SeoOptions): string | undefined {
128
147
  return directives.length > 0 ? directives.join(", ") : undefined;
129
148
  }
130
149
 
150
+ function isJsonLdArray(
151
+ value: JsonLdDocument | readonly JsonLdDocument[],
152
+ ): value is readonly JsonLdDocument[] {
153
+ return Array.isArray(value);
154
+ }
155
+
131
156
  function normalizeJsonLd(
132
157
  value: SeoOptions["jsonLd"],
133
- ): readonly JsonLdValue[] {
158
+ ): readonly JsonLdDocument[] {
134
159
  if (value === undefined) return [];
135
- return Array.isArray(value) ? value : [value];
160
+ return isJsonLdArray(value) ? value : [value];
136
161
  }
137
162
 
138
163
  /**
@@ -177,7 +202,7 @@ function escapeJsonForHtml(value: string): string {
177
202
  * HTML-significant characters are escaped after JSON encoding, preventing a
178
203
  * value such as `</script>` from closing the containing script element.
179
204
  */
180
- export function serializeJsonLd(value: JsonLdValue): string {
205
+ export function serializeJsonLd(value: JsonLdDocument): string {
181
206
  try {
182
207
  return escapeJsonForHtml(JSON.stringify(value) ?? "null");
183
208
  } catch (error) {