@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 +42 -0
- package/dist/core.d.ts +11 -3
- package/dist/core.js +19 -2
- package/package.json +5 -2
- package/src/core.ts +30 -5
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?:
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
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
|
|
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?:
|
|
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
|
|
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
|
|
158
|
+
): readonly JsonLdDocument[] {
|
|
134
159
|
if (value === undefined) return [];
|
|
135
|
-
return
|
|
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:
|
|
205
|
+
export function serializeJsonLd(value: JsonLdDocument): string {
|
|
181
206
|
try {
|
|
182
207
|
return escapeJsonForHtml(JSON.stringify(value) ?? "null");
|
|
183
208
|
} catch (error) {
|