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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,8 +56,8 @@ import Head from "@ascribed/astro/Head.astro";
56
56
  const { entry } = Astro.props;
57
57
  ---
58
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`} />
59
+ <!-- Everything the head says about the page, from its record. -->
60
+ <Head entry={entry} title={(title, record) => `${title} · Loom`} name="Loom" />
61
61
  <Elements />
62
62
  </head>
63
63
  <Availability available={entry.data.available} />
@@ -65,7 +65,9 @@ const { entry } = Astro.props;
65
65
  <h1 set:html={entry.data.formatted.title} />
66
66
  ```
67
67
 
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.
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.
69
71
 
70
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.
71
73
 
package/dist/content.d.ts CHANGED
@@ -2,7 +2,8 @@ import { glob } from "astro/loaders";
2
2
  import { type PageRecord } from "./records.js";
3
3
  import { type NavEntry, type NavGroup, type SidebarGroup } from "./nav.js";
4
4
  export type { NavGroup, SidebarGroup, SidebarPage } from "./nav.js";
5
- export type { Listing, PageRecord } from "./records.js";
5
+ export type { Listing, ListingFrom, PageRecord, Step } from "./records.js";
6
+ export { headTags, scriptJson, type Card, type HeadOptions, type HeadTag } from "./head.js";
6
7
  /**
7
8
  * The collection's loader and schema: the `glob` loader on the site output's
8
9
  * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).
package/dist/content.js CHANGED
@@ -24,6 +24,7 @@ import { glob } from "astro/loaders";
24
24
  import { astroRoot, collections, routes, siteRoot } from "virtual:ascribe/site";
25
25
  import { recordFor } from "./records.js";
26
26
  import { pageRoute, sidebarGroups, } from "./nav.js";
27
+ export { headTags, scriptJson } from "./head.js";
27
28
  /**
28
29
  * The collection's loader and schema: the `glob` loader on the site output's
29
30
  * pages, with the generated Zod schema (`_ascribe/schema.ts`'s `schema`).
@@ -1 +1 @@
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"]}
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":"AAAA,0EAA0E;AAC1E,sDAAsD;AACtD,4EAA4E;AAC5E,+EAA+E;AAC/E,2EAA2E;AAC3E,6BAA6B;AAC7B,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,2BAA2B;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,kEAAkE;AAClE,SAAS,YAAY,CAAC,QAAgB;IACpC,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;AAClD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB,EAAE,SAAiB;IAC7D,MAAM,IAAI,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;IACtC,OAAO,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACtE,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,QAAgB,EAChB,MAAW,EACX,MAAM,GAAG,EAAE;IAEX,OAAO,aAAa,CAClB,YAAY,CAAC,QAAQ,CAAC,EACtB,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,CAC9D,CAAC;AACJ,CAAC;AAED,uHAAuH;AACvH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,SAAiB,EACjB,MAAW,EACX,MAAM,GAAG,EAAE;IAEX,OAAO,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAAY,EAAE,EAAU;IACnD,IACE,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CACrB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,EACtB,GAAG,EAAE,CAAC,KAAK,CACZ,CAAC;QAEF,OAAO,KAAK,CAAC;IACf,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,KAAK,GAA2B;IACpC,MAAM,EAAE,iBAAiB;IACzB,MAAM,EAAE,iBAAiB;IACzB,OAAO,EAAE,kBAAkB;IAC3B,OAAO,EAAE,0BAA0B;IACnC,MAAM,EAAE,0BAA0B;IAClC,MAAM,EAAE,2BAA2B;IACnC,MAAM,EAAE,yBAAyB;IACjC,KAAK,EAAE,8BAA8B;IACrC,OAAO,EAAE,0BAA0B;IACnC,MAAM,EAAE,yBAAyB;IACjC,KAAK,EAAE,gCAAgC;IACvC,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,WAAW;IACnB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,eAAe;CACxB,CAAC;AAIF,4HAA4H;AAC5H,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,IAAY;IAC5D,OAAO,mBAAmB,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,GAAG,IAAI,iBAAiB,CAAC,CAAC;AAC/E,CAAC;AAED,+HAA+H;AAC/H,MAAM,UAAU,gBAAgB,CAAC,SAAiB,EAAE,IAAY;IAC9D,OAAO,mBAAmB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AAC9C,CAAC;AAED,4FAA4F;AAC5F,SAAS,mBAAmB,CAAC,IAAY,EAAE,MAAc;IACvD,uEAAuE;IACvE,gGAAgG;IAChG,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,MAAM,QAAQ,GACZ,CAAE,GAAgC,CAAC,WAAW,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7F,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC;YACjF,OAAO,IAAI,EAAE,CAAC;QAChB,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC1C,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,EAAE,CAAC;QACtE,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CACb,CAAC,IAAI,EAAE,EAAE;YACP,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;gBAAE,OAAO,IAAI,EAAE,CAAC;YAClC,GAAG,CAAC,SAAS,CACX,cAAc,EACd,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,0BAA0B,CACtE,CAAC;YACF,GAAG,CAAC,SAAS,CAAC,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;gBAAE,OAAO,KAAK,GAAG,CAAC,GAAG,EAAE,CAAC;YACjD,gBAAgB,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnC,CAAC,EACD,GAAG,EAAE,CAAC,IAAI,EAAE,CACb,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC","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[];
package/dist/head.js ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The tags of a page's head that are about the page, in order: the title and
3
+ * the description; the canonical link; the robots rule; the link to the
4
+ * Markdown version, when the build publishes one and the page is offered; the
5
+ * Open Graph tags a link preview reads, the card's among them when there is
6
+ * one; the breadcrumbs, as structured data, when there are two steps or more;
7
+ * and, on the page at the host's root, the site's name, as structured data.
8
+ * The title, the description, and the address are the record's, so the head
9
+ * can't disagree with the sitemap or `llms.txt`.
10
+ */
11
+ export function headTags(record, options) {
12
+ const tags = [];
13
+ const meta = (attributes) => tags.push({ tag: "meta", attributes });
14
+ const link = (attributes) => tags.push({ tag: "link", attributes });
15
+ // A step's address is on the record's site when it has one.
16
+ const absolute = (route) => new URL(route, record.canonical ?? options.address).href;
17
+ const title = record.title === undefined
18
+ ? undefined
19
+ : options.title === undefined
20
+ ? record.title
21
+ : options.title(record.title, record);
22
+ if (title !== undefined)
23
+ tags.push({ tag: "title", text: title });
24
+ if (record.description !== undefined)
25
+ meta({ name: "description", content: record.description });
26
+ if (record.canonical !== undefined)
27
+ link({ rel: "canonical", href: record.canonical });
28
+ if (record.listing === "unlisted")
29
+ meta({ name: "robots", content: "noindex" });
30
+ if (record.listing === "unquoted")
31
+ meta({ name: "robots", content: "nosnippet" });
32
+ if (record.markdown !== undefined && record.listing !== "unlisted")
33
+ link({ rel: "alternate", type: "text/markdown", href: record.markdown });
34
+ meta({ property: "og:type", content: "website" });
35
+ if (options.name !== undefined)
36
+ meta({ property: "og:site_name", content: options.name });
37
+ if (title !== undefined)
38
+ meta({ property: "og:title", content: title });
39
+ if (record.description !== undefined)
40
+ meta({ property: "og:description", content: record.description });
41
+ if (record.canonical !== undefined)
42
+ meta({ property: "og:url", content: record.canonical });
43
+ const card = options.card;
44
+ if (card !== undefined) {
45
+ meta({ property: "og:image", content: new URL(card.src, options.address).href });
46
+ if (card.width !== undefined)
47
+ meta({ property: "og:image:width", content: String(card.width) });
48
+ if (card.height !== undefined)
49
+ meta({ property: "og:image:height", content: String(card.height) });
50
+ meta({ property: "og:image:alt", content: card.alt });
51
+ }
52
+ // Google reads a trail of two steps or more.
53
+ const steps = record.breadcrumbs ?? [];
54
+ if (steps.length >= 2) {
55
+ tags.push({
56
+ tag: "script",
57
+ json: {
58
+ "@context": "https://schema.org",
59
+ "@type": "BreadcrumbList",
60
+ itemListElement: steps.map((step, at) => ({
61
+ "@type": "ListItem",
62
+ position: at + 1,
63
+ name: step.title,
64
+ item: absolute(step.route),
65
+ })),
66
+ },
67
+ });
68
+ }
69
+ // The site's name, on the home page alone: the host's root.
70
+ if (options.name !== undefined && record.route === "/") {
71
+ tags.push({
72
+ tag: "script",
73
+ json: {
74
+ "@context": "https://schema.org",
75
+ "@type": "WebSite",
76
+ name: options.name,
77
+ url: absolute("/"),
78
+ },
79
+ });
80
+ }
81
+ return tags;
82
+ }
83
+ /**
84
+ * JSON for a `<script>` element: `<` written as `\u003c`, so no string in it
85
+ * can close the element.
86
+ */
87
+ export function scriptJson(json) {
88
+ return JSON.stringify(json).replaceAll("<", "\\u003c");
89
+ }
90
+ /** The tags `Head.astro` writes that a head should have once, each with how to find it. */
91
+ const ONCE = [
92
+ { name: "<title>", pattern: /<title[\s>]/gi },
93
+ ...["description", "robots"].map((name) => ({
94
+ name: `<meta name="${name}">`,
95
+ pattern: new RegExp(`<meta\\s[^>]*name=["']?${name}["'\\s/>]`, "gi"),
96
+ })),
97
+ { name: '<link rel="canonical">', pattern: /<link\s[^>]*rel=["']?canonical["'\s/>]/gi },
98
+ ...["og:type", "og:site_name", "og:title", "og:description", "og:url"].map((name) => ({
99
+ name: `<meta property="${name}">`,
100
+ pattern: new RegExp(`<meta\\s[^>]*property=["']?${name}["'\\s/>]`, "gi"),
101
+ })),
102
+ ];
103
+ /**
104
+ * The tags that a page's `<head>` has more than once, of those `Head.astro`
105
+ * writes, named as `<title>` or `<meta property="og:title">` are. A layout
106
+ * that includes the component and writes one of them too has two.
107
+ */
108
+ export function repeatedHeadTags(html) {
109
+ const head = /<head[\s>][\s\S]*?<\/head>/i.exec(html)?.[0];
110
+ if (head === undefined)
111
+ return [];
112
+ return ONCE.filter(({ pattern }) => (head.match(pattern) ?? []).length > 1).map(({ name }) => name);
113
+ }
114
+ //# sourceMappingURL=head.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"head.js","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"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","sourcesContent":["// What a page's head says about the page, from its record: the tags\n// `Head.astro` writes, as data, so each can be tested against the record. And\n// the check `astro dev` makes that a layout doesn't write one of them again.\nimport type { PageRecord } from \"./records.js\";\n\n/** A page's social card: its image, what it shows, and its size in pixels. */\nexport interface Card {\n /** The image's address: absolute, or from the host's root, resolved against the site's address. */\n src: string;\n /** What the image shows, for a reader who can't see it. */\n alt: string;\n width?: number;\n height?: number;\n}\n\n/** What a site says about itself, which a record doesn't hold. */\nexport interface HeadOptions {\n /** The whole title, from the page's title and its record. Default: the page's title. */\n title?: ((title: string, record: PageRecord) => string) | undefined;\n /** The site's name, for `og:site_name` and, on the page at the host's root, the site-name structured data. */\n name?: string | undefined;\n /** The page's social card. Without one, no image tags. */\n card?: Card | undefined;\n /** The site's address (Astro's `site`, or the request's), for addresses the record doesn't make absolute. */\n address: string | URL;\n}\n\n/** One tag of the head. */\nexport type HeadTag =\n | { tag: \"title\"; text: string }\n | { tag: \"meta\" | \"link\"; attributes: Record<string, string> }\n | { tag: \"script\"; json: unknown };\n\n/**\n * The tags of a page's head that are about the page, in order: the title and\n * the description; the canonical link; the robots rule; the link to the\n * Markdown version, when the build publishes one and the page is offered; the\n * Open Graph tags a link preview reads, the card's among them when there is\n * one; the breadcrumbs, as structured data, when there are two steps or more;\n * and, on the page at the host's root, the site's name, as structured data.\n * The title, the description, and the address are the record's, so the head\n * can't disagree with the sitemap or `llms.txt`.\n */\nexport function headTags(record: PageRecord, options: HeadOptions): HeadTag[] {\n const tags: HeadTag[] = [];\n const meta = (attributes: Record<string, string>) => tags.push({ tag: \"meta\", attributes });\n const link = (attributes: Record<string, string>) => tags.push({ tag: \"link\", attributes });\n // A step's address is on the record's site when it has one.\n const absolute = (route: string) => new URL(route, record.canonical ?? options.address).href;\n\n const title =\n record.title === undefined\n ? undefined\n : options.title === undefined\n ? record.title\n : options.title(record.title, record);\n if (title !== undefined) tags.push({ tag: \"title\", text: title });\n if (record.description !== undefined) meta({ name: \"description\", content: record.description });\n if (record.canonical !== undefined) link({ rel: \"canonical\", href: record.canonical });\n if (record.listing === \"unlisted\") meta({ name: \"robots\", content: \"noindex\" });\n if (record.listing === \"unquoted\") meta({ name: \"robots\", content: \"nosnippet\" });\n if (record.markdown !== undefined && record.listing !== \"unlisted\")\n link({ rel: \"alternate\", type: \"text/markdown\", href: record.markdown });\n\n meta({ property: \"og:type\", content: \"website\" });\n if (options.name !== undefined) meta({ property: \"og:site_name\", content: options.name });\n if (title !== undefined) meta({ property: \"og:title\", content: title });\n if (record.description !== undefined)\n meta({ property: \"og:description\", content: record.description });\n if (record.canonical !== undefined) meta({ property: \"og:url\", content: record.canonical });\n const card = options.card;\n if (card !== undefined) {\n meta({ property: \"og:image\", content: new URL(card.src, options.address).href });\n if (card.width !== undefined) meta({ property: \"og:image:width\", content: String(card.width) });\n if (card.height !== undefined)\n meta({ property: \"og:image:height\", content: String(card.height) });\n meta({ property: \"og:image:alt\", content: card.alt });\n }\n\n // Google reads a trail of two steps or more.\n const steps = record.breadcrumbs ?? [];\n if (steps.length >= 2) {\n tags.push({\n tag: \"script\",\n json: {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: steps.map((step, at) => ({\n \"@type\": \"ListItem\",\n position: at + 1,\n name: step.title,\n item: absolute(step.route),\n })),\n },\n });\n }\n // The site's name, on the home page alone: the host's root.\n if (options.name !== undefined && record.route === \"/\") {\n tags.push({\n tag: \"script\",\n json: {\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebSite\",\n name: options.name,\n url: absolute(\"/\"),\n },\n });\n }\n return tags;\n}\n\n/**\n * JSON for a `<script>` element: `<` written as `\\u003c`, so no string in it\n * can close the element.\n */\nexport function scriptJson(json: unknown): string {\n return JSON.stringify(json).replaceAll(\"<\", \"\\\\u003c\");\n}\n\n/** The tags `Head.astro` writes that a head should have once, each with how to find it. */\nconst ONCE: { name: string; pattern: RegExp }[] = [\n { name: \"<title>\", pattern: /<title[\\s>]/gi },\n ...[\"description\", \"robots\"].map((name) => ({\n name: `<meta name=\"${name}\">`,\n pattern: new RegExp(`<meta\\\\s[^>]*name=[\"']?${name}[\"'\\\\s/>]`, \"gi\"),\n })),\n { name: '<link rel=\"canonical\">', pattern: /<link\\s[^>]*rel=[\"']?canonical[\"'\\s/>]/gi },\n ...[\"og:type\", \"og:site_name\", \"og:title\", \"og:description\", \"og:url\"].map((name) => ({\n name: `<meta property=\"${name}\">`,\n pattern: new RegExp(`<meta\\\\s[^>]*property=[\"']?${name}[\"'\\\\s/>]`, \"gi\"),\n })),\n];\n\n/**\n * The tags that a page's `<head>` has more than once, of those `Head.astro`\n * writes, named as `<title>` or `<meta property=\"og:title\">` are. A layout\n * that includes the component and writes one of them too has two.\n */\nexport function repeatedHeadTags(html: string): string[] {\n const head = /<head[\\s>][\\s\\S]*?<\\/head>/i.exec(html)?.[0];\n if (head === undefined) return [];\n return ONCE.filter(({ pattern }) => (head.match(pattern) ?? []).length > 1).map(\n ({ name }) => name,\n );\n}\n"]}
@@ -0,0 +1,50 @@
1
+ /** The headers a host sends with one path, matched exactly. */
2
+ export interface HeaderRule {
3
+ /** The path, from the host's root. */
4
+ path: string;
5
+ /** Each header's value, by its name. */
6
+ headers: Record<string, string>;
7
+ }
8
+ /** The rules in a build's site output, or `undefined` when it has no list. */
9
+ export declare function readHeaders(siteRoot: string): HeaderRule[] | undefined;
10
+ /**
11
+ * The headers the rules in the build whose site output is at `siteRoot` give
12
+ * `pathname`, a path from the host's root: none when no rule names it. Read
13
+ * again when the file changes, as it does with each build in `astro dev`.
14
+ */
15
+ export declare function headersFor(siteRoot: string, pathname: string): Record<string, string>;
16
+ /**
17
+ * Every project's rules as one list, sorted by path: a path two projects
18
+ * both have keeps the first's rule, as the first entry's files win.
19
+ */
20
+ export declare function mergeRules(projects: readonly (readonly HeaderRule[])[]): HeaderRule[];
21
+ /** The rules in Netlify's `_headers` format: each path, then its headers indented below it. */
22
+ export declare function netlifyHeaders(rules: readonly HeaderRule[]): string;
23
+ /**
24
+ * The rules, each with only the headers the site's own `_headers` doesn't
25
+ * give its path, leaving out a rule it has whole. Empty when it has every
26
+ * line.
27
+ */
28
+ export declare function missingRules(own: string, rules: readonly HeaderRule[]): HeaderRule[];
29
+ /** What the build does about the host's headers. */
30
+ export type HeadersOutcome = {
31
+ kind: "none";
32
+ } | {
33
+ kind: "write";
34
+ text: string;
35
+ } | {
36
+ kind: "note";
37
+ text: string;
38
+ } | {
39
+ kind: "fail";
40
+ text: string;
41
+ };
42
+ /** What the build does about `_headers`, given every project's rules and the site. */
43
+ export declare function headersOutcome(sources: {
44
+ /** The rules of every project that sets `[consumer] headers = "netlify"`. */
45
+ rules: readonly HeaderRule[];
46
+ /** Astro's `base`. */
47
+ base: string;
48
+ /** The site's own `public/_headers`, when it has one. */
49
+ own: string | undefined;
50
+ }): HeadersOutcome;