@minidocs/astro 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,10 +16,24 @@ import { defineConfig } from 'astro/config';
16
16
  import minidocs from '@minidocs/astro';
17
17
 
18
18
  export default defineConfig({
19
- integrations: [mdx(), minidocs({ prefix: 'docs' })],
19
+ integrations: [mdx(), minidocs()],
20
20
  });
21
21
  ```
22
22
 
23
+ ```ts
24
+ // src/content.config.ts
25
+ import { defineCollection } from 'astro:content';
26
+ import { z } from 'astro/zod';
27
+ import { glob } from '@minidocs/astro/loaders';
28
+
29
+ const docs = defineCollection({
30
+ loader: glob({ base: './src/content/docs', pattern: '**/*.{md,mdx}', prefix: 'docs' }),
31
+ schema: z.object({ title: z.string() }),
32
+ });
33
+
34
+ export const collections = { docs };
35
+ ```
36
+
23
37
  Requires Astro 7 and Node.js 22.22 or later. See [Getting Started](https://github.com/Ivan-1F/minidocs/blob/main/docs/src/content/docs/en/getting-started.mdx) for the rest of the setup.
24
38
 
25
39
  ## License
package/dist/index.d.mts CHANGED
@@ -1,8 +1,6 @@
1
1
  import { AstroIntegration } from "astro";
2
2
  //#region src/settings.d.ts
3
3
  interface MinidocsOptions {
4
- /** Path segment the docs live under, e.g. `docs` for `/docs/...`. Defaults to no prefix. */
5
- prefix?: string;
6
4
  /** Locales to try, in order, when a page is not translated. Locales without an entry never fall back. Requires `i18n`. */
7
5
  fallback?: Record<string, string | string[]>;
8
6
  /** Markdown syntax minidocs adds on top of the configured processor. */
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { t as contentOptions } from "./routing-TMqRCY2Q.mjs";
2
- import { a as renderConfigTypes, i as registry, n as remarkContentLinks, r as satteriContentLinks } from "./links-UTLyoo_o.mjs";
1
+ import { t as contentOptions } from "./routing-D28b92w8.mjs";
2
+ import { a as registry, c as renderConfigTypes, i as namedCollections, n as remarkContentLinks, o as findUnbuiltPages, r as satteriContentLinks } from "./links-Dp7d5R-O.mjs";
3
3
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
4
  import { dirname } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -175,7 +175,6 @@ function extendProcessor(processor, callouts) {
175
175
  const trim = (path) => path.replace(/^\/+|\/+$/g, "");
176
176
  function resolveSettings(config, options) {
177
177
  return {
178
- prefix: trim(options.prefix ?? ""),
179
178
  base: trim(config.base),
180
179
  trailingSlash: config.trailingSlash === "always" || config.trailingSlash === "ignore" && config.build.format === "directory",
181
180
  i18n: resolveI18n(config.i18n, options),
@@ -208,6 +207,7 @@ const CONFIG_ID = "virtual:minidocs/config";
208
207
  const RENDERERS_ID = "virtual:minidocs/renderers";
209
208
  function minidocs(options = {}) {
210
209
  let hasMdx = false;
210
+ let isStatic = false;
211
211
  return {
212
212
  name: "minidocs",
213
213
  hooks: {
@@ -223,13 +223,16 @@ function minidocs(options = {}) {
223
223
  resolveId: (id) => id === CONFIG_ID ? `\0${CONFIG_ID}` : void 0,
224
224
  load: (id) => id === `\0${CONFIG_ID}` ? `export default ${JSON.stringify({
225
225
  ...registry.settings,
226
- bases: Object.fromEntries(Object.entries(registry.collections).map(([name, { base }]) => [name, base])),
227
- pageIds: Object.fromEntries(Object.entries(registry.collections).map(([name, collection]) => [name, pageIds(collection)]))
226
+ collections: Object.fromEntries(Object.entries(namedCollections()).map(([name, collection]) => [name, {
227
+ base: collection.base,
228
+ prefix: collection.prefix,
229
+ pageIds: pageIds(collection)
230
+ }]))
228
231
  })};` : void 0
229
232
  }, {
230
233
  name: "minidocs:renderers",
231
234
  resolveId: (id) => id === RENDERERS_ID ? `\0${RENDERERS_ID}` : void 0,
232
- load: (id) => id === `\0${RENDERERS_ID}` ? hasMdx ? `import { getContainerRenderer } from '@astrojs/mdx';\nexport default [getContainerRenderer()];` : "export default [];" : void 0
235
+ load: (id) => id === `\0${RENDERERS_ID}` ? hasMdx ? `import { getContainerRenderer } from '@astrojs/mdx/container-renderer';\nexport default [getContainerRenderer()];` : "export default [];" : void 0
233
236
  }],
234
237
  ssr: { noExternal: ["@minidocs/astro", "@minidocs/core"] }
235
238
  } });
@@ -238,6 +241,7 @@ function minidocs(options = {}) {
238
241
  registry.settings = resolveSettings(config, options);
239
242
  registry.root = fileURLToPath(config.root);
240
243
  hasMdx = config.integrations.some(({ name }) => name === "@astrojs/mdx");
244
+ isStatic = config.output === "static" && !config.adapter;
241
245
  const injected = injectTypes({
242
246
  filename: "types.d.ts",
243
247
  content: renderConfigTypes(registry.settings)
@@ -248,15 +252,24 @@ function minidocs(options = {}) {
248
252
  mkdirSync(dirname(path), { recursive: true });
249
253
  writeFileSync(path, "export {};\n");
250
254
  }
255
+ },
256
+ "astro:build:done"({ pages }) {
257
+ if (!isStatic || !registry.settings) return;
258
+ const built = new Set(pages.map(({ pathname }) => pathname.replace(/^\/+|\/+$/g, "")));
259
+ const missing = findUnbuiltPages(registry.settings, namedCollections(), built);
260
+ if (missing.length === 0) return;
261
+ const shown = missing.slice(0, 10).map((page) => ` ${page}`);
262
+ if (missing.length > shown.length) shown.push(` and ${missing.length - shown.length} more`);
263
+ throw new Error(`[minidocs] No route built these pages, so links to them are broken. Check that each collection's route file sits in the folder its prefix names.\n${shown.join("\n")}`);
251
264
  }
252
265
  }
253
266
  };
254
267
  }
255
268
  /** Ids of every page in a collection, in any locale. */
256
- function pageIds({ base, filePaths }) {
269
+ function pageIds(collection) {
257
270
  if (!registry.settings) return [];
258
- const { locales, localeDirs } = contentOptions(registry.settings, base);
259
- return [...new Set(filePaths.map((filePath) => parseFilePath(filePath, base, locales, localeDirs).id))];
271
+ const { locales, localeDirs } = contentOptions(registry.settings, collection.base);
272
+ return [...new Set(collection.filePaths.map((filePath) => parseFilePath(filePath, collection.base, locales, localeDirs).id))];
260
273
  }
261
274
  //#endregion
262
275
  export { minidocs as default };
@@ -1,4 +1,4 @@
1
- import { n as href, t as contentOptions } from "./routing-TMqRCY2Q.mjs";
1
+ import { i as markdownHref, n as href, o as routePath, t as contentOptions } from "./routing-D28b92w8.mjs";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { dirname, posix, relative, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -8,6 +8,14 @@ const union = (values) => {
8
8
  const list = [...new Set(values)].sort().map((value) => JSON.stringify(value));
9
9
  return list.length > 0 ? list.join(" | ") : "never";
10
10
  };
11
+ const byName = (collections) => Object.entries(collections).sort(([a], [b]) => a < b ? -1 : 1);
12
+ /** Every page a collection needs a route for, with the locale its URL is built with. */
13
+ function pagesOf(settings, collection) {
14
+ return listPages(collection.filePaths.map((filePath) => ({ filePath })), contentOptions(settings, collection.base)).map((page) => ({
15
+ ...page,
16
+ urlLocale: settings.i18n ? page.locale : void 0
17
+ }));
18
+ }
11
19
  /** Renders the site-wide types, known as soon as the config is resolved. */
12
20
  function renderConfigTypes(settings) {
13
21
  return [
@@ -27,14 +35,14 @@ function renderTypes(settings, collections) {
27
35
  "declare module '@minidocs/astro/runtime' {",
28
36
  " interface Collections {"
29
37
  ];
30
- for (const [name, { base, filePaths }] of Object.entries(collections).sort(([a], [b]) => a < b ? -1 : 1)) {
31
- const options = contentOptions(settings, base);
32
- const pages = listPages(filePaths.map((filePath) => ({ filePath })), options);
38
+ for (const [name, collection] of byName(collections)) {
39
+ const options = contentOptions(settings, collection.base);
40
+ const pages = pagesOf(settings, collection);
33
41
  const idsAndDirs = (locale) => {
34
42
  const inLocale = pages.filter((page) => page.locale === locale);
35
43
  const dirs = /* @__PURE__ */ new Set([""]);
36
44
  for (const page of inLocale) {
37
- const { dir } = parseFilePath(page.entry.filePath, base, options.locales, options.localeDirs);
45
+ const { dir } = parseFilePath(page.entry.filePath, collection.base, options.locales, options.localeDirs);
38
46
  const segments = dir.split("/").filter(Boolean);
39
47
  for (let i = 1; i <= segments.length; i++) dirs.add(segments.slice(0, i).join("/"));
40
48
  }
@@ -45,7 +53,7 @@ function renderTypes(settings, collections) {
45
53
  };
46
54
  lines.push(`\t\t${JSON.stringify(name)}: {`);
47
55
  if (settings.i18n) {
48
- const perLocale = settings.i18n.locales.map((locale) => [JSON.stringify(locale), idsAndDirs(locale)]);
56
+ const perLocale = options.locales.map((locale) => [JSON.stringify(locale), idsAndDirs(locale)]);
49
57
  lines.push(" pages: {", ...perLocale.map(([locale, { ids }]) => `\t\t\t\t${locale}: ${ids};`), " };");
50
58
  lines.push(" dirs: {", ...perLocale.map(([locale, { dirs }]) => `\t\t\t\t${locale}: ${dirs};`), " };");
51
59
  } else {
@@ -57,25 +65,66 @@ function renderTypes(settings, collections) {
57
65
  lines.push(" }", "}", "export {};", "");
58
66
  return lines.join("\n");
59
67
  }
68
+ /**
69
+ * Finds pages that would be served at the same URL, across all collections, for HTML pages and Markdown versions alike.
70
+ * Returns one message per pair of files.
71
+ */
72
+ function findUrlConflicts(settings, collections) {
73
+ const owners = /* @__PURE__ */ new Map();
74
+ const conflicts = /* @__PURE__ */ new Map();
75
+ for (const [name, collection] of byName(collections)) for (const page of pagesOf(settings, collection)) {
76
+ const owner = `${page.entry.filePath} (${name})`;
77
+ const urls = [href(settings, collection, page.id, page.urlLocale), markdownHref(settings, collection, page.id, page.urlLocale)];
78
+ for (const url of urls) {
79
+ const other = owners.get(url);
80
+ if (other === void 0) owners.set(url, owner);
81
+ else if (!conflicts.has(`${other}\n${owner}`)) conflicts.set(`${other}\n${owner}`, `"${url}" is the URL of two pages:\n ${other}\n ${owner}`);
82
+ }
83
+ }
84
+ return [...conflicts.values()];
85
+ }
86
+ /**
87
+ * Finds pages that no route generated, given the paths of built pages below `base`.
88
+ * A route file outside the folder that `prefix` names builds its pages elsewhere, and every link to them breaks.
89
+ */
90
+ function findUnbuiltPages(settings, collections, built) {
91
+ const missing = [];
92
+ for (const [name, collection] of byName(collections)) for (const page of pagesOf(settings, collection)) {
93
+ const path = routePath(settings, collection, page.id, page.urlLocale) ?? "";
94
+ if (!built.has(path)) missing.push(`/${path} for ${page.entry.filePath} (${name}, prefix "${collection.prefix}")`);
95
+ }
96
+ return missing;
97
+ }
60
98
  //#endregion
61
99
  //#region src/registry.ts
62
100
  const key = Symbol.for("minidocs.registry");
63
101
  const scope = globalThis;
64
- scope[key] ??= { collections: {} };
102
+ scope[key] ??= {
103
+ collections: {},
104
+ pendingLinks: []
105
+ };
65
106
  const registry = scope[key];
107
+ /** Collections whose loader has run, keyed by collection name. */
108
+ function namedCollections() {
109
+ return Object.fromEntries(Object.values(registry.collections).filter((state) => state.name !== void 0).map((state) => [state.name, state]));
110
+ }
66
111
  //#endregion
67
112
  //#region src/links.ts
68
113
  /** Link to another page from a page in the same locale, relative so it stays in whatever locale the page is shown in. */
69
- function relativeHref(settings, fromId, toId, locale) {
70
- const from = href(settings, fromId, locale);
71
- const to = href(settings, toId, locale);
72
- const fromDir = from.slice(0, from.lastIndexOf("/") + 1);
73
- if (to.endsWith("/")) return `${posix.relative(fromDir, to) || "."}/`;
74
- return posix.join(posix.relative(fromDir, posix.dirname(to)), posix.basename(to));
114
+ function relativeHref(settings, from, to, locale) {
115
+ const fromHref = href(settings, from.collection, from.id, locale);
116
+ const toHref = href(settings, to.collection, to.id, locale);
117
+ const fromDir = fromHref.slice(0, fromHref.lastIndexOf("/") + 1);
118
+ if (toHref.endsWith("/")) return `${posix.relative(fromDir, toHref) || "."}/`;
119
+ return posix.join(posix.relative(fromDir, posix.dirname(toHref)), posix.basename(toHref));
120
+ }
121
+ /** The collection whose root holds a project-relative file, the innermost one if roots are nested. */
122
+ function collectionOf(collections, file) {
123
+ return collections.filter(({ base }) => file.startsWith(`${base}/`)).reduce((best, collection) => best && best.base.length > collection.base.length ? best : collection, void 0);
75
124
  }
76
125
  const EXCLUDED = "points to a page that the collection's filter excludes.";
77
126
  /**
78
- * Rewrites a relative link to a content file into the URL of its page.
127
+ * Rewrites a relative link to a content file, in any collection, into the URL of its page.
79
128
  * Links within one locale become relative URLs, so pages shown through fallback keep linking within the shown locale.
80
129
  * Returns `undefined` for links that are not to `.md` or `.mdx` files. Throws when the target does not exist.
81
130
  */
@@ -85,13 +134,23 @@ function resolveContentLink(settings, source, url, root) {
85
134
  const [, path, hash = ""] = match;
86
135
  const absolute = resolve(root, dirname(source.file), decodeURI(path));
87
136
  const target = relative(root, absolute).split(sep).join("/");
88
- if (!target.startsWith(`${source.base}/`)) throw new Error(`"${url}" points outside the content collection, so it has no page.`);
137
+ const collection = collectionOf(source.collections, target);
138
+ if (!collection) throw new Error(`"${url}" points outside every collection loaded with minidocs' glob, so it has no page.`);
89
139
  if (!existsSync(absolute)) throw new Error(`"${url}" points to a file that does not exist.`);
90
- if (source.isExcluded?.(target)) throw new Error(`"${url}" ${EXCLUDED}`);
91
- const { locales, localeDirs } = contentOptions(settings, source.base);
92
- const from = parseFilePath(source.file, source.base, locales, localeDirs);
93
- const to = parseFilePath(target, source.base, locales, localeDirs);
94
- return (from.locale === to.locale ? relativeHref(settings, from.id, to.id, settings.i18n ? from.locale : void 0) : href(settings, to.id, to.locale)) + hash;
140
+ if (source.isExcluded?.(target, collection)) throw new Error(`"${url}" ${EXCLUDED}`);
141
+ const parse = (file, of) => {
142
+ const { locales, localeDirs } = contentOptions(settings, of.base);
143
+ return parseFilePath(file, of.base, locales, localeDirs);
144
+ };
145
+ const from = parse(source.file, source.collection);
146
+ const to = parse(target, collection);
147
+ return (from.locale === to.locale ? relativeHref(settings, {
148
+ collection: source.collection,
149
+ id: from.id
150
+ }, {
151
+ collection,
152
+ id: to.id
153
+ }, settings.i18n ? from.locale : void 0) : href(settings, collection, to.id, to.locale)) + hash;
95
154
  }
96
155
  /**
97
156
  * Resolves a link found in a Markdown file, or returns `undefined` to leave it alone.
@@ -101,30 +160,36 @@ function rewrite(filePath, url) {
101
160
  const { settings, root } = registry;
102
161
  if (!settings || !root) return void 0;
103
162
  const file = relative(root, filePath).split(sep).join("/");
104
- const collection = Object.values(registry.collections).find(({ base }) => file.startsWith(`${base}/`));
163
+ const collections = Object.values(registry.collections);
164
+ const collection = collectionOf(collections, file);
105
165
  if (!collection) return void 0;
106
- const isExcluded = (target) => {
107
- if (collection.loading) {
108
- collection.pendingLinks.push({
166
+ const ready = (state) => state.loaded && !state.loading;
167
+ const isExcluded = (target, to) => {
168
+ if (!ready(collection) || !ready(to)) {
169
+ registry.pendingLinks.push({
109
170
  file,
110
171
  url,
111
- target
172
+ target,
173
+ source: collection,
174
+ to
112
175
  });
113
176
  return false;
114
177
  }
115
- return collection.excluded.has(target);
178
+ return to.excluded.has(target);
116
179
  };
117
180
  return resolveContentLink(settings, {
118
181
  file,
119
- base: collection.base,
182
+ collection,
183
+ collections,
120
184
  isExcluded
121
185
  }, url, root);
122
186
  }
123
- /** Checks links that Markdown rendered during a load, now that every entry has been filtered. */
124
- function checkPendingLinks(collection) {
125
- const pending = collection.pendingLinks;
126
- collection.pendingLinks = [];
127
- const broken = pending.filter(({ file, target }) => !collection.excluded.has(file) && collection.excluded.has(target));
187
+ /** Checks links rendered while a collection involved was loading, once both collections have loaded. */
188
+ function checkPendingLinks() {
189
+ const ready = (state) => state.loaded && !state.loading;
190
+ const due = registry.pendingLinks.filter(({ source, to }) => ready(source) && ready(to));
191
+ registry.pendingLinks = registry.pendingLinks.filter((link) => !due.includes(link));
192
+ const broken = due.filter(({ file, target, source, to }) => !source.excluded.has(file) && to.excluded.has(target));
128
193
  if (broken.length === 0) return;
129
194
  const root = registry.root ?? "";
130
195
  throw new Error(broken.map(({ file, url }) => failure(resolve(root, file), url, /* @__PURE__ */ new Error(`"${url}" ${EXCLUDED}`)).message).join("\n"));
@@ -174,4 +239,4 @@ function remarkContentLinks() {
174
239
  };
175
240
  }
176
241
  //#endregion
177
- export { renderConfigTypes as a, registry as i, remarkContentLinks as n, renderTypes as o, satteriContentLinks as r, checkPendingLinks as t };
242
+ export { registry as a, renderConfigTypes as c, namedCollections as i, renderTypes as l, remarkContentLinks as n, findUnbuiltPages as o, satteriContentLinks as r, findUrlConflicts as s, checkPendingLinks as t };
@@ -7,14 +7,19 @@ export interface FilterEntry {
7
7
  data: Record<string, unknown>;
8
8
  filePath?: string | undefined;
9
9
  }
10
+ export interface MinidocsGlobOptions {
11
+ /** Path segment the pages live under, e.g. `docs` for `/docs/...`. Defaults to the site root. */
12
+ prefix?: string;
13
+ /** Entries for which it returns `false` never reach the collection. */
14
+ filter?: (entry: FilterEntry) => boolean;
15
+ }
10
16
  /**
11
- * Astro's `glob()` loader, plus recording of the collection root so minidocs can
12
- * derive locales and page ids, and generate types for them.
17
+ * Astro's `glob()` loader, plus recording of the collection root and URL prefix so minidocs can
18
+ * derive locales, page ids and URLs, and generate types for them.
13
19
  * Entries rejected by `filter` never reach the collection, so types, links and pages all agree on what exists.
14
20
  */
15
- export declare function glob({ filter, ...options }: GlobOptions & {
21
+ export declare function glob({ prefix, filter, ...options }: GlobOptions & {
16
22
  base: string | URL;
17
- filter?: (entry: FilterEntry) => boolean;
18
- }): Loader;
23
+ } & MinidocsGlobOptions): Loader;
19
24
  //#endregion
20
25
  export {};
package/dist/loaders.mjs CHANGED
@@ -1,36 +1,43 @@
1
- import { i as registry, o as renderTypes, t as checkPendingLinks } from "./links-UTLyoo_o.mjs";
1
+ import { a as registry, i as namedCollections, l as renderTypes, s as findUrlConflicts, t as checkPendingLinks } from "./links-Dp7d5R-O.mjs";
2
2
  import { mkdirSync, writeFileSync } from "node:fs";
3
- import { dirname, relative } from "node:path";
3
+ import { dirname, relative, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { glob as glob$1 } from "astro/loaders";
6
6
  //#region src/loaders.ts
7
+ /** Collection root relative to the project, as Astro's glob resolves `base`. */
8
+ const relativeBase = (base, root) => relative(root, base instanceof URL ? fileURLToPath(base) : resolve(root, base)).replaceAll("\\", "/");
9
+ function register(base, declared) {
10
+ registry.collections[base] ??= {
11
+ base,
12
+ ...declared,
13
+ filePaths: [],
14
+ excluded: /* @__PURE__ */ new Set(),
15
+ loading: false,
16
+ loaded: false
17
+ };
18
+ return Object.assign(registry.collections[base], declared);
19
+ }
7
20
  /**
8
- * Astro's `glob()` loader, plus recording of the collection root so minidocs can
9
- * derive locales and page ids, and generate types for them.
21
+ * Astro's `glob()` loader, plus recording of the collection root and URL prefix so minidocs can
22
+ * derive locales, page ids and URLs, and generate types for them.
10
23
  * Entries rejected by `filter` never reach the collection, so types, links and pages all agree on what exists.
11
24
  */
12
- function glob({ filter, ...options }) {
25
+ function glob({ prefix = "", filter, ...options }) {
13
26
  const inner = glob$1(options);
27
+ const declared = { prefix: prefix.replace(/^\/+|\/+$/g, "") };
28
+ if (registry.root) register(relativeBase(options.base, registry.root), declared);
14
29
  return {
15
30
  ...inner,
16
31
  name: "minidocs-glob",
17
32
  async load(context) {
18
- const baseUrl = options.base instanceof URL ? options.base : new URL(options.base, context.config.root);
19
- const base = relative(fileURLToPath(context.config.root), fileURLToPath(baseUrl)).replaceAll("\\", "/");
33
+ const state = register(relativeBase(options.base, fileURLToPath(context.config.root)), declared);
34
+ if (state.name !== void 0 && state.name !== context.collection) throw new Error(`[minidocs] Collections "${state.name}" and "${context.collection}" are both loaded from ${state.base}.`);
35
+ state.name = context.collection;
20
36
  let scheduled;
21
37
  const update = () => {
22
38
  clearTimeout(scheduled);
23
- scheduled = setTimeout(() => writeTypes(context), 50);
24
- };
25
- registry.collections[context.collection] ??= {
26
- base,
27
- filePaths: [],
28
- excluded: /* @__PURE__ */ new Set(),
29
- loading: false,
30
- pendingLinks: []
39
+ scheduled = setTimeout(() => writeTypes(context, state, (message) => context.logger.error(message)), 50);
31
40
  };
32
- const state = registry.collections[context.collection];
33
- state.base = base;
34
41
  const store = new Proxy(context.store, { get(target, property, receiver) {
35
42
  const value = Reflect.get(target, property, receiver);
36
43
  if (property !== "set" && property !== "delete" && property !== "clear") return value;
@@ -57,20 +64,25 @@ function glob({ filter, ...options }) {
57
64
  } finally {
58
65
  state.loading = false;
59
66
  }
67
+ state.loaded = true;
60
68
  clearTimeout(scheduled);
61
- writeTypes(context);
62
- checkPendingLinks(state);
69
+ writeTypes(context, state, (message) => {
70
+ throw new Error(message);
71
+ });
72
+ checkPendingLinks();
63
73
  }
64
74
  };
65
75
  }
66
- function writeTypes(context) {
76
+ function writeTypes(context, state, report) {
67
77
  const { settings, typesFile } = registry;
68
78
  if (!settings || !typesFile) throw new Error("[minidocs] Add the minidocs() integration to your Astro config to use glob from '@minidocs/astro/loaders'.");
69
- const filePaths = context.store.values().map((entry) => entry.filePath).filter((filePath) => Boolean(filePath));
70
- registry.collections[context.collection].filePaths = filePaths;
79
+ state.filePaths = context.store.values().map((entry) => entry.filePath).filter((filePath) => Boolean(filePath));
80
+ const collections = namedCollections();
71
81
  const path = fileURLToPath(typesFile);
72
82
  mkdirSync(dirname(path), { recursive: true });
73
- writeFileSync(path, renderTypes(settings, registry.collections));
83
+ writeFileSync(path, renderTypes(settings, collections));
84
+ const conflicts = findUrlConflicts(settings, collections);
85
+ if (conflicts.length > 0) report(conflicts.map((conflict) => `[minidocs] ${conflict}`).join("\n"));
74
86
  }
75
87
  //#endregion
76
88
  export { glob };
@@ -0,0 +1,60 @@
1
+ //#region src/routing.ts
2
+ /** The locale every page of a single-language site belongs to inside core. Never exposed to users. */
3
+ const SINGLE_LOCALE = "default";
4
+ /** Options for core: locale directories for multilingual sites, one implicit locale otherwise. */
5
+ function contentOptions(settings, base) {
6
+ return settings.i18n ? {
7
+ base,
8
+ locales: settings.i18n.locales,
9
+ fallback: settings.i18n.fallback
10
+ } : {
11
+ base,
12
+ locales: [SINGLE_LOCALE],
13
+ localeDirs: false
14
+ };
15
+ }
16
+ /** The URL segment of a page id: the root index page has none. */
17
+ function slugOf(id) {
18
+ return id === "index" ? "" : id;
19
+ }
20
+ const normalizeLocale = (locale) => locale.replaceAll("_", "-").toLowerCase();
21
+ /** The URL segment of a locale, `undefined` for the default locale when it is not prefixed, as rest parameters expect. */
22
+ function localeParam(settings, locale) {
23
+ const { i18n } = settings;
24
+ if (!i18n || locale === void 0 || locale === i18n.defaultLocale && !i18n.prefixDefaultLocale) return void 0;
25
+ return normalizeLocale(locale);
26
+ }
27
+ /**
28
+ * Path of a page below `base`, e.g. `en/docs/getting-started`, without slashes around it.
29
+ * `undefined` stands for the site root.
30
+ */
31
+ function routePath(settings, collection, id, locale) {
32
+ return [
33
+ localeParam(settings, locale),
34
+ collection.prefix,
35
+ slugOf(id)
36
+ ].filter(Boolean).join("/") || void 0;
37
+ }
38
+ /** Link to a page, including `base` and the trailing slash Astro would use. */
39
+ function href(settings, collection, id, locale) {
40
+ const path = [settings.base, routePath(settings, collection, id, locale)].filter(Boolean).join("/");
41
+ if (!path) return "/";
42
+ return `/${path}${settings.trailingSlash ? "/" : ""}`;
43
+ }
44
+ /**
45
+ * Path of the Markdown version of a page below the locale segment, e.g. `docs/getting-started`.
46
+ * The root of a site without a prefix has no segment of its own, so it becomes `index`.
47
+ */
48
+ function markdownPath(collection, id) {
49
+ return [collection.prefix, slugOf(id)].filter(Boolean).join("/") || "index";
50
+ }
51
+ /** Link to the Markdown version of a page, including `base`. */
52
+ function markdownHref(settings, collection, id, locale) {
53
+ return `/${[
54
+ settings.base,
55
+ localeParam(settings, locale),
56
+ markdownPath(collection, id)
57
+ ].filter(Boolean).join("/")}.md`;
58
+ }
59
+ //#endregion
60
+ export { markdownPath as a, markdownHref as i, href as n, routePath as o, localeParam as r, slugOf as s, contentOptions as t };
@@ -20,6 +20,8 @@ type IsI18n = Config extends {
20
20
  export type Locale = Config extends {
21
21
  locale: infer L extends string;
22
22
  } ? L : string;
23
+ /** Names of the collections loaded with minidocs' glob, or any string before their types are generated. */
24
+ export type CollectionName = [keyof Collections] extends [never] ? string : Extract<keyof Collections, string>;
23
25
  type Known<C extends string> = C extends keyof Collections ? Collections[C] : never;
24
26
  type Lookup<T, L> = T extends string ? T : L extends keyof T ? Extract<T[L], string> : never;
25
27
  type Field<C extends string, K extends string, L> = [Known<C>] extends [never] ? string : Known<C> extends Record<K, infer T> ? Lookup<T, L> : string;
@@ -73,15 +75,29 @@ export type DocTree<E extends DocEntry> = IsI18n extends true ? Record<Locale, D
73
75
  interface PageInfo<E extends DocEntry> {
74
76
  id: Id<E>;
75
77
  entry: E;
76
- /** Route parameter for `src/pages/[...path].astro`. `undefined` is the site root. */
77
- path: string | undefined;
78
+ /** Route parameters for `src/pages/docs/[...slug].astro`. `slug` is `undefined` for the root page of the collection. */
79
+ params: {
80
+ slug: string | undefined;
81
+ };
78
82
  href: string;
79
- /** Route parameter for `src/pages/[...path].md.ts`. */
80
- markdownPath: string;
83
+ /** Route parameters for `src/pages/[...path].md.ts`, which serves the Markdown versions of every collection. */
84
+ markdownParams: {
85
+ path: string;
86
+ };
81
87
  /** Link to the Markdown version of the page. */
82
88
  markdownHref: string;
83
89
  }
84
90
  interface LocalizedPageInfo<E extends DocEntry> extends PageInfo<E> {
91
+ /** Route parameters for `src/pages/[...locale]/docs/[...slug].astro`. `locale` is `undefined` for the default locale when it is not prefixed. */
92
+ params: {
93
+ locale: string | undefined;
94
+ slug: string | undefined;
95
+ };
96
+ /** Route parameters for `src/pages/[...locale]/[...path].md.ts`, which serves the Markdown versions of every collection. */
97
+ markdownParams: {
98
+ locale: string | undefined;
99
+ path: string;
100
+ };
85
101
  /** Locale the page is shown in. */
86
102
  locale: Locale;
87
103
  /** Locale the content comes from. Differs from `locale` only for fallback pages. */
@@ -97,10 +113,13 @@ export type DocPage<E extends DocEntry> = IsI18n extends true ? LocalizedPageInf
97
113
  type LocaleArgs = IsI18n extends true ? [locale: Locale] : [];
98
114
  /** Builds the navigation tree. Multilingual sites get one tree per locale, built by running `build` once for each. */
99
115
  export declare function defineTree<E extends DocEntry>(entries: readonly E[], build: (context: BuildContext<E>) => DocNode<E>[] | Promise<DocNode<E>[]>): Promise<DocTree<E>>;
100
- /** Link to a page, including `base` and the trailing slash Astro would use. */
101
- export declare function toHref(id: string, ...locale: LocaleArgs): string;
102
- /** Route parameter for a page in `src/pages/[...path].astro`. `undefined` is the site root. */
103
- export declare function toPath(id: string, ...locale: LocaleArgs): string | undefined;
116
+ /** Link to a page, including `base` and the trailing slash Astro would use. Multilingual sites take a locale. */
117
+ export declare function toHref<C extends CollectionName>(collection: C, id: PageIdOf<C>, ...locale: LocaleArgs): string;
118
+ /**
119
+ * The `[...locale]` route parameter of a locale, for the site's own pages such as a home page or a 404 page.
120
+ * `undefined` for the default locale when it is not prefixed.
121
+ */
122
+ export declare function toLocaleParam(locale: Locale): string | undefined;
104
123
  /** Whether a component renders for the Markdown version of a page, where it should output plain HTML that converts cleanly. */
105
124
  export declare function isMarkdown(astro: {
106
125
  locals: object;
@@ -122,23 +141,46 @@ export type MarkdownPage<E extends DocEntry> = IsI18n extends true ? {
122
141
  * Renders the content of a page and converts it into Markdown for agents, without a title.
123
142
  * Components can check `isMarkdown(Astro)` to render a form that converts cleanly.
124
143
  */
125
- export declare function toMarkdown<E extends DocEntry>(page: MarkdownPage<E>, options?: MarkdownOptions): Promise<string>;
144
+ export declare function toMarkdown(page: MarkdownPage<DocEntry>, options?: MarkdownOptions): Promise<string>;
145
+ /** A node of a tree from any collection, so trees of several collections can be joined into one file. */
146
+ interface TreeNode {
147
+ label: string;
148
+ page?: {
149
+ id: string;
150
+ entry: DocEntry;
151
+ } | undefined;
152
+ href?: string | undefined;
153
+ children: readonly TreeNode[];
154
+ }
155
+ /**
156
+ * Entries of every collection found in a tree, which may join trees of several collections.
157
+ * A tree node is structurally the same as a group wrapping it, so the walk stops at a fixed depth instead.
158
+ */
159
+ type EntryOf<N, Depth extends unknown[] = []> = Depth['length'] extends 4 ? never : N extends {
160
+ page?: infer P;
161
+ children: readonly (infer C)[];
162
+ } ? (P extends {
163
+ entry: infer E;
164
+ } ? E : never) | EntryOf<C, [...Depth, unknown]> : never;
126
165
  /**
127
166
  * Renders a tree as llms.txt. Top-level pages come first, every top-level node with children becomes a section,
128
167
  * and links point to the Markdown versions of pages. The `index` page's description becomes the summary.
129
168
  */
130
- export declare function toLlmsTxt<E extends DocEntry>(nodes: DocNode<E>[], options: LlmsTxtOptions<E>): string;
169
+ export declare function toLlmsTxt<N extends TreeNode>(nodes: readonly N[], options: LlmsTxtOptions<EntryOf<N>>): string;
131
170
  /**
132
171
  * Every page of a tree in reading order, as Markdown under its title and source URL, for llms-full.txt.
133
172
  * Like llms.txt, links and images are full URLs when `site` is set.
134
173
  */
135
- export declare function toLlmsFullTxt<E extends DocEntry>(nodes: DocNode<E>[], options?: MarkdownOptions): Promise<string>;
174
+ export declare function toLlmsFullTxt<N extends TreeNode>(nodes: readonly N[], options?: MarkdownOptions): Promise<string>;
136
175
  /** Every page that needs a route. Multilingual sites include every locale and untranslated pages served through fallback. */
137
176
  export declare function listPages<E extends DocEntry>(entries: readonly E[]): DocPage<E>[];
138
177
  interface NavNode {
139
178
  page?: {
140
179
  id: string;
141
180
  locale?: string;
181
+ entry: {
182
+ collection: string;
183
+ };
142
184
  } | undefined;
143
185
  href?: string | undefined;
144
186
  children: NavNode[];
package/dist/runtime.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as routePath, i as markdownPath, n as href, r as markdownHref, t as contentOptions } from "./routing-TMqRCY2Q.mjs";
1
+ import { a as markdownPath, i as markdownHref, n as href, r as localeParam, s as slugOf, t as contentOptions } from "./routing-D28b92w8.mjs";
2
2
  import { MissingPageError, createTree, findPath, listPages as listPages$1, navState as navState$1, neighbors, tocTree } from "@minidocs/core";
3
3
  import config from "virtual:minidocs/config";
4
4
  import { fromHtml } from "hast-util-from-html";
@@ -8,11 +8,12 @@ import { toMarkdown as toMarkdown$1 } from "mdast-util-to-markdown";
8
8
  //#region src/llms.ts
9
9
  const escapeLabel = (text) => text.replace(/[[\]\\]/g, "\\$&");
10
10
  const oneLine = (text) => text.replace(/\s+/g, " ").trim();
11
- /** Pages of a tree in reading order, each page once. */
11
+ /** Pages of a tree in reading order, each page once. Trees of several collections can be joined, so a page is its collection and id. */
12
12
  function pagesInOrder(nodes) {
13
13
  const pages = /* @__PURE__ */ new Map();
14
14
  const visit = (node) => {
15
- if (node.page && !pages.has(node.page.id)) pages.set(node.page.id, node.page);
15
+ const key = node.page && `${node.page.entry.collection}\0${node.page.id}`;
16
+ if (key && !pages.has(key)) pages.set(key, node.page);
16
17
  node.children.forEach(visit);
17
18
  };
18
19
  nodes.forEach(visit);
@@ -149,13 +150,16 @@ function htmlToMarkdown(html, context) {
149
150
  }
150
151
  //#endregion
151
152
  //#region src/runtime.ts
152
- function optionsFor(entries) {
153
- const collection = entries[0]?.collection;
154
- if (!collection) throw new Error("[minidocs] Expected at least one entry.");
155
- const base = config.bases[collection];
156
- if (base === void 0) throw new Error(`[minidocs] Collection "${collection}" must be loaded with glob from '@minidocs/astro/loaders'.`);
157
- if (entries.some((entry) => entry.collection !== collection)) throw new Error("[minidocs] All entries must come from the same collection.");
158
- return contentOptions(config, base);
153
+ function collectionNamed(name) {
154
+ const collection = config.collections[name];
155
+ if (!collection) throw new Error(`[minidocs] Collection "${name}" must be loaded with glob from '@minidocs/astro/loaders'.`);
156
+ return collection;
157
+ }
158
+ function collectionOf(entries) {
159
+ const name = entries[0]?.collection;
160
+ if (!name) throw new Error("[minidocs] Expected at least one entry.");
161
+ if (entries.some((entry) => entry.collection !== name)) throw new Error("[minidocs] All entries must come from the same collection.");
162
+ return collectionNamed(name);
159
163
  }
160
164
  /** Drops locale details from nodes of single-language sites, where they carry no meaning. */
161
165
  function withoutLocale(nodes) {
@@ -170,7 +174,8 @@ function withoutLocale(nodes) {
170
174
  }
171
175
  /** Builds the navigation tree. Multilingual sites get one tree per locale, built by running `build` once for each. */
172
176
  async function defineTree(entries, build) {
173
- const options = optionsFor(entries);
177
+ const collection = collectionOf(entries);
178
+ const options = contentOptions(config, collection.base);
174
179
  const run = build;
175
180
  let tree;
176
181
  try {
@@ -179,18 +184,22 @@ async function defineTree(entries, build) {
179
184
  label: (entry) => entry.data.title
180
185
  }, config.i18n ? run : ({ locale: _, ...helpers }) => run(helpers));
181
186
  } catch (error) {
182
- if (error instanceof MissingPageError && config.pageIds[entries[0].collection]?.includes(error.id)) throw new Error(`[minidocs] Page "${error.id}" is in the collection but not among the entries passed to defineTree. To leave pages out, use the filter option of glob from '@minidocs/astro/loaders' instead of filtering the entries.`, { cause: error });
187
+ if (error instanceof MissingPageError && collection.pageIds.includes(error.id)) throw new Error(`[minidocs] Page "${error.id}" is in the collection but not among the entries passed to defineTree. To leave pages out, use the filter option of glob from '@minidocs/astro/loaders' instead of filtering the entries.`, { cause: error });
183
188
  throw error;
184
189
  }
185
190
  return config.i18n ? tree : withoutLocale(tree[options.locales[0]]);
186
191
  }
187
- /** Link to a page, including `base` and the trailing slash Astro would use. */
188
- function toHref(id, ...locale) {
189
- return href(config, id, locale[0]);
192
+ /** Link to a page, including `base` and the trailing slash Astro would use. Multilingual sites take a locale. */
193
+ function toHref(collection, id, ...locale) {
194
+ return href(config, collectionNamed(collection), id, locale[0]);
190
195
  }
191
- /** Route parameter for a page in `src/pages/[...path].astro`. `undefined` is the site root. */
192
- function toPath(id, ...locale) {
193
- return routePath(config, id, locale[0]);
196
+ /**
197
+ * The `[...locale]` route parameter of a locale, for the site's own pages such as a home page or a 404 page.
198
+ * `undefined` for the default locale when it is not prefixed.
199
+ */
200
+ function toLocaleParam(locale) {
201
+ if (!config.i18n) throw new Error("[minidocs] toLocaleParam requires `i18n` in your Astro config.");
202
+ return localeParam(config, locale);
194
203
  }
195
204
  const MARKDOWN = "minidocs:markdown";
196
205
  /** Whether a component renders for the Markdown version of a page, where it should output plain HTML that converts cleanly. */
@@ -212,16 +221,17 @@ async function containerFor(extra) {
212
221
  }
213
222
  return container;
214
223
  }
215
- const markdownPages = /* @__PURE__ */ new Map();
216
- /** Markdown versions of every page in a collection, keyed by the URL path of the HTML page. */
217
- function markdownPagesOf(collection) {
218
- let pages = markdownPages.get(collection);
219
- if (!pages) {
220
- pages = /* @__PURE__ */ new Map();
221
- for (const id of config.pageIds[collection] ?? []) for (const locale of config.i18n?.locales ?? [void 0]) pages.set(pathKey(href(config, id, locale)), markdownHref(config, id, locale));
222
- markdownPages.set(collection, pages);
224
+ let markdownPages;
225
+ /** Markdown versions of every page on the site, keyed by the URL path of the HTML page, so links into any collection convert. */
226
+ function markdownPagesOnSite() {
227
+ if (!markdownPages) {
228
+ markdownPages = /* @__PURE__ */ new Map();
229
+ for (const collection of Object.values(config.collections)) {
230
+ const locales = config.i18n?.locales ?? [void 0];
231
+ for (const id of collection.pageIds) for (const locale of locales) markdownPages.set(pathKey(href(config, collection, id, locale)), markdownHref(config, collection, id, locale));
232
+ }
223
233
  }
224
- return pages;
234
+ return markdownPages;
225
235
  }
226
236
  /** Full URL of a path when `site` is set, for text that is often pasted where paths do not resolve. */
227
237
  const fullUrl = (path) => config.site ? new URL(path, config.site).href : path;
@@ -246,12 +256,12 @@ async function renderMarkdown(page, options, resolve) {
246
256
  throw error;
247
257
  }
248
258
  return htmlToMarkdown(html, {
249
- pageHref: href(config, id, locale),
250
- markdownOf: (path) => markdownPagesOf(entry.collection).get(path),
259
+ pageHref: href(config, collectionNamed(entry.collection), id, locale),
260
+ markdownOf: (path) => markdownPagesOnSite().get(path),
251
261
  resolve
252
262
  });
253
263
  }
254
- const markdownUrl = ({ id, locale }) => fullUrl(markdownHref(config, id, locale));
264
+ const markdownUrl = ({ id, locale, entry }) => fullUrl(markdownHref(config, collectionNamed(entry.collection), id, locale));
255
265
  /**
256
266
  * Renders a tree as llms.txt. Top-level pages come first, every top-level node with children becomes a section,
257
267
  * and links point to the Markdown versions of pages. The `index` page's description becomes the summary.
@@ -269,30 +279,37 @@ async function toLlmsFullTxt(nodes, options = {}) {
269
279
  }
270
280
  /** Every page that needs a route. Multilingual sites include every locale and untranslated pages served through fallback. */
271
281
  function listPages(entries) {
272
- const pages = listPages$1(entries, optionsFor(entries));
282
+ const collection = collectionOf(entries);
283
+ const pages = listPages$1(entries, contentOptions(config, collection.base));
273
284
  if (!config.i18n) return pages.map(({ id, entry }) => ({
274
285
  id,
275
286
  entry,
276
- path: routePath(config, id),
277
- href: href(config, id),
278
- markdownPath: markdownPath(config, id),
279
- markdownHref: markdownHref(config, id)
287
+ params: { slug: slugOf(id) || void 0 },
288
+ href: href(config, collection, id),
289
+ markdownParams: { path: markdownPath(collection, id) },
290
+ markdownHref: markdownHref(config, collection, id)
280
291
  }));
281
292
  const alternates = /* @__PURE__ */ new Map();
282
293
  for (const page of pages) {
283
294
  const list = alternates.get(page.id) ?? [];
284
295
  list.push({
285
296
  locale: page.locale,
286
- href: href(config, page.id, page.locale)
297
+ href: href(config, collection, page.id, page.locale)
287
298
  });
288
299
  alternates.set(page.id, list);
289
300
  }
290
301
  return pages.map((page) => ({
291
302
  ...page,
292
- path: routePath(config, page.id, page.locale),
293
- href: href(config, page.id, page.locale),
294
- markdownPath: markdownPath(config, page.id, page.locale),
295
- markdownHref: markdownHref(config, page.id, page.locale),
303
+ params: {
304
+ locale: localeParam(config, page.locale),
305
+ slug: slugOf(page.id) || void 0
306
+ },
307
+ href: href(config, collection, page.id, page.locale),
308
+ markdownParams: {
309
+ locale: localeParam(config, page.locale),
310
+ path: markdownPath(collection, page.id)
311
+ },
312
+ markdownHref: markdownHref(config, collection, page.id, page.locale),
296
313
  alternates: alternates.get(page.id)
297
314
  }));
298
315
  }
@@ -301,10 +318,10 @@ function navState(nodes, id) {
301
318
  const state = navState$1(nodes, id);
302
319
  return { of(node) {
303
320
  return {
304
- href: node.page ? href(config, node.page.id, node.page.locale) : node.href,
321
+ href: node.page ? href(config, collectionNamed(node.page.entry.collection), node.page.id, node.page.locale) : node.href,
305
322
  ...state.of(node)
306
323
  };
307
324
  } };
308
325
  }
309
326
  //#endregion
310
- export { defineTree, findPath, isMarkdown, listPages, navState, neighbors, toHref, toLlmsFullTxt, toLlmsTxt, toMarkdown, toPath, tocTree };
327
+ export { defineTree, findPath, isMarkdown, listPages, navState, neighbors, toHref, toLlmsFullTxt, toLlmsTxt, toLocaleParam, toMarkdown, tocTree };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minidocs/astro",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Astro integration for building documentation sites from composable primitives",
5
5
  "keywords": [
6
6
  "astro",
@@ -58,7 +58,7 @@
58
58
  "mdast-util-gfm": "^3.1.0",
59
59
  "mdast-util-to-markdown": "^2.2.0",
60
60
  "remark-directive": "^4.0.0",
61
- "@minidocs/core": "^0.1.0"
61
+ "@minidocs/core": "0.2.0"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/hast": "^3.0.5",
@@ -7,7 +7,8 @@
7
7
  --md-color-text: light-dark(#3f3f46, #d4d4d8);
8
8
  --md-color-muted: light-dark(#71717a, #a1a1aa);
9
9
  --md-color-border: light-dark(#e4e4e7, #2e2e33);
10
- --md-color-accent: light-dark(#0b7a6e, #4fd1bf);
10
+ /* Neutral by default, so a site brings its own color by setting this one variable */
11
+ --md-color-accent: var(--md-color-heading);
11
12
  --md-color-hover: color-mix(in srgb, var(--md-color-heading) 5%, transparent);
12
13
  --md-color-current: color-mix(in srgb, var(--md-color-accent) 9%, transparent);
13
14
  --md-color-code-bg: light-dark(#f6f6f7, #202024);
@@ -9,24 +9,34 @@
9
9
  border-block-start: 1px solid var(--md-color-border);
10
10
  }
11
11
 
12
+ /* Padding gives hover a background like the sidebar's, and margins on the outer side keep text aligned */
12
13
  .md-pager-link {
13
14
  display: flex;
14
15
  flex-direction: column;
15
16
  gap: 0.125rem;
17
+ margin-block: -0.5rem;
18
+ margin-inline: -0.75rem 0;
19
+ padding: 0.5rem 0.75rem;
20
+ border-radius: var(--md-radius);
16
21
  color: var(--md-color-heading);
17
22
  text-decoration: none;
23
+ transition: background-color 0.15s;
24
+ }
25
+
26
+ .md-pager-link:hover {
27
+ background: var(--md-color-hover);
18
28
  }
19
29
 
20
30
  /* The next page stays on the end side even when there is no previous one */
21
31
  .md-pager-link[rel="next"] {
22
32
  grid-column: 2;
33
+ margin-inline: 0 -0.75rem;
23
34
  text-align: end;
24
35
  }
25
36
 
26
37
  .md-pager-link:focus-visible {
27
- border-radius: 2px;
28
38
  outline: var(--md-focus-ring);
29
- outline-offset: 4px;
39
+ outline-offset: -2px;
30
40
  }
31
41
 
32
42
  .md-pager-label {
@@ -36,15 +46,10 @@
36
46
 
37
47
  .md-pager-title {
38
48
  font-weight: 500;
39
- transition: color 0.15s;
40
- }
41
-
42
- .md-pager-link:hover .md-pager-title {
43
- color: var(--md-color-accent);
44
49
  }
45
50
 
46
51
  @media (prefers-reduced-motion: reduce) {
47
- .md-pager-title {
52
+ .md-pager-link {
48
53
  transition: none;
49
54
  }
50
55
  }
@@ -151,9 +151,14 @@
151
151
  background: var(--md-color-hover);
152
152
  }
153
153
 
154
+ /* The underline keeps matches visible even when the accent is the heading color */
154
155
  .md-search-results mark {
155
156
  background: none;
156
157
  color: var(--md-color-accent);
158
+ text-decoration: underline;
159
+ text-decoration-color: color-mix(in srgb, var(--md-color-accent) 50%, transparent);
160
+ text-decoration-thickness: 1px;
161
+ text-underline-offset: 0.2em;
157
162
  }
158
163
 
159
164
  .md-search-idle,
@@ -1,48 +0,0 @@
1
- //#region src/routing.ts
2
- /** The locale every page of a single-language site belongs to inside core. Never exposed to users. */
3
- const SINGLE_LOCALE = "default";
4
- /** Options for core: locale directories for multilingual sites, one implicit locale otherwise. */
5
- function contentOptions(settings, base) {
6
- return settings.i18n ? {
7
- base,
8
- locales: settings.i18n.locales,
9
- fallback: settings.i18n.fallback
10
- } : {
11
- base,
12
- locales: [SINGLE_LOCALE],
13
- localeDirs: false
14
- };
15
- }
16
- /** The URL segment of a page id: the root index page has none. */
17
- function slugOf(id) {
18
- return id === "index" ? "" : id;
19
- }
20
- const normalizeLocale = (locale) => locale.replaceAll("_", "-").toLowerCase();
21
- /**
22
- * Route parameter for a page, e.g. `en/docs/getting-started`, without `base` or slashes around it.
23
- * `undefined` stands for the site root, as Astro expects for rest parameters.
24
- */
25
- function routePath(settings, id, locale) {
26
- const { i18n } = settings;
27
- return [
28
- !i18n || locale === void 0 || locale === i18n.defaultLocale && !i18n.prefixDefaultLocale ? "" : normalizeLocale(locale),
29
- settings.prefix,
30
- slugOf(id)
31
- ].filter(Boolean).join("/") || void 0;
32
- }
33
- /** Link to a page, including `base` and the trailing slash Astro would use. */
34
- function href(settings, id, locale) {
35
- const path = [settings.base, routePath(settings, id, locale)].filter(Boolean).join("/");
36
- if (!path) return "/";
37
- return `/${path}${settings.trailingSlash ? "/" : ""}`;
38
- }
39
- /** Route parameter for a page in `src/pages/[...path].md.ts`. The site root has no segment of its own, so it becomes `index`. */
40
- function markdownPath(settings, id, locale) {
41
- return routePath(settings, id, locale) ?? "index";
42
- }
43
- /** Link to the Markdown version of a page, including `base`. */
44
- function markdownHref(settings, id, locale) {
45
- return `/${[settings.base, markdownPath(settings, id, locale)].filter(Boolean).join("/")}.md`;
46
- }
47
- //#endregion
48
- export { routePath as a, markdownPath as i, href as n, markdownHref as r, contentOptions as t };