@ascribed/astro 0.2.1-next.16 → 0.2.1-next.17
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 +5 -3
- package/dist/content.d.ts +2 -1
- package/dist/content.js +1 -0
- package/dist/content.js.map +1 -1
- package/dist/files.d.ts +5 -2
- package/dist/files.js +13 -5
- package/dist/files.js.map +1 -1
- package/dist/head-check.d.ts +6 -0
- package/dist/head-check.js +28 -0
- package/dist/head-check.js.map +1 -0
- package/dist/head.d.ts +54 -0
- package/dist/head.js +114 -0
- package/dist/head.js.map +1 -0
- package/dist/headers.d.ts +50 -0
- package/dist/headers.js +155 -0
- package/dist/headers.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +45 -11
- package/dist/index.js.map +1 -1
- package/dist/project.d.ts +2 -0
- package/dist/project.js +1 -0
- package/dist/project.js.map +1 -1
- package/dist/records.d.ts +66 -8
- package/dist/records.js +35 -6
- package/dist/records.js.map +1 -1
- package/dist/root-files.d.ts +9 -4
- package/dist/root-files.js +6 -3
- package/dist/root-files.js.map +1 -1
- package/package.json +5 -5
- package/src/Head.astro +41 -23
- package/starter/src/layouts/Docs.astro +1 -1
package/dist/project.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["// What the integration needs from `ascribe.toml`: where a build's site output\n// goes, and the consumer settings that must agree with `astro.config`.\n// The compiler validates the whole file; this reads\n// only what it compiled.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { parse } from \"smol-toml\";\n\n/** The parts of an Ascribe project the integration reads. */\nexport interface ProjectInfo {\n /** The directory holding `ascribe.toml`. */\n dir: string;\n /** The path of `ascribe.toml`. */\n configPath: string;\n /** Source files and assets watched during development. */\n contentRoot: string;\n /** Generated output, never treated as a source change. */\n outputRoot: string;\n /** `[consumer]`, with the profile's defaults. */\n consumer: {\n site: string | undefined;\n basePath: string;\n trailingSlash: \"always\" | \"never\";\n /** Whether builds publish what agents read: `llms.txt` and each page's Markdown, in the plain output. */\n agents: boolean;\n };\n /** The names of `[builds]`'s tables, in order: empty when there are none, and the one build is `site`. */\n builds: string[];\n /** The site output's root for a build: `<output-dir>/<build>/site`. */\n siteRoot(build: string): string;\n /** The plain output's root for a build: `<output-dir>/<build>/plain`. */\n plainRoot(build: string): string;\n}\n\n/** Reads `ascribe.toml` in `dir`. Throws a readable error if it can't be read. */\nexport function readProject(dir: string): ProjectInfo {\n const configPath = path.join(dir, \"ascribe.toml\");\n let table: Record<string, unknown>;\n try {\n table = parse(readFileSync(configPath, \"utf8\"));\n } catch (error) {\n throw new Error(\n `@ascribed/astro can't read ${configPath}: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n const project = section(table, \"project\");\n const consumer = section(table, \"consumer\");\n const outputDir =\n typeof project[\"output-dir\"] === \"string\" ? project[\"output-dir\"] : \".ascribe/build\";\n const contentDir = typeof project[\"content-root\"] === \"string\" ? project[\"content-root\"] : \"docs\";\n const trailingSlash = consumer[\"trailing-slash\"] === \"never\" ? \"never\" : \"always\";\n return {\n dir,\n configPath,\n contentRoot: path.resolve(dir, contentDir),\n outputRoot: path.resolve(dir, outputDir),\n consumer: {\n site: typeof consumer[\"site\"] === \"string\" ? consumer[\"site\"] : undefined,\n basePath: normalizeBase(\n typeof consumer[\"base-path\"] === \"string\" ? consumer[\"base-path\"] : \"/\",\n ),\n trailingSlash,\n agents: consumer[\"agents\"] === true,\n },\n builds: Object.keys(section(table, \"builds\")),\n siteRoot: (build) => path.resolve(dir, outputDir, build, \"site\"),\n plainRoot: (build) => path.resolve(dir, outputDir, build, \"plain\"),\n };\n}\n\nfunction section(table: Record<string, unknown>, name: string): Record<string, unknown> {\n const value = table[name];\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\n/** A base path with a leading and a trailing `/`, as Ascribe's router has it. */\nexport function normalizeBase(base: string): string {\n let result = base.trim();\n if (!result.startsWith(\"/\")) result = `/${result}`;\n if (!result.endsWith(\"/\")) result += \"/\";\n return result;\n}\n\n/**\n * The route of a base path, ending the way `trailingSlash` says the site's\n * routes do: the site's root for Astro's `base`, a project's root for its\n * `[consumer] base-path`. (`import.meta.env.BASE_URL` isn't replaced in a\n * package's components.)\n */\nexport function homeRoute(base: string, trailingSlash: \"always\" | \"never\" | \"ignore\"): string {\n const root = normalizeBase(base);\n return trailingSlash === \"never\" && root !== \"/\" ? root.slice(0, -1) : root;\n}\n\n/** Astro's routing settings, as the integration reads and supplies them. */\nexport interface AstroRouting {\n base: string;\n trailingSlash: \"always\" | \"never\" | \"ignore\";\n site: string | undefined;\n}\n\n/**\n * Astro's routing with what astro.config leaves unset taken from `[consumer]`,\n * and the settings taken, for `updateConfig`. Astro gives an integration its\n * configuration with its defaults filled in, so unset is the default: no\n * `site`, `trailingSlash: \"ignore\"`, and `base: \"/\"`. `site` and\n * `trailingSlash` come from the first project that has them, since every\n * project must agree with them anyway. `base` comes from the first project's\n * `base-path` only with `supplyBase`, as with one project; with several, it\n * stays the site's, the root every base path is under. A base Astro has is\n * rewritten to end the way a supplied `trailingSlash` says, as Astro writes\n * it when the setting is its own.\n */\nexport function supplyRouting(\n projects: readonly ProjectInfo[],\n astro: AstroRouting,\n supplyBase: boolean,\n): { routing: AstroRouting; supplied: Partial<AstroRouting> } {\n const supplied: Partial<AstroRouting> = {};\n if (astro.site === undefined) {\n const site = projects.find((project) => project.consumer.site !== undefined)?.consumer.site;\n if (site !== undefined) supplied.site = site;\n }\n const first = projects[0];\n if (first === undefined) return { routing: astro, supplied };\n if (astro.trailingSlash === \"ignore\") supplied.trailingSlash = first.consumer.trailingSlash;\n const trailingSlash = supplied.trailingSlash ?? astro.trailingSlash;\n const base = homeRoute(\n supplyBase && normalizeBase(astro.base) === \"/\" ? first.consumer.basePath : astro.base,\n trailingSlash,\n );\n if (base !== astro.base) supplied.base = base;\n return { routing: { ...astro, ...supplied }, supplied };\n}\n\n/**\n * The ways `ascribe.toml`'s `[consumer]` disagrees with Astro's configuration,\n * as sentences. Ascribe writes every link with its own settings, so a site\n * whose routes differ would have broken links.\n */\nexport function consumerMismatches(\n project: ProjectInfo,\n astro: { base: string; trailingSlash: string; site: string | undefined },\n): string[] {\n const problems: string[] = [];\n const { basePath, trailingSlash, site } = project.consumer;\n // A base path under Astro's `base` leaves the rest of the site to other pages, such as a home page.\n if (!basePath.startsWith(normalizeBase(astro.base))) {\n problems.push(\n `[consumer] base-path is \"${basePath}\", but Astro's \\`base\\` is \"${astro.base}\". Set base-path to that path or a path under it.`,\n );\n }\n // Astro's \"ignore\" serves both forms, so Ascribe's links work either way.\n if (astro.trailingSlash !== \"ignore\" && astro.trailingSlash !== trailingSlash) {\n problems.push(\n `[consumer] trailing-slash is \"${trailingSlash}\", but Astro's \\`trailingSlash\\` is \"${astro.trailingSlash}\".`,\n );\n }\n if (site !== undefined && astro.site !== undefined && origin(site) !== origin(astro.site)) {\n problems.push(`[consumer] site is \"${site}\", but Astro's \\`site\\` is \"${astro.site}\".`);\n }\n return problems;\n}\n\nfunction origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return url;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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","sourcesContent":["// What the integration needs from `ascribe.toml`: where a build's site output\n// goes, and the consumer settings that must agree with `astro.config`.\n// The compiler validates the whole file; this reads\n// only what it compiled.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { parse } from \"smol-toml\";\n\n/** The parts of an Ascribe project the integration reads. */\nexport interface ProjectInfo {\n /** The directory holding `ascribe.toml`. */\n dir: string;\n /** The path of `ascribe.toml`. */\n configPath: string;\n /** Source files and assets watched during development. */\n contentRoot: string;\n /** Generated output, never treated as a source change. */\n outputRoot: string;\n /** `[consumer]`, with the profile's defaults. */\n consumer: {\n site: string | undefined;\n basePath: string;\n trailingSlash: \"always\" | \"never\";\n /** Whether builds publish what agents read: `llms.txt` and each page's Markdown, in the plain output. */\n agents: boolean;\n /** The host whose format the site's response headers are written in: `\"netlify\"`, or none. */\n headers: \"netlify\" | undefined;\n };\n /** The names of `[builds]`'s tables, in order: empty when there are none, and the one build is `site`. */\n builds: string[];\n /** The site output's root for a build: `<output-dir>/<build>/site`. */\n siteRoot(build: string): string;\n /** The plain output's root for a build: `<output-dir>/<build>/plain`. */\n plainRoot(build: string): string;\n}\n\n/** Reads `ascribe.toml` in `dir`. Throws a readable error if it can't be read. */\nexport function readProject(dir: string): ProjectInfo {\n const configPath = path.join(dir, \"ascribe.toml\");\n let table: Record<string, unknown>;\n try {\n table = parse(readFileSync(configPath, \"utf8\"));\n } catch (error) {\n throw new Error(\n `@ascribed/astro can't read ${configPath}: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n const project = section(table, \"project\");\n const consumer = section(table, \"consumer\");\n const outputDir =\n typeof project[\"output-dir\"] === \"string\" ? project[\"output-dir\"] : \".ascribe/build\";\n const contentDir = typeof project[\"content-root\"] === \"string\" ? project[\"content-root\"] : \"docs\";\n const trailingSlash = consumer[\"trailing-slash\"] === \"never\" ? \"never\" : \"always\";\n return {\n dir,\n configPath,\n contentRoot: path.resolve(dir, contentDir),\n outputRoot: path.resolve(dir, outputDir),\n consumer: {\n site: typeof consumer[\"site\"] === \"string\" ? consumer[\"site\"] : undefined,\n basePath: normalizeBase(\n typeof consumer[\"base-path\"] === \"string\" ? consumer[\"base-path\"] : \"/\",\n ),\n trailingSlash,\n agents: consumer[\"agents\"] === true,\n headers: consumer[\"headers\"] === \"netlify\" ? \"netlify\" : undefined,\n },\n builds: Object.keys(section(table, \"builds\")),\n siteRoot: (build) => path.resolve(dir, outputDir, build, \"site\"),\n plainRoot: (build) => path.resolve(dir, outputDir, build, \"plain\"),\n };\n}\n\nfunction section(table: Record<string, unknown>, name: string): Record<string, unknown> {\n const value = table[name];\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\n/** A base path with a leading and a trailing `/`, as Ascribe's router has it. */\nexport function normalizeBase(base: string): string {\n let result = base.trim();\n if (!result.startsWith(\"/\")) result = `/${result}`;\n if (!result.endsWith(\"/\")) result += \"/\";\n return result;\n}\n\n/**\n * The route of a base path, ending the way `trailingSlash` says the site's\n * routes do: the site's root for Astro's `base`, a project's root for its\n * `[consumer] base-path`. (`import.meta.env.BASE_URL` isn't replaced in a\n * package's components.)\n */\nexport function homeRoute(base: string, trailingSlash: \"always\" | \"never\" | \"ignore\"): string {\n const root = normalizeBase(base);\n return trailingSlash === \"never\" && root !== \"/\" ? root.slice(0, -1) : root;\n}\n\n/** Astro's routing settings, as the integration reads and supplies them. */\nexport interface AstroRouting {\n base: string;\n trailingSlash: \"always\" | \"never\" | \"ignore\";\n site: string | undefined;\n}\n\n/**\n * Astro's routing with what astro.config leaves unset taken from `[consumer]`,\n * and the settings taken, for `updateConfig`. Astro gives an integration its\n * configuration with its defaults filled in, so unset is the default: no\n * `site`, `trailingSlash: \"ignore\"`, and `base: \"/\"`. `site` and\n * `trailingSlash` come from the first project that has them, since every\n * project must agree with them anyway. `base` comes from the first project's\n * `base-path` only with `supplyBase`, as with one project; with several, it\n * stays the site's, the root every base path is under. A base Astro has is\n * rewritten to end the way a supplied `trailingSlash` says, as Astro writes\n * it when the setting is its own.\n */\nexport function supplyRouting(\n projects: readonly ProjectInfo[],\n astro: AstroRouting,\n supplyBase: boolean,\n): { routing: AstroRouting; supplied: Partial<AstroRouting> } {\n const supplied: Partial<AstroRouting> = {};\n if (astro.site === undefined) {\n const site = projects.find((project) => project.consumer.site !== undefined)?.consumer.site;\n if (site !== undefined) supplied.site = site;\n }\n const first = projects[0];\n if (first === undefined) return { routing: astro, supplied };\n if (astro.trailingSlash === \"ignore\") supplied.trailingSlash = first.consumer.trailingSlash;\n const trailingSlash = supplied.trailingSlash ?? astro.trailingSlash;\n const base = homeRoute(\n supplyBase && normalizeBase(astro.base) === \"/\" ? first.consumer.basePath : astro.base,\n trailingSlash,\n );\n if (base !== astro.base) supplied.base = base;\n return { routing: { ...astro, ...supplied }, supplied };\n}\n\n/**\n * The ways `ascribe.toml`'s `[consumer]` disagrees with Astro's configuration,\n * as sentences. Ascribe writes every link with its own settings, so a site\n * whose routes differ would have broken links.\n */\nexport function consumerMismatches(\n project: ProjectInfo,\n astro: { base: string; trailingSlash: string; site: string | undefined },\n): string[] {\n const problems: string[] = [];\n const { basePath, trailingSlash, site } = project.consumer;\n // A base path under Astro's `base` leaves the rest of the site to other pages, such as a home page.\n if (!basePath.startsWith(normalizeBase(astro.base))) {\n problems.push(\n `[consumer] base-path is \"${basePath}\", but Astro's \\`base\\` is \"${astro.base}\". Set base-path to that path or a path under it.`,\n );\n }\n // Astro's \"ignore\" serves both forms, so Ascribe's links work either way.\n if (astro.trailingSlash !== \"ignore\" && astro.trailingSlash !== trailingSlash) {\n problems.push(\n `[consumer] trailing-slash is \"${trailingSlash}\", but Astro's \\`trailingSlash\\` is \"${astro.trailingSlash}\".`,\n );\n }\n if (site !== undefined && astro.site !== undefined && origin(site) !== origin(astro.site)) {\n problems.push(`[consumer] site is \"${site}\", but Astro's \\`site\\` is \"${astro.site}\".`);\n }\n return problems;\n}\n\nfunction origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return url;\n }\n}\n"]}
|
package/dist/records.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
/** How a page is treated: listed, listed and not to be quoted, or not listed. */
|
|
2
2
|
export type Listing = "listed" | "unquoted" | "unlisted";
|
|
3
|
+
/**
|
|
4
|
+
* Where a page's listing comes from: `default` when nothing sets it, `page`
|
|
5
|
+
* for the page's own, and for a rule of `[listing]` its level (`site`,
|
|
6
|
+
* `folder`, or `type`), its folder or type, the rule as written, and its line
|
|
7
|
+
* in `ascribe.toml`.
|
|
8
|
+
*/
|
|
9
|
+
export interface ListingFrom {
|
|
10
|
+
level: "default" | "page" | "site" | "folder" | "type";
|
|
11
|
+
folder?: string;
|
|
12
|
+
type?: string;
|
|
13
|
+
rule?: string;
|
|
14
|
+
line?: number;
|
|
15
|
+
}
|
|
3
16
|
/** One page's record, with the route it's keyed by. */
|
|
4
17
|
export interface PageRecord {
|
|
5
18
|
/** The page's route, from the host's root. */
|
|
@@ -13,17 +26,52 @@ export interface PageRecord {
|
|
|
13
26
|
/** The page's own address, absolute, when the project sets `[consumer] site`. */
|
|
14
27
|
canonical?: string;
|
|
15
28
|
listing: Listing;
|
|
29
|
+
/** Where `listing` comes from: the page, a rule of `[listing]`, or neither. */
|
|
30
|
+
listing_from: ListingFrom;
|
|
16
31
|
/** The address of the page's Markdown version, from the host's root, when the build publishes one. */
|
|
17
32
|
markdown?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The page's place in the site, from the top down to the page itself: the
|
|
35
|
+
* page at each of its route's folders that has one, then the page. Absent
|
|
36
|
+
* in records an earlier Ascribe wrote.
|
|
37
|
+
*/
|
|
38
|
+
breadcrumbs?: Step[];
|
|
39
|
+
}
|
|
40
|
+
/** One step of a page's breadcrumbs: a page's route, from the host's root, and its title. */
|
|
41
|
+
export interface Step {
|
|
42
|
+
route: string;
|
|
43
|
+
title: string;
|
|
44
|
+
}
|
|
45
|
+
/** What a kind of crawler is for: gathering training data, building an AI search index, or fetching at a person's request. */
|
|
46
|
+
type Purpose = "training" | "search" | "on-request";
|
|
47
|
+
/** The group of `robots.txt` a disallowed purpose writes. */
|
|
48
|
+
export interface RobotsGroup {
|
|
49
|
+
purpose: Purpose;
|
|
50
|
+
/** Every token of the purpose the binary knows, each a `User-agent` line. */
|
|
51
|
+
user_agents: string[];
|
|
52
|
+
/** The group's rules: `Disallow: /`. */
|
|
53
|
+
lines: string[];
|
|
54
|
+
}
|
|
55
|
+
/** What a project says to crawlers. */
|
|
56
|
+
export interface Robots {
|
|
57
|
+
/** The project's own lines, with paths from the host's root. */
|
|
58
|
+
lines: string[];
|
|
59
|
+
/** What the project says of each purpose it sets. */
|
|
60
|
+
purposes?: Partial<Record<Purpose, "allow" | "disallow">>;
|
|
61
|
+
/** A group for each purpose the project disallows. */
|
|
62
|
+
groups?: RobotsGroup[];
|
|
18
63
|
}
|
|
19
64
|
/** `_ascribe/pages.json`. */
|
|
20
65
|
export interface PageRecords {
|
|
21
66
|
version: number;
|
|
22
67
|
site?: string;
|
|
23
68
|
pages: Record<string, Omit<PageRecord, "route">>;
|
|
24
|
-
robots:
|
|
25
|
-
|
|
26
|
-
|
|
69
|
+
robots: Robots;
|
|
70
|
+
}
|
|
71
|
+
/** What one project says to crawlers, by the name of its first entry. */
|
|
72
|
+
export interface ProjectRobots {
|
|
73
|
+
name: string;
|
|
74
|
+
robots: Robots;
|
|
27
75
|
}
|
|
28
76
|
/** The records in a build's site output, or `undefined` when there aren't any. */
|
|
29
77
|
export declare function readRecords(siteRoot: string): PageRecords | undefined;
|
|
@@ -68,11 +116,21 @@ export declare function sitemapAddresses(sources: SitemapSources): string[];
|
|
|
68
116
|
/** A sitemap of `addresses`, with no dates. */
|
|
69
117
|
export declare function sitemapXml(addresses: readonly string[]): string;
|
|
70
118
|
/**
|
|
71
|
-
* A site's `robots.txt` from each project
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
119
|
+
* A site's `robots.txt` from what each project says to crawlers: a group for
|
|
120
|
+
* each purpose a project disallows, once for the site, then a
|
|
121
|
+
* `User-agent: *` group with every project's lines before its first
|
|
122
|
+
* `User-agent` line, which are for every crawler, then the groups each
|
|
123
|
+
* project wrote whole, then the sitemap's address. `undefined` when it would
|
|
124
|
+
* say nothing.
|
|
125
|
+
*/
|
|
126
|
+
export declare function robotsText(projects: readonly Robots[], sitemap: string | undefined): string | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* Two projects that say contradictory things about one purpose, as
|
|
129
|
+
* sentences: one allows it and the other disallows it. A site has one
|
|
130
|
+
* `robots.txt`, so they must agree. A project that says nothing of a purpose
|
|
131
|
+
* agrees with either.
|
|
75
132
|
*/
|
|
76
|
-
export declare function
|
|
133
|
+
export declare function purposeConflicts(projects: readonly ProjectRobots[]): string[];
|
|
77
134
|
/** Where the sitemap is, from the host's root: at Astro's `base`. */
|
|
78
135
|
export declare function sitemapRoute(base: string): string;
|
|
136
|
+
export {};
|
package/dist/records.js
CHANGED
|
@@ -122,21 +122,33 @@ export function sitemapXml(addresses) {
|
|
|
122
122
|
}
|
|
123
123
|
const USER_AGENT = /^\s*user-agent\s*:/i;
|
|
124
124
|
/**
|
|
125
|
-
* A site's `robots.txt` from each project
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
125
|
+
* A site's `robots.txt` from what each project says to crawlers: a group for
|
|
126
|
+
* each purpose a project disallows, once for the site, then a
|
|
127
|
+
* `User-agent: *` group with every project's lines before its first
|
|
128
|
+
* `User-agent` line, which are for every crawler, then the groups each
|
|
129
|
+
* project wrote whole, then the sitemap's address. `undefined` when it would
|
|
130
|
+
* say nothing.
|
|
129
131
|
*/
|
|
130
132
|
export function robotsText(projects, sitemap) {
|
|
133
|
+
const purposes = new Map();
|
|
131
134
|
const everyone = [];
|
|
132
135
|
const groups = [];
|
|
133
|
-
for (const
|
|
136
|
+
for (const robots of projects) {
|
|
137
|
+
for (const group of robots.groups ?? []) {
|
|
138
|
+
if (purposes.has(group.purpose))
|
|
139
|
+
continue;
|
|
140
|
+
purposes.set(group.purpose, [
|
|
141
|
+
...group.user_agents.map((agent) => `User-agent: ${agent}`),
|
|
142
|
+
...group.lines,
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
const lines = robots.lines;
|
|
134
146
|
const first = lines.findIndex((line) => USER_AGENT.test(line));
|
|
135
147
|
everyone.push(...(first === -1 ? lines : lines.slice(0, first)));
|
|
136
148
|
if (first !== -1)
|
|
137
149
|
groups.push(lines.slice(first));
|
|
138
150
|
}
|
|
139
|
-
const blocks = [];
|
|
151
|
+
const blocks = [...purposes.values()];
|
|
140
152
|
if (everyone.length > 0)
|
|
141
153
|
blocks.push(["User-agent: *", ...everyone]);
|
|
142
154
|
blocks.push(...groups);
|
|
@@ -146,6 +158,23 @@ export function robotsText(projects, sitemap) {
|
|
|
146
158
|
return undefined;
|
|
147
159
|
return `${blocks.map((block) => block.join("\n")).join("\n\n")}\n`;
|
|
148
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Two projects that say contradictory things about one purpose, as
|
|
163
|
+
* sentences: one allows it and the other disallows it. A site has one
|
|
164
|
+
* `robots.txt`, so they must agree. A project that says nothing of a purpose
|
|
165
|
+
* agrees with either.
|
|
166
|
+
*/
|
|
167
|
+
export function purposeConflicts(projects) {
|
|
168
|
+
const problems = [];
|
|
169
|
+
for (const purpose of ["training", "search", "on-request"]) {
|
|
170
|
+
const allow = projects.find((p) => p.robots.purposes?.[purpose] === "allow");
|
|
171
|
+
const disallow = projects.find((p) => p.robots.purposes?.[purpose] === "disallow");
|
|
172
|
+
if (allow === undefined || disallow === undefined)
|
|
173
|
+
continue;
|
|
174
|
+
problems.push(`"${disallow.name}" has [crawlers] ${purpose} = "disallow" and "${allow.name}" has ${purpose} = "allow". The site has one robots.txt, so its projects must agree: make them the same, or remove one.`);
|
|
175
|
+
}
|
|
176
|
+
return problems;
|
|
177
|
+
}
|
|
149
178
|
/** Where the sitemap is, from the host's root: at Astro's `base`. */
|
|
150
179
|
export function sitemapRoute(base) {
|
|
151
180
|
return `${normalizeBase(base)}sitemap.xml`;
|
package/dist/records.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"records.js","sourceRoot":"","sources":["../src/records.ts"],"names":[],"mappings":"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","sourcesContent":["// The page records: `_ascribe/pages.json` in a build's site output\n// (output-layout contract §1.3), what each page tells every reader that isn't\n// a person. The integration writes from them a page's head (`Head.astro`),\n// the site's one sitemap, and its one `robots.txt`, from every project's\n// records together, at the host's root.\nimport { readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The records' format version this reads. */\nconst VERSION = 1;\n\n/** How a page is treated: listed, listed and not to be quoted, or not listed. */\nexport type Listing = \"listed\" | \"unquoted\" | \"unlisted\";\n\n/** One page's record, with the route it's keyed by. */\nexport interface PageRecord {\n /** The page's route, from the host's root. */\n route: string;\n /** The page's path from the content root, with `/`. */\n source: string;\n /** The page's title, as plain text. */\n title?: string;\n /** The page's description, as plain text on one line. */\n description?: string;\n /** The page's own address, absolute, when the project sets `[consumer] site`. */\n canonical?: string;\n listing: Listing;\n /** The address of the page's Markdown version, from the host's root, when the build publishes one. */\n markdown?: string;\n}\n\n/** `_ascribe/pages.json`. */\nexport interface PageRecords {\n version: number;\n site?: string;\n pages: Record<string, Omit<PageRecord, \"route\">>;\n robots: { lines: string[] };\n}\n\nfunction recordsFile(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"pages.json\");\n}\n\n/** The records in a build's site output, or `undefined` when there aren't any. */\nexport function readRecords(siteRoot: string): PageRecords | undefined {\n const file = recordsFile(siteRoot);\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return undefined;\n }\n const records = JSON.parse(text) as PageRecords;\n if (records.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(records.version)} of the page records, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return records;\n}\n\n/** A build's records by source path, as last read, and when its file was written. */\nconst bySource = new Map<string, { written: number; pages: Map<string, PageRecord> }>();\n\n/**\n * The record of the page whose source path is `source`, in the build whose\n * site output is at `siteRoot`, or `undefined` when it has none. Read again\n * when the file changes, as it does with each build in `astro dev`.\n */\nexport function recordFor(siteRoot: string, source: string): PageRecord | undefined {\n let written: number;\n try {\n written = statSync(recordsFile(siteRoot)).mtimeMs;\n } catch {\n return undefined;\n }\n let known = bySource.get(siteRoot);\n if (known === undefined || known.written !== written) {\n const pages = new Map<string, PageRecord>();\n for (const record of pageList(readRecords(siteRoot))) {\n if (!pages.has(record.source)) pages.set(record.source, record);\n }\n known = { written, pages };\n bySource.set(siteRoot, known);\n }\n return known.pages.get(source);\n}\n\n/** The records' pages, each with its route, in the file's order. */\nexport function pageList(records: PageRecords | undefined): PageRecord[] {\n return Object.entries(records?.pages ?? {}).map(([route, page]) => ({ route, ...page }));\n}\n\n/**\n * A route as the sitemap compares it: with a leading `/` and no trailing one,\n * except at the root, so `/docs/` and `/docs` are one page.\n */\nexport function routeKey(route: string): string {\n const withSlash = route.startsWith(\"/\") ? route : `/${route}`;\n return withSlash.length > 1 && withSlash.endsWith(\"/\") ? withSlash.slice(0, -1) : withSlash;\n}\n\n/** A route ending the way Astro's `trailingSlash` says, as a built page's address. */\nexport function routeAs(route: string, trailingSlash: \"always\" | \"never\" | \"ignore\"): string {\n if (trailingSlash === \"never\") return routeKey(route);\n if (trailingSlash === \"always\" && !route.endsWith(\"/\")) return `${route}/`;\n return route;\n}\n\n/** What the sitemap is drawn from. */\nexport interface SitemapSources {\n /** Every project's records. */\n projects: readonly PageRecord[][];\n /**\n * Whether a project's page has a route in the site: was built, or, in\n * `astro dev`, matches one of the site's routes.\n */\n routed: (route: string) => boolean;\n /** The site's own pages, each by its route from the host's root. */\n sitePages: readonly string[];\n /** Routes from the host's root to leave out: forwarding pages, the not-found page, and the site's list. */\n excluded: readonly string[];\n /** Astro's `site`. */\n site: string;\n}\n\n/**\n * The sitemap's addresses, absolute, sorted, each once: every listed page of\n * every project that has a route in the site, and every page of the site's\n * own, less the excluded routes. A project's page that isn't listed is left\n * out, and so is a page of the site's own at the same route as one.\n */\nexport function sitemapAddresses(sources: SitemapSources): string[] {\n const excluded = new Set(sources.excluded.map(routeKey));\n const records = new Set<string>();\n const routes = new Map<string, string>();\n for (const pages of sources.projects) {\n for (const page of pages) {\n const key = routeKey(page.route);\n records.add(key);\n if (page.listing === \"unlisted\" || excluded.has(key) || !sources.routed(page.route)) continue;\n if (!routes.has(key)) routes.set(key, page.route);\n }\n }\n for (const route of sources.sitePages) {\n const key = routeKey(route);\n if (records.has(key) || excluded.has(key) || routes.has(key)) continue;\n routes.set(key, route);\n }\n return [\n ...new Set([...routes.values()].map((route) => new URL(route, sources.site).href)),\n ].sort();\n}\n\nfunction escapeXml(text: string): string {\n return text\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\n/** A sitemap of `addresses`, with no dates. */\nexport function sitemapXml(addresses: readonly string[]): string {\n const urls = addresses.map((address) => ` <url><loc>${escapeXml(address)}</loc></url>\\n`);\n return (\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n' +\n urls.join(\"\") +\n \"</urlset>\\n\"\n );\n}\n\nconst USER_AGENT = /^\\s*user-agent\\s*:/i;\n\n/**\n * A site's `robots.txt` from each project's lines: a `User-agent: *` group\n * with every project's lines before its first `User-agent` line, which are\n * for every crawler, then the groups each project wrote whole, then the\n * sitemap's address. `undefined` when it would say nothing.\n */\nexport function robotsText(\n projects: readonly (readonly string[])[],\n sitemap: string | undefined,\n): string | undefined {\n const everyone: string[] = [];\n const groups: string[][] = [];\n for (const lines of projects) {\n const first = lines.findIndex((line) => USER_AGENT.test(line));\n everyone.push(...(first === -1 ? lines : lines.slice(0, first)));\n if (first !== -1) groups.push(lines.slice(first) as string[]);\n }\n const blocks: string[][] = [];\n if (everyone.length > 0) blocks.push([\"User-agent: *\", ...everyone]);\n blocks.push(...groups);\n if (sitemap !== undefined) blocks.push([`Sitemap: ${sitemap}`]);\n if (blocks.length === 0) return undefined;\n return `${blocks.map((block) => block.join(\"\\n\")).join(\"\\n\\n\")}\\n`;\n}\n\n/** Where the sitemap is, from the host's root: at Astro's `base`. */\nexport function sitemapRoute(base: string): string {\n return `${normalizeBase(base)}sitemap.xml`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"records.js","sourceRoot":"","sources":["../src/records.ts"],"names":[],"mappings":"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","sourcesContent":["// The page records: `_ascribe/pages.json` in a build's site output\n// (output-layout contract §1.3), what each page tells every reader that isn't\n// a person. The integration writes from them a page's head (`Head.astro`),\n// the site's one sitemap, and its one `robots.txt`, from every project's\n// records together, at the host's root.\nimport { readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The records' format version this reads. */\nconst VERSION = 1;\n\n/** How a page is treated: listed, listed and not to be quoted, or not listed. */\nexport type Listing = \"listed\" | \"unquoted\" | \"unlisted\";\n\n/**\n * Where a page's listing comes from: `default` when nothing sets it, `page`\n * for the page's own, and for a rule of `[listing]` its level (`site`,\n * `folder`, or `type`), its folder or type, the rule as written, and its line\n * in `ascribe.toml`.\n */\nexport interface ListingFrom {\n level: \"default\" | \"page\" | \"site\" | \"folder\" | \"type\";\n folder?: string;\n type?: string;\n rule?: string;\n line?: number;\n}\n\n/** One page's record, with the route it's keyed by. */\nexport interface PageRecord {\n /** The page's route, from the host's root. */\n route: string;\n /** The page's path from the content root, with `/`. */\n source: string;\n /** The page's title, as plain text. */\n title?: string;\n /** The page's description, as plain text on one line. */\n description?: string;\n /** The page's own address, absolute, when the project sets `[consumer] site`. */\n canonical?: string;\n listing: Listing;\n /** Where `listing` comes from: the page, a rule of `[listing]`, or neither. */\n listing_from: ListingFrom;\n /** The address of the page's Markdown version, from the host's root, when the build publishes one. */\n markdown?: string;\n /**\n * The page's place in the site, from the top down to the page itself: the\n * page at each of its route's folders that has one, then the page. Absent\n * in records an earlier Ascribe wrote.\n */\n breadcrumbs?: Step[];\n}\n\n/** One step of a page's breadcrumbs: a page's route, from the host's root, and its title. */\nexport interface Step {\n route: string;\n title: string;\n}\n\n/** What a kind of crawler is for: gathering training data, building an AI search index, or fetching at a person's request. */\ntype Purpose = \"training\" | \"search\" | \"on-request\";\n\n/** The group of `robots.txt` a disallowed purpose writes. */\nexport interface RobotsGroup {\n purpose: Purpose;\n /** Every token of the purpose the binary knows, each a `User-agent` line. */\n user_agents: string[];\n /** The group's rules: `Disallow: /`. */\n lines: string[];\n}\n\n/** What a project says to crawlers. */\nexport interface Robots {\n /** The project's own lines, with paths from the host's root. */\n lines: string[];\n /** What the project says of each purpose it sets. */\n purposes?: Partial<Record<Purpose, \"allow\" | \"disallow\">>;\n /** A group for each purpose the project disallows. */\n groups?: RobotsGroup[];\n}\n\n/** `_ascribe/pages.json`. */\nexport interface PageRecords {\n version: number;\n site?: string;\n pages: Record<string, Omit<PageRecord, \"route\">>;\n robots: Robots;\n}\n\n/** What one project says to crawlers, by the name of its first entry. */\nexport interface ProjectRobots {\n name: string;\n robots: Robots;\n}\n\nfunction recordsFile(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"pages.json\");\n}\n\n/** The records in a build's site output, or `undefined` when there aren't any. */\nexport function readRecords(siteRoot: string): PageRecords | undefined {\n const file = recordsFile(siteRoot);\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return undefined;\n }\n const records = JSON.parse(text) as PageRecords;\n if (records.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(records.version)} of the page records, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return records;\n}\n\n/** A build's records by source path, as last read, and when its file was written. */\nconst bySource = new Map<string, { written: number; pages: Map<string, PageRecord> }>();\n\n/**\n * The record of the page whose source path is `source`, in the build whose\n * site output is at `siteRoot`, or `undefined` when it has none. Read again\n * when the file changes, as it does with each build in `astro dev`.\n */\nexport function recordFor(siteRoot: string, source: string): PageRecord | undefined {\n let written: number;\n try {\n written = statSync(recordsFile(siteRoot)).mtimeMs;\n } catch {\n return undefined;\n }\n let known = bySource.get(siteRoot);\n if (known === undefined || known.written !== written) {\n const pages = new Map<string, PageRecord>();\n for (const record of pageList(readRecords(siteRoot))) {\n if (!pages.has(record.source)) pages.set(record.source, record);\n }\n known = { written, pages };\n bySource.set(siteRoot, known);\n }\n return known.pages.get(source);\n}\n\n/** The records' pages, each with its route, in the file's order. */\nexport function pageList(records: PageRecords | undefined): PageRecord[] {\n return Object.entries(records?.pages ?? {}).map(([route, page]) => ({ route, ...page }));\n}\n\n/**\n * A route as the sitemap compares it: with a leading `/` and no trailing one,\n * except at the root, so `/docs/` and `/docs` are one page.\n */\nexport function routeKey(route: string): string {\n const withSlash = route.startsWith(\"/\") ? route : `/${route}`;\n return withSlash.length > 1 && withSlash.endsWith(\"/\") ? withSlash.slice(0, -1) : withSlash;\n}\n\n/** A route ending the way Astro's `trailingSlash` says, as a built page's address. */\nexport function routeAs(route: string, trailingSlash: \"always\" | \"never\" | \"ignore\"): string {\n if (trailingSlash === \"never\") return routeKey(route);\n if (trailingSlash === \"always\" && !route.endsWith(\"/\")) return `${route}/`;\n return route;\n}\n\n/** What the sitemap is drawn from. */\nexport interface SitemapSources {\n /** Every project's records. */\n projects: readonly PageRecord[][];\n /**\n * Whether a project's page has a route in the site: was built, or, in\n * `astro dev`, matches one of the site's routes.\n */\n routed: (route: string) => boolean;\n /** The site's own pages, each by its route from the host's root. */\n sitePages: readonly string[];\n /** Routes from the host's root to leave out: forwarding pages, the not-found page, and the site's list. */\n excluded: readonly string[];\n /** Astro's `site`. */\n site: string;\n}\n\n/**\n * The sitemap's addresses, absolute, sorted, each once: every listed page of\n * every project that has a route in the site, and every page of the site's\n * own, less the excluded routes. A project's page that isn't listed is left\n * out, and so is a page of the site's own at the same route as one.\n */\nexport function sitemapAddresses(sources: SitemapSources): string[] {\n const excluded = new Set(sources.excluded.map(routeKey));\n const records = new Set<string>();\n const routes = new Map<string, string>();\n for (const pages of sources.projects) {\n for (const page of pages) {\n const key = routeKey(page.route);\n records.add(key);\n if (page.listing === \"unlisted\" || excluded.has(key) || !sources.routed(page.route)) continue;\n if (!routes.has(key)) routes.set(key, page.route);\n }\n }\n for (const route of sources.sitePages) {\n const key = routeKey(route);\n if (records.has(key) || excluded.has(key) || routes.has(key)) continue;\n routes.set(key, route);\n }\n return [\n ...new Set([...routes.values()].map((route) => new URL(route, sources.site).href)),\n ].sort();\n}\n\nfunction escapeXml(text: string): string {\n return text\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\n/** A sitemap of `addresses`, with no dates. */\nexport function sitemapXml(addresses: readonly string[]): string {\n const urls = addresses.map((address) => ` <url><loc>${escapeXml(address)}</loc></url>\\n`);\n return (\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n' +\n urls.join(\"\") +\n \"</urlset>\\n\"\n );\n}\n\nconst USER_AGENT = /^\\s*user-agent\\s*:/i;\n\n/**\n * A site's `robots.txt` from what each project says to crawlers: a group for\n * each purpose a project disallows, once for the site, then a\n * `User-agent: *` group with every project's lines before its first\n * `User-agent` line, which are for every crawler, then the groups each\n * project wrote whole, then the sitemap's address. `undefined` when it would\n * say nothing.\n */\nexport function robotsText(\n projects: readonly Robots[],\n sitemap: string | undefined,\n): string | undefined {\n const purposes = new Map<Purpose, string[]>();\n const everyone: string[] = [];\n const groups: string[][] = [];\n for (const robots of projects) {\n for (const group of robots.groups ?? []) {\n if (purposes.has(group.purpose)) continue;\n purposes.set(group.purpose, [\n ...group.user_agents.map((agent) => `User-agent: ${agent}`),\n ...group.lines,\n ]);\n }\n const lines = robots.lines;\n const first = lines.findIndex((line) => USER_AGENT.test(line));\n everyone.push(...(first === -1 ? lines : lines.slice(0, first)));\n if (first !== -1) groups.push(lines.slice(first));\n }\n const blocks: string[][] = [...purposes.values()];\n if (everyone.length > 0) blocks.push([\"User-agent: *\", ...everyone]);\n blocks.push(...groups);\n if (sitemap !== undefined) blocks.push([`Sitemap: ${sitemap}`]);\n if (blocks.length === 0) return undefined;\n return `${blocks.map((block) => block.join(\"\\n\")).join(\"\\n\\n\")}\\n`;\n}\n\n/**\n * Two projects that say contradictory things about one purpose, as\n * sentences: one allows it and the other disallows it. A site has one\n * `robots.txt`, so they must agree. A project that says nothing of a purpose\n * agrees with either.\n */\nexport function purposeConflicts(projects: readonly ProjectRobots[]): string[] {\n const problems: string[] = [];\n for (const purpose of [\"training\", \"search\", \"on-request\"] as const) {\n const allow = projects.find((p) => p.robots.purposes?.[purpose] === \"allow\");\n const disallow = projects.find((p) => p.robots.purposes?.[purpose] === \"disallow\");\n if (allow === undefined || disallow === undefined) continue;\n problems.push(\n `\"${disallow.name}\" has [crawlers] ${purpose} = \"disallow\" and \"${allow.name}\" has ${purpose} = \"allow\". The site has one robots.txt, so its projects must agree: make them the same, or remove one.`,\n );\n }\n return problems;\n}\n\n/** Where the sitemap is, from the host's root: at Astro's `base`. */\nexport function sitemapRoute(base: string): string {\n return `${normalizeBase(base)}sitemap.xml`;\n}\n"]}
|
package/dist/root-files.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import { type SitemapSources } from "./records.js";
|
|
2
|
+
import { type ProjectRobots, type SitemapSources } from "./records.js";
|
|
3
3
|
/** What the files are drawn from. */
|
|
4
4
|
export interface RootFileSources extends Omit<SitemapSources, "site"> {
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/** What each project says to crawlers, each project once. */
|
|
6
|
+
robots: readonly ProjectRobots[];
|
|
7
7
|
/** Astro's `site`, when it's set. */
|
|
8
8
|
site: string | undefined;
|
|
9
9
|
/** Astro's `base`. */
|
|
@@ -18,11 +18,16 @@ interface RootFileNote {
|
|
|
18
18
|
level: "info" | "warn";
|
|
19
19
|
text: string;
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* The files, each `undefined` when none is written, what to say about them,
|
|
23
|
+
* and the problems that stop `robots.txt` from being written, which fail a
|
|
24
|
+
* build.
|
|
25
|
+
*/
|
|
22
26
|
export interface RootFiles {
|
|
23
27
|
sitemap: string | undefined;
|
|
24
28
|
robots: string | undefined;
|
|
25
29
|
notes: RootFileNote[];
|
|
30
|
+
problems: string[];
|
|
26
31
|
}
|
|
27
32
|
/** The sitemap and `robots.txt` for a site, and what the build says about them. */
|
|
28
33
|
export declare function rootFiles(sources: RootFileSources): RootFiles;
|
package/dist/root-files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { normalizeBase } from "./project.js";
|
|
2
|
-
import { robotsText, routeAs, routeKey, sitemapAddresses, sitemapRoute, sitemapXml, } from "./records.js";
|
|
2
|
+
import { purposeConflicts, robotsText, routeAs, routeKey, sitemapAddresses, sitemapRoute, sitemapXml, } from "./records.js";
|
|
3
3
|
/** The sitemap and `robots.txt` for a site, and what the build says about them. */
|
|
4
4
|
export function rootFiles(sources) {
|
|
5
5
|
const notes = [];
|
|
@@ -22,7 +22,10 @@ export function rootFiles(sources) {
|
|
|
22
22
|
sitemap = sitemapXml(sitemapAddresses({ ...sources, site: sources.site }));
|
|
23
23
|
sitemapAddress = new URL(sitemapRoute(base), sources.site).href;
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
const problems = purposeConflicts(sources.robots);
|
|
26
|
+
let robots = problems.length > 0
|
|
27
|
+
? undefined
|
|
28
|
+
: robotsText(sources.robots.map((project) => project.robots), sitemapAddress);
|
|
26
29
|
if (robots !== undefined && base !== "/") {
|
|
27
30
|
notes.push({
|
|
28
31
|
level: "info",
|
|
@@ -37,7 +40,7 @@ export function rootFiles(sources) {
|
|
|
37
40
|
});
|
|
38
41
|
robots = undefined;
|
|
39
42
|
}
|
|
40
|
-
return { sitemap, robots, notes };
|
|
43
|
+
return { sitemap, robots, notes, problems };
|
|
41
44
|
}
|
|
42
45
|
/**
|
|
43
46
|
* A dev-server middleware serving the sitemap at `<base>sitemap.xml` and
|
package/dist/root-files.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-files.js","sourceRoot":"","sources":["../src/root-files.ts"],"names":[],"mappings":"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","sourcesContent":["// The site's sitemap and `robots.txt`, from every project's page records\n// together: one of each for the site, at the host's root, which is above the\n// base path of every project that isn't mounted there. `astro build` writes\n// them into its output, and `astro dev` serves them.\n//\n// A site whose Astro `base` isn't `/` has no host root: its sitemap goes\n// under the base, and no `robots.txt` is written, since one under a path does\n// nothing. The build says what the host's own needs instead.\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { normalizeBase } from \"./project.js\";\nimport {\n robotsText,\n routeAs,\n routeKey,\n sitemapAddresses,\n sitemapRoute,\n sitemapXml,\n type SitemapSources,\n} from \"./records.js\";\n\n/** What the files are drawn from. */\nexport interface RootFileSources extends Omit<SitemapSources, \"site\"> {\n /** Each project's `robots.txt` lines, from the host's root, each project once. */\n lines: readonly (readonly string[])[];\n /** Astro's `site`, when it's set. */\n site: string | undefined;\n /** Astro's `base`. */\n base: string;\n /** Whether the site has its own `robots.txt`, in `public/`. */\n ownRobots: boolean;\n /** Whether the site has its own `sitemap.xml`, in `public/`. */\n ownSitemap: boolean;\n}\n\n/** A thing the build says about the files, and how loudly. */\ninterface RootFileNote {\n level: \"info\" | \"warn\";\n text: string;\n}\n\n/** The files, each `undefined` when none is written, and what to say about them. */\nexport interface RootFiles {\n sitemap: string | undefined;\n robots: string | undefined;\n notes: RootFileNote[];\n}\n\n/** The sitemap and `robots.txt` for a site, and what the build says about them. */\nexport function rootFiles(sources: RootFileSources): RootFiles {\n const notes: RootFileNote[] = [];\n const base = normalizeBase(sources.base);\n let sitemap: string | undefined;\n let sitemapAddress: string | undefined;\n if (sources.ownSitemap) {\n // The site's own, at the same address, which robots.txt points to when there's an address.\n notes.push({ level: \"info\", text: \"no sitemap.xml: public/sitemap.xml is the site's own.\" });\n if (sources.site !== undefined) sitemapAddress = new URL(sitemapRoute(base), sources.site).href;\n } else if (sources.site === undefined) {\n notes.push({\n level: \"warn\",\n text: \"no sitemap.xml: a sitemap lists pages by their absolute addresses, and the site has no address. Set [consumer] site in ascribe.toml, or Astro's `site`.\",\n });\n } else {\n sitemap = sitemapXml(sitemapAddresses({ ...sources, site: sources.site }));\n sitemapAddress = new URL(sitemapRoute(base), sources.site).href;\n }\n let robots = robotsText(sources.lines, sitemapAddress);\n if (robots !== undefined && base !== \"/\") {\n notes.push({\n level: \"info\",\n text: `no robots.txt: Astro's \\`base\\` is \"${base}\", and a crawler reads robots.txt only at the host's root. Put these lines in the host's own:\\n${robots}`,\n });\n robots = undefined;\n } else if (robots !== undefined && sources.ownRobots) {\n notes.push({\n level: \"info\",\n text: `no robots.txt: public/robots.txt is the site's own. Ascribe's lines for it:\\n${robots}`,\n });\n robots = undefined;\n }\n return { sitemap, robots, notes };\n}\n\ntype Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;\n\n/**\n * A dev-server middleware serving the sitemap at `<base>sitemap.xml` and\n * `robots.txt` at `/`, each as `files` has it when it's asked for, so each\n * rebuild's records are served.\n */\nexport function rootFilesMiddleware(base: string, files: () => RootFiles): Middleware {\n const sitemapPath = sitemapRoute(base);\n return (req, res, next) => {\n if (req.method !== \"GET\" && req.method !== \"HEAD\") return next();\n // Vite strips the base from `req.url`; connect keeps the original in `originalUrl`.\n const pathname =\n ((req as { originalUrl?: string }).originalUrl ?? req.url ?? \"\").split(/[?#]/, 1)[0] ?? \"\";\n if (pathname !== sitemapPath && pathname !== \"/robots.txt\") return next();\n const current = files();\n const [text, type] =\n pathname === sitemapPath\n ? [current.sitemap, \"application/xml; charset=utf-8\"]\n : [current.robots, \"text/plain; charset=utf-8\"];\n if (text === undefined) return next();\n res.setHeader(\"Content-Type\", type);\n res.setHeader(\"Content-Length\", Buffer.byteLength(text));\n res.end(req.method === \"HEAD\" ? undefined : text);\n };\n}\n\n/** A route as `astro:routes:resolved` gives it, as much as these read. */\nexport interface AstroRoute {\n pattern: string;\n patternRegex: RegExp;\n params: string[];\n type: string;\n origin: string;\n}\n\n/** Astro's routing, as much as these read. */\ninterface Routing {\n base: string;\n trailingSlash: \"always\" | \"never\" | \"ignore\";\n}\n\n/**\n * The site's own pages `astro dev` can name: each route of the site's own\n * with no parameters, other than the not-found and error pages, by its route\n * from the host's root. A page a route's `getStaticPaths` lists is known only\n * to a build.\n */\nexport function devSitePages(routes: readonly AstroRoute[], astro: Routing): string[] {\n const root = normalizeBase(astro.base);\n return routes\n .filter(\n (route) =>\n route.type === \"page\" &&\n route.origin === \"project\" &&\n route.params.length === 0 &&\n route.pattern !== \"/404\" &&\n route.pattern !== \"/500\",\n )\n .map((route) => routeAs(`${root}${route.pattern.replace(/^\\//, \"\")}`, astro.trailingSlash));\n}\n\n/**\n * Whether a project's page, by its route from the host's root, has a route\n * in the site in `astro dev`: whether one of the site's own routes matches\n * it. A project no route shows, such as one whose collection a page only\n * quotes from, has no pages in the sitemap.\n */\nexport function devRouted(\n routes: readonly AstroRoute[],\n astro: Routing,\n): (route: string) => boolean {\n const root = normalizeBase(astro.base);\n const pages = routes.filter((route) => route.type === \"page\" && route.origin === \"project\");\n return (route) => {\n if (!`${route}/`.startsWith(root)) return false;\n const under = `/${route.slice(root.length)}`;\n const forms = new Set([under, routeKey(under), `${routeKey(under)}/`]);\n return pages.some((page) => [...forms].some((form) => page.patternRegex.test(form)));\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"root-files.js","sourceRoot":"","sources":["../src/root-files.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,OAAO,EACP,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,UAAU,GAGX,MAAM,cAAc,CAAC;AAkCtB,mFAAmF;AACnF,MAAM,UAAU,SAAS,CAAC,OAAwB;IAChD,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,OAA2B,CAAC;IAChC,IAAI,cAAkC,CAAC;IACvC,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACvB,2FAA2F;QAC3F,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,uDAAuD,EAAE,CAAC,CAAC;QAC7F,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS;YAAE,cAAc,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;IAClG,CAAC;SAAM,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC;YACT,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,yJAAyJ;SAChK,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,UAAU,CAAC,gBAAgB,CAAC,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC3E,cAAc,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;IAClE,CAAC;IACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,IAAI,MAAM,GACR,QAAQ,CAAC,MAAM,GAAG,CAAC;QACjB,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,UAAU,CACR,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAC/C,cAAc,CACf,CAAC;IACR,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;QACzC,KAAK,CAAC,IAAI,CAAC;YACT,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,uCAAuC,IAAI,kGAAkG,MAAM,EAAE;SAC5J,CAAC,CAAC;QACH,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;SAAM,IAAI,MAAM,KAAK,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACrD,KAAK,CAAC,IAAI,CAAC;YACT,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,gFAAgF,MAAM,EAAE;SAC/F,CAAC,CAAC;QACH,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;AAC9C,CAAC;AAID;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,KAAsB;IACtE,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;YAAE,OAAO,IAAI,EAAE,CAAC;QACjE,oFAAoF;QACpF,MAAM,QAAQ,GACZ,CAAE,GAAgC,CAAC,WAAW,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7F,IAAI,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,aAAa;YAAE,OAAO,IAAI,EAAE,CAAC;QAC1E,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;QACxB,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAChB,QAAQ,KAAK,WAAW;YACtB,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,gCAAgC,CAAC;YACrD,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;QACpD,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,EAAE,CAAC;QACtC,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;QACpC,GAAG,CAAC,SAAS,CAAC,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QACzD,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC,CAAC;AACJ,CAAC;AAiBD;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAA6B,EAAE,KAAc;IACxE,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,MAAM;SACV,MAAM,CACL,CAAC,KAAK,EAAE,EAAE,CACR,KAAK,CAAC,IAAI,KAAK,MAAM;QACrB,KAAK,CAAC,MAAM,KAAK,SAAS;QAC1B,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;QACzB,KAAK,CAAC,OAAO,KAAK,MAAM;QACxB,KAAK,CAAC,OAAO,KAAK,MAAM,CAC3B;SACA,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC;AAChG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CACvB,MAA6B,EAC7B,KAAc;IAEd,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC;IAC5F,OAAO,CAAC,KAAK,EAAE,EAAE;QACf,IAAI,CAAC,GAAG,KAAK,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QAChD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QACvE,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvF,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// The site's sitemap and `robots.txt`, from every project's page records\n// together: one of each for the site, at the host's root, which is above the\n// base path of every project that isn't mounted there. `astro build` writes\n// them into its output, and `astro dev` serves them.\n//\n// A site whose Astro `base` isn't `/` has no host root: its sitemap goes\n// under the base, and no `robots.txt` is written, since one under a path does\n// nothing. The build says what the host's own needs instead.\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { normalizeBase } from \"./project.js\";\nimport {\n purposeConflicts,\n robotsText,\n routeAs,\n routeKey,\n sitemapAddresses,\n sitemapRoute,\n sitemapXml,\n type ProjectRobots,\n type SitemapSources,\n} from \"./records.js\";\n\n/** What the files are drawn from. */\nexport interface RootFileSources extends Omit<SitemapSources, \"site\"> {\n /** What each project says to crawlers, each project once. */\n robots: readonly ProjectRobots[];\n /** Astro's `site`, when it's set. */\n site: string | undefined;\n /** Astro's `base`. */\n base: string;\n /** Whether the site has its own `robots.txt`, in `public/`. */\n ownRobots: boolean;\n /** Whether the site has its own `sitemap.xml`, in `public/`. */\n ownSitemap: boolean;\n}\n\n/** A thing the build says about the files, and how loudly. */\ninterface RootFileNote {\n level: \"info\" | \"warn\";\n text: string;\n}\n\n/**\n * The files, each `undefined` when none is written, what to say about them,\n * and the problems that stop `robots.txt` from being written, which fail a\n * build.\n */\nexport interface RootFiles {\n sitemap: string | undefined;\n robots: string | undefined;\n notes: RootFileNote[];\n problems: string[];\n}\n\n/** The sitemap and `robots.txt` for a site, and what the build says about them. */\nexport function rootFiles(sources: RootFileSources): RootFiles {\n const notes: RootFileNote[] = [];\n const base = normalizeBase(sources.base);\n let sitemap: string | undefined;\n let sitemapAddress: string | undefined;\n if (sources.ownSitemap) {\n // The site's own, at the same address, which robots.txt points to when there's an address.\n notes.push({ level: \"info\", text: \"no sitemap.xml: public/sitemap.xml is the site's own.\" });\n if (sources.site !== undefined) sitemapAddress = new URL(sitemapRoute(base), sources.site).href;\n } else if (sources.site === undefined) {\n notes.push({\n level: \"warn\",\n text: \"no sitemap.xml: a sitemap lists pages by their absolute addresses, and the site has no address. Set [consumer] site in ascribe.toml, or Astro's `site`.\",\n });\n } else {\n sitemap = sitemapXml(sitemapAddresses({ ...sources, site: sources.site }));\n sitemapAddress = new URL(sitemapRoute(base), sources.site).href;\n }\n const problems = purposeConflicts(sources.robots);\n let robots =\n problems.length > 0\n ? undefined\n : robotsText(\n sources.robots.map((project) => project.robots),\n sitemapAddress,\n );\n if (robots !== undefined && base !== \"/\") {\n notes.push({\n level: \"info\",\n text: `no robots.txt: Astro's \\`base\\` is \"${base}\", and a crawler reads robots.txt only at the host's root. Put these lines in the host's own:\\n${robots}`,\n });\n robots = undefined;\n } else if (robots !== undefined && sources.ownRobots) {\n notes.push({\n level: \"info\",\n text: `no robots.txt: public/robots.txt is the site's own. Ascribe's lines for it:\\n${robots}`,\n });\n robots = undefined;\n }\n return { sitemap, robots, notes, problems };\n}\n\ntype Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;\n\n/**\n * A dev-server middleware serving the sitemap at `<base>sitemap.xml` and\n * `robots.txt` at `/`, each as `files` has it when it's asked for, so each\n * rebuild's records are served.\n */\nexport function rootFilesMiddleware(base: string, files: () => RootFiles): Middleware {\n const sitemapPath = sitemapRoute(base);\n return (req, res, next) => {\n if (req.method !== \"GET\" && req.method !== \"HEAD\") return next();\n // Vite strips the base from `req.url`; connect keeps the original in `originalUrl`.\n const pathname =\n ((req as { originalUrl?: string }).originalUrl ?? req.url ?? \"\").split(/[?#]/, 1)[0] ?? \"\";\n if (pathname !== sitemapPath && pathname !== \"/robots.txt\") return next();\n const current = files();\n const [text, type] =\n pathname === sitemapPath\n ? [current.sitemap, \"application/xml; charset=utf-8\"]\n : [current.robots, \"text/plain; charset=utf-8\"];\n if (text === undefined) return next();\n res.setHeader(\"Content-Type\", type);\n res.setHeader(\"Content-Length\", Buffer.byteLength(text));\n res.end(req.method === \"HEAD\" ? undefined : text);\n };\n}\n\n/** A route as `astro:routes:resolved` gives it, as much as these read. */\nexport interface AstroRoute {\n pattern: string;\n patternRegex: RegExp;\n params: string[];\n type: string;\n origin: string;\n}\n\n/** Astro's routing, as much as these read. */\ninterface Routing {\n base: string;\n trailingSlash: \"always\" | \"never\" | \"ignore\";\n}\n\n/**\n * The site's own pages `astro dev` can name: each route of the site's own\n * with no parameters, other than the not-found and error pages, by its route\n * from the host's root. A page a route's `getStaticPaths` lists is known only\n * to a build.\n */\nexport function devSitePages(routes: readonly AstroRoute[], astro: Routing): string[] {\n const root = normalizeBase(astro.base);\n return routes\n .filter(\n (route) =>\n route.type === \"page\" &&\n route.origin === \"project\" &&\n route.params.length === 0 &&\n route.pattern !== \"/404\" &&\n route.pattern !== \"/500\",\n )\n .map((route) => routeAs(`${root}${route.pattern.replace(/^\\//, \"\")}`, astro.trailingSlash));\n}\n\n/**\n * Whether a project's page, by its route from the host's root, has a route\n * in the site in `astro dev`: whether one of the site's own routes matches\n * it. A project no route shows, such as one whose collection a page only\n * quotes from, has no pages in the sitemap.\n */\nexport function devRouted(\n routes: readonly AstroRoute[],\n astro: Routing,\n): (route: string) => boolean {\n const root = normalizeBase(astro.base);\n const pages = routes.filter((route) => route.type === \"page\" && route.origin === \"project\");\n return (route) => {\n if (!`${route}/`.startsWith(root)) return false;\n const under = `/${route.slice(root.length)}`;\n const forms = new Set([under, routeKey(under), `${routeKey(under)}/`]);\n return pages.some((page) => [...forms].some((form) => page.patternRegex.test(form)));\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ascribed/astro",
|
|
3
|
-
"version": "0.2.1-next.
|
|
3
|
+
"version": "0.2.1-next.17",
|
|
4
4
|
"description": "The Astro integration for Ascribe.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"node": ">=24"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@ascribed/cli": "0.2.1-next.
|
|
58
|
-
"@ascribed/elements": "0.2.1-next.
|
|
59
|
-
"@ascribed/review": "0.2.1-next.
|
|
57
|
+
"@ascribed/cli": "0.2.1-next.17",
|
|
58
|
+
"@ascribed/elements": "0.2.1-next.17",
|
|
59
|
+
"@ascribed/review": "0.2.1-next.17",
|
|
60
60
|
"github-slugger": "^2.0.0",
|
|
61
61
|
"property-information": "^7.2.0",
|
|
62
62
|
"smol-toml": "^1.9.0"
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"publishConfig": {
|
|
93
93
|
"access": "public"
|
|
94
94
|
},
|
|
95
|
-
"gitHead": "
|
|
95
|
+
"gitHead": "605791982664a254a310026f05ebe69184943131",
|
|
96
96
|
"scripts": {
|
|
97
97
|
"build": "tsc -p tsconfig.build.json",
|
|
98
98
|
"typecheck": "tsc --noEmit",
|
package/src/Head.astro
CHANGED
|
@@ -1,40 +1,58 @@
|
|
|
1
1
|
---
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// when it
|
|
6
|
-
//
|
|
7
|
-
//
|
|
2
|
+
// Everything a page's head says about the page, from its record in the
|
|
3
|
+
// build's `_ascribe/pages.json`: its `<title>` and description, its canonical
|
|
4
|
+
// link, `<meta name="robots">` (`noindex` when it isn't listed, `nosnippet`
|
|
5
|
+
// when it's listed and not to be quoted), a link to its Markdown version when
|
|
6
|
+
// the build publishes one and the page is offered, the Open Graph tags a link
|
|
7
|
+
// preview reads, and its breadcrumbs as structured data. Each comes from the
|
|
8
|
+
// record, as the sitemap and `llms.txt` do, so none can disagree with them. A
|
|
9
|
+
// layout includes it in `<head>`, given the page's collection entry:
|
|
8
10
|
//
|
|
9
|
-
// <Head
|
|
11
|
+
// <Head
|
|
12
|
+
// entry={entry}
|
|
13
|
+
// title={(page, record) => (record.route === "/" ? "Loom" : `${page} · Loom`)}
|
|
14
|
+
// name="Loom"
|
|
15
|
+
// card={{ src: "/social-card.png", alt: "Loom's docs", width: 1200, height: 630 }}
|
|
16
|
+
// />
|
|
10
17
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// the site's
|
|
14
|
-
|
|
18
|
+
// What the site knows and the record doesn't is given by property: `title`,
|
|
19
|
+
// a function from the page's title and its record, which has its route, to
|
|
20
|
+
// the whole title; `name`, the site's name, for `og:site_name` and, on the
|
|
21
|
+
// page at the host's root, the site-name structured data; and `card`, the
|
|
22
|
+
// page's social card. The layout keeps the character set, the viewport,
|
|
23
|
+
// icons, theme colors, and styles. A page with no record, such as an Astro
|
|
24
|
+
// page of the site's own, gets nothing: it writes its own head.
|
|
25
|
+
import { headTags, pageRecord, scriptJson, type Card, type PageRecord } from "@ascribed/astro/content";
|
|
15
26
|
|
|
16
27
|
interface Props {
|
|
17
28
|
/** The page's collection entry, or `undefined` for a page that has none. */
|
|
18
29
|
entry?: { filePath?: string | undefined; data?: unknown } | undefined;
|
|
19
30
|
/** The whole title, from the page's title and its record. Default: the page's title. */
|
|
20
31
|
title?: (title: string, record: PageRecord) => string;
|
|
32
|
+
/** The site's name. */
|
|
33
|
+
name?: string;
|
|
34
|
+
/** The page's social card. Without one, no image tags. */
|
|
35
|
+
card?: Card;
|
|
21
36
|
}
|
|
22
37
|
|
|
23
|
-
const { entry, title } = Astro.props;
|
|
38
|
+
const { entry, title, name, card } = Astro.props;
|
|
24
39
|
const record = entry === undefined ? undefined : pageRecord(entry);
|
|
25
|
-
const
|
|
26
|
-
record
|
|
27
|
-
?
|
|
28
|
-
: title
|
|
29
|
-
? record.title
|
|
30
|
-
: title(record.title, record);
|
|
40
|
+
const tags =
|
|
41
|
+
record === undefined
|
|
42
|
+
? []
|
|
43
|
+
: headTags(record, { title, name, card, address: Astro.site ?? Astro.url });
|
|
31
44
|
---
|
|
32
45
|
|
|
33
|
-
{whole !== undefined && <title>{whole}</title>}
|
|
34
46
|
{
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
tags.map((tag) =>
|
|
48
|
+
tag.tag === "title" ? (
|
|
49
|
+
<title>{tag.text}</title>
|
|
50
|
+
) : tag.tag === "meta" ? (
|
|
51
|
+
<meta {...tag.attributes} />
|
|
52
|
+
) : tag.tag === "link" ? (
|
|
53
|
+
<link {...tag.attributes} />
|
|
54
|
+
) : (
|
|
55
|
+
<script is:inline type="application/ld+json" set:html={scriptJson(tag.json)} />
|
|
56
|
+
),
|
|
37
57
|
)
|
|
38
58
|
}
|
|
39
|
-
{record?.canonical !== undefined && <link rel="canonical" href={record.canonical} />}
|
|
40
|
-
{record?.listing === "unlisted" && <meta name="robots" content="noindex" />}
|
|
@@ -3,7 +3,7 @@ import { getCollection, type CollectionEntry } from "astro:content";
|
|
|
3
3
|
import type { ComponentProps } from "astro/types";
|
|
4
4
|
import Availability from "@ascribed/astro/Availability.astro";
|
|
5
5
|
import Elements from "@ascribed/astro/Elements.astro";
|
|
6
|
-
//
|
|
6
|
+
// Everything the head says about the page, from its record.
|
|
7
7
|
import Head from "@ascribed/astro/Head.astro";
|
|
8
8
|
import { sidebar } from "@ascribed/astro/content";
|
|
9
9
|
import { nav } from "../nav.ts";
|