@ascribed/astro 0.2.1-next.14 → 0.2.1-next.16

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 CHANGED
@@ -16,13 +16,13 @@ import { defineConfig } from "astro/config";
16
16
  import ascribe from "@ascribed/astro";
17
17
 
18
18
  export default defineConfig({
19
- site: "https://docs.example.com", // as in ascribe.toml's [consumer]
20
- base: "/docs",
21
- trailingSlash: "never",
19
+ // `site`, `base`, and `trailingSlash` come from ascribe.toml's [consumer].
22
20
  integrations: [ascribe({ build: "site" })],
23
21
  });
24
22
  ```
25
23
 
24
+ Astro's `site`, `base`, and `trailingSlash` come from `ascribe.toml`'s `[consumer]` when the Astro config leaves them out. With `projects`, `site` and `trailingSlash` come from the first entry whose project sets them, and `base` stays the site's. Set in the Astro config, each must agree with `[consumer]`, or the build fails.
25
+
26
26
  ```ts
27
27
  // src/content.config.ts
28
28
  import { defineCollection } from "astro:content";
@@ -35,7 +35,7 @@ export const collections = { docs: defineCollection(ascribeCollection({ schema }
35
35
 
36
36
  If the Ascribe project is outside the Astro root (`project: "../docs"`), import `../.astro/integrations/_ascribed_astro/schema.ts` instead: the integration copies the schema there after each build, so TypeScript finds `astro/zod` from it.
37
37
 
38
- `[consumer] base-path` can be Astro's `base` or a path under it, so a home page can sit at `/` in front of docs at `/docs/`. Several projects can share one site, each under its own base path:
38
+ `[consumer] base-path` can be Astro's `base` or a path under it, so a home page can sit at `/` in front of docs at `/docs/`: list the docs in `projects`, even alone, so Astro's `base` stays `/`. Several projects can share one site, each under its own base path:
39
39
 
40
40
  ```js
41
41
  ascribe({
@@ -52,15 +52,24 @@ ascribe({
52
52
  // A layout
53
53
  import Elements from "@ascribed/astro/Elements.astro";
54
54
  import Availability from "@ascribed/astro/Availability.astro";
55
+ import Head from "@ascribed/astro/Head.astro";
55
56
  const { entry } = Astro.props;
56
57
  ---
57
- <head><Elements /></head>
58
+ <head>
59
+ <!-- The title, description, canonical link, and robots rule, from the page's record. -->
60
+ <Head entry={entry} title={(title, record) => `${title} · Loom`} />
61
+ <Elements />
62
+ </head>
58
63
  <Availability available={entry.data.available} />
59
64
  <!-- With `inline = "code"` on `title`: its code spans as <code>. -->
60
65
  <h1 set:html={entry.data.formatted.title} />
61
66
  ```
62
67
 
63
- The [Astro guide](https://ascribed-dev.com/guides/astro/) walks through a site: `ascribe.toml`, the collection, the route, the layout, the options, [a home page in front of the docs](https://ascribed-dev.com/guides/astro/#a-home-page-in-front-of-the-docs), [several projects in one site](https://ascribed-dev.com/guides/astro/#several-projects-in-one-site), what `astro dev` does, and [review in the site preview](https://ascribed-dev.com/guides/astro/#review-in-the-site-preview). [Review](https://ascribed-dev.com/guides/review/) walks through reviewing a pull request on your site.
68
+ Each build writes a record of every page, `_ascribe/pages.json`, and the integration writes from every project's records what search engines read: `Head.astro` gives a page its `<title>`, description, canonical link, and `noindex` when it's `listing: unlisted`; and the site gets one `sitemap.xml`, at Astro's `base`, and one `robots.txt`, at the host's root, with each project's `[crawlers] lines`, in `astro build` and `astro dev`. A page with no record, such as an Astro page of the site's own, gets nothing from `Head.astro` and is in the sitemap as a page Astro built. A site with its own `public/robots.txt` or `public/sitemap.xml` keeps it, and a site whose `base` isn't `/` gets no `robots.txt`: the build prints the lines its host's needs. With no `site`, there's no sitemap.
69
+
70
+ `npx @ascribed/astro init` writes those files into an Astro project once, as yours from then on: the collection (`src/content.config.ts`), the route (`src/pages/[...slug].astro`), a layout that loads the elements (`src/layouts/Docs.astro`), and the sidebar's list (`src/nav.ts`). It never writes over a file. `--project <dir>` names the folder holding `ascribe.toml`, and `--build <name>` the build, when there are several. The layout's sidebar comes from `sidebar(nav, entries)` in `@ascribed/astro/content`, which gives each listed page its entry and address, and fails the build when the list leaves out a page or names one that isn't there.
71
+
72
+ The [Astro guide](https://ascribed-dev.com/go/astro-guide/) walks through a site: `ascribe.toml`, the collection, the route, the layout, the options, [a home page in front of the docs](https://ascribed-dev.com/go/a-home-page-in-front-of-the-docs/), [several projects in one site](https://ascribed-dev.com/go/several-projects-in-one-site/), search engines and crawlers, what `astro dev` does, and [review in the site preview](https://ascribed-dev.com/go/review-in-the-site-preview/). [Review](https://ascribed-dev.com/go/review-guide/) walks through reviewing a pull request on your site.
64
73
 
65
74
  ## Options
66
75
 
@@ -73,13 +82,16 @@ The [Astro guide](https://ascribed-dev.com/guides/astro/) walks through a site:
73
82
  | `anchors` | Source anchors (`ascribe build --anchors`), for review: `"dev"` in `astro dev` only, `true` always. By default, `false`, though `review` turns them on in `astro dev`. |
74
83
  | `review` | The **Ascribe review** app in `astro dev`'s toolbar: a pull request's changes and comments on the real page. `false` leaves it out. By default, `true`. Never in `astro build`. With `projects`, an entry's name reviews that entry, and `true` the first. |
75
84
  | `codeTitles` | A code block's title (`title="…"` in its info string, as `@snippet` writes it) above it: a Shiki transformer puts the block in a `<figure class="code-title">` with a `<figcaption>`. `false` leaves it out. By default, `true`. |
85
+ | `sitemap` | `{ exclude: [...] }`: routes of the site's own pages, from the host's root, to leave out of the sitemap, such as one that carries `noindex`. |
76
86
 
77
87
  ## Development
78
88
 
79
89
  | File | Role |
80
90
  |---|---|
81
91
  | `src/index.ts` | The integration |
82
- | `src/content.ts` | `ascribeCollection`, for `content.config.ts` |
92
+ | `src/content.ts` | `ascribeCollection`, for `content.config.ts`, and `sidebar`, for a layout |
93
+ | `src/nav.ts` | The sidebar: entry ids, routes, and the check of the site's list against the collection |
94
+ | `src/init.ts`, `src/cli.ts`, `starter/` | `npx @ascribed/astro init`, the package's command (`ascribe-astro`), and the files it writes. `examples/astro-site`'s are what it writes for that project; `test/init.test.ts` holds that, and rewrites them with `ASCRIBE_BLESS=1` |
83
95
  | `src/Elements.astro` | Loads `@ascribed/elements` (stylesheet and script) |
84
96
  | `src/Availability.astro` | A page's availability as the element library's badge |
85
97
  | `src/code-titles.ts` | The Shiki transformer that shows code block titles |
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // `npx @ascribed/astro`: the package's command, `ascribe-astro`. Its one
3
+ // subcommand, `init`, writes the starter (src/init.ts).
4
+ import { parseArgs } from "node:util";
5
+ import { init, STARTER_FILES } from "./init.js";
6
+ const USAGE = `Usage: npx @ascribed/astro init [--project <dir>] [--build <name>]
7
+
8
+ Writes the files every site built with Ascribe starts from into the Astro
9
+ project in the current folder, once: ${STARTER_FILES.join(", ")}.
10
+ It writes nothing if any of them is there.
11
+
12
+ Options:
13
+ --project <dir> The folder holding ascribe.toml, from the current folder. Default: the current folder.
14
+ --build <name> The build the site shows. Default: the project's one build.
15
+ -h, --help Print this.`;
16
+ function main(argv) {
17
+ let parsed;
18
+ try {
19
+ parsed = parseArgs({
20
+ args: argv,
21
+ allowPositionals: true,
22
+ options: {
23
+ project: { type: "string" },
24
+ build: { type: "string" },
25
+ help: { type: "boolean", short: "h" },
26
+ },
27
+ });
28
+ }
29
+ catch (error) {
30
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${USAGE}\n`);
31
+ return 2;
32
+ }
33
+ const { values, positionals } = parsed;
34
+ if (values.help === true) {
35
+ process.stdout.write(`${USAGE}\n`);
36
+ return 0;
37
+ }
38
+ if (positionals.length !== 1 || positionals[0] !== "init") {
39
+ process.stderr.write(`${USAGE}\n`);
40
+ return 2;
41
+ }
42
+ return run(values.project, values.build);
43
+ }
44
+ async function run(project, build) {
45
+ try {
46
+ const result = await init({ root: process.cwd(), project, build });
47
+ const options = [`build: ${JSON.stringify(result.build)}`];
48
+ if (project !== undefined)
49
+ options.push(`project: ${JSON.stringify(project)}`);
50
+ process.stdout.write(`${result.written.map((file) => `wrote ${file}`).join("\n")}\n\n` +
51
+ `Next, if astro.config.mjs doesn't have it, add the integration:\n` +
52
+ ` integrations: [ascribe({ ${options.join(", ")} })],\n` +
53
+ `then run \`npx astro dev\`.\n`);
54
+ return 0;
55
+ }
56
+ catch (error) {
57
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
58
+ return 2;
59
+ }
60
+ }
61
+ process.exitCode = await main(process.argv.slice(2));
62
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n// `npx @ascribed/astro`: the package's command, `ascribe-astro`. Its one\n// subcommand, `init`, writes the starter (src/init.ts).\nimport { parseArgs } from \"node:util\";\nimport { init, STARTER_FILES } from \"./init.js\";\n\nconst USAGE = `Usage: npx @ascribed/astro init [--project <dir>] [--build <name>]\n\nWrites the files every site built with Ascribe starts from into the Astro\nproject in the current folder, once: ${STARTER_FILES.join(\", \")}.\nIt writes nothing if any of them is there.\n\nOptions:\n --project <dir> The folder holding ascribe.toml, from the current folder. Default: the current folder.\n --build <name> The build the site shows. Default: the project's one build.\n -h, --help Print this.`;\n\nfunction main(argv: string[]): Promise<number> | number {\n let parsed;\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n project: { type: \"string\" },\n build: { type: \"string\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n });\n } catch (error) {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n\\n${USAGE}\\n`);\n return 2;\n }\n const { values, positionals } = parsed;\n if (values.help === true) {\n process.stdout.write(`${USAGE}\\n`);\n return 0;\n }\n if (positionals.length !== 1 || positionals[0] !== \"init\") {\n process.stderr.write(`${USAGE}\\n`);\n return 2;\n }\n return run(values.project, values.build);\n}\n\nasync function run(project: string | undefined, build: string | undefined): Promise<number> {\n try {\n const result = await init({ root: process.cwd(), project, build });\n const options = [`build: ${JSON.stringify(result.build)}`];\n if (project !== undefined) options.push(`project: ${JSON.stringify(project)}`);\n process.stdout.write(\n `${result.written.map((file) => `wrote ${file}`).join(\"\\n\")}\\n\\n` +\n `Next, if astro.config.mjs doesn't have it, add the integration:\\n` +\n ` integrations: [ascribe({ ${options.join(\", \")} })],\\n` +\n `then run \\`npx astro dev\\`.\\n`,\n );\n return 0;\n } catch (error) {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n return 2;\n }\n}\n\nprocess.exitCode = await main(process.argv.slice(2));\n"]}
package/dist/content.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import { glob } from "astro/loaders";
2
+ import { type PageRecord } from "./records.js";
3
+ import { type NavEntry, type NavGroup, type SidebarGroup } from "./nav.js";
4
+ export type { NavGroup, SidebarGroup, SidebarPage } from "./nav.js";
5
+ export type { Listing, PageRecord } from "./records.js";
2
6
  /**
3
7
  * The collection's loader and schema: the `glob` loader on the site output's
4
8
  * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).
@@ -17,3 +21,25 @@ export declare function ascribeCollection<S>(options: {
17
21
  loader: ReturnType<typeof glob>;
18
22
  schema: S;
19
23
  };
24
+ /**
25
+ * A layout's sidebar: `nav`'s groups, each page with its collection entry and
26
+ * its address, which is the one Ascribe's links use. `entries` is the
27
+ * collection (`await getCollection("docs")`), and `name` says which project's,
28
+ * as for `ascribeCollection`; with several projects and no `name`, it's the one
29
+ * the entries were loaded from. Throws, naming each, when `nav` leaves out a page
30
+ * of the collection, names a file that isn't one, or names a page twice, so the
31
+ * build fails until the list is complete.
32
+ */
33
+ export declare function sidebar<E extends NavEntry>(nav: readonly NavGroup[], entries: readonly E[], options?: {
34
+ name?: string;
35
+ }): SidebarGroup<E>[];
36
+ /**
37
+ * A collection entry's page record, from its build's `_ascribe/pages.json`:
38
+ * its route, title, description, canonical address, and listing, as every
39
+ * output Ascribe writes has them. `undefined` for an entry that isn't from an
40
+ * Ascribe collection, or whose page has no record.
41
+ */
42
+ export declare function pageRecord(entry: {
43
+ filePath?: string | undefined;
44
+ data?: unknown;
45
+ }): PageRecord | undefined;
package/dist/content.js CHANGED
@@ -13,10 +13,17 @@
13
13
  // export const collections = {
14
14
  // docs: defineCollection(ascribeCollection({ schema: docs, name: "docs" })),
15
15
  // };
16
+ //
17
+ // A layout's sidebar, from the site's list of pages (`src/nav.ts`, which
18
+ // `npx @ascribed/astro init` writes), checked against the collection:
19
+ //
20
+ // const groups = sidebar(nav, await getCollection("docs"));
16
21
  import { pathToFileURL } from "node:url";
17
22
  import path from "node:path";
18
23
  import { glob } from "astro/loaders";
19
- import { collections, siteRoot } from "virtual:ascribe/site";
24
+ import { astroRoot, collections, routes, siteRoot } from "virtual:ascribe/site";
25
+ import { recordFor } from "./records.js";
26
+ import { pageRoute, sidebarGroups, } from "./nav.js";
20
27
  /**
21
28
  * The collection's loader and schema: the `glob` loader on the site output's
22
29
  * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).
@@ -33,6 +40,68 @@ export function ascribeCollection(options) {
33
40
  const base = pathToFileURL(rootFor(options.name) + path.sep);
34
41
  return { loader: glob({ pattern: ["**/*.md", "!_ascribe/**"], base }), schema: options.schema };
35
42
  }
43
+ /**
44
+ * A layout's sidebar: `nav`'s groups, each page with its collection entry and
45
+ * its address, which is the one Ascribe's links use. `entries` is the
46
+ * collection (`await getCollection("docs")`), and `name` says which project's,
47
+ * as for `ascribeCollection`; with several projects and no `name`, it's the one
48
+ * the entries were loaded from. Throws, naming each, when `nav` leaves out a page
49
+ * of the collection, names a file that isn't one, or names a page twice, so the
50
+ * build fails until the list is complete.
51
+ */
52
+ export function sidebar(nav, entries, options = {}) {
53
+ const root = sidebarRoot(entries, options.name);
54
+ const at = routes.find((route) => route.siteRoot === root);
55
+ const basePath = at?.basePath ?? "/";
56
+ const trailingSlash = at?.trailingSlash ?? "always";
57
+ return sidebarGroups(nav, entries, (id) => pageRoute(id, basePath, trailingSlash), (entry) => sourceFile(root, entry));
58
+ }
59
+ /**
60
+ * A collection entry's page record, from its build's `_ascribe/pages.json`:
61
+ * its route, title, description, canonical address, and listing, as every
62
+ * output Ascribe writes has them. `undefined` for an entry that isn't from an
63
+ * Ascribe collection, or whose page has no record.
64
+ */
65
+ export function pageRecord(entry) {
66
+ if (entry.filePath === undefined)
67
+ return undefined;
68
+ const file = path.resolve(astroRoot, entry.filePath);
69
+ const roots = [...Object.values(collections), ...(siteRoot === "" ? [] : [siteRoot])];
70
+ const root = roots.find((candidate) => inside(candidate, file));
71
+ if (root === undefined)
72
+ return undefined;
73
+ const data = entry.data;
74
+ const source = typeof data?.source === "string"
75
+ ? data.source
76
+ : path.relative(root, file).split(path.sep).join("/");
77
+ return recordFor(root, source);
78
+ }
79
+ /**
80
+ * The site output root of the collection a sidebar lists: the entry `name`
81
+ * names, or, with several and no name, the one the entries were loaded from.
82
+ */
83
+ function sidebarRoot(entries, name) {
84
+ const file = entries[0]?.filePath;
85
+ if (name === undefined && Object.keys(collections).length > 1 && file !== undefined) {
86
+ const from = Object.values(collections).find((root) => inside(root, file));
87
+ if (from !== undefined)
88
+ return from;
89
+ }
90
+ return rootFor(name);
91
+ }
92
+ /** Whether `file`, from the Astro root as an entry's `filePath` is, is under `root`. */
93
+ function inside(root, file) {
94
+ const relative = path.relative(root, path.resolve(astroRoot, file));
95
+ return !relative.startsWith("..") && !path.isAbsolute(relative);
96
+ }
97
+ /** An entry's source file, by its path in the site output, which mirrors the content root. */
98
+ function sourceFile(root, entry) {
99
+ if (entry.filePath === undefined)
100
+ return `the page ${entry.id}`;
101
+ return inside(root, entry.filePath)
102
+ ? path.relative(root, path.resolve(astroRoot, entry.filePath)).split(path.sep).join("/")
103
+ : entry.filePath;
104
+ }
36
105
  /** The site output root of the entry `name` names. */
37
106
  function rootFor(name) {
38
107
  const names = Object.keys(collections);
@@ -1 +1 @@
1
- {"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,6EAA6E;AAC7E,2EAA2E;AAC3E,EAAE;AACF,sDAAsD;AACtD,iEAAiE;AACjE,6EAA6E;AAC7E,0FAA0F;AAC1F,EAAE;AACF,0DAA0D;AAC1D,EAAE;AACF,4FAA4F;AAC5F,iCAAiC;AACjC,iFAAiF;AACjF,OAAO;AACP,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAI,OAAqC;IAIxE,iEAAiE;IACjE,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7D,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,SAAS,EAAE,cAAc,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;AAClG,CAAC;AAED,sDAAsD;AACtD,SAAS,OAAO,CAAC,IAAwB;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACb,0DAA0D,IAAI,qHAAqH,CACpL,CAAC;QACJ,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAW,CAAW,CAAC;QACzE,MAAM,IAAI,KAAK,CACb,mGAAmG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5I,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,0DAA0D,IAAI,mCAAmC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC1I,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["// The content collection over a build's site output. Import it from\n// `content.config.ts`, which Vite loads, so it can read what the integration\n// resolved (`virtual:ascribe/site`) instead of finding the project itself.\n//\n// import { defineCollection } from \"astro:content\";\n// import { ascribeCollection } from \"@ascribed/astro/content\";\n// import { schema } from \"../.ascribe/build/site/site/_ascribe/schema.ts\";\n// export const collections = { docs: defineCollection(ascribeCollection({ schema })) };\n//\n// With several projects, each collection names its entry:\n//\n// import { schema as docs } from \"../.astro/integrations/_ascribed_astro/docs/schema.ts\";\n// export const collections = {\n// docs: defineCollection(ascribeCollection({ schema: docs, name: \"docs\" })),\n// };\nimport { pathToFileURL } from \"node:url\";\nimport path from \"node:path\";\nimport { glob } from \"astro/loaders\";\nimport { collections, siteRoot } from \"virtual:ascribe/site\";\n\n/**\n * The collection's loader and schema: the `glob` loader on the site output's\n * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).\n * With several projects (`ascribe({ projects })`), `name` says which entry's\n * site output; it can be left out when there's one entry.\n *\n * Entry ids are Astro's own (`guides/my-setup` for `Guides/My Setup.md`, `index`\n * for the root page), which is what `AstroRouter` in `ascribe-resolve`\n * computes, so a page's route is the base path plus its id, and the root page is\n * at the base path itself.\n */\nexport function ascribeCollection<S>(options: { schema: S; name?: string }): {\n loader: ReturnType<typeof glob>;\n schema: S;\n} {\n // `_ascribe/` holds the schema and published files, never pages.\n const base = pathToFileURL(rootFor(options.name) + path.sep);\n return { loader: glob({ pattern: [\"**/*.md\", \"!_ascribe/**\"], base }), schema: options.schema };\n}\n\n/** The site output root of the entry `name` names. */\nfunction rootFor(name: string | undefined): string {\n const names = Object.keys(collections);\n if (names.length === 0) {\n if (name !== undefined) {\n throw new Error(\n `@ascribed/astro: ascribeCollection was given the name \"${name}\", but ascribe() was given one project, which has no name. Leave out \\`name\\`, or list the project in \\`projects\\`.`,\n );\n }\n return siteRoot;\n }\n if (name === undefined) {\n if (names.length === 1) return collections[names[0] as string] as string;\n throw new Error(\n `@ascribed/astro: ascribeCollection needs a \\`name\\` when ascribe() has several projects: one of ${names.map((n) => `\"${n}\"`).join(\", \")}.`,\n );\n }\n const root = collections[name];\n if (root === undefined) {\n throw new Error(\n `@ascribed/astro: ascribeCollection was given the name \"${name}\", and ascribe()'s projects are ${names.map((n) => `\"${n}\"`).join(\", \")}.`,\n );\n }\n return root;\n}\n"]}
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"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","sourcesContent":["// The content collection over a build's site output. Import it from\n// `content.config.ts`, which Vite loads, so it can read what the integration\n// resolved (`virtual:ascribe/site`) instead of finding the project itself.\n//\n// import { defineCollection } from \"astro:content\";\n// import { ascribeCollection } from \"@ascribed/astro/content\";\n// import { schema } from \"../.ascribe/build/site/site/_ascribe/schema.ts\";\n// export const collections = { docs: defineCollection(ascribeCollection({ schema })) };\n//\n// With several projects, each collection names its entry:\n//\n// import { schema as docs } from \"../.astro/integrations/_ascribed_astro/docs/schema.ts\";\n// export const collections = {\n// docs: defineCollection(ascribeCollection({ schema: docs, name: \"docs\" })),\n// };\n//\n// A layout's sidebar, from the site's list of pages (`src/nav.ts`, which\n// `npx @ascribed/astro init` writes), checked against the collection:\n//\n// const groups = sidebar(nav, await getCollection(\"docs\"));\nimport { pathToFileURL } from \"node:url\";\nimport path from \"node:path\";\nimport { glob } from \"astro/loaders\";\nimport { astroRoot, collections, routes, siteRoot } from \"virtual:ascribe/site\";\nimport { recordFor, type PageRecord } from \"./records.js\";\nimport {\n pageRoute,\n sidebarGroups,\n type NavEntry,\n type NavGroup,\n type SidebarGroup,\n} from \"./nav.js\";\n\nexport type { NavGroup, SidebarGroup, SidebarPage } from \"./nav.js\";\nexport type { Listing, PageRecord } from \"./records.js\";\n\n/**\n * The collection's loader and schema: the `glob` loader on the site output's\n * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).\n * With several projects (`ascribe({ projects })`), `name` says which entry's\n * site output; it can be left out when there's one entry.\n *\n * Entry ids are Astro's own (`guides/my-setup` for `Guides/My Setup.md`, `index`\n * for the root page), which is what `AstroRouter` in `ascribe-resolve`\n * computes, so a page's route is the base path plus its id, and the root page is\n * at the base path itself.\n */\nexport function ascribeCollection<S>(options: { schema: S; name?: string }): {\n loader: ReturnType<typeof glob>;\n schema: S;\n} {\n // `_ascribe/` holds the schema and published files, never pages.\n const base = pathToFileURL(rootFor(options.name) + path.sep);\n return { loader: glob({ pattern: [\"**/*.md\", \"!_ascribe/**\"], base }), schema: options.schema };\n}\n\n/**\n * A layout's sidebar: `nav`'s groups, each page with its collection entry and\n * its address, which is the one Ascribe's links use. `entries` is the\n * collection (`await getCollection(\"docs\")`), and `name` says which project's,\n * as for `ascribeCollection`; with several projects and no `name`, it's the one\n * the entries were loaded from. Throws, naming each, when `nav` leaves out a page\n * of the collection, names a file that isn't one, or names a page twice, so the\n * build fails until the list is complete.\n */\nexport function sidebar<E extends NavEntry>(\n nav: readonly NavGroup[],\n entries: readonly E[],\n options: { name?: string } = {},\n): SidebarGroup<E>[] {\n const root = sidebarRoot(entries, options.name);\n const at = routes.find((route) => route.siteRoot === root);\n const basePath = at?.basePath ?? \"/\";\n const trailingSlash = at?.trailingSlash ?? \"always\";\n return sidebarGroups(\n nav,\n entries,\n (id) => pageRoute(id, basePath, trailingSlash),\n (entry) => sourceFile(root, entry),\n );\n}\n\n/**\n * A collection entry's page record, from its build's `_ascribe/pages.json`:\n * its route, title, description, canonical address, and listing, as every\n * output Ascribe writes has them. `undefined` for an entry that isn't from an\n * Ascribe collection, or whose page has no record.\n */\nexport function pageRecord(entry: {\n filePath?: string | undefined;\n data?: unknown;\n}): PageRecord | undefined {\n if (entry.filePath === undefined) return undefined;\n const file = path.resolve(astroRoot, entry.filePath);\n const roots = [...Object.values(collections), ...(siteRoot === \"\" ? [] : [siteRoot])];\n const root = roots.find((candidate) => inside(candidate, file));\n if (root === undefined) return undefined;\n const data = entry.data as { source?: unknown } | undefined;\n const source =\n typeof data?.source === \"string\"\n ? data.source\n : path.relative(root, file).split(path.sep).join(\"/\");\n return recordFor(root, source);\n}\n\n/**\n * The site output root of the collection a sidebar lists: the entry `name`\n * names, or, with several and no name, the one the entries were loaded from.\n */\nfunction sidebarRoot(entries: readonly NavEntry[], name: string | undefined): string {\n const file = entries[0]?.filePath;\n if (name === undefined && Object.keys(collections).length > 1 && file !== undefined) {\n const from = Object.values(collections).find((root) => inside(root, file));\n if (from !== undefined) return from;\n }\n return rootFor(name);\n}\n\n/** Whether `file`, from the Astro root as an entry's `filePath` is, is under `root`. */\nfunction inside(root: string, file: string): boolean {\n const relative = path.relative(root, path.resolve(astroRoot, file));\n return !relative.startsWith(\"..\") && !path.isAbsolute(relative);\n}\n\n/** An entry's source file, by its path in the site output, which mirrors the content root. */\nfunction sourceFile(root: string, entry: NavEntry): string {\n if (entry.filePath === undefined) return `the page ${entry.id}`;\n return inside(root, entry.filePath)\n ? path.relative(root, path.resolve(astroRoot, entry.filePath)).split(path.sep).join(\"/\")\n : entry.filePath;\n}\n\n/** The site output root of the entry `name` names. */\nfunction rootFor(name: string | undefined): string {\n const names = Object.keys(collections);\n if (names.length === 0) {\n if (name !== undefined) {\n throw new Error(\n `@ascribed/astro: ascribeCollection was given the name \"${name}\", but ascribe() was given one project, which has no name. Leave out \\`name\\`, or list the project in \\`projects\\`.`,\n );\n }\n return siteRoot;\n }\n if (name === undefined) {\n if (names.length === 1) return collections[names[0] as string] as string;\n throw new Error(\n `@ascribed/astro: ascribeCollection needs a \\`name\\` when ascribe() has several projects: one of ${names.map((n) => `\"${n}\"`).join(\", \")}.`,\n );\n }\n const root = collections[name];\n if (root === undefined) {\n throw new Error(\n `@ascribed/astro: ascribeCollection was given the name \"${name}\", and ascribe()'s projects are ${names.map((n) => `\"${n}\"`).join(\", \")}.`,\n );\n }\n return root;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -30,6 +30,15 @@ interface SharedOptions {
30
30
  * `<figure class="code-title">` with a `<figcaption>`. Default `true`.
31
31
  */
32
32
  codeTitles?: boolean;
33
+ /**
34
+ * The site's one sitemap, at `<base>sitemap.xml`, of every listed page of
35
+ * every project and every page of the site's own. `exclude` lists routes,
36
+ * from the host's root, of the site's own pages to leave out of it, such
37
+ * as one that carries `noindex`.
38
+ */
39
+ sitemap?: {
40
+ exclude?: string[];
41
+ };
33
42
  }
34
43
  /** Options of the integration with one project. */
35
44
  export interface OneProjectOptions extends SharedOptions {
@@ -51,6 +60,7 @@ export type AscribeOptions = OneProjectOptions | ProjectsOptions;
51
60
  /**
52
61
  * Runs `ascribe build` for each project, checks the site's routing, adds the markdown plugin, adds a
53
62
  * page for each permalink's address, and serves published files, and, with `[consumer] agents = true`,
54
- * `llms.txt` and each page's Markdown, each at its project's base path.
63
+ * `llms.txt` and each page's Markdown, each at its project's base path; and writes the site's one
64
+ * sitemap and `robots.txt` from every project's page records.
55
65
  */
56
66
  export default function ascribe(options: AscribeOptions): AstroIntegration;
package/dist/index.js CHANGED
@@ -4,7 +4,8 @@
4
4
  // // astro.config.mjs
5
5
  // import ascribe from "@ascribed/astro";
6
6
  // export default defineConfig({
7
- // base: "/docs", trailingSlash: "never", // as in ascribe.toml's [consumer]
7
+ // // `site`, `base`, and `trailingSlash` come from ascribe.toml's
8
+ // // [consumer] when they're left out here; set here, they must agree.
8
9
  // integrations: [ascribe({ build: "site" })],
9
10
  // });
10
11
  //
@@ -19,6 +20,11 @@
19
20
  // // a layout
20
21
  // import Elements from "@ascribed/astro/Elements.astro"; // <Elements /> in <head>
21
22
  // import Availability from "@ascribed/astro/Availability.astro"; // the page's availability
23
+ // import Head from "@ascribed/astro/Head.astro"; // <Head entry={entry} /> in <head>
24
+ //
25
+ // // the sidebar: src/nav.ts's list, checked against the collection
26
+ // import { sidebar } from "@ascribed/astro/content";
27
+ // const groups = sidebar(nav, await getCollection("docs"));
22
28
  //
23
29
  // // src/pages/404.astro
24
30
  // import MissingPermalink from "@ascribed/astro/MissingPermalink.astro"; // a permalink that isn't here
@@ -33,22 +39,25 @@
33
39
  // review: "docs",
34
40
  // })
35
41
  // ascribeCollection({ schema, name: "docs" })
36
- import { mkdirSync } from "node:fs";
42
+ import { existsSync, mkdirSync } from "node:fs";
43
+ import { writeFile } from "node:fs/promises";
37
44
  import path from "node:path";
38
45
  import { fileURLToPath } from "node:url";
39
46
  import { findBinary } from "./binary.js";
40
47
  import { codeTitles } from "./code-titles.js";
41
48
  import { SeenFiles, watchDev } from "./dev.js";
42
49
  import { agentsMiddleware, copyAgentFiles, copyPublishedFiles, filesMiddleware, folderUnder, } from "./files.js";
43
- import { hasPermalinks, permalinkPattern, readPermalinks } from "./permalinks.js";
44
- import { consumerMismatches, homeRoute, readProject } from "./project.js";
50
+ import { forwardingRoutes, hasPermalinks, permalinkPattern, readPermalinks } from "./permalinks.js";
51
+ import { consumerMismatches, homeRoute, normalizeBase, readProject, supplyRouting, } from "./project.js";
45
52
  import { basePathClashes, nameProblems, pagesUnderOtherPaths, permalinkProblems, } from "./projects.js";
53
+ import { pageList, readRecords, routeAs, routeKey, sitemapRoute } from "./records.js";
46
54
  import rehypeAscribeAttributes from "./rehype.js";
47
55
  import { removeDevFile, siteUrl, writeDevFile } from "./review/devfile.js";
48
56
  import { runDiff, runPrompt } from "./review/diff.js";
49
57
  import { APP_ID } from "./review/protocol.js";
50
58
  import { readRoutes } from "./review/routes.js";
51
59
  import { channelProblem, ReviewServer } from "./review/server.js";
60
+ import { devRouted, devSitePages, rootFiles, rootFilesMiddleware, } from "./root-files.js";
52
61
  import { anchorsFor, hasWarnings, reviewFor, runBuild } from "./run.js";
53
62
  import { satteriAscribeAttributes } from "./satteri.js";
54
63
  import { copySchema } from "./schema.js";
@@ -65,7 +74,8 @@ function fail(problems, what) {
65
74
  /**
66
75
  * Runs `ascribe build` for each project, checks the site's routing, adds the markdown plugin, adds a
67
76
  * page for each permalink's address, and serves published files, and, with `[consumer] agents = true`,
68
- * `llms.txt` and each page's Markdown, each at its project's base path.
77
+ * `llms.txt` and each page's Markdown, each at its project's base path; and writes the site's one
78
+ * sitemap and `robots.txt` from every project's page records.
69
79
  */
70
80
  export default function ascribe(options) {
71
81
  /** With `projects`; otherwise the one entry has no name a collection gives. */
@@ -80,11 +90,44 @@ export default function ascribe(options) {
80
90
  let devCommand = false;
81
91
  let devHttps = false;
82
92
  let review;
93
+ /** The site's routes, as Astro resolved them, for what `astro dev` serves of the site's own. */
94
+ let astroRoutes = [];
95
+ /** The site's `public/` folder, where its own `robots.txt` would be. */
96
+ let publicDir = "";
97
+ /** What the sitemap and `robots.txt` are drawn from, given the site's pages and its routes. */
98
+ const rootSources = (sitePages, routed) => {
99
+ const base = routing?.base ?? "/";
100
+ const records = entries.map((entry) => readRecords(entry.siteRoot));
101
+ // Entries of one project share its `ascribe.toml`, and so its lines.
102
+ const lines = new Map();
103
+ for (const [i, entry] of entries.entries()) {
104
+ if (!lines.has(entry.project.dir))
105
+ lines.set(entry.project.dir, records[i]?.robots.lines ?? []);
106
+ }
107
+ const root = normalizeBase(base);
108
+ return {
109
+ projects: records.map(pageList),
110
+ routed,
111
+ sitePages,
112
+ excluded: [
113
+ ...forwardingRoutes(entries.map((entry) => readPermalinks(entry.siteRoot))),
114
+ `${root}404`,
115
+ `${root}500`,
116
+ ...(options.sitemap?.exclude ?? []),
117
+ ],
118
+ lines: [...lines.values()],
119
+ site: routing?.site,
120
+ base,
121
+ ownRobots: existsSync(path.join(publicDir, "robots.txt")),
122
+ ownSitemap: existsSync(path.join(publicDir, "sitemap.xml")),
123
+ };
124
+ };
83
125
  return {
84
126
  name: "@ascribed/astro",
85
127
  hooks: {
86
128
  "astro:config:setup": async ({ config, command, logger, updateConfig, addDevToolbarApp, createCodegenDir, injectRoute, }) => {
87
129
  const root = fileURLToPath(config.root);
130
+ publicDir = fileURLToPath(config.publicDir);
88
131
  const list = options.projects ?? [
89
132
  {
90
133
  name: "",
@@ -107,8 +150,11 @@ export default function ascribe(options) {
107
150
  schemaDir: named ? path.join(codegen, entry.name) : codegen,
108
151
  };
109
152
  });
153
+ // What astro.config leaves unset comes from [consumer]: `site` and `trailingSlash` from the
154
+ // first project that has them, and with one project, `base` from its base path. With
155
+ // `projects`, `base` stays the site's, the root every base path is under.
156
+ const { routing: astro, supplied } = supplyRouting(entries.map((entry) => entry.project), { base: config.base, trailingSlash: config.trailingSlash, site: config.site }, !named);
110
157
  // `ascribe.toml`'s routing must be Astro's, or every link Ascribe writes is wrong.
111
- const astro = { base: config.base, trailingSlash: config.trailingSlash, site: config.site };
112
158
  for (const entry of entries) {
113
159
  fail(consumerMismatches(entry.project, astro), named
114
160
  ? `"${entry.name}"'s ascribe.toml and astro.config disagree`
@@ -216,15 +262,16 @@ export default function ascribe(options) {
216
262
  const served = lists.find(hasPermalinks) ?? lists[0];
217
263
  if (served !== undefined) {
218
264
  injectRoute({
219
- pattern: permalinkPattern(served.path, config.base),
265
+ pattern: permalinkPattern(served.path, astro.base),
220
266
  entrypoint: new URL("../src/Permalink.astro", import.meta.url),
221
267
  prerender: true,
222
268
  });
223
269
  }
224
270
  }
225
- const projectHome = (entry) => homeRoute(entry.project.consumer.basePath, config.trailingSlash);
271
+ const projectHome = (entry) => homeRoute(entry.project.consumer.basePath, astro.trailingSlash);
226
272
  // Dev serving of `_ascribe/files/`, and of the plain output for agents; the build copies them in `astro:build:done`.
227
273
  updateConfig({
274
+ ...supplied,
228
275
  ...(options.codeTitles === false
229
276
  ? {}
230
277
  : { markdown: { shikiConfig: { transformers: [codeTitles] } } }),
@@ -237,9 +284,15 @@ export default function ascribe(options) {
237
284
  resolveId: (id) => (id === SITE_MODULE ? `\0${SITE_MODULE}` : undefined),
238
285
  load: (id) => id === `\0${SITE_MODULE}`
239
286
  ? `export const siteRoot = ${JSON.stringify(entries[0]?.siteRoot ?? "")};\n` +
240
- `export const home = ${JSON.stringify(homeRoute(config.base, config.trailingSlash))};\n` +
287
+ `export const astroRoot = ${JSON.stringify(root)};\n` +
288
+ `export const home = ${JSON.stringify(homeRoute(astro.base, astro.trailingSlash))};\n` +
241
289
  `export const collections = ${JSON.stringify(named ? Object.fromEntries(entries.map((e) => [e.name, e.siteRoot])) : {})};\n` +
242
- `export const permalinks = ${JSON.stringify(entries.map((e) => ({ siteRoot: e.siteRoot, home: projectHome(e) })))};`
290
+ `export const permalinks = ${JSON.stringify(entries.map((e) => ({ siteRoot: e.siteRoot, home: projectHome(e) })))};\n` +
291
+ `export const routes = ${JSON.stringify(entries.map((e) => ({
292
+ siteRoot: e.siteRoot,
293
+ basePath: e.project.consumer.basePath,
294
+ trailingSlash: e.project.consumer.trailingSlash,
295
+ })))};`
243
296
  : undefined,
244
297
  },
245
298
  {
@@ -251,12 +304,17 @@ export default function ascribe(options) {
251
304
  if (entry.agentsRoot !== undefined)
252
305
  server.middlewares.use(agentsMiddleware(entry.agentsRoot, base));
253
306
  }
307
+ // The sitemap and robots.txt, as the build would write them now.
308
+ server.middlewares.use(rootFilesMiddleware(astro.base, () => rootFiles(rootSources(devSitePages(astroRoutes, astro), devRouted(astroRoutes, astro)))));
254
309
  },
255
310
  },
256
311
  ],
257
312
  },
258
313
  });
259
314
  },
315
+ "astro:routes:resolved": ({ routes }) => {
316
+ astroRoutes = routes;
317
+ },
260
318
  "astro:server:setup": ({ server, refreshContent, logger, toolbar }) => {
261
319
  if (!devCommand || !routing)
262
320
  return;
@@ -331,8 +389,26 @@ export default function ascribe(options) {
331
389
  for (const group of groups)
332
390
  removeDevFile(group.project.dir, process.pid);
333
391
  },
334
- "astro:build:done": async ({ dir, logger }) => {
392
+ "astro:build:done": async ({ dir, pages, logger }) => {
335
393
  const base = routing?.base ?? "/";
394
+ const trailingSlash = routing?.trailingSlash ?? "ignore";
395
+ // Every page Astro built, by its route from the host's root. Astro lists
396
+ // pages only, not endpoints such as a feed, so a dot in a page's last
397
+ // segment, as in `/v1.2`, is still a page.
398
+ const built = pages.map((page) => routeAs(`${normalizeBase(base)}${page.pathname.replace(/^\//, "")}`, trailingSlash));
399
+ const builtKeys = new Set(built.map(routeKey));
400
+ const files = rootFiles(rootSources(built, (route) => builtKeys.has(routeKey(route))));
401
+ for (const note of files.notes)
402
+ logger[note.level](note.text);
403
+ const out = fileURLToPath(dir);
404
+ if (files.sitemap !== undefined) {
405
+ await writeFile(path.join(out, "sitemap.xml"), files.sitemap);
406
+ logger.info(`wrote ${sitemapRoute(base)}`);
407
+ }
408
+ if (files.robots !== undefined) {
409
+ await writeFile(path.join(out, "robots.txt"), files.robots);
410
+ logger.info("wrote /robots.txt");
411
+ }
336
412
  // Entries of one project publish at one base path: the first entry's copies go last, so they win.
337
413
  for (const entry of [...entries].reverse()) {
338
414
  const folder = folderUnder(entry.project.consumer.basePath, base);