@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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { canonicalUrl } from "./Canonical.js";
|
|
2
|
+
import { Result } from "effect";
|
|
3
|
+
import { APIReference, JsonLdDocument, NodeRef, Person, SoftwareSourceCode, TechArticle } from "@effected/schema-org";
|
|
4
|
+
|
|
5
|
+
//#region src/StructuredData.ts
|
|
6
|
+
/**
|
|
7
|
+
* The package node's `@id`.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A fragment on the package's own route rather than a bare URL, so the node
|
|
11
|
+
* is distinguishable from the page that happens to sit at that route. Every
|
|
12
|
+
* page in the API references this same id, which is what makes the package
|
|
13
|
+
* node deduplicate across a crawl.
|
|
14
|
+
*/
|
|
15
|
+
const packageId = (input) => `${canonicalUrl(input.siteUrl, input.baseRoute)}#source`;
|
|
16
|
+
/** An author or maintainer's `@id`, scoped to the package that credits them. */
|
|
17
|
+
const personId = (input, name) => `${canonicalUrl(input.siteUrl, input.baseRoute)}#person-${encodeURIComponent(name)}`;
|
|
18
|
+
/**
|
|
19
|
+
* The people nodes a package credits, and refs to them.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* The author is a `Person` when the manifest named a human and an
|
|
23
|
+
* `Organization` when the name reads as a scope — but npm carries no such
|
|
24
|
+
* distinction, so guessing would be fabrication. Everyone is a `Person`, which
|
|
25
|
+
* is what the manifest field is documented to hold. An organization ends up
|
|
26
|
+
* modelled as a person with an organization's name, which is imprecise rather
|
|
27
|
+
* than wrong; inventing a type from a string's shape would be neither.
|
|
28
|
+
*/
|
|
29
|
+
function peopleNodes(input) {
|
|
30
|
+
const nodes = [];
|
|
31
|
+
const authors = [];
|
|
32
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33
|
+
const add = (name, url) => {
|
|
34
|
+
const id = personId(input, name);
|
|
35
|
+
if (seen.has(id)) return;
|
|
36
|
+
seen.add(id);
|
|
37
|
+
nodes.push(Person.make({
|
|
38
|
+
"@id": id,
|
|
39
|
+
name,
|
|
40
|
+
...url !== void 0 ? { url } : {}
|
|
41
|
+
}));
|
|
42
|
+
authors.push(NodeRef.to(id));
|
|
43
|
+
};
|
|
44
|
+
if (input.attribution.authorName !== void 0) add(input.attribution.authorName, input.attribution.authorUrl);
|
|
45
|
+
for (const maintainer of input.attribution.maintainerNames) add(maintainer);
|
|
46
|
+
return {
|
|
47
|
+
nodes,
|
|
48
|
+
authors
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The `SoftwareSourceCode` node for a documented package, plus the people it
|
|
53
|
+
* credits.
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* Note `version`, not `softwareVersion` — the latter reads like the right name
|
|
57
|
+
* and is defined on `SoftwareApplication`, not here. It would serialize fine
|
|
58
|
+
* and be silently ignored; the conformance validator is what catches it.
|
|
59
|
+
*
|
|
60
|
+
* `license` carries the canonical SPDX page for EVERY license the expression
|
|
61
|
+
* names — schema.org's `license` accepts an array, and an `AND` expression has
|
|
62
|
+
* no single answer to give. The URLs come from each catalog entry's own
|
|
63
|
+
* `referenceUrl`, never from concatenating an id onto
|
|
64
|
+
* `https://spdx.org/licenses/`: that is the string-building the catalog exists
|
|
65
|
+
* to prevent, and it is wrong for a `LicenseRef`, which has no such page. A
|
|
66
|
+
* license outside the catalog drops out of the array rather than appearing as
|
|
67
|
+
* a fabricated URL.
|
|
68
|
+
*
|
|
69
|
+
* @param input - the per-API facts
|
|
70
|
+
* @returns the reusable context every page in the API derives against
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
function packageContext(input) {
|
|
75
|
+
const { nodes: people, authors } = peopleNodes(input);
|
|
76
|
+
const facts = input.attribution;
|
|
77
|
+
const pkg = SoftwareSourceCode.make({
|
|
78
|
+
"@id": packageId(input),
|
|
79
|
+
name: input.packageName,
|
|
80
|
+
url: canonicalUrl(input.siteUrl, input.baseRoute),
|
|
81
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
82
|
+
...input.version !== void 0 ? { version: input.version } : {},
|
|
83
|
+
...facts.repositoryUrl !== void 0 ? { codeRepository: facts.repositoryUrl } : {},
|
|
84
|
+
programmingLanguage: ["TypeScript"],
|
|
85
|
+
...facts.licenseUrls.length > 0 ? { license: [...facts.licenseUrls] } : {},
|
|
86
|
+
...authors.length > 0 ? { author: [...authors] } : {},
|
|
87
|
+
...facts.keywords.length > 0 ? { keywords: [...facts.keywords] } : {},
|
|
88
|
+
...facts.homepage !== void 0 ? { sameAs: [facts.homepage] } : {}
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
id: packageId(input),
|
|
92
|
+
nodes: [pkg, ...people],
|
|
93
|
+
siteUrl: input.siteUrl,
|
|
94
|
+
...input.version !== void 0 ? { version: input.version } : {}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Derive the schema.org graph for one documentation page.
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* Three linked nodes plus the package's people: a `SoftwareSourceCode` for the
|
|
102
|
+
* package, a `TechArticle` for the page, and an `APIReference` for the symbol
|
|
103
|
+
* the page documents. The article `isPartOf` the package and its `mainEntity`
|
|
104
|
+
* is the symbol, so a crawler reading any one node can reach the other two.
|
|
105
|
+
*
|
|
106
|
+
* Serialize the result with `JsonLdDocument.toScriptBody()`, never with
|
|
107
|
+
* `JSON.stringify(graph.toJsonLd())` — `toScriptBody` is the only serializer
|
|
108
|
+
* that escapes the sequences that would close the surrounding `<script>`
|
|
109
|
+
* element, and it is idempotent, so an adapter layering its own escaping over
|
|
110
|
+
* it is a no-op rather than a double-escape.
|
|
111
|
+
*
|
|
112
|
+
* @param pkg - the per-API context from {@link packageContext}
|
|
113
|
+
* @param page - the per-page facts
|
|
114
|
+
* @returns the assembled graph, or the identity failure that stopped it
|
|
115
|
+
*
|
|
116
|
+
* @public
|
|
117
|
+
*/
|
|
118
|
+
function derive(pkg, page) {
|
|
119
|
+
const pageUrl = canonicalUrl(pkg.siteUrl, page.pageRoute);
|
|
120
|
+
const articleId = `${pageUrl}#article`;
|
|
121
|
+
const symbolId = `${pageUrl}#symbol`;
|
|
122
|
+
const symbol = APIReference.make({
|
|
123
|
+
"@id": symbolId,
|
|
124
|
+
name: page.symbolName,
|
|
125
|
+
url: pageUrl,
|
|
126
|
+
description: page.description,
|
|
127
|
+
isPartOf: [NodeRef.to(pkg.id)],
|
|
128
|
+
...pkg.version !== void 0 ? { assemblyVersion: pkg.version } : {},
|
|
129
|
+
programmingModel: "TypeScript"
|
|
130
|
+
});
|
|
131
|
+
const article = TechArticle.make({
|
|
132
|
+
"@id": articleId,
|
|
133
|
+
name: page.symbolName,
|
|
134
|
+
headline: page.symbolName,
|
|
135
|
+
url: pageUrl,
|
|
136
|
+
description: page.description,
|
|
137
|
+
articleSection: [page.section],
|
|
138
|
+
datePublished: page.publishedTime,
|
|
139
|
+
dateModified: page.modifiedTime,
|
|
140
|
+
isPartOf: [NodeRef.to(pkg.id)],
|
|
141
|
+
mainEntity: NodeRef.to(symbolId),
|
|
142
|
+
inLanguage: "en"
|
|
143
|
+
});
|
|
144
|
+
return JsonLdDocument.buildResult([
|
|
145
|
+
...pkg.nodes,
|
|
146
|
+
article,
|
|
147
|
+
symbol
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* {@link derive}, serialized to the text an adapter embeds in a `<script>`.
|
|
152
|
+
*
|
|
153
|
+
* @remarks
|
|
154
|
+
* The convenience the adapter actually wants: a page's structured data as a
|
|
155
|
+
* string. Callers that need the graph itself — a conformance check in a test,
|
|
156
|
+
* say — use {@link derive}.
|
|
157
|
+
*
|
|
158
|
+
* @param pkg - the per-API context from {@link packageContext}
|
|
159
|
+
* @param page - the per-page facts
|
|
160
|
+
* @returns the script body, or the identity failure
|
|
161
|
+
*
|
|
162
|
+
* @public
|
|
163
|
+
*/
|
|
164
|
+
function deriveScriptBody(pkg, page) {
|
|
165
|
+
return Result.map(derive(pkg, page), (graph) => graph.toScriptBody());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//#endregion
|
|
169
|
+
export { derive, deriveScriptBody, packageContext };
|