react-cheminfo 0.35.0 → 0.36.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/bin/check-seo.mjs +333 -0
- package/lib/language/core/index.d.ts +2 -0
- package/lib/language/core/index.d.ts.map +1 -1
- package/lib/language/core/index.js +1 -0
- package/lib/language/core/index.js.map +1 -1
- package/lib/language/core/languagePath.d.ts +52 -0
- package/lib/language/core/languagePath.d.ts.map +1 -0
- package/lib/language/core/languagePath.js +65 -0
- package/lib/language/core/languagePath.js.map +1 -0
- package/lib/seo/core/alternates.d.ts +40 -0
- package/lib/seo/core/alternates.d.ts.map +1 -0
- package/lib/seo/core/alternates.js +42 -0
- package/lib/seo/core/alternates.js.map +1 -0
- package/lib/seo/core/index.d.ts +3 -0
- package/lib/seo/core/index.d.ts.map +1 -1
- package/lib/seo/core/index.js +2 -0
- package/lib/seo/core/index.js.map +1 -1
- package/lib/seo/core/noscript.d.ts +9 -0
- package/lib/seo/core/noscript.d.ts.map +1 -1
- package/lib/seo/core/noscript.js +7 -2
- package/lib/seo/core/noscript.js.map +1 -1
- package/lib/seo/core/pageMeta.d.ts +23 -0
- package/lib/seo/core/pageMeta.d.ts.map +1 -1
- package/lib/seo/core/pageMeta.js +52 -3
- package/lib/seo/core/pageMeta.js.map +1 -1
- package/lib/seo/core/pageProse.d.ts +76 -0
- package/lib/seo/core/pageProse.d.ts.map +1 -0
- package/lib/seo/core/pageProse.js +92 -0
- package/lib/seo/core/pageProse.js.map +1 -0
- package/lib/seo/core/routeProblems.d.ts +33 -0
- package/lib/seo/core/routeProblems.d.ts.map +1 -0
- package/lib/seo/core/routeProblems.js +89 -0
- package/lib/seo/core/routeProblems.js.map +1 -0
- package/lib/seo/core/routes.d.ts +14 -0
- package/lib/seo/core/routes.d.ts.map +1 -1
- package/lib/seo/core/routes.js.map +1 -1
- package/lib/seo/core/siteFiles.d.ts.map +1 -1
- package/lib/seo/core/siteFiles.js +2 -1
- package/lib/seo/core/siteFiles.js.map +1 -1
- package/lib/seo/vite/prerender.d.ts +13 -0
- package/lib/seo/vite/prerender.d.ts.map +1 -1
- package/lib/seo/vite/prerender.js +17 -10
- package/lib/seo/vite/prerender.js.map +1 -1
- package/package.json +2 -1
- package/src/language/core/index.ts +2 -0
- package/src/language/core/languagePath.ts +78 -0
- package/src/seo/core/alternates.ts +65 -0
- package/src/seo/core/index.ts +3 -0
- package/src/seo/core/noscript.ts +18 -2
- package/src/seo/core/pageMeta.ts +74 -3
- package/src/seo/core/pageProse.ts +146 -0
- package/src/seo/core/routeProblems.ts +111 -0
- package/src/seo/core/routes.ts +14 -0
- package/src/seo/core/siteFiles.ts +2 -1
- package/src/seo/vite/prerender.ts +30 -11
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Report every page of a build a search engine cannot tell apart from the rest
|
|
4
|
+
* of the site: a missing or repeated title, a description outside the room a
|
|
5
|
+
* result gives it, an absent canonical, card or crawl path, a sitemap naming a
|
|
6
|
+
* page the build never wrote, and a social card pointing at a file that is not
|
|
7
|
+
* there.
|
|
8
|
+
*
|
|
9
|
+
* It reads the build rather than the route table, because the defects that
|
|
10
|
+
* actually cost us ranking were never in the prose — they were a site whose
|
|
11
|
+
* head injector was never wired, which writes one generic page for every
|
|
12
|
+
* address and looks entirely correct in the source. `routeProblems` from
|
|
13
|
+
* `react-cheminfo/core` checks the prose; this checks that it arrived.
|
|
14
|
+
*
|
|
15
|
+
* Wire it as an npm script, after the build:
|
|
16
|
+
* "check-seo": "node node_modules/react-cheminfo/bin/check-seo.mjs dist"
|
|
17
|
+
*
|
|
18
|
+
* Usage: check-seo.mjs [directory] [--quiet]
|
|
19
|
+
* directory the build output, `dist` when none is given
|
|
20
|
+
* --quiet say nothing when the build is clean
|
|
21
|
+
*
|
|
22
|
+
* A page carrying `<!--cheminfo:head-->` has its head written per request by a
|
|
23
|
+
* server, so its own tags are that server's business and are reported as
|
|
24
|
+
* deferred rather than missing. Everything the build still owns is checked.
|
|
25
|
+
*
|
|
26
|
+
* Exits 1 when anything is reported, 2 when it was called wrongly, 0 otherwise.
|
|
27
|
+
*/
|
|
28
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
29
|
+
import { join, relative } from 'node:path';
|
|
30
|
+
import process from 'node:process';
|
|
31
|
+
|
|
32
|
+
import { PAGE_HEAD_MARKER, withheldPhrase } from '../lib/core.js';
|
|
33
|
+
|
|
34
|
+
import { countOf, usageError } from './cli.mjs';
|
|
35
|
+
|
|
36
|
+
const TOOL = 'check-seo';
|
|
37
|
+
const SKIPPED = new Set(['assets', 'node_modules']);
|
|
38
|
+
const DESCRIPTION_MIN = 110;
|
|
39
|
+
const DESCRIPTION_MAX = 160;
|
|
40
|
+
|
|
41
|
+
const roots = [];
|
|
42
|
+
let quiet = false;
|
|
43
|
+
|
|
44
|
+
for (const argument of process.argv.slice(2)) {
|
|
45
|
+
if (argument === '--quiet') {
|
|
46
|
+
quiet = true;
|
|
47
|
+
} else if (argument.startsWith('-')) {
|
|
48
|
+
process.exit(usageError(TOOL, `unknown option: ${argument}`));
|
|
49
|
+
} else {
|
|
50
|
+
roots.push(argument);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const root = roots[0] ?? 'dist';
|
|
54
|
+
if (roots.length > 1) {
|
|
55
|
+
process.exit(usageError(TOOL, 'one directory at a time'));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const stats = statSync(root, { throwIfNoEntry: false });
|
|
59
|
+
if (stats === undefined || !stats.isDirectory()) {
|
|
60
|
+
process.exit(usageError(TOOL, `no such build directory: ${root}`));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pages = [];
|
|
64
|
+
collect(root, pages);
|
|
65
|
+
if (pages.length === 0) {
|
|
66
|
+
process.exit(usageError(TOOL, `no index.html under ${root}`));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const problems = [];
|
|
70
|
+
const deferred = [];
|
|
71
|
+
const excluded = [];
|
|
72
|
+
const titles = new Map();
|
|
73
|
+
const descriptions = new Map();
|
|
74
|
+
|
|
75
|
+
for (const page of pages) {
|
|
76
|
+
const html = readFileSync(page.file, 'utf8');
|
|
77
|
+
const at = page.address;
|
|
78
|
+
|
|
79
|
+
// A page that takes itself out of the index has nothing to be indexed under,
|
|
80
|
+
// and an admin site is right to do it. Saying its head is wrong would be
|
|
81
|
+
// reporting a decision as a defect.
|
|
82
|
+
if (/<meta\s+name="robots"\s+content="[^"]*noindex/i.test(html)) {
|
|
83
|
+
excluded.push(at);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (html.includes(PAGE_HEAD_MARKER)) {
|
|
88
|
+
deferred.push(at);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
checkHead(at, html);
|
|
92
|
+
|
|
93
|
+
if (!/<html[^>]*\slang="[^"]+"/i.test(html)) {
|
|
94
|
+
problems.push(`${at}: no lang on <html>.`);
|
|
95
|
+
}
|
|
96
|
+
if (!html.includes('application/ld+json')) {
|
|
97
|
+
problems.push(`${at}: no structured-data block.`);
|
|
98
|
+
}
|
|
99
|
+
if (!/<noscript[\s>]/i.test(html)) {
|
|
100
|
+
problems.push(
|
|
101
|
+
`${at}: no <noscript> crawl path — a crawler that does not render sees an empty page.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const keywords = tag(html, /<meta\s+name="keywords"\s+content="([^"]*)"/i);
|
|
105
|
+
if (keywords !== undefined) {
|
|
106
|
+
problems.push(
|
|
107
|
+
`${at}: a keywords meta tag — no search engine has read one since 2009.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// A build whose every page defers its head is the shell of a site a server
|
|
113
|
+
// writes the head of: the sitemap it serves names addresses this build was
|
|
114
|
+
// never going to write, and the card, the structured data and the crawl path
|
|
115
|
+
// arrive with the page rather than with the file. There is nothing here to
|
|
116
|
+
// check, and checking it anyway would report a correct site as broken — that
|
|
117
|
+
// server's own route tests are what covers it.
|
|
118
|
+
const served = deferred.length + excluded.length === pages.length;
|
|
119
|
+
if (!served) checkSiteFiles();
|
|
120
|
+
|
|
121
|
+
let report = '';
|
|
122
|
+
for (const problem of problems) report += `${problem}\n`;
|
|
123
|
+
|
|
124
|
+
if (problems.length > 0) {
|
|
125
|
+
report += `${countOf(problems.length, 'problem')} in ${countOf(pages.length, 'page')}\n`;
|
|
126
|
+
process.stdout.write(report);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
if (!quiet) {
|
|
130
|
+
if (served && deferred.length === 0) {
|
|
131
|
+
process.stdout.write(
|
|
132
|
+
`${countOf(pages.length, 'page')} answering noindex: deliberately out of the index\n`,
|
|
133
|
+
);
|
|
134
|
+
} else if (served) {
|
|
135
|
+
process.stdout.write(
|
|
136
|
+
`${countOf(pages.length, 'page')} writing its head per request: covered by the server's own route tests, not here\n`,
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
const note =
|
|
140
|
+
deferred.length === 0
|
|
141
|
+
? ''
|
|
142
|
+
: `, ${countOf(deferred.length, 'page')} writing its head per request`;
|
|
143
|
+
process.stdout.write(
|
|
144
|
+
`no indexing problem in ${countOf(pages.length, 'page')}${note}\n`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check the tags a result, a card and a canonical are built from.
|
|
151
|
+
* @param {string} at - The address the page was written at.
|
|
152
|
+
* @param {string} html - The page.
|
|
153
|
+
*/
|
|
154
|
+
function checkHead(at, html) {
|
|
155
|
+
const title = tag(html, /<title>([\s\S]*?)<\/title>/i);
|
|
156
|
+
if (title === undefined || title.trim() === '') {
|
|
157
|
+
problems.push(`${at}: no title.`);
|
|
158
|
+
} else {
|
|
159
|
+
const first = titles.get(title);
|
|
160
|
+
if (first === undefined) titles.set(title, at);
|
|
161
|
+
else problems.push(`${at}: the title repeats the one at ${first}.`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const description = tag(
|
|
165
|
+
html,
|
|
166
|
+
/<meta\s+name="description"\s+content="([^"]*)"/i,
|
|
167
|
+
);
|
|
168
|
+
if (description === undefined || description === '') {
|
|
169
|
+
problems.push(`${at}: no description.`);
|
|
170
|
+
} else {
|
|
171
|
+
if (description.length < DESCRIPTION_MIN) {
|
|
172
|
+
problems.push(
|
|
173
|
+
`${at}: the description is ${description.length} characters — at least ${DESCRIPTION_MIN}.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (description.length > DESCRIPTION_MAX) {
|
|
177
|
+
problems.push(
|
|
178
|
+
`${at}: the description is ${description.length} characters — at most ${DESCRIPTION_MAX}.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const first = descriptions.get(description);
|
|
182
|
+
if (first === undefined) descriptions.set(description, at);
|
|
183
|
+
else problems.push(`${at}: the description repeats the one at ${first}.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const withheld =
|
|
187
|
+
withheldPhrase(title ?? '') ?? withheldPhrase(description ?? '');
|
|
188
|
+
if (withheld !== undefined) {
|
|
189
|
+
problems.push(
|
|
190
|
+
`${at}: the snippet says "${withheld}" — a site names no repository, tracker or licence of ours.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const canonical = tag(html, /<link\s+rel="canonical"\s+href="([^"]*)"/i);
|
|
195
|
+
if (canonical === undefined) {
|
|
196
|
+
problems.push(`${at}: no canonical address.`);
|
|
197
|
+
} else if (canonical.includes('?')) {
|
|
198
|
+
problems.push(`${at}: the canonical carries a query string.`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const property of ['og:title', 'og:description', 'og:url']) {
|
|
202
|
+
const pattern = new RegExp(
|
|
203
|
+
String.raw`<meta\s+property="${property}"\s+content="[^"]`,
|
|
204
|
+
'i',
|
|
205
|
+
);
|
|
206
|
+
if (!pattern.test(html)) problems.push(`${at}: no ${property}.`);
|
|
207
|
+
}
|
|
208
|
+
if (!/<meta\s+name="twitter:card"/i.test(html)) {
|
|
209
|
+
problems.push(`${at}: no twitter:card, so a shared link unfurls bare.`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const image = tag(html, /<meta\s+property="og:image"\s+content="([^"]*)"/i);
|
|
213
|
+
if (image === undefined) {
|
|
214
|
+
problems.push(`${at}: no og:image.`);
|
|
215
|
+
} else {
|
|
216
|
+
const own = ownPath(image);
|
|
217
|
+
if (own !== undefined && !exists(join(root, own))) {
|
|
218
|
+
problems.push(
|
|
219
|
+
`${at}: og:image names ${own}, which the build never wrote.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Check the two files a crawler asks for before it reads any page. */
|
|
226
|
+
function checkSiteFiles() {
|
|
227
|
+
const robots = join(root, 'robots.txt');
|
|
228
|
+
const sitemap = join(root, 'sitemap.xml');
|
|
229
|
+
|
|
230
|
+
if (!exists(robots)) {
|
|
231
|
+
problems.push('robots.txt: not in the build.');
|
|
232
|
+
} else {
|
|
233
|
+
const named = /^\s*Sitemap:\s*(\S+)\s*$/im.exec(
|
|
234
|
+
readFileSync(robots, 'utf8'),
|
|
235
|
+
);
|
|
236
|
+
if (named !== null && !exists(sitemap)) {
|
|
237
|
+
problems.push(
|
|
238
|
+
`robots.txt names ${named[1]}, which the build never wrote — a sitemap that answers 404 is reported on every fetch.`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!exists(sitemap)) {
|
|
244
|
+
problems.push('sitemap.xml: not in the build.');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const xml = readFileSync(sitemap, 'utf8');
|
|
248
|
+
const written = new Set(pages.map((page) => page.address));
|
|
249
|
+
const claimed = new Set();
|
|
250
|
+
let listed = 0;
|
|
251
|
+
for (const match of xml.matchAll(/<loc>([^<]+)<\/loc>/g)) {
|
|
252
|
+
listed++;
|
|
253
|
+
const own = ownPath(match[1] ?? '');
|
|
254
|
+
if (own !== undefined) claimed.add(own === '' ? '/' : own);
|
|
255
|
+
if (own !== undefined && !written.has(own === '' ? '/' : own)) {
|
|
256
|
+
problems.push(
|
|
257
|
+
`sitemap.xml lists ${match[1]}, which the build never wrote.`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (listed === 0) problems.push('sitemap.xml lists no address.');
|
|
262
|
+
|
|
263
|
+
// The other direction, which is the one that quietly costs traffic: a page
|
|
264
|
+
// the build wrote and the sitemap never named is a page nothing links a
|
|
265
|
+
// crawler to but the crawl path, and it is the long tail — an element, a
|
|
266
|
+
// space group, an exercise — that goes missing first.
|
|
267
|
+
for (const page of pages) {
|
|
268
|
+
if (deferred.includes(page.address) || excluded.includes(page.address)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!claimed.has(page.address)) {
|
|
272
|
+
problems.push(
|
|
273
|
+
`${page.address}: written by the build and named in no sitemap entry.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The part of an address this build is answerable for.
|
|
281
|
+
* @param {string} target - An absolute address, or a path.
|
|
282
|
+
* @returns {string | undefined} Its path, or `undefined` when it is somebody
|
|
283
|
+
* else's host.
|
|
284
|
+
*/
|
|
285
|
+
function ownPath(target) {
|
|
286
|
+
if (!URL.canParse(target)) {
|
|
287
|
+
return target.startsWith('/') ? trimSlash(target) : undefined;
|
|
288
|
+
}
|
|
289
|
+
return trimSlash(new URL(target).pathname);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @param {string} path - A path.
|
|
294
|
+
* @returns {string} The same path without its trailing slash, `/` kept.
|
|
295
|
+
*/
|
|
296
|
+
function trimSlash(path) {
|
|
297
|
+
return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* @param {string} html - The page.
|
|
302
|
+
* @param {RegExp} pattern - A pattern whose first group is the value.
|
|
303
|
+
* @returns {string | undefined} The value, or `undefined` when it is absent.
|
|
304
|
+
*/
|
|
305
|
+
function tag(html, pattern) {
|
|
306
|
+
return pattern.exec(html)?.[1];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {string} path - A path.
|
|
311
|
+
* @returns {boolean} Whether anything is there.
|
|
312
|
+
*/
|
|
313
|
+
function exists(path) {
|
|
314
|
+
return statSync(path, { throwIfNoEntry: false }) !== undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Add every built page under a path to the list, with the address it answers.
|
|
319
|
+
* @param {string} path - A directory of the build.
|
|
320
|
+
* @param {Array<{ file: string, address: string }>} pages - Collected so far.
|
|
321
|
+
*/
|
|
322
|
+
function collect(path, pages) {
|
|
323
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
324
|
+
if (entry.name.startsWith('.') || SKIPPED.has(entry.name)) continue;
|
|
325
|
+
const next = join(path, entry.name);
|
|
326
|
+
if (entry.isDirectory()) {
|
|
327
|
+
collect(next, pages);
|
|
328
|
+
} else if (entry.name === 'index.html') {
|
|
329
|
+
const address = `/${relative(root, next).replace(/index\.html$/, '')}`;
|
|
330
|
+
pages.push({ file: next, address: trimSlash(address) });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { languageName } from './languageName.ts';
|
|
2
|
+
export type { LanguagePath } from './languagePath.ts';
|
|
3
|
+
export { readLanguagePath, withLanguagePath } from './languagePath.ts';
|
|
2
4
|
export { LANGUAGE_PARAM, isLanguageTag, readLanguageParam, withLanguageParam, } from './languageParam.ts';
|
|
3
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/language/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EACL,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/language/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EACL,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/language/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/language/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EACL,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The language a translated site routes by: a prefix on its own addresses.
|
|
3
|
+
*
|
|
4
|
+
* `/fr/tutorial` is the French tutorial, and English is written with no prefix
|
|
5
|
+
* at all, so every link already handed out in a course stays alive and the site
|
|
6
|
+
* does not acquire a second spelling of its own front page.
|
|
7
|
+
*
|
|
8
|
+
* This is the half of the design that makes a translation *findable*. A site
|
|
9
|
+
* that keeps its language in a query parameter, in the hash or in
|
|
10
|
+
* `localStorage` serves five languages at one address: a crawler sees one page,
|
|
11
|
+
* indexes one language, and the other four may as well not have been written.
|
|
12
|
+
* The parameter stays — it is how the language travels to a sibling that may
|
|
13
|
+
* not speak it (`./languageParam.ts`) — but it is not how a translated site
|
|
14
|
+
* routes.
|
|
15
|
+
*
|
|
16
|
+
* The prefix sits between the mount and the route, so a tool served as one of
|
|
17
|
+
* several on a shared host reads `/surge/fr/exercises`: the mount comes off
|
|
18
|
+
* first (`router/core/basePath.ts`), then the language, then the table is read.
|
|
19
|
+
*/
|
|
20
|
+
import type { Language } from '../../i18n/core/languages.ts';
|
|
21
|
+
/** An address split into the language it names and the page it opens. */
|
|
22
|
+
export interface LanguagePath {
|
|
23
|
+
/** The language named, or the default one when the address names none. */
|
|
24
|
+
language: Language;
|
|
25
|
+
/** The address from the site's own root, with the prefix taken off. */
|
|
26
|
+
path: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read the language prefix off one of the site's own addresses.
|
|
30
|
+
*
|
|
31
|
+
* Only a language the site actually speaks is read as a prefix: on a site with
|
|
32
|
+
* no Italian, `/it` is whatever page is called `it`, not an empty Italian front
|
|
33
|
+
* page. So the list is the site's, never the family's.
|
|
34
|
+
* @param path - An address from the site's own root, the mount already off,
|
|
35
|
+
* e.g. `/fr/tutorial`.
|
|
36
|
+
* @param languages - The languages this site is written in, the default one
|
|
37
|
+
* included.
|
|
38
|
+
* @returns The language it names and the page it opens.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readLanguagePath(path: string, languages: readonly Language[]): LanguagePath;
|
|
41
|
+
/**
|
|
42
|
+
* Write one of the site's own addresses in a language.
|
|
43
|
+
*
|
|
44
|
+
* The default language is written with no prefix: an address naming no language
|
|
45
|
+
* is English, which is what keeps every link handed out before the site was
|
|
46
|
+
* translated pointing at the same page.
|
|
47
|
+
* @param language - The language to write it in.
|
|
48
|
+
* @param path - The address from the site's own root, e.g. `/tutorial`.
|
|
49
|
+
* @returns The address under that language, e.g. `/fr/tutorial`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function withLanguagePath(language: Language, path: string): string;
|
|
52
|
+
//# sourceMappingURL=languagePath.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"languagePath.d.ts","sourceRoot":"","sources":["../../../src/language/core/languagePath.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAI7D,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,EAAE,QAAQ,CAAC;IACnB,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,SAAS,QAAQ,EAAE,GAC7B,YAAY,CAcd;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAIzE"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The language a translated site routes by: a prefix on its own addresses.
|
|
3
|
+
*
|
|
4
|
+
* `/fr/tutorial` is the French tutorial, and English is written with no prefix
|
|
5
|
+
* at all, so every link already handed out in a course stays alive and the site
|
|
6
|
+
* does not acquire a second spelling of its own front page.
|
|
7
|
+
*
|
|
8
|
+
* This is the half of the design that makes a translation *findable*. A site
|
|
9
|
+
* that keeps its language in a query parameter, in the hash or in
|
|
10
|
+
* `localStorage` serves five languages at one address: a crawler sees one page,
|
|
11
|
+
* indexes one language, and the other four may as well not have been written.
|
|
12
|
+
* The parameter stays — it is how the language travels to a sibling that may
|
|
13
|
+
* not speak it (`./languageParam.ts`) — but it is not how a translated site
|
|
14
|
+
* routes.
|
|
15
|
+
*
|
|
16
|
+
* The prefix sits between the mount and the route, so a tool served as one of
|
|
17
|
+
* several on a shared host reads `/surge/fr/exercises`: the mount comes off
|
|
18
|
+
* first (`router/core/basePath.ts`), then the language, then the table is read.
|
|
19
|
+
*/
|
|
20
|
+
import { DEFAULT_LANGUAGE } from "../../i18n/core/languages.js";
|
|
21
|
+
import { trimTrailingSlash } from "../../router/core/address.js";
|
|
22
|
+
/**
|
|
23
|
+
* Read the language prefix off one of the site's own addresses.
|
|
24
|
+
*
|
|
25
|
+
* Only a language the site actually speaks is read as a prefix: on a site with
|
|
26
|
+
* no Italian, `/it` is whatever page is called `it`, not an empty Italian front
|
|
27
|
+
* page. So the list is the site's, never the family's.
|
|
28
|
+
* @param path - An address from the site's own root, the mount already off,
|
|
29
|
+
* e.g. `/fr/tutorial`.
|
|
30
|
+
* @param languages - The languages this site is written in, the default one
|
|
31
|
+
* included.
|
|
32
|
+
* @returns The language it names and the page it opens.
|
|
33
|
+
*/
|
|
34
|
+
export function readLanguagePath(path, languages) {
|
|
35
|
+
const opened = path.startsWith('/') ? path : `/${path}`;
|
|
36
|
+
const cut = opened.indexOf('/', 1);
|
|
37
|
+
const first = cut === -1 ? opened.slice(1) : opened.slice(1, cut);
|
|
38
|
+
for (const language of languages) {
|
|
39
|
+
if (language === DEFAULT_LANGUAGE || language !== first)
|
|
40
|
+
continue;
|
|
41
|
+
const rest = cut === -1 ? '/' : opened.slice(cut);
|
|
42
|
+
return { language, path: trimTrailingSlash(rest) || '/' };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
language: DEFAULT_LANGUAGE,
|
|
46
|
+
path: trimTrailingSlash(opened) || '/',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Write one of the site's own addresses in a language.
|
|
51
|
+
*
|
|
52
|
+
* The default language is written with no prefix: an address naming no language
|
|
53
|
+
* is English, which is what keeps every link handed out before the site was
|
|
54
|
+
* translated pointing at the same page.
|
|
55
|
+
* @param language - The language to write it in.
|
|
56
|
+
* @param path - The address from the site's own root, e.g. `/tutorial`.
|
|
57
|
+
* @returns The address under that language, e.g. `/fr/tutorial`.
|
|
58
|
+
*/
|
|
59
|
+
export function withLanguagePath(language, path) {
|
|
60
|
+
const opened = path.startsWith('/') ? path : `/${path}`;
|
|
61
|
+
if (language === DEFAULT_LANGUAGE)
|
|
62
|
+
return opened;
|
|
63
|
+
return opened === '/' ? `/${language}` : `/${language}${opened}`;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=languagePath.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"languagePath.js","sourceRoot":"","sources":["../../../src/language/core/languagePath.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAUjE;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,SAA8B;IAE9B,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;IACxD,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAElE,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,QAAQ,KAAK,gBAAgB,IAAI,QAAQ,KAAK,KAAK;YAAE,SAAS;QAClE,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAClD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,iBAAiB,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;IAC5D,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,gBAAgB;QAC1B,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,IAAI,GAAG;KACvC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAkB,EAAE,IAAY;IAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;IACxD,IAAI,QAAQ,KAAK,gBAAgB;QAAE,OAAO,MAAM,CAAC;IACjD,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,GAAG,MAAM,EAAE,CAAC;AACnE,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tags that tell a search engine five addresses are one page written five
|
|
3
|
+
* times.
|
|
4
|
+
*
|
|
5
|
+
* Without them a translated site is read as five sites that happen to look
|
|
6
|
+
* alike: the French tutorial competes with the English one instead of being
|
|
7
|
+
* offered to a French reader, and a search engine picks one of them to keep.
|
|
8
|
+
* With them each address is served to the audience it was written for, and the
|
|
9
|
+
* four it was not stop diluting it.
|
|
10
|
+
*
|
|
11
|
+
* Every page names **every** language including itself — a set that does not
|
|
12
|
+
* list the page it is on is ignored whole — plus `x-default`, which is where a
|
|
13
|
+
* reader whose language we do not speak is sent.
|
|
14
|
+
*/
|
|
15
|
+
import type { Language } from '../../i18n/core/languages.ts';
|
|
16
|
+
/** Which page, in which languages, served from where. */
|
|
17
|
+
export interface AlternateOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Where the site is served, mount path included and no trailing slash, e.g.
|
|
20
|
+
* `https://3d.cheminfo.org`.
|
|
21
|
+
*/
|
|
22
|
+
origin: string;
|
|
23
|
+
/**
|
|
24
|
+
* The page, from the site's own root and with no language prefix on it, e.g.
|
|
25
|
+
* `/tutorial`.
|
|
26
|
+
*/
|
|
27
|
+
path: string;
|
|
28
|
+
/**
|
|
29
|
+
* Every language the site is written in. One language alone means there is
|
|
30
|
+
* nothing to point at, and no tag is written.
|
|
31
|
+
*/
|
|
32
|
+
languages: readonly Language[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The `rel="alternate"` block for one page of a translated site.
|
|
36
|
+
* @param options - The page, its languages and where the site is served.
|
|
37
|
+
* @returns The tags, one per line, or `''` when the site speaks one language.
|
|
38
|
+
*/
|
|
39
|
+
export declare function alternateLinkTags(options: AlternateOptions): string;
|
|
40
|
+
//# sourceMappingURL=alternates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alternates.d.ts","sourceRoot":"","sources":["../../../src/seo/core/alternates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAK7D,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAYnE"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tags that tell a search engine five addresses are one page written five
|
|
3
|
+
* times.
|
|
4
|
+
*
|
|
5
|
+
* Without them a translated site is read as five sites that happen to look
|
|
6
|
+
* alike: the French tutorial competes with the English one instead of being
|
|
7
|
+
* offered to a French reader, and a search engine picks one of them to keep.
|
|
8
|
+
* With them each address is served to the audience it was written for, and the
|
|
9
|
+
* four it was not stop diluting it.
|
|
10
|
+
*
|
|
11
|
+
* Every page names **every** language including itself — a set that does not
|
|
12
|
+
* list the page it is on is ignored whole — plus `x-default`, which is where a
|
|
13
|
+
* reader whose language we do not speak is sent.
|
|
14
|
+
*/
|
|
15
|
+
import { DEFAULT_LANGUAGE } from "../../i18n/core/languages.js";
|
|
16
|
+
import { withLanguagePath } from "../../language/core/languagePath.js";
|
|
17
|
+
import { escapeAttribute } from "../../share/core/escape.js";
|
|
18
|
+
/**
|
|
19
|
+
* The `rel="alternate"` block for one page of a translated site.
|
|
20
|
+
* @param options - The page, its languages and where the site is served.
|
|
21
|
+
* @returns The tags, one per line, or `''` when the site speaks one language.
|
|
22
|
+
*/
|
|
23
|
+
export function alternateLinkTags(options) {
|
|
24
|
+
const { origin, path, languages } = options;
|
|
25
|
+
if (languages.length < 2)
|
|
26
|
+
return '';
|
|
27
|
+
const tags = [];
|
|
28
|
+
for (const language of languages) {
|
|
29
|
+
tags.push(linkTag(language, address(origin, language, path)));
|
|
30
|
+
}
|
|
31
|
+
// A reader we have no language for gets the unprefixed address, which is the
|
|
32
|
+
// one every link handed out before the site was translated already points at.
|
|
33
|
+
tags.push(linkTag('x-default', address(origin, DEFAULT_LANGUAGE, path)));
|
|
34
|
+
return tags.join('\n');
|
|
35
|
+
}
|
|
36
|
+
function address(origin, language, path) {
|
|
37
|
+
return `${origin}${withLanguagePath(language, path)}`;
|
|
38
|
+
}
|
|
39
|
+
function linkTag(hreflang, href) {
|
|
40
|
+
return `<link rel="alternate" hreflang="${escapeAttribute(hreflang)}" href="${escapeAttribute(href)}" />`;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=alternates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alternates.js","sourceRoot":"","sources":["../../../src/seo/core/alternates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAqB7D;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAyB;IACzD,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAC5C,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAEpC,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IAChE,CAAC;IACD,6EAA6E;IAC7E,8EAA8E;IAC9E,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IACzE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,SAAS,OAAO,CAAC,MAAc,EAAE,QAAkB,EAAE,IAAY;IAC/D,OAAO,GAAG,MAAM,GAAG,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,SAAS,OAAO,CAAC,QAAgB,EAAE,IAAY;IAC7C,OAAO,mCAAmC,eAAe,CAAC,QAAQ,CAAC,WAAW,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC;AAC5G,CAAC"}
|
package/lib/seo/core/index.d.ts
CHANGED
|
@@ -4,10 +4,13 @@ export type { NoscriptEcosystem, NoscriptHrefs, NoscriptOptions, NoscriptRoute,
|
|
|
4
4
|
export { noscriptIndex } from './noscript.ts';
|
|
5
5
|
export type { PageMetaOptions } from './pageMeta.ts';
|
|
6
6
|
export { injectPageMeta, pageDocumentMeta, pageHeadTags } from './pageMeta.ts';
|
|
7
|
+
export type { PageContent, PageSection, PageTable } from './pageProse.ts';
|
|
8
|
+
export { pageProseHtml } from './pageProse.ts';
|
|
7
9
|
export type { PlainDescriptionOptions } from './plainDescription.ts';
|
|
8
10
|
export { plainDescription, plainProse } from './plainDescription.ts';
|
|
9
11
|
export type { RobotsDisallow } from './robots.ts';
|
|
10
12
|
export { robotsTxt } from './robots.ts';
|
|
13
|
+
export { routeProblems, withheldPhrase } from './routeProblems.ts';
|
|
11
14
|
export type { RouteMeta } from './routes.ts';
|
|
12
15
|
export { assertRoutes, homeRoute, pageMetaFor, routeFor } from './routes.ts';
|
|
13
16
|
export type { SiteFilesOptions } from './siteFiles.ts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/seo/core/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,aAAa,EACb,YAAY,GACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC/E,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACrE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC7E,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/seo/core/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,aAAa,EACb,YAAY,GACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC/E,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACrE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC7E,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/lib/seo/core/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { writeDocumentMeta } from "./documentMeta.js";
|
|
2
2
|
export { noscriptIndex } from "./noscript.js";
|
|
3
3
|
export { injectPageMeta, pageDocumentMeta, pageHeadTags } from "./pageMeta.js";
|
|
4
|
+
export { pageProseHtml } from "./pageProse.js";
|
|
4
5
|
export { plainDescription, plainProse } from "./plainDescription.js";
|
|
5
6
|
export { robotsTxt } from "./robots.js";
|
|
7
|
+
export { routeProblems, withheldPhrase } from "./routeProblems.js";
|
|
6
8
|
export { assertRoutes, homeRoute, pageMetaFor, routeFor } from "./routes.js";
|
|
7
9
|
export { sitemapXml } from "./siteFiles.js";
|
|
8
10
|
export { startDocumentMeta } from "./startDocumentMeta.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/seo/core/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAQtD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE/E,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAErE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/seo/core/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAQtD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE/E,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAErE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE7E,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* the tool needs JavaScript, and links the addresses it answers.
|
|
7
7
|
*/
|
|
8
8
|
import type { SiteId } from '../../ecosystem/core/sites.ts';
|
|
9
|
+
import type { PageContent } from './pageProse.ts';
|
|
9
10
|
import type { RouteMeta } from './routes.ts';
|
|
10
11
|
import type { SiteFilesOptions } from './siteFiles.ts';
|
|
11
12
|
/** How the addresses of the site's own pages are written. */
|
|
@@ -80,6 +81,14 @@ export interface NoscriptText {
|
|
|
80
81
|
export interface NoscriptOptions extends Omit<SiteFilesOptions, 'routes'>, Omit<NoscriptText, 'routes'> {
|
|
81
82
|
/** The addresses it links, each with the label it is linked under. */
|
|
82
83
|
routes: readonly NoscriptRoute[];
|
|
84
|
+
/**
|
|
85
|
+
* What the page being written says, above the crawl path. The block is the
|
|
86
|
+
* only text a crawler is handed before anything runs, so a page with nothing
|
|
87
|
+
* of its own here is indexed on its title and on this site's menu — which
|
|
88
|
+
* every other page of the site carries too.
|
|
89
|
+
* @default undefined — the block is the menu alone
|
|
90
|
+
*/
|
|
91
|
+
content?: PageContent;
|
|
83
92
|
}
|
|
84
93
|
/**
|
|
85
94
|
* The `noscript` block, ready to put in the body.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noscript.d.ts","sourceRoot":"","sources":["../../../src/seo/core/noscript.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAiB,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAK3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGvD,6DAA6D;AAC7D,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,CAAC;AAEpD,6DAA6D;AAC7D,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CACrC;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,iBAAiB,CAAC;IACxC;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CACnC;AAED,yDAAyD;AACzD,MAAM,WAAW,eACf,SAAQ,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC;IACtE,sEAAsE;IACtE,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"noscript.d.ts","sourceRoot":"","sources":["../../../src/seo/core/noscript.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAiB,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAK3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGvD,6DAA6D;AAC7D,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,CAAC;AAEpD,6DAA6D;AAC7D,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CACrC;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,iBAAiB,CAAC;IACxC;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CACnC;AAED,yDAAyD;AACzD,MAAM,WAAW,eACf,SAAQ,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC;IACtE,sEAAsE;IACtE,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;IACjC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAoB9D"}
|