@ascribed/astro 0.2.1-next.15 → 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.
Files changed (51) hide show
  1. package/README.md +21 -7
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +62 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/content.d.ts +27 -0
  6. package/dist/content.js +71 -1
  7. package/dist/content.js.map +1 -1
  8. package/dist/files.d.ts +5 -2
  9. package/dist/files.js +13 -5
  10. package/dist/files.js.map +1 -1
  11. package/dist/head-check.d.ts +6 -0
  12. package/dist/head-check.js +28 -0
  13. package/dist/head-check.js.map +1 -0
  14. package/dist/head.d.ts +54 -0
  15. package/dist/head.js +114 -0
  16. package/dist/head.js.map +1 -0
  17. package/dist/headers.d.ts +50 -0
  18. package/dist/headers.js +155 -0
  19. package/dist/headers.js.map +1 -0
  20. package/dist/index.d.ts +12 -1
  21. package/dist/index.js +124 -14
  22. package/dist/index.js.map +1 -1
  23. package/dist/init.d.ts +41 -0
  24. package/dist/init.js +130 -0
  25. package/dist/init.js.map +1 -0
  26. package/dist/nav.d.ts +51 -0
  27. package/dist/nav.js +97 -0
  28. package/dist/nav.js.map +1 -0
  29. package/dist/permalinks.d.ts +6 -0
  30. package/dist/permalinks.js +13 -0
  31. package/dist/permalinks.js.map +1 -1
  32. package/dist/project.d.ts +26 -0
  33. package/dist/project.js +32 -0
  34. package/dist/project.js.map +1 -1
  35. package/dist/projects.d.ts +0 -6
  36. package/dist/projects.js +1 -14
  37. package/dist/projects.js.map +1 -1
  38. package/dist/records.d.ts +136 -0
  39. package/dist/records.js +182 -0
  40. package/dist/records.js.map +1 -0
  41. package/dist/root-files.d.ts +68 -0
  42. package/dist/root-files.js +103 -0
  43. package/dist/root-files.js.map +1 -0
  44. package/dist/run.js +3 -2
  45. package/dist/run.js.map +1 -1
  46. package/package.json +12 -6
  47. package/src/Head.astro +58 -0
  48. package/starter/src/content.config.ts +8 -0
  49. package/starter/src/layouts/Docs.astro +87 -0
  50. package/starter/src/nav.ts +13 -0
  51. package/starter/src/pages/[...slug].astro +20 -0
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,26 @@ 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
+ <!-- Everything the head says about the page, from its record. -->
60
+ <Head entry={entry} title={(title, record) => `${title} · Loom`} name="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/docs/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/docs/guides/astro/#a-home-page-in-front-of-the-docs), [several projects in one site](https://ascribed-dev.com/docs/guides/astro/#several-projects-in-one-site), what `astro dev` does, and [review in the site preview](https://ascribed-dev.com/docs/guides/astro/#review-in-the-site-preview). [Review](https://ascribed-dev.com/docs/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, `noindex` when it's `unlisted`, and `nosnippet` when it's `unquoted`, a link to its Markdown version, the Open Graph tags a link preview reads, with the site's name and a social card given as properties, and its breadcrumbs as structured data, and `astro dev` warns about a tag a layout writes again; 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` and a group for each kind of crawler it disallows, such as `training = "disallow"`, in `astro build` and `astro dev`; projects that disagree on a kind fail the build. 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
+ A page's Markdown version has no head, so its rules are response headers, which each build lists in `_ascribe/headers.json`: `X-Robots-Tag` when the page isn't listed, and a canonical `Link` to the page. `astro dev` sends them. With `[consumer] headers = "netlify"`, `astro build` writes them into the site's `_headers`; a site with its own `public/_headers` keeps it, and the build fails with the lines it lacks.
71
+
72
+ `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.
73
+
74
+ 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
75
 
65
76
  ## Options
66
77
 
@@ -73,13 +84,16 @@ The [Astro guide](https://ascribed-dev.com/docs/guides/astro/) walks through a s
73
84
  | `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
85
  | `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
86
  | `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`. |
87
+ | `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
88
 
77
89
  ## Development
78
90
 
79
91
  | File | Role |
80
92
  |---|---|
81
93
  | `src/index.ts` | The integration |
82
- | `src/content.ts` | `ascribeCollection`, for `content.config.ts` |
94
+ | `src/content.ts` | `ascribeCollection`, for `content.config.ts`, and `sidebar`, for a layout |
95
+ | `src/nav.ts` | The sidebar: entry ids, routes, and the check of the site's list against the collection |
96
+ | `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
97
  | `src/Elements.astro` | Loads `@ascribed/elements` (stylesheet and script) |
84
98
  | `src/Availability.astro` | A page's availability as the element library's badge |
85
99
  | `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,9 @@
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, ListingFrom, PageRecord, Step } from "./records.js";
6
+ export { headTags, scriptJson, type Card, type HeadOptions, type HeadTag } from "./head.js";
2
7
  /**
3
8
  * The collection's loader and schema: the `glob` loader on the site output's
4
9
  * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).
@@ -17,3 +22,25 @@ export declare function ascribeCollection<S>(options: {
17
22
  loader: ReturnType<typeof glob>;
18
23
  schema: S;
19
24
  };
25
+ /**
26
+ * A layout's sidebar: `nav`'s groups, each page with its collection entry and
27
+ * its address, which is the one Ascribe's links use. `entries` is the
28
+ * collection (`await getCollection("docs")`), and `name` says which project's,
29
+ * as for `ascribeCollection`; with several projects and no `name`, it's the one
30
+ * the entries were loaded from. Throws, naming each, when `nav` leaves out a page
31
+ * of the collection, names a file that isn't one, or names a page twice, so the
32
+ * build fails until the list is complete.
33
+ */
34
+ export declare function sidebar<E extends NavEntry>(nav: readonly NavGroup[], entries: readonly E[], options?: {
35
+ name?: string;
36
+ }): SidebarGroup<E>[];
37
+ /**
38
+ * A collection entry's page record, from its build's `_ascribe/pages.json`:
39
+ * its route, title, description, canonical address, and listing, as every
40
+ * output Ascribe writes has them. `undefined` for an entry that isn't from an
41
+ * Ascribe collection, or whose page has no record.
42
+ */
43
+ export declare function pageRecord(entry: {
44
+ filePath?: string | undefined;
45
+ data?: unknown;
46
+ }): PageRecord | undefined;
package/dist/content.js CHANGED
@@ -13,10 +13,18 @@
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";
27
+ export { headTags, scriptJson } from "./head.js";
20
28
  /**
21
29
  * The collection's loader and schema: the `glob` loader on the site output's
22
30
  * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).
@@ -33,6 +41,68 @@ export function ascribeCollection(options) {
33
41
  const base = pathToFileURL(rootFor(options.name) + path.sep);
34
42
  return { loader: glob({ pattern: ["**/*.md", "!_ascribe/**"], base }), schema: options.schema };
35
43
  }
44
+ /**
45
+ * A layout's sidebar: `nav`'s groups, each page with its collection entry and
46
+ * its address, which is the one Ascribe's links use. `entries` is the
47
+ * collection (`await getCollection("docs")`), and `name` says which project's,
48
+ * as for `ascribeCollection`; with several projects and no `name`, it's the one
49
+ * the entries were loaded from. Throws, naming each, when `nav` leaves out a page
50
+ * of the collection, names a file that isn't one, or names a page twice, so the
51
+ * build fails until the list is complete.
52
+ */
53
+ export function sidebar(nav, entries, options = {}) {
54
+ const root = sidebarRoot(entries, options.name);
55
+ const at = routes.find((route) => route.siteRoot === root);
56
+ const basePath = at?.basePath ?? "/";
57
+ const trailingSlash = at?.trailingSlash ?? "always";
58
+ return sidebarGroups(nav, entries, (id) => pageRoute(id, basePath, trailingSlash), (entry) => sourceFile(root, entry));
59
+ }
60
+ /**
61
+ * A collection entry's page record, from its build's `_ascribe/pages.json`:
62
+ * its route, title, description, canonical address, and listing, as every
63
+ * output Ascribe writes has them. `undefined` for an entry that isn't from an
64
+ * Ascribe collection, or whose page has no record.
65
+ */
66
+ export function pageRecord(entry) {
67
+ if (entry.filePath === undefined)
68
+ return undefined;
69
+ const file = path.resolve(astroRoot, entry.filePath);
70
+ const roots = [...Object.values(collections), ...(siteRoot === "" ? [] : [siteRoot])];
71
+ const root = roots.find((candidate) => inside(candidate, file));
72
+ if (root === undefined)
73
+ return undefined;
74
+ const data = entry.data;
75
+ const source = typeof data?.source === "string"
76
+ ? data.source
77
+ : path.relative(root, file).split(path.sep).join("/");
78
+ return recordFor(root, source);
79
+ }
80
+ /**
81
+ * The site output root of the collection a sidebar lists: the entry `name`
82
+ * names, or, with several and no name, the one the entries were loaded from.
83
+ */
84
+ function sidebarRoot(entries, name) {
85
+ const file = entries[0]?.filePath;
86
+ if (name === undefined && Object.keys(collections).length > 1 && file !== undefined) {
87
+ const from = Object.values(collections).find((root) => inside(root, file));
88
+ if (from !== undefined)
89
+ return from;
90
+ }
91
+ return rootFor(name);
92
+ }
93
+ /** Whether `file`, from the Astro root as an entry's `filePath` is, is under `root`. */
94
+ function inside(root, file) {
95
+ const relative = path.relative(root, path.resolve(astroRoot, file));
96
+ return !relative.startsWith("..") && !path.isAbsolute(relative);
97
+ }
98
+ /** An entry's source file, by its path in the site output, which mirrors the content root. */
99
+ function sourceFile(root, entry) {
100
+ if (entry.filePath === undefined)
101
+ return `the page ${entry.id}`;
102
+ return inside(root, entry.filePath)
103
+ ? path.relative(root, path.resolve(astroRoot, entry.filePath)).split(path.sep).join("/")
104
+ : entry.filePath;
105
+ }
36
106
  /** The site output root of the entry `name` names. */
37
107
  function rootFor(name) {
38
108
  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, ListingFrom, PageRecord, Step } from \"./records.js\";\nexport { headTags, scriptJson, type Card, type HeadOptions, type HeadTag } from \"./head.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/files.d.ts CHANGED
@@ -14,6 +14,9 @@ export declare function copyAgentFiles(plainRoot: string, outDir: URL, folder?:
14
14
  type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
15
15
  /** A dev-server middleware serving the published files at `<base>_ascribe/files/`, `base` being the project's base path. */
16
16
  export declare function filesMiddleware(siteRoot: string, base: string): Middleware;
17
- /** A dev-server middleware serving the plain output for agents at `<base>`: `llms.txt`, each page's `.md`, and their files. */
18
- export declare function agentsMiddleware(plainRoot: string, base: string): Middleware;
17
+ /**
18
+ * A dev-server middleware serving the plain output for agents at `<base>`: `llms.txt`, each page's
19
+ * `.md`, and their files, each with the headers `headers` gives its path from the host's root.
20
+ */
21
+ export declare function agentsMiddleware(plainRoot: string, base: string, headers?: (pathname: string) => Record<string, string>): Middleware;
19
22
  export {};
package/dist/files.js CHANGED
@@ -65,12 +65,18 @@ const TYPES = {
65
65
  export function filesMiddleware(siteRoot, base) {
66
66
  return directoryMiddleware(publishedDir(siteRoot), `${base}_ascribe/files/`);
67
67
  }
68
- /** A dev-server middleware serving the plain output for agents at `<base>`: `llms.txt`, each page's `.md`, and their files. */
69
- export function agentsMiddleware(plainRoot, base) {
70
- return directoryMiddleware(plainRoot, base);
68
+ /**
69
+ * A dev-server middleware serving the plain output for agents at `<base>`: `llms.txt`, each page's
70
+ * `.md`, and their files, each with the headers `headers` gives its path from the host's root.
71
+ */
72
+ export function agentsMiddleware(plainRoot, base, headers = () => ({})) {
73
+ return directoryMiddleware(plainRoot, base, headers);
71
74
  }
72
- /** A middleware serving the files in `root` at the URL path `prefix`, which ends in `/`. */
73
- function directoryMiddleware(root, prefix) {
75
+ /**
76
+ * A middleware serving the files in `root` at the URL path `prefix`, which ends in `/`, each with
77
+ * the headers `headers` gives its path.
78
+ */
79
+ function directoryMiddleware(root, prefix, headers = () => ({})) {
74
80
  // Only under the base path, as in the build. Vite strips the base from
75
81
  // `req.url` before a plugin's middleware runs, but connect keeps the original in `originalUrl`.
76
82
  return (req, res, next) => {
@@ -92,6 +98,8 @@ function directoryMiddleware(root, prefix) {
92
98
  return next();
93
99
  res.setHeader("Content-Type", TYPES[path.extname(file).toLowerCase()] ?? "application/octet-stream");
94
100
  res.setHeader("Content-Length", info.size);
101
+ for (const [name, value] of Object.entries(headers(pathname)))
102
+ res.setHeader(name, value);
95
103
  if (req.method === "HEAD")
96
104
  return void res.end();
97
105
  createReadStream(file).pipe(res);
package/dist/files.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"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","sourcesContent":["// Serving `_ascribe/files/`: the files pages link to that aren't pages or\n// images. Astro doesn't copy them, so the integration\n// does: into the build's output directory, and in dev through a middleware.\n// Both put them at `<base-path>_ascribe/files/`, the URL the site output links\n// to, where `<base-path>` is the project's `[consumer] base-path`: Astro's\n// `base` or a path under it.\n//\n// With `[consumer] agents = true`, the same for the plain output, which is\n// laid out by URL: `llms.txt`, each page's Markdown, and their files, all at\n// `<base-path>` and below.\nimport { createReadStream } from \"node:fs\";\nimport { cp, stat } from \"node:fs/promises\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The directory of published files inside a site output root. */\nfunction publishedDir(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"files\");\n}\n\n/**\n * The base path's folder in Astro's build output, which is at Astro's `base`:\n * `docs/` for a base path of `/docs/` under a `base` of `/`, `\"\"` when they're the same.\n */\nexport function folderUnder(basePath: string, astroBase: string): string {\n const root = normalizeBase(astroBase);\n return basePath.startsWith(root) ? basePath.slice(root.length) : \"\";\n}\n\n/**\n * Copies the published files to `<outDir>/<folder>_ascribe/files/`, `folder` being the\n * base path's (`folderUnder`). Returns whether there were any.\n */\nexport async function copyPublishedFiles(\n siteRoot: string,\n outDir: URL,\n folder = \"\",\n): Promise<boolean> {\n return copyDirectory(\n publishedDir(siteRoot),\n path.join(fileURLToPath(outDir), folder, \"_ascribe\", \"files\"),\n );\n}\n\n/** Copies the plain output for agents to the base path's folder of the build output. Returns whether there was one. */\nexport async function copyAgentFiles(\n plainRoot: string,\n outDir: URL,\n folder = \"\",\n): Promise<boolean> {\n return copyDirectory(plainRoot, path.join(fileURLToPath(outDir), folder));\n}\n\nasync function copyDirectory(from: string, to: string): Promise<boolean> {\n if (\n !(await stat(from).then(\n (s) => s.isDirectory(),\n () => false,\n ))\n )\n return false;\n await cp(from, to, { recursive: true });\n return true;\n}\n\nconst TYPES: Record<string, string> = {\n \".pdf\": \"application/pdf\",\n \".zip\": \"application/zip\",\n \".json\": \"application/json\",\n \".yaml\": \"text/yaml; charset=utf-8\",\n \".yml\": \"text/yaml; charset=utf-8\",\n \".txt\": \"text/plain; charset=utf-8\",\n \".csv\": \"text/csv; charset=utf-8\",\n \".md\": \"text/markdown; charset=utf-8\",\n \".html\": \"text/html; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".svg\": \"image/svg+xml\",\n};\n\ntype Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;\n\n/** A dev-server middleware serving the published files at `<base>_ascribe/files/`, `base` being the project's base path. */\nexport function filesMiddleware(siteRoot: string, base: string): Middleware {\n return directoryMiddleware(publishedDir(siteRoot), `${base}_ascribe/files/`);\n}\n\n/** A dev-server middleware serving the plain output for agents at `<base>`: `llms.txt`, each page's `.md`, and their files. */\nexport function agentsMiddleware(plainRoot: string, base: string): Middleware {\n return directoryMiddleware(plainRoot, base);\n}\n\n/** A middleware serving the files in `root` at the URL path `prefix`, which ends in `/`. */\nfunction directoryMiddleware(root: string, prefix: string): Middleware {\n // Only under the base path, as in the build. Vite strips the base from\n // `req.url` before a plugin's middleware runs, but connect keeps the original in `originalUrl`.\n return (req, res, next) => {\n const pathname =\n ((req as { originalUrl?: string }).originalUrl ?? req.url ?? \"\").split(/[?#]/, 1)[0] ?? \"\";\n if (!pathname.startsWith(prefix) || (req.method !== \"GET\" && req.method !== \"HEAD\"))\n return next();\n let relative: string;\n try {\n relative = decodeURIComponent(pathname.slice(prefix.length));\n } catch {\n return next();\n }\n const file = path.resolve(root, relative);\n if (file !== root && !file.startsWith(root + path.sep)) return next();\n stat(file).then(\n (info) => {\n if (!info.isFile()) return next();\n res.setHeader(\n \"Content-Type\",\n TYPES[path.extname(file).toLowerCase()] ?? \"application/octet-stream\",\n );\n res.setHeader(\"Content-Length\", info.size);\n if (req.method === \"HEAD\") return void res.end();\n createReadStream(file).pipe(res);\n },\n () => next(),\n );\n };\n}\n"]}
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"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","sourcesContent":["// Serving `_ascribe/files/`: the files pages link to that aren't pages or\n// images. Astro doesn't copy them, so the integration\n// does: into the build's output directory, and in dev through a middleware.\n// Both put them at `<base-path>_ascribe/files/`, the URL the site output links\n// to, where `<base-path>` is the project's `[consumer] base-path`: Astro's\n// `base` or a path under it.\n//\n// With `[consumer] agents = true`, the same for the plain output, which is\n// laid out by URL: `llms.txt`, each page's Markdown, and their files, all at\n// `<base-path>` and below.\nimport { createReadStream } from \"node:fs\";\nimport { cp, stat } from \"node:fs/promises\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The directory of published files inside a site output root. */\nfunction publishedDir(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"files\");\n}\n\n/**\n * The base path's folder in Astro's build output, which is at Astro's `base`:\n * `docs/` for a base path of `/docs/` under a `base` of `/`, `\"\"` when they're the same.\n */\nexport function folderUnder(basePath: string, astroBase: string): string {\n const root = normalizeBase(astroBase);\n return basePath.startsWith(root) ? basePath.slice(root.length) : \"\";\n}\n\n/**\n * Copies the published files to `<outDir>/<folder>_ascribe/files/`, `folder` being the\n * base path's (`folderUnder`). Returns whether there were any.\n */\nexport async function copyPublishedFiles(\n siteRoot: string,\n outDir: URL,\n folder = \"\",\n): Promise<boolean> {\n return copyDirectory(\n publishedDir(siteRoot),\n path.join(fileURLToPath(outDir), folder, \"_ascribe\", \"files\"),\n );\n}\n\n/** Copies the plain output for agents to the base path's folder of the build output. Returns whether there was one. */\nexport async function copyAgentFiles(\n plainRoot: string,\n outDir: URL,\n folder = \"\",\n): Promise<boolean> {\n return copyDirectory(plainRoot, path.join(fileURLToPath(outDir), folder));\n}\n\nasync function copyDirectory(from: string, to: string): Promise<boolean> {\n if (\n !(await stat(from).then(\n (s) => s.isDirectory(),\n () => false,\n ))\n )\n return false;\n await cp(from, to, { recursive: true });\n return true;\n}\n\nconst TYPES: Record<string, string> = {\n \".pdf\": \"application/pdf\",\n \".zip\": \"application/zip\",\n \".json\": \"application/json\",\n \".yaml\": \"text/yaml; charset=utf-8\",\n \".yml\": \"text/yaml; charset=utf-8\",\n \".txt\": \"text/plain; charset=utf-8\",\n \".csv\": \"text/csv; charset=utf-8\",\n \".md\": \"text/markdown; charset=utf-8\",\n \".html\": \"text/html; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".svg\": \"image/svg+xml\",\n};\n\ntype Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;\n\n/** A dev-server middleware serving the published files at `<base>_ascribe/files/`, `base` being the project's base path. */\nexport function filesMiddleware(siteRoot: string, base: string): Middleware {\n return directoryMiddleware(publishedDir(siteRoot), `${base}_ascribe/files/`);\n}\n\n/**\n * A dev-server middleware serving the plain output for agents at `<base>`: `llms.txt`, each page's\n * `.md`, and their files, each with the headers `headers` gives its path from the host's root.\n */\nexport function agentsMiddleware(\n plainRoot: string,\n base: string,\n headers: (pathname: string) => Record<string, string> = () => ({}),\n): Middleware {\n return directoryMiddleware(plainRoot, base, headers);\n}\n\n/**\n * A middleware serving the files in `root` at the URL path `prefix`, which ends in `/`, each with\n * the headers `headers` gives its path.\n */\nfunction directoryMiddleware(\n root: string,\n prefix: string,\n headers: (pathname: string) => Record<string, string> = () => ({}),\n): Middleware {\n // Only under the base path, as in the build. Vite strips the base from\n // `req.url` before a plugin's middleware runs, but connect keeps the original in `originalUrl`.\n return (req, res, next) => {\n const pathname =\n ((req as { originalUrl?: string }).originalUrl ?? req.url ?? \"\").split(/[?#]/, 1)[0] ?? \"\";\n if (!pathname.startsWith(prefix) || (req.method !== \"GET\" && req.method !== \"HEAD\"))\n return next();\n let relative: string;\n try {\n relative = decodeURIComponent(pathname.slice(prefix.length));\n } catch {\n return next();\n }\n const file = path.resolve(root, relative);\n if (file !== root && !file.startsWith(root + path.sep)) return next();\n stat(file).then(\n (info) => {\n if (!info.isFile()) return next();\n res.setHeader(\n \"Content-Type\",\n TYPES[path.extname(file).toLowerCase()] ?? \"application/octet-stream\",\n );\n res.setHeader(\"Content-Length\", info.size);\n for (const [name, value] of Object.entries(headers(pathname))) res.setHeader(name, value);\n if (req.method === \"HEAD\") return void res.end();\n createReadStream(file).pipe(res);\n },\n () => next(),\n );\n };\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import type { MiddlewareHandler } from "astro";
2
+ /** Where the integration leaves its logger's `warn` for this middleware, which Vite loads apart from it. */
3
+ export declare const WARN: unique symbol;
4
+ /** The warnings for `html`, the page at `route`: one per repeated tag not yet warned about. */
5
+ export declare function headWarnings(html: string, route: string, seen?: Set<string>): string[];
6
+ export declare const onRequest: MiddlewareHandler;
@@ -0,0 +1,28 @@
1
+ import { repeatedHeadTags } from "./head.js";
2
+ /** Where the integration leaves its logger's `warn` for this middleware, which Vite loads apart from it. */
3
+ export const WARN = Symbol.for("@ascribed/astro:head-warn");
4
+ /** The tags already warned about. */
5
+ const warned = new Set();
6
+ /** The warnings for `html`, the page at `route`: one per repeated tag not yet warned about. */
7
+ export function headWarnings(html, route, seen = warned) {
8
+ return repeatedHeadTags(html)
9
+ .filter((tag) => {
10
+ if (seen.has(tag))
11
+ return false;
12
+ seen.add(tag);
13
+ return true;
14
+ })
15
+ .map((tag) => `${route}'s head has ${tag} twice. Head.astro writes it from the page's record, so the layout doesn't need to.`);
16
+ }
17
+ export const onRequest = async (context, next) => {
18
+ const response = await next();
19
+ if (!(response.headers.get("content-type") ?? "").startsWith("text/html"))
20
+ return response;
21
+ const html = await response.text();
22
+ const warn = globalThis[WARN] ??
23
+ ((message) => console.warn(`[@ascribed/astro] ${message}`));
24
+ for (const message of headWarnings(html, context.url.pathname))
25
+ warn(message);
26
+ return new Response(html, response);
27
+ };
28
+ //# sourceMappingURL=head-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"head-check.js","sourceRoot":"","sources":["../src/head-check.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,4GAA4G;AAC5G,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;AAE5D,qCAAqC;AACrC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;AAEjC,+FAA+F;AAC/F,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,KAAa,EAAE,IAAI,GAAgB,MAAM;IAClF,OAAO,gBAAgB,CAAC,IAAI,CAAC;SAC1B,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE;QACd,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAChC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;SACD,GAAG,CACF,CAAC,GAAG,EAAE,EAAE,CACN,GAAG,KAAK,eAAe,GAAG,qFAAqF,CAClH,CAAC;AACN,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAsB,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAClE,MAAM,QAAQ,GAAG,MAAM,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC3F,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,MAAM,IAAI,GACN,UAAsC,CAAC,IAAI,CAA6C;QAC1F,CAAC,CAAC,OAAe,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,qBAAqB,OAAO,EAAE,CAAC,CAAC,CAAC;IACtE,KAAK,MAAM,OAAO,IAAI,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9E,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACtC,CAAC,CAAC","sourcesContent":["// `astro dev`'s check of each page's head: a tag `Head.astro` writes that the\n// head has twice, because the layout writes it too, is warned about once,\n// naming the tag. Not an error: a site may have a reason. The integration adds\n// this middleware in `astro dev` only.\nimport type { MiddlewareHandler } from \"astro\";\nimport { repeatedHeadTags } from \"./head.js\";\n\n/** Where the integration leaves its logger's `warn` for this middleware, which Vite loads apart from it. */\nexport const WARN = Symbol.for(\"@ascribed/astro:head-warn\");\n\n/** The tags already warned about. */\nconst warned = new Set<string>();\n\n/** The warnings for `html`, the page at `route`: one per repeated tag not yet warned about. */\nexport function headWarnings(html: string, route: string, seen: Set<string> = warned): string[] {\n return repeatedHeadTags(html)\n .filter((tag) => {\n if (seen.has(tag)) return false;\n seen.add(tag);\n return true;\n })\n .map(\n (tag) =>\n `${route}'s head has ${tag} twice. Head.astro writes it from the page's record, so the layout doesn't need to.`,\n );\n}\n\nexport const onRequest: MiddlewareHandler = async (context, next) => {\n const response = await next();\n if (!(response.headers.get(\"content-type\") ?? \"\").startsWith(\"text/html\")) return response;\n const html = await response.text();\n const warn =\n ((globalThis as Record<symbol, unknown>)[WARN] as ((message: string) => void) | undefined) ??\n ((message: string) => console.warn(`[@ascribed/astro] ${message}`));\n for (const message of headWarnings(html, context.url.pathname)) warn(message);\n return new Response(html, response);\n};\n"]}
package/dist/head.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { PageRecord } from "./records.js";
2
+ /** A page's social card: its image, what it shows, and its size in pixels. */
3
+ export interface Card {
4
+ /** The image's address: absolute, or from the host's root, resolved against the site's address. */
5
+ src: string;
6
+ /** What the image shows, for a reader who can't see it. */
7
+ alt: string;
8
+ width?: number;
9
+ height?: number;
10
+ }
11
+ /** What a site says about itself, which a record doesn't hold. */
12
+ export interface HeadOptions {
13
+ /** The whole title, from the page's title and its record. Default: the page's title. */
14
+ title?: ((title: string, record: PageRecord) => string) | undefined;
15
+ /** The site's name, for `og:site_name` and, on the page at the host's root, the site-name structured data. */
16
+ name?: string | undefined;
17
+ /** The page's social card. Without one, no image tags. */
18
+ card?: Card | undefined;
19
+ /** The site's address (Astro's `site`, or the request's), for addresses the record doesn't make absolute. */
20
+ address: string | URL;
21
+ }
22
+ /** One tag of the head. */
23
+ export type HeadTag = {
24
+ tag: "title";
25
+ text: string;
26
+ } | {
27
+ tag: "meta" | "link";
28
+ attributes: Record<string, string>;
29
+ } | {
30
+ tag: "script";
31
+ json: unknown;
32
+ };
33
+ /**
34
+ * The tags of a page's head that are about the page, in order: the title and
35
+ * the description; the canonical link; the robots rule; the link to the
36
+ * Markdown version, when the build publishes one and the page is offered; the
37
+ * Open Graph tags a link preview reads, the card's among them when there is
38
+ * one; the breadcrumbs, as structured data, when there are two steps or more;
39
+ * and, on the page at the host's root, the site's name, as structured data.
40
+ * The title, the description, and the address are the record's, so the head
41
+ * can't disagree with the sitemap or `llms.txt`.
42
+ */
43
+ export declare function headTags(record: PageRecord, options: HeadOptions): HeadTag[];
44
+ /**
45
+ * JSON for a `<script>` element: `<` written as `\u003c`, so no string in it
46
+ * can close the element.
47
+ */
48
+ export declare function scriptJson(json: unknown): string;
49
+ /**
50
+ * The tags that a page's `<head>` has more than once, of those `Head.astro`
51
+ * writes, named as `<title>` or `<meta property="og:title">` are. A layout
52
+ * that includes the component and writes one of them too has two.
53
+ */
54
+ export declare function repeatedHeadTags(html: string): string[];