@tsdoctor/seo 0.1.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/Attribution.js +99 -0
- package/Canonical.js +99 -0
- package/HeadTag.js +69 -0
- package/LICENSE +21 -0
- package/OpenGraph.js +156 -0
- package/README.md +94 -0
- package/Seo.js +49 -0
- package/StructuredData.js +169 -0
- package/index.d.ts +543 -0
- package/index.js +8 -0
- package/package.json +48 -0
- package/tsdoc-metadata.json +11 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { PackageManifest } from "@effected/package-json";
|
|
2
|
+
import { Result, Schema } from "effect";
|
|
3
|
+
import { ConflictingTermError, DuplicateNodeIdError, InvalidNodeIdError, JsonLdDocument, JsonLdNode } from "@effected/schema-org";
|
|
4
|
+
//#region src/Attribution.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The attribution a page can credit, derived from one package manifest.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* `licenseIds` and `primaryLicenseId` are deliberately both present.
|
|
10
|
+
* `primaryLicenseId` is absent for an `AND` expression, where every term binds
|
|
11
|
+
* at once and naming one would silently drop a license that legally applies;
|
|
12
|
+
* `licenseIds` always lists every license named. A consumer wanting one value
|
|
13
|
+
* reads `primaryLicenseId` and degrades when it is absent; one that can carry
|
|
14
|
+
* several (schema.org's `license` accepts an array) reads `licenseIds`.
|
|
15
|
+
*
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
interface AttributionFacts {
|
|
19
|
+
/** The `author` field's name. */
|
|
20
|
+
readonly authorName?: string;
|
|
21
|
+
/** The `author` field's homepage, when it carried one. */
|
|
22
|
+
readonly authorUrl?: string;
|
|
23
|
+
/** Every `maintainers` entry's name, in manifest order. */
|
|
24
|
+
readonly maintainerNames: ReadonlyArray<string>;
|
|
25
|
+
/**
|
|
26
|
+
* The browsable URL of **this package** within its repository.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* `Repository.directoryUrl` when the host's subdirectory convention is
|
|
30
|
+
* known, falling back to `Repository.browseUrl` otherwise — see
|
|
31
|
+
* {@link attributionFacts} for why the fallback is deliberate.
|
|
32
|
+
*/
|
|
33
|
+
readonly repositoryUrl?: string;
|
|
34
|
+
/** The `homepage` field, verbatim. */
|
|
35
|
+
readonly homepage?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Every SPDX identifier the `license` expression names, in written order.
|
|
38
|
+
* Empty when the manifest carries no license, or one that is not SPDX.
|
|
39
|
+
*/
|
|
40
|
+
readonly licenseIds: ReadonlyArray<string>;
|
|
41
|
+
/**
|
|
42
|
+
* The single identifier the expression can be said to be under, absent for
|
|
43
|
+
* an `AND`.
|
|
44
|
+
*/
|
|
45
|
+
readonly primaryLicenseId?: string;
|
|
46
|
+
/**
|
|
47
|
+
* The canonical SPDX page for {@link AttributionFacts.primaryLicenseId},
|
|
48
|
+
* absent for a `LicenseRef` or any id outside the catalog.
|
|
49
|
+
*/
|
|
50
|
+
readonly licenseUrl?: string;
|
|
51
|
+
/**
|
|
52
|
+
* The canonical SPDX page for EVERY license the expression names, in
|
|
53
|
+
* written order, skipping any that has none.
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* The plural counterpart to {@link AttributionFacts.licenseUrl}, and the
|
|
57
|
+
* one a consumer wants when the field it is filling accepts several —
|
|
58
|
+
* schema.org's `license` does. `licenseUrl` names only the primary, and an
|
|
59
|
+
* `AND` expression has no primary, so a dual-licensed package reading only
|
|
60
|
+
* the singular gets nothing at all.
|
|
61
|
+
*
|
|
62
|
+
* Shorter than `licenseIds` whenever a named license is outside the SPDX
|
|
63
|
+
* catalog (a `LicenseRef`), which is why the two are not index-aligned.
|
|
64
|
+
*/
|
|
65
|
+
readonly licenseUrls: ReadonlyArray<string>;
|
|
66
|
+
/** The `keywords` field, verbatim. */
|
|
67
|
+
readonly keywords: ReadonlyArray<string>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Derive the attribution facts a documentation page can credit from a decoded
|
|
71
|
+
* package manifest.
|
|
72
|
+
*
|
|
73
|
+
* @remarks
|
|
74
|
+
* Total: a manifest carrying none of these fields yields empty arrays and no
|
|
75
|
+
* optional properties, never a failure. Per-field degradation is the contract —
|
|
76
|
+
* an unparseable license drops only the license facts, an unrecognized
|
|
77
|
+
* repository reference drops only the repository URL.
|
|
78
|
+
*
|
|
79
|
+
* @param manifest - the decoded manifest to read
|
|
80
|
+
* @returns the facts, with every underivable field absent
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* import { PackageManifest } from "@effected/package-json";
|
|
85
|
+
* import { attributionFacts } from "@tsdoctor/seo";
|
|
86
|
+
* import { Effect } from "effect";
|
|
87
|
+
*
|
|
88
|
+
* const program = Effect.gen(function* () {
|
|
89
|
+
* const manifest = yield* PackageManifest.decode({
|
|
90
|
+
* name: "@scope/pkg",
|
|
91
|
+
* version: "1.0.0",
|
|
92
|
+
* license: "MIT",
|
|
93
|
+
* repository: { url: "github:owner/repo", directory: "packages/pkg" },
|
|
94
|
+
* });
|
|
95
|
+
* const facts = attributionFacts(manifest);
|
|
96
|
+
* console.log(facts.primaryLicenseId, facts.repositoryUrl);
|
|
97
|
+
* // => "MIT" "https://github.com/owner/repo/tree/HEAD/packages/pkg"
|
|
98
|
+
* });
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @public
|
|
102
|
+
*/
|
|
103
|
+
declare function attributionFacts(manifest: PackageManifest): AttributionFacts;
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/Canonical.d.ts
|
|
106
|
+
/**
|
|
107
|
+
* URL derivation for a documentation page: the site URL prefix, the canonical
|
|
108
|
+
* page URL, absolute image URLs and image MIME mapping.
|
|
109
|
+
*
|
|
110
|
+
* @remarks
|
|
111
|
+
* `imageMimeType`, `resolveUrl` and `deriveSiteUrl` moved here verbatim from
|
|
112
|
+
* the RSPress adapter's `og-resolver.ts`. `resolveUrl` was `resolveOgUrl`
|
|
113
|
+
* there; canonical links resolve through the same function, so the
|
|
114
|
+
* OG-specific name no longer fits. Everything here is total and synchronous —
|
|
115
|
+
* no Effect, no filesystem.
|
|
116
|
+
*
|
|
117
|
+
* @packageDocumentation
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* The `og:image:type` value for a detected image format, or `undefined` for a
|
|
121
|
+
* format with no mapping.
|
|
122
|
+
*
|
|
123
|
+
* @public
|
|
124
|
+
*/
|
|
125
|
+
declare function imageMimeType(type: string | undefined): string | undefined;
|
|
126
|
+
/**
|
|
127
|
+
* Turn a configured URL into an absolute one.
|
|
128
|
+
*
|
|
129
|
+
* @returns The absolute URL, or `undefined` when the input is neither an
|
|
130
|
+
* absolute `http(s)` URL nor a site-root-relative path. A bare relative path
|
|
131
|
+
* is deliberately rejected rather than guessed at — there is no base to
|
|
132
|
+
* resolve it against that would not silently produce a broken link.
|
|
133
|
+
*
|
|
134
|
+
* @public
|
|
135
|
+
*/
|
|
136
|
+
declare function resolveUrl(siteUrl: string, url: string): string | undefined;
|
|
137
|
+
/**
|
|
138
|
+
* Derive the site URL prefix from the framework's own config.
|
|
139
|
+
*
|
|
140
|
+
* @remarks
|
|
141
|
+
* Replaces the RSPress plugin's former `siteUrl` option. RSPress already knows
|
|
142
|
+
* where a site is deployed — {@link https://rspress.rs/api/config/config-basic#siteorigin | `siteOrigin`}
|
|
143
|
+
* plus `base` — so asking for it a second time invited the two to disagree, and
|
|
144
|
+
* a plugin-level answer that contradicted the site's own would silently emit
|
|
145
|
+
* canonical and `og:url` tags pointing at a host the site is not served from.
|
|
146
|
+
*
|
|
147
|
+
* RSPress concatenates as `siteOrigin + base + routePath`, and **this follows
|
|
148
|
+
* its documented fallback exactly**: with no `siteOrigin`, RSPress uses
|
|
149
|
+
* `base + routePath`. So an unset origin yields a ROOT-RELATIVE prefix rather
|
|
150
|
+
* than nothing.
|
|
151
|
+
*
|
|
152
|
+
* That fallback is what makes the tags inspectable in `rspress dev`, where the
|
|
153
|
+
* site is served from `localhost` and no configured origin could be correct
|
|
154
|
+
* anyway. A root-relative `/images/og.png` resolves against the page's own
|
|
155
|
+
* origin in the browser; it is a *relative* path (`images/og.png`, no leading
|
|
156
|
+
* slash) that has no base to resolve against, and this never emits one.
|
|
157
|
+
*
|
|
158
|
+
* @returns The prefix to put in front of a route that already begins with `/`.
|
|
159
|
+
* `""` when the site declares neither `siteOrigin` nor a non-root `base`, which
|
|
160
|
+
* leaves every URL root-relative. Never has a trailing slash, since every
|
|
161
|
+
* caller appends a route starting with `/`.
|
|
162
|
+
*
|
|
163
|
+
* @public
|
|
164
|
+
*/
|
|
165
|
+
declare function deriveSiteUrl(siteOrigin: string | undefined, base: string | undefined): string;
|
|
166
|
+
/**
|
|
167
|
+
* The canonical URL for a page.
|
|
168
|
+
*
|
|
169
|
+
* @remarks
|
|
170
|
+
* With no configured origin the prefix is `""`, so the result is
|
|
171
|
+
* root-relative (`/api/class/foo`) rather than absent. That matches RSPress's
|
|
172
|
+
* own documented `base + routePath` fallback and keeps the tag inspectable
|
|
173
|
+
* under a dev server, where no configured origin could be correct.
|
|
174
|
+
*
|
|
175
|
+
* @public
|
|
176
|
+
*/
|
|
177
|
+
declare function canonicalUrl(siteUrl: string, pageRoute: string): string;
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/HeadTag.d.ts
|
|
180
|
+
/**
|
|
181
|
+
* The neutral head-tag vocabulary every adapter renders.
|
|
182
|
+
*
|
|
183
|
+
* @remarks
|
|
184
|
+
* A `HeadTag` is deliberately not a framework element. RSPress renders one
|
|
185
|
+
* into a frontmatter `head` pair; VitePress renders the same value into a
|
|
186
|
+
* `transformHead` entry. Keeping the type this dumb is what makes the second
|
|
187
|
+
* adapter cheap.
|
|
188
|
+
*
|
|
189
|
+
* @packageDocumentation
|
|
190
|
+
*/
|
|
191
|
+
/**
|
|
192
|
+
* One tag destined for a page's `<head>`.
|
|
193
|
+
*
|
|
194
|
+
* @public
|
|
195
|
+
*/
|
|
196
|
+
interface HeadTag {
|
|
197
|
+
readonly tag: "meta" | "link" | "script";
|
|
198
|
+
readonly attrs: Readonly<Record<string, string>>;
|
|
199
|
+
/** Element content. Only meaningful for `script`. */
|
|
200
|
+
readonly body?: string;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Escape a JSON string so it cannot terminate the `<script>` element that
|
|
204
|
+
* carries it.
|
|
205
|
+
*
|
|
206
|
+
* @remarks
|
|
207
|
+
* Every string in a JSON-LD graph originates in author-written TSDoc, so a
|
|
208
|
+
* summary containing the literal `</script>` would close the element early and
|
|
209
|
+
* inject markup into the page. `JSON.stringify` does not escape it.
|
|
210
|
+
*
|
|
211
|
+
* Escaping both angle brackets as `<` / `>` is valid JSON that
|
|
212
|
+
* parses back to the original characters, so the graph a consumer reads is
|
|
213
|
+
* unchanged while the element becomes unclosable from inside.
|
|
214
|
+
*
|
|
215
|
+
* `&` is escaped for the same reason at a different layer: XHTML parses
|
|
216
|
+
* script content as ordinary element content, where a bare `&` is a
|
|
217
|
+
* well-formedness error. An HTML-parsed page tolerates it; an XHTML-served one
|
|
218
|
+
* does not, and nothing in a docs pipeline guarantees which a consumer serves.
|
|
219
|
+
*
|
|
220
|
+
* The escape is idempotent — no escape sequence it emits contains `<`, `>` or
|
|
221
|
+
* `&` — so a body that arrives already escaped by an upstream serializer
|
|
222
|
+
* survives a second pass unchanged.
|
|
223
|
+
*
|
|
224
|
+
* @public
|
|
225
|
+
*/
|
|
226
|
+
declare function escapeScriptBody(json: string): string;
|
|
227
|
+
/** An Open Graph style `<meta property=… content=…>`. @public */
|
|
228
|
+
declare function meta(property: string, content: string): HeadTag;
|
|
229
|
+
/** A Twitter/standard style `<meta name=… content=…>`. @public */
|
|
230
|
+
declare function metaNamed(name: string, content: string): HeadTag;
|
|
231
|
+
/** A `<link rel=… href=…>`. @public */
|
|
232
|
+
declare function link(rel: string, href: string): HeadTag;
|
|
233
|
+
/** A `<script type="application/ld+json">` carrying an escaped body. @public */
|
|
234
|
+
declare function jsonLd(json: string): HeadTag;
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/OpenGraph.d.ts
|
|
237
|
+
/**
|
|
238
|
+
* Structured Open Graph image metadata (alternative to a plain URL string).
|
|
239
|
+
*
|
|
240
|
+
* @public
|
|
241
|
+
*/
|
|
242
|
+
declare const OpenGraphImageMetadata: Schema.Struct<{
|
|
243
|
+
/** Absolute URL of the image. */
|
|
244
|
+
readonly url: Schema.String;
|
|
245
|
+
/** HTTPS URL of the image (for secure contexts). */
|
|
246
|
+
readonly secureUrl: Schema.optional<Schema.String>;
|
|
247
|
+
/** MIME type of the image (e.g. `"image/png"`). */
|
|
248
|
+
readonly type: Schema.optional<Schema.String>;
|
|
249
|
+
/** Image width in pixels. */
|
|
250
|
+
readonly width: Schema.optional<Schema.Number>;
|
|
251
|
+
/** Image height in pixels. */
|
|
252
|
+
readonly height: Schema.optional<Schema.Number>;
|
|
253
|
+
/** Alt text for the image. */
|
|
254
|
+
readonly alt: Schema.optional<Schema.String>;
|
|
255
|
+
}>;
|
|
256
|
+
/** @public */
|
|
257
|
+
type OpenGraphImageMetadata = typeof OpenGraphImageMetadata.Type;
|
|
258
|
+
/**
|
|
259
|
+
* Open Graph image: either a plain URL string or structured `OpenGraphImageMetadata`.
|
|
260
|
+
*
|
|
261
|
+
* @public
|
|
262
|
+
*/
|
|
263
|
+
declare const OpenGraphImageConfig: Schema.Union<readonly [Schema.String, Schema.Struct<{
|
|
264
|
+
/** Absolute URL of the image. */
|
|
265
|
+
readonly url: Schema.String;
|
|
266
|
+
/** HTTPS URL of the image (for secure contexts). */
|
|
267
|
+
readonly secureUrl: Schema.optional<Schema.String>;
|
|
268
|
+
/** MIME type of the image (e.g. `"image/png"`). */
|
|
269
|
+
readonly type: Schema.optional<Schema.String>;
|
|
270
|
+
/** Image width in pixels. */
|
|
271
|
+
readonly width: Schema.optional<Schema.Number>;
|
|
272
|
+
/** Image height in pixels. */
|
|
273
|
+
readonly height: Schema.optional<Schema.Number>;
|
|
274
|
+
/** Alt text for the image. */
|
|
275
|
+
readonly alt: Schema.optional<Schema.String>;
|
|
276
|
+
}>]>;
|
|
277
|
+
/** @public */
|
|
278
|
+
type OpenGraphImageConfig = typeof OpenGraphImageConfig.Type;
|
|
279
|
+
/**
|
|
280
|
+
* Resolved Open Graph metadata for one documentation page.
|
|
281
|
+
*
|
|
282
|
+
* @public
|
|
283
|
+
*/
|
|
284
|
+
declare const OpenGraphMetadata: Schema.Struct<{
|
|
285
|
+
/** Canonical site base URL. */
|
|
286
|
+
readonly siteUrl: Schema.String;
|
|
287
|
+
/** Page route path (e.g. `/api/classes/myclass`). */
|
|
288
|
+
readonly pageRoute: Schema.String;
|
|
289
|
+
/** Page description for the `og:description` tag. */
|
|
290
|
+
readonly description: Schema.String;
|
|
291
|
+
/** ISO 8601 date string for `article:published_time`. */
|
|
292
|
+
readonly publishedTime: Schema.String;
|
|
293
|
+
/** ISO 8601 date string for `article:modified_time`. */
|
|
294
|
+
readonly modifiedTime: Schema.String;
|
|
295
|
+
/** Article section label (e.g. `"API"`). */
|
|
296
|
+
readonly section: Schema.String;
|
|
297
|
+
/** Article tag keywords. */
|
|
298
|
+
readonly tags: Schema.mutable<Schema.$Array<Schema.String>>;
|
|
299
|
+
/** Optional structured image metadata. */
|
|
300
|
+
readonly ogImage: Schema.optional<Schema.Struct<{
|
|
301
|
+
/** Absolute URL of the image. */
|
|
302
|
+
readonly url: Schema.String;
|
|
303
|
+
/** HTTPS URL of the image (for secure contexts). */
|
|
304
|
+
readonly secureUrl: Schema.optional<Schema.String>;
|
|
305
|
+
/** MIME type of the image (e.g. `"image/png"`). */
|
|
306
|
+
readonly type: Schema.optional<Schema.String>;
|
|
307
|
+
/** Image width in pixels. */
|
|
308
|
+
readonly width: Schema.optional<Schema.Number>;
|
|
309
|
+
/** Image height in pixels. */
|
|
310
|
+
readonly height: Schema.optional<Schema.Number>;
|
|
311
|
+
/** Alt text for the image. */
|
|
312
|
+
readonly alt: Schema.optional<Schema.String>;
|
|
313
|
+
}>>;
|
|
314
|
+
/** Open Graph object type (e.g. `"article"`). */
|
|
315
|
+
readonly ogType: Schema.String;
|
|
316
|
+
}>;
|
|
317
|
+
/** @public */
|
|
318
|
+
type OpenGraphMetadata = typeof OpenGraphMetadata.Type;
|
|
319
|
+
/**
|
|
320
|
+
* Descriptive alt text for a package's (or one API's) OG image.
|
|
321
|
+
*
|
|
322
|
+
* @public
|
|
323
|
+
*/
|
|
324
|
+
declare function ogAltText(packageName: string, apiName?: string): string;
|
|
325
|
+
/**
|
|
326
|
+
* Assemble the complete Open Graph metadata for one documentation page.
|
|
327
|
+
*
|
|
328
|
+
* @public
|
|
329
|
+
*/
|
|
330
|
+
declare function createPageMetadata(options: {
|
|
331
|
+
siteUrl: string;
|
|
332
|
+
pageRoute: string;
|
|
333
|
+
description: string;
|
|
334
|
+
publishedTime: string;
|
|
335
|
+
modifiedTime: string;
|
|
336
|
+
section: string;
|
|
337
|
+
packageName: string;
|
|
338
|
+
ogImage?: OpenGraphImageMetadata;
|
|
339
|
+
}): OpenGraphMetadata;
|
|
340
|
+
/**
|
|
341
|
+
* The Open Graph block for a page.
|
|
342
|
+
*
|
|
343
|
+
* @remarks
|
|
344
|
+
* Each optional image sub-tag is emitted only when the resolved image actually
|
|
345
|
+
* carries it — an `og:image:width` with no width is a tag a crawler reads as a
|
|
346
|
+
* declared-but-empty dimension rather than an absent one.
|
|
347
|
+
*
|
|
348
|
+
* @public
|
|
349
|
+
*/
|
|
350
|
+
declare function openGraphTags(metadata: OpenGraphMetadata): ReadonlyArray<HeadTag>;
|
|
351
|
+
/**
|
|
352
|
+
* Twitter card tags derived from the same metadata as the Open Graph block.
|
|
353
|
+
*
|
|
354
|
+
* @remarks
|
|
355
|
+
* Twitter reads most `og:` tags directly, so only what it does not infer is
|
|
356
|
+
* emitted here. The card type is a function of whether an image exists —
|
|
357
|
+
* `summary_large_image` with one, `summary` without — because declaring the
|
|
358
|
+
* large card with no image renders as a broken preview rather than degrading
|
|
359
|
+
* to the small one.
|
|
360
|
+
*
|
|
361
|
+
* Twitter's tags use `name`, not `property`.
|
|
362
|
+
*
|
|
363
|
+
* @public
|
|
364
|
+
*/
|
|
365
|
+
declare function twitterTags(metadata: OpenGraphMetadata, site?: string): ReadonlyArray<HeadTag>;
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/Seo.d.ts
|
|
368
|
+
/**
|
|
369
|
+
* Everything {@link headTags} needs about one documentation page.
|
|
370
|
+
*
|
|
371
|
+
* @public
|
|
372
|
+
*/
|
|
373
|
+
interface SeoPageInput {
|
|
374
|
+
/** Site URL prefix, from `deriveSiteUrl`. `""` leaves every URL root-relative. */
|
|
375
|
+
readonly siteUrl: string;
|
|
376
|
+
/** Page route path, beginning with `/`. */
|
|
377
|
+
readonly pageRoute: string;
|
|
378
|
+
/** Page description, used for both `og:description` and `twitter:description`. */
|
|
379
|
+
readonly description: string;
|
|
380
|
+
/** ISO 8601 date string for `article:published_time`. */
|
|
381
|
+
readonly publishedTime: string;
|
|
382
|
+
/** ISO 8601 date string for `article:modified_time`. */
|
|
383
|
+
readonly modifiedTime: string;
|
|
384
|
+
/** Article section label (e.g. `"Classes"`). */
|
|
385
|
+
readonly section: string;
|
|
386
|
+
/** The documented package's npm name. */
|
|
387
|
+
readonly packageName: string;
|
|
388
|
+
/** Resolved Open Graph image, when the API declares one. */
|
|
389
|
+
readonly ogImage?: OpenGraphImageMetadata;
|
|
390
|
+
/** The `twitter:site` handle, when the site declares one. */
|
|
391
|
+
readonly twitterSite?: string;
|
|
392
|
+
/** A serialized JSON-LD graph. Absent until structured data is wired. */
|
|
393
|
+
readonly structuredData?: string;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Every `<head>` tag for one documentation page.
|
|
397
|
+
*
|
|
398
|
+
* @remarks
|
|
399
|
+
* The order — canonical link, Open Graph block, Twitter block, then the
|
|
400
|
+
* JSON-LD script — is fixed. It carries no semantics: a crawler reads the
|
|
401
|
+
* tags as a set. It is fixed so that a page's emitted head is stable
|
|
402
|
+
* build-to-build and a diff over generated pages stays readable.
|
|
403
|
+
*
|
|
404
|
+
* @public
|
|
405
|
+
*/
|
|
406
|
+
declare function headTags(input: SeoPageInput): ReadonlyArray<HeadTag>;
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/StructuredData.d.ts
|
|
409
|
+
/**
|
|
410
|
+
* Every way {@link derive} can fail, all of them identity problems raised by
|
|
411
|
+
* `JsonLdDocument.buildResult`.
|
|
412
|
+
*
|
|
413
|
+
* @public
|
|
414
|
+
*/
|
|
415
|
+
type StructuredDataError = ConflictingTermError | DuplicateNodeIdError | InvalidNodeIdError;
|
|
416
|
+
/**
|
|
417
|
+
* The facts about a documented package that every page in it shares.
|
|
418
|
+
*
|
|
419
|
+
* @remarks
|
|
420
|
+
* A value of this shape is derived **once per API** and carried across the
|
|
421
|
+
* page pipeline. Deriving it per page would build several hundred identical
|
|
422
|
+
* nodes in a build, and re-run the attribution derivation behind each one.
|
|
423
|
+
*
|
|
424
|
+
* @public
|
|
425
|
+
*/
|
|
426
|
+
interface PackageNodeInput {
|
|
427
|
+
/** Site URL prefix, from `deriveSiteUrl`. `""` leaves every id root-relative. */
|
|
428
|
+
readonly siteUrl: string;
|
|
429
|
+
/** The route the package's documentation is mounted at, beginning with `/`. */
|
|
430
|
+
readonly baseRoute: string;
|
|
431
|
+
/** The documented package's npm name. */
|
|
432
|
+
readonly packageName: string;
|
|
433
|
+
/** The package version, when the manifest carried one. */
|
|
434
|
+
readonly version?: string;
|
|
435
|
+
/** The package description, when the manifest carried one. */
|
|
436
|
+
readonly description?: string;
|
|
437
|
+
/** Attribution derived from the manifest via {@link attributionFacts}. */
|
|
438
|
+
readonly attribution: AttributionFacts;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* The facts about one documentation page.
|
|
442
|
+
*
|
|
443
|
+
* @public
|
|
444
|
+
*/
|
|
445
|
+
interface PageNodeInput {
|
|
446
|
+
/** Page route path, beginning with `/`. */
|
|
447
|
+
readonly pageRoute: string;
|
|
448
|
+
/** The documented symbol's display name (e.g. `"Pipeline"`). */
|
|
449
|
+
readonly symbolName: string;
|
|
450
|
+
/** Page description, reused as both nodes' `description`. */
|
|
451
|
+
readonly description: string;
|
|
452
|
+
/** Article section label (e.g. `"Classes"`). */
|
|
453
|
+
readonly section: string;
|
|
454
|
+
/** ISO 8601 date string for `datePublished`. */
|
|
455
|
+
readonly publishedTime: string;
|
|
456
|
+
/** ISO 8601 date string for `dateModified`. */
|
|
457
|
+
readonly modifiedTime: string;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* The per-API package facts, resolved once into the nodes every page reuses.
|
|
461
|
+
*
|
|
462
|
+
* @remarks
|
|
463
|
+
* Opaque by intent: build it with {@link packageContext} and carry it, don't
|
|
464
|
+
* assemble one by hand. The whole reason it exists is that the nodes inside it
|
|
465
|
+
* are identical on every page of an API, and rebuilding them per page would
|
|
466
|
+
* mint several hundred copies per build.
|
|
467
|
+
*
|
|
468
|
+
* @public
|
|
469
|
+
*/
|
|
470
|
+
interface PackageContext {
|
|
471
|
+
/** The `@id` every page's nodes reference the package by. */
|
|
472
|
+
readonly id: string;
|
|
473
|
+
/** The package node and every person node it credits. */
|
|
474
|
+
readonly nodes: ReadonlyArray<JsonLdNode>;
|
|
475
|
+
/** Carried through so page ids resolve against the same prefix. */
|
|
476
|
+
readonly siteUrl: string;
|
|
477
|
+
/** Carried through for the symbol node's `assemblyVersion`. */
|
|
478
|
+
readonly version?: string;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* The `SoftwareSourceCode` node for a documented package, plus the people it
|
|
482
|
+
* credits.
|
|
483
|
+
*
|
|
484
|
+
* @remarks
|
|
485
|
+
* Note `version`, not `softwareVersion` — the latter reads like the right name
|
|
486
|
+
* and is defined on `SoftwareApplication`, not here. It would serialize fine
|
|
487
|
+
* and be silently ignored; the conformance validator is what catches it.
|
|
488
|
+
*
|
|
489
|
+
* `license` carries the canonical SPDX page for EVERY license the expression
|
|
490
|
+
* names — schema.org's `license` accepts an array, and an `AND` expression has
|
|
491
|
+
* no single answer to give. The URLs come from each catalog entry's own
|
|
492
|
+
* `referenceUrl`, never from concatenating an id onto
|
|
493
|
+
* `https://spdx.org/licenses/`: that is the string-building the catalog exists
|
|
494
|
+
* to prevent, and it is wrong for a `LicenseRef`, which has no such page. A
|
|
495
|
+
* license outside the catalog drops out of the array rather than appearing as
|
|
496
|
+
* a fabricated URL.
|
|
497
|
+
*
|
|
498
|
+
* @param input - the per-API facts
|
|
499
|
+
* @returns the reusable context every page in the API derives against
|
|
500
|
+
*
|
|
501
|
+
* @public
|
|
502
|
+
*/
|
|
503
|
+
declare function packageContext(input: PackageNodeInput): PackageContext;
|
|
504
|
+
/**
|
|
505
|
+
* Derive the schema.org graph for one documentation page.
|
|
506
|
+
*
|
|
507
|
+
* @remarks
|
|
508
|
+
* Three linked nodes plus the package's people: a `SoftwareSourceCode` for the
|
|
509
|
+
* package, a `TechArticle` for the page, and an `APIReference` for the symbol
|
|
510
|
+
* the page documents. The article `isPartOf` the package and its `mainEntity`
|
|
511
|
+
* is the symbol, so a crawler reading any one node can reach the other two.
|
|
512
|
+
*
|
|
513
|
+
* Serialize the result with `JsonLdDocument.toScriptBody()`, never with
|
|
514
|
+
* `JSON.stringify(graph.toJsonLd())` — `toScriptBody` is the only serializer
|
|
515
|
+
* that escapes the sequences that would close the surrounding `<script>`
|
|
516
|
+
* element, and it is idempotent, so an adapter layering its own escaping over
|
|
517
|
+
* it is a no-op rather than a double-escape.
|
|
518
|
+
*
|
|
519
|
+
* @param pkg - the per-API context from {@link packageContext}
|
|
520
|
+
* @param page - the per-page facts
|
|
521
|
+
* @returns the assembled graph, or the identity failure that stopped it
|
|
522
|
+
*
|
|
523
|
+
* @public
|
|
524
|
+
*/
|
|
525
|
+
declare function derive(pkg: PackageContext, page: PageNodeInput): Result.Result<JsonLdDocument, StructuredDataError>;
|
|
526
|
+
/**
|
|
527
|
+
* {@link derive}, serialized to the text an adapter embeds in a `<script>`.
|
|
528
|
+
*
|
|
529
|
+
* @remarks
|
|
530
|
+
* The convenience the adapter actually wants: a page's structured data as a
|
|
531
|
+
* string. Callers that need the graph itself — a conformance check in a test,
|
|
532
|
+
* say — use {@link derive}.
|
|
533
|
+
*
|
|
534
|
+
* @param pkg - the per-API context from {@link packageContext}
|
|
535
|
+
* @param page - the per-page facts
|
|
536
|
+
* @returns the script body, or the identity failure
|
|
537
|
+
*
|
|
538
|
+
* @public
|
|
539
|
+
*/
|
|
540
|
+
declare function deriveScriptBody(pkg: PackageContext, page: PageNodeInput): Result.Result<string, StructuredDataError>;
|
|
541
|
+
//#endregion
|
|
542
|
+
export { type AttributionFacts, type HeadTag, OpenGraphImageConfig, OpenGraphImageMetadata, OpenGraphMetadata, type PackageContext, type PackageNodeInput, type PageNodeInput, type SeoPageInput, type StructuredDataError, attributionFacts, canonicalUrl, createPageMetadata, derive, deriveScriptBody, deriveSiteUrl, escapeScriptBody, headTags, imageMimeType, jsonLd, link, meta, metaNamed, ogAltText, openGraphTags, packageContext, resolveUrl, twitterTags };
|
|
543
|
+
//# sourceMappingURL=index.d.ts.map
|
package/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { attributionFacts } from "./Attribution.js";
|
|
2
|
+
import { canonicalUrl, deriveSiteUrl, imageMimeType, resolveUrl } from "./Canonical.js";
|
|
3
|
+
import { escapeScriptBody, jsonLd, link, meta, metaNamed } from "./HeadTag.js";
|
|
4
|
+
import { OpenGraphImageConfig, OpenGraphImageMetadata, OpenGraphMetadata, createPageMetadata, ogAltText, openGraphTags, twitterTags } from "./OpenGraph.js";
|
|
5
|
+
import { headTags } from "./Seo.js";
|
|
6
|
+
import { derive, deriveScriptBody, packageContext } from "./StructuredData.js";
|
|
7
|
+
|
|
8
|
+
export { OpenGraphImageConfig, OpenGraphImageMetadata, OpenGraphMetadata, attributionFacts, canonicalUrl, createPageMetadata, derive, deriveScriptBody, deriveSiteUrl, escapeScriptBody, headTags, imageMimeType, jsonLd, link, meta, metaNamed, ogAltText, openGraphTags, packageContext, resolveUrl, twitterTags };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tsdoctor/seo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Framework-neutral head metadata for static TypeScript API documentation: schema.org JSON-LD, Open Graph and Twitter card vocabulary, canonical URLs, and package attribution derived from an api.json model plus its manifest.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"seo",
|
|
8
|
+
"schema.org",
|
|
9
|
+
"json-ld",
|
|
10
|
+
"open-graph",
|
|
11
|
+
"documentation",
|
|
12
|
+
"effect"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/spencerbeggs/tsdoctor#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/spencerbeggs/tsdoctor/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/spencerbeggs/tsdoctor.git",
|
|
21
|
+
"directory": "packages/seo"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": {
|
|
25
|
+
"name": "C. Spencer Beggs",
|
|
26
|
+
"email": "spencer@beggs.codes",
|
|
27
|
+
"url": "https://spencerbeg.gs"
|
|
28
|
+
},
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"type": "module",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./index.d.ts",
|
|
34
|
+
"import": "./index.js",
|
|
35
|
+
"default": "./index.js"
|
|
36
|
+
},
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@effected/package-json": "^0.13.0",
|
|
41
|
+
"@effected/schema-org": "^0.1.0",
|
|
42
|
+
"@effected/spdx": "^0.5.0",
|
|
43
|
+
"effect": "4.0.0-rc.109"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=24.11.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.59.0"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|