@f5-sales-demo/starlight-llms-txt 1.3.1
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/CHANGELOG.md +171 -0
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/entryToSimpleMarkdown.ts +209 -0
- package/env.d.ts +13 -0
- package/federated-sites.ts +64 -0
- package/generator.ts +73 -0
- package/index.ts +111 -0
- package/llms-full.txt.ts +18 -0
- package/llms-locale-full.txt.ts +19 -0
- package/llms-locale-small.txt.ts +21 -0
- package/llms-small.txt.ts +19 -0
- package/llms-tiered.txt.ts +69 -0
- package/llms.txt.ts +58 -0
- package/package.json +59 -0
- package/sidebar-nav.ts +29 -0
- package/tier-tree-render.ts +43 -0
- package/tier-tree.ts +190 -0
- package/types.ts +62 -0
- package/utils.ts +34 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# starlight-llms-txt
|
|
2
|
+
|
|
3
|
+
## 1.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#195](https://github.com/f5xc-salesdemos/starlight-llms-txt/pull/195) [`5b734d4`](https://github.com/f5xc-salesdemos/starlight-llms-txt/commit/5b734d4affd4e288238703f774f1215c7197847d) Thanks [@robinmordasiewicz](https://github.com/robinmordasiewicz)! - Widen `peerDependencies.astro` from `^6.0.0` to `>=6.0.0` so consumers can use astro 7+.
|
|
8
|
+
|
|
9
|
+
## 1.3.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#41](https://github.com/f5xc-salesdemos/starlight-llms-txt/pull/41) [`193dd0d`](https://github.com/f5xc-salesdemos/starlight-llms-txt/commit/193dd0d1bc4b8870a40f7fdf4fbf5435b04fba96) Thanks [@robinmordasiewicz](https://github.com/robinmordasiewicz)! - Add auto-tiered `.txt` hierarchy for progressive AI discovery. Every hint link in `llms.txt` now points to a `.txt` file. Content lives only at leaf nodes; intermediate tiers contain metadata hints. Replaces `customSets` and `perPageMarkdown` with a single `tieredHierarchy` option (enabled by default).
|
|
14
|
+
|
|
15
|
+
## 1.2.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#30](https://github.com/f5xc-salesdemos/starlight-llms-txt/pull/30) [`a4e3c1c`](https://github.com/f5xc-salesdemos/starlight-llms-txt/commit/a4e3c1c5b9135f7b311733e6982a5e7ae0ff49e0) Thanks [@robinmordasiewicz](https://github.com/robinmordasiewicz)! - Deduplicate Sections entries when a node matches a custom set — prevents duplicate Guides/Functions lines in the llms.txt output for subcategory-aware sites.
|
|
20
|
+
|
|
21
|
+
## 1.2.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- [#25](https://github.com/f5xc-salesdemos/starlight-llms-txt/pull/25) [`068535a`](https://github.com/f5xc-salesdemos/starlight-llms-txt/commit/068535ab4d725cc0c9095f0162031c306a0675a3) Thanks [@robinmordasiewicz](https://github.com/robinmordasiewicz)! - Make ## Sections link to custom set \_llms-txt/\*.txt files instead of individual page URLs when a section node matches a custom set label. When sidebarNav is enabled, custom sets are moved from Documentation Sets into Sections to avoid duplication.
|
|
26
|
+
|
|
27
|
+
## 1.2.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- [#18](https://github.com/f5xc-salesdemos/starlight-llms-txt/pull/18) [`f850ddd`](https://github.com/f5xc-salesdemos/starlight-llms-txt/commit/f850dddf7f7791c11ac15c0d1dfc379bb4262351) Thanks [@robinmordasiewicz](https://github.com/robinmordasiewicz)! - Add subcategory-aware grouping to section tree and auto-generate custom sets from frontmatter
|
|
32
|
+
|
|
33
|
+
## 1.1.0
|
|
34
|
+
|
|
35
|
+
### Minor Changes
|
|
36
|
+
|
|
37
|
+
- [#3](https://github.com/f5xc-salesdemos/starlight-llms-txt/pull/3) [`c59c96d`](https://github.com/f5xc-salesdemos/starlight-llms-txt/commit/c59c96d3a3569552c3b70dec2cbf253d2bc9ccaa) Thanks [@robinmordasiewicz](https://github.com/robinmordasiewicz)! - Add categorized federation support. Federated sites can now be grouped by category with optional section headings and descriptions. Fully backwards-compatible — when no categories are defined, rendering is unchanged.
|
|
38
|
+
|
|
39
|
+
## 1.0.0
|
|
40
|
+
|
|
41
|
+
### Major Changes
|
|
42
|
+
|
|
43
|
+
- [#1](https://github.com/f5xc-salesdemos/starlight-llms-txt/pull/1) [`9ef7e93`](https://github.com/f5xc-salesdemos/starlight-llms-txt/commit/9ef7e93d1685e1d5ccc0dd43f0b4de6dded1522f) Thanks [@robinmordasiewicz](https://github.com/robinmordasiewicz)! - Initial release of the F5 XC Sales Demos fork, derived from `starlight-llms-txt@0.8.1`. Adds:
|
|
44
|
+
|
|
45
|
+
- `perPageMarkdown` — per-page `.md` endpoints (Tier 4 routing in the xcsh#223 cascading knowledge hierarchy). Rebased from [delucis#32](https://github.com/delucis/starlight-llms-txt/pull/32) by Matthias Vallentin, with `excludePages` extended to accept glob patterns.
|
|
46
|
+
- `sidebarNav` — sidebar hierarchy in `llms.txt` (Tier 2 routing), with frontmatter descriptions inlined automatically.
|
|
47
|
+
- `federatedSites` — cross-repo links block in `llms.txt` (Tier 1 routing).
|
|
48
|
+
|
|
49
|
+
The `starlight-llms-txt` package is authored by Chris Swithinbank (delucis). This fork exists to ship features needed by the f5xc-salesdemos documentation federation; we intend to upstream compatible features once they have been validated in production.
|
|
50
|
+
|
|
51
|
+
### Minor Changes
|
|
52
|
+
|
|
53
|
+
- [#1](https://github.com/f5xc-salesdemos/starlight-llms-txt/pull/1) [`940de8c`](https://github.com/f5xc-salesdemos/starlight-llms-txt/commit/940de8ce11bd6d7124b6e41708c991adc576dc59) Thanks [@robinmordasiewicz](https://github.com/robinmordasiewicz)! - Add `federatedSites` option. When set to a non-empty array, the plugin includes a `## Federated Sites` block in `llms.txt` listing links to other sites' `llms.txt` entry points. Intended for docs portals that federate out to product-specific documentation.
|
|
54
|
+
|
|
55
|
+
- [#1](https://github.com/f5xc-salesdemos/starlight-llms-txt/pull/1) [`a8e692c`](https://github.com/f5xc-salesdemos/starlight-llms-txt/commit/a8e692c82c96bbfe36d3fc993d2fca4d0d5dc74e) Thanks [@robinmordasiewicz](https://github.com/robinmordasiewicz)! - Add individual Markdown file generation for each documentation page
|
|
56
|
+
|
|
57
|
+
Implements the second part of the llmstxt.org standard proposal by generating clean Markdown versions of each documentation page. These individual `.md` files are accessible at the same URL path with `.md` appended, allowing LLMs to fetch specific documentation pages on-demand.
|
|
58
|
+
|
|
59
|
+
New configuration option:
|
|
60
|
+
|
|
61
|
+
- `perPageMarkdown`: Enable individual page generation. Set to `true` for defaults, or an object for advanced configuration:
|
|
62
|
+
- `extensionStrategy`: Control URL pattern - `'append'` or `'replace'` (default: `'append'`)
|
|
63
|
+
- `excludePages`: Exclude specific pages from .md generation (default: `['404']`)
|
|
64
|
+
|
|
65
|
+
- [#1](https://github.com/f5xc-salesdemos/starlight-llms-txt/pull/1) [`46d0562`](https://github.com/f5xc-salesdemos/starlight-llms-txt/commit/46d0562410dd4ddea05fd6f3b9b4bfb732253891) Thanks [@robinmordasiewicz](https://github.com/robinmordasiewicz)! - Add `sidebarNav` option. When enabled, the plugin includes a `## Sections` block in `llms.txt` with the site's pages grouped hierarchically. Entries include frontmatter descriptions inline when present.
|
|
66
|
+
|
|
67
|
+
## 0.8.1
|
|
68
|
+
|
|
69
|
+
### Patch Changes
|
|
70
|
+
|
|
71
|
+
- [`29e5efb`](https://github.com/delucis/starlight-llms-txt/commit/29e5efb3a7d4d8b2167696e87b7f4d60db1ac93a) Thanks [@mvanhorn](https://github.com/mvanhorn)! - Strips HTML comments from llms.txt files. `<!-- ... -->` style comments in `.md` files are no longer emitted in the generated files.
|
|
72
|
+
|
|
73
|
+
## 0.8.0
|
|
74
|
+
|
|
75
|
+
### Minor Changes
|
|
76
|
+
|
|
77
|
+
- [#80](https://github.com/delucis/starlight-llms-txt/pull/80) [`dea7b22`](https://github.com/delucis/starlight-llms-txt/commit/dea7b22b6821168b9982fa9d8840163c6f55b70e) Thanks [@nonoakij](https://github.com/nonoakij)! - Adds support for Astro v6 and Starlight v0.38, drops support for lower versions.
|
|
78
|
+
|
|
79
|
+
## 0.7.0
|
|
80
|
+
|
|
81
|
+
### Minor Changes
|
|
82
|
+
|
|
83
|
+
- [#43](https://github.com/delucis/starlight-llms-txt/pull/43) [`1db4591`](https://github.com/delucis/starlight-llms-txt/commit/1db45917d329e7e2ec00630a0c517b973c08fe5f) Thanks [@sanscontext](https://github.com/sanscontext)! - Enforces llms.txt files to be prerendered at build time.
|
|
84
|
+
Previously, sites using Astro’s `output: server` configuration would generate llms.txt files on-demand, which can be slow, and additionally was incompatible with the [custom sets](https://delucis.github.io/starlight-llms-txt/configuration/#customsets) feature.
|
|
85
|
+
This change means that llms.txt files are statically generated even for sites using `output: server`.
|
|
86
|
+
|
|
87
|
+
⚠️ **Potentially breaking change:** If you were relying on on-demand rendered llms.txt files, for example by using middleware to gate access, this may be a breaking change. Please [share your use case](https://github.com/delucis/starlight-llms-txt/issues) to let us know if you need this.
|
|
88
|
+
|
|
89
|
+
## 0.6.1
|
|
90
|
+
|
|
91
|
+
### Patch Changes
|
|
92
|
+
|
|
93
|
+
- [#49](https://github.com/delucis/starlight-llms-txt/pull/49) [`56b8233`](https://github.com/delucis/starlight-llms-txt/commit/56b823325bd42374300597a82b0f04e289be4b25) Thanks [@delucis](https://github.com/delucis)! - No code changes. This release is the first published using OIDC trusted publisher configuration for improved security.
|
|
94
|
+
|
|
95
|
+
## 0.6.0
|
|
96
|
+
|
|
97
|
+
### Minor Changes
|
|
98
|
+
|
|
99
|
+
- [#30](https://github.com/delucis/starlight-llms-txt/pull/30) [`a1650c9`](https://github.com/delucis/starlight-llms-txt/commit/a1650c92b16377d9abdcccc8b2a68b34bc695796) Thanks [@alvinometric](https://github.com/alvinometric)! - Adds a new `rawContent` option to skip the Markdown processing pipeline
|
|
100
|
+
|
|
101
|
+
## 0.5.1
|
|
102
|
+
|
|
103
|
+
### Patch Changes
|
|
104
|
+
|
|
105
|
+
- [#22](https://github.com/delucis/starlight-llms-txt/pull/22) [`2a8102a`](https://github.com/delucis/starlight-llms-txt/commit/2a8102a2554ac80495568b89acba2bb5a437d206) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes output of Expressive Code `diff` codeblocks with a `lang` attribute
|
|
106
|
+
|
|
107
|
+
## 0.5.0
|
|
108
|
+
|
|
109
|
+
### Minor Changes
|
|
110
|
+
|
|
111
|
+
- [#18](https://github.com/delucis/starlight-llms-txt/pull/18) [`52838f6`](https://github.com/delucis/starlight-llms-txt/commit/52838f63fc5280436982880744921dac923d4be2) Thanks [@pelikhan](https://github.com/pelikhan)! - Add page separator configuration object
|
|
112
|
+
|
|
113
|
+
## 0.4.1
|
|
114
|
+
|
|
115
|
+
### Patch Changes
|
|
116
|
+
|
|
117
|
+
- [#13](https://github.com/delucis/starlight-llms-txt/pull/13) [`629fa9b`](https://github.com/delucis/starlight-llms-txt/commit/629fa9b00444a70ebf9aac3e15375f400f8a10cc) Thanks [@jumski](https://github.com/jumski)! - Filters out draft pages from output
|
|
118
|
+
|
|
119
|
+
## 0.4.0
|
|
120
|
+
|
|
121
|
+
### Minor Changes
|
|
122
|
+
|
|
123
|
+
- [#10](https://github.com/delucis/starlight-llms-txt/pull/10) [`7f914f5`](https://github.com/delucis/starlight-llms-txt/commit/7f914f526dbe504ed3e3763864fd9ec1d5150d0d) Thanks [@delucis](https://github.com/delucis)! - Adds a new `customSets` option to support breaking up large docs into multiple custom document sets
|
|
124
|
+
|
|
125
|
+
### Patch Changes
|
|
126
|
+
|
|
127
|
+
- [`0c0678d`](https://github.com/delucis/starlight-llms-txt/commit/0c0678da19f1f9981f808a1fa0e1a01c77a26b4d) Thanks [@delucis](https://github.com/delucis)! - Improves rendering of Starlight’s `<FileTree>` component by removing “Directory” labels
|
|
128
|
+
|
|
129
|
+
## 0.3.0
|
|
130
|
+
|
|
131
|
+
### Minor Changes
|
|
132
|
+
|
|
133
|
+
- [#7](https://github.com/delucis/starlight-llms-txt/pull/7) [`cba125e`](https://github.com/delucis/starlight-llms-txt/commit/cba125ed259601895ba78f6da95a55564b914470) Thanks [@hippotastic](https://github.com/hippotastic)! - Adds options to promote or demote pages in the order of the `llms-full.txt` and `llms-small.txt` output files
|
|
134
|
+
|
|
135
|
+
## 0.2.1
|
|
136
|
+
|
|
137
|
+
### Patch Changes
|
|
138
|
+
|
|
139
|
+
- [`39760e7`](https://github.com/delucis/starlight-llms-txt/commit/39760e70e921b685bc6dc6a5338f8f80bf79e57e) Thanks [@delucis](https://github.com/delucis)! - Removes Expressive Code’s “Terminal Window” labels from output
|
|
140
|
+
|
|
141
|
+
- [`26fa616`](https://github.com/delucis/starlight-llms-txt/commit/26fa616793798bda41911bfe7dc229475f89db26) Thanks [@delucis](https://github.com/delucis)! - Fixes a bug where pages excluded using the `exclude` configuration option were excluded in `llms-full.txt` instead of only in `llms-small.txt`
|
|
142
|
+
|
|
143
|
+
## 0.2.0
|
|
144
|
+
|
|
145
|
+
### Minor Changes
|
|
146
|
+
|
|
147
|
+
- [#4](https://github.com/delucis/starlight-llms-txt/pull/4) [`a4a77ca`](https://github.com/delucis/starlight-llms-txt/commit/a4a77ca433b7cee7cbeb3c603498e760cd037867) Thanks [@delucis](https://github.com/delucis)! - Adds support for generating a smaller `llms-small.txt` file for smaller context windows
|
|
148
|
+
|
|
149
|
+
- [`618fa88`](https://github.com/delucis/starlight-llms-txt/commit/618fa882d29bc4b7ce054392c9b65d97ce1ceb82) Thanks [@delucis](https://github.com/delucis)! - Adds support for including additional optional links in the main `llms.txt` entrypoint
|
|
150
|
+
|
|
151
|
+
### Patch Changes
|
|
152
|
+
|
|
153
|
+
- [#4](https://github.com/delucis/starlight-llms-txt/pull/4) [`a4a77ca`](https://github.com/delucis/starlight-llms-txt/commit/a4a77ca433b7cee7cbeb3c603498e760cd037867) Thanks [@delucis](https://github.com/delucis)! - Sort pages in llms.txt output
|
|
154
|
+
|
|
155
|
+
## 0.1.2
|
|
156
|
+
|
|
157
|
+
### Patch Changes
|
|
158
|
+
|
|
159
|
+
- [`d5ca030`](https://github.com/delucis/starlight-llms-txt/commit/d5ca0307192585f141164dd8328f244f32db5a90) Thanks [@delucis](https://github.com/delucis)! - Improves rendering of Starlight Tabs component in output
|
|
160
|
+
|
|
161
|
+
## 0.1.1
|
|
162
|
+
|
|
163
|
+
### Patch Changes
|
|
164
|
+
|
|
165
|
+
- [`04f641c`](https://github.com/delucis/starlight-llms-txt/commit/04f641c48dd70acf480c80df26d9e2f774510428) Thanks [@delucis](https://github.com/delucis)! - Preserves language metadata on code blocks
|
|
166
|
+
|
|
167
|
+
## 0.1.0
|
|
168
|
+
|
|
169
|
+
### Minor Changes
|
|
170
|
+
|
|
171
|
+
- [`249438b`](https://github.com/delucis/starlight-llms-txt/commit/249438b23d2998ef79a1bbb19ac7a532938f7ade) Thanks [@delucis](https://github.com/delucis)! - Initial release
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present, delucis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @f5-sales-demo/starlight-llms-txt
|
|
2
|
+
|
|
3
|
+
Fork of [`starlight-llms-txt`](https://github.com/delucis/starlight-llms-txt) by Chris Swithinbank, extended for the [f5-sales-demo](https://github.com/f5-sales-demo) documentation federation.
|
|
4
|
+
|
|
5
|
+
## Additions over upstream
|
|
6
|
+
|
|
7
|
+
- `perPageMarkdown` — per-page `.md` endpoints
|
|
8
|
+
- `sidebarNav` — sidebar hierarchy in `llms.txt`, with frontmatter descriptions inlined automatically
|
|
9
|
+
- `federatedSites` — cross-repo links for federated doc portals
|
|
10
|
+
|
|
11
|
+
See the [configuration docs](https://f5-sales-demo.github.io/starlight-llms-txt/configuration/) for the full option reference.
|
|
12
|
+
|
|
13
|
+
## Relationship to upstream
|
|
14
|
+
|
|
15
|
+
Compatible features are intended to land upstream at `delucis/starlight-llms-txt` after production validation. Until then, this package tracks the f5-sales-demo integration needs.
|
|
16
|
+
|
|
17
|
+
## License
|
|
18
|
+
|
|
19
|
+
MIT — copyright Chris Swithinbank, fork modifications by f5-sales-demo contributors.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { type CollectionEntry, render } from 'astro:content';
|
|
2
|
+
import { starlightLllmsTxtContext } from 'virtual:starlight-llms-txt/context';
|
|
3
|
+
import mdxServer from '@astrojs/mdx/server.js';
|
|
4
|
+
import type { APIContext } from 'astro';
|
|
5
|
+
import { experimental_AstroContainer } from 'astro/container';
|
|
6
|
+
import type { RootContent } from 'hast';
|
|
7
|
+
import { matches, select, selectAll } from 'hast-util-select';
|
|
8
|
+
import rehypeParse from 'rehype-parse';
|
|
9
|
+
import rehypeRemark from 'rehype-remark';
|
|
10
|
+
import remarkGfm from 'remark-gfm';
|
|
11
|
+
import remarkStringify from 'remark-stringify';
|
|
12
|
+
import { unified } from 'unified';
|
|
13
|
+
import { remove } from 'unist-util-remove';
|
|
14
|
+
|
|
15
|
+
/** Minification defaults */
|
|
16
|
+
const minifyDefaults = {
|
|
17
|
+
note: true,
|
|
18
|
+
tip: true,
|
|
19
|
+
caution: false,
|
|
20
|
+
danger: false,
|
|
21
|
+
details: true,
|
|
22
|
+
whitespace: true,
|
|
23
|
+
customSelectors: [],
|
|
24
|
+
};
|
|
25
|
+
/** Resolved minification options */
|
|
26
|
+
const minify = { ...minifyDefaults, ...starlightLllmsTxtContext.minify };
|
|
27
|
+
/** Selectors for elements to remove during minification. */
|
|
28
|
+
const selectors = [...minify.customSelectors];
|
|
29
|
+
if (minify.details) selectors.unshift('details');
|
|
30
|
+
|
|
31
|
+
const astroContainer = await experimental_AstroContainer.create({
|
|
32
|
+
renderers: [{ name: 'astro:jsx', ssr: mdxServer }],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const htmlToMarkdownPipeline = unified()
|
|
36
|
+
.use(rehypeParse, { fragment: true })
|
|
37
|
+
.use(function minifyLlmsTxt() {
|
|
38
|
+
return (tree, file) => {
|
|
39
|
+
if (!file.data.starlightLlmsTxt.minify) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
remove(tree, (_node) => {
|
|
43
|
+
const node = _node as RootContent;
|
|
44
|
+
|
|
45
|
+
// Remove elements matching any selectors to be minified:
|
|
46
|
+
for (const selector of selectors) {
|
|
47
|
+
if (matches(selector, node)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Remove aside components:
|
|
53
|
+
if (matches('.starlight-aside', node)) {
|
|
54
|
+
for (const variant of ['note', 'tip', 'caution', 'danger'] as const) {
|
|
55
|
+
if (minify[variant] && matches(`.starlight-aside--${variant}`, node)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
});
|
|
63
|
+
return tree;
|
|
64
|
+
};
|
|
65
|
+
})
|
|
66
|
+
.use(function improveExpressiveCodeHandling() {
|
|
67
|
+
return (tree) => {
|
|
68
|
+
const ecInstances = selectAll('.expressive-code', tree as Parameters<typeof selectAll>[1]);
|
|
69
|
+
for (const instance of ecInstances) {
|
|
70
|
+
// Remove the “Terminal Window” label from Expressive Code terminal frames.
|
|
71
|
+
const figcaption = select('figcaption', instance);
|
|
72
|
+
if (figcaption) {
|
|
73
|
+
const terminalWindowTextIndex = figcaption.children.findIndex((child) => matches('span.sr-only', child));
|
|
74
|
+
if (terminalWindowTextIndex > -1) {
|
|
75
|
+
figcaption.children.splice(terminalWindowTextIndex, 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const pre = select('pre', instance);
|
|
79
|
+
const code = select('code', instance);
|
|
80
|
+
// Use Expressive Code’s `data-language=*` attribute to set a `language-*` class name.
|
|
81
|
+
// This is what `hast-util-to-mdast` checks for code language metadata.
|
|
82
|
+
if (pre?.properties.dataLanguage && code) {
|
|
83
|
+
if (!Array.isArray(code.properties.className)) code.properties.className = [];
|
|
84
|
+
|
|
85
|
+
const diffLines =
|
|
86
|
+
pre.properties.dataLanguage === 'diff'
|
|
87
|
+
? []
|
|
88
|
+
: code.children.filter((child) => matches('div.ec-line.ins, div.ec-line.del', child));
|
|
89
|
+
if (diffLines.length === 0) {
|
|
90
|
+
code.properties.className.push(`language-${pre.properties.dataLanguage}`);
|
|
91
|
+
} else {
|
|
92
|
+
code.properties.className.push('language-diff');
|
|
93
|
+
for (const line of diffLines) {
|
|
94
|
+
if (line.type !== 'element') continue;
|
|
95
|
+
const classes = line.properties?.className;
|
|
96
|
+
if (typeof classes !== 'string' && !Array.isArray(classes)) continue;
|
|
97
|
+
const marker = classes.includes('ins') ? '+' : '-';
|
|
98
|
+
const span = select('span:not(.indent)', line);
|
|
99
|
+
const firstChild = span?.children[0];
|
|
100
|
+
if (firstChild?.type === 'text') {
|
|
101
|
+
firstChild.value = `${marker}${firstChild.value}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
})
|
|
109
|
+
.use(function improveTabsHandling() {
|
|
110
|
+
return (tree) => {
|
|
111
|
+
const tabInstances = selectAll('starlight-tabs', tree as Parameters<typeof selectAll>[1]);
|
|
112
|
+
for (const instance of tabInstances) {
|
|
113
|
+
const tabs = selectAll('[role="tab"]', instance);
|
|
114
|
+
const panels = selectAll('[role="tabpanel"]', instance);
|
|
115
|
+
// Convert parent `<starlight-tabs>` element to empty unordered list.
|
|
116
|
+
instance.tagName = 'ul';
|
|
117
|
+
instance.properties = {};
|
|
118
|
+
instance.children = [];
|
|
119
|
+
// Iterate over tabs and panels to build a list with tab label as initial list text.
|
|
120
|
+
for (let i = 0; i < Math.min(tabs.length, panels.length); i++) {
|
|
121
|
+
const tab = tabs[i];
|
|
122
|
+
const panel = panels[i];
|
|
123
|
+
if (!tab || !panel) continue;
|
|
124
|
+
// Filter out extra whitespace and icons from tab contents.
|
|
125
|
+
const tabLabel = tab.children
|
|
126
|
+
.filter((child) => child.type === 'text' && child.value.trim())
|
|
127
|
+
.map((child) => child.type === 'text' && child.value.trim())
|
|
128
|
+
.join('');
|
|
129
|
+
// Add list entry for this tab and panel.
|
|
130
|
+
instance.children.push({
|
|
131
|
+
type: 'element',
|
|
132
|
+
tagName: 'li',
|
|
133
|
+
properties: {},
|
|
134
|
+
children: [
|
|
135
|
+
{
|
|
136
|
+
type: 'element',
|
|
137
|
+
tagName: 'p',
|
|
138
|
+
children: [{ type: 'text', value: tabLabel }],
|
|
139
|
+
properties: {},
|
|
140
|
+
},
|
|
141
|
+
panel,
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
})
|
|
148
|
+
.use(function improveFileTreeHandling() {
|
|
149
|
+
return (tree) => {
|
|
150
|
+
const trees = selectAll('starlight-file-tree', tree as Parameters<typeof selectAll>[1]);
|
|
151
|
+
for (const tree of trees) {
|
|
152
|
+
// Remove “Directory” screen reader labels from <FileTree> entries.
|
|
153
|
+
remove(tree, (_node) => {
|
|
154
|
+
const node = _node as RootContent;
|
|
155
|
+
return matches('.sr-only', node);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
})
|
|
160
|
+
.use(function removeHtmlComments() {
|
|
161
|
+
return (tree) => {
|
|
162
|
+
remove(tree, ({ type }) => type === 'comment');
|
|
163
|
+
};
|
|
164
|
+
})
|
|
165
|
+
.use(function removeHeadingAnchorLinks() {
|
|
166
|
+
return (tree) => {
|
|
167
|
+
// Remove Starlight's heading anchor links (e.g. "Section titled …").
|
|
168
|
+
// These are sibling <a> elements next to headings inside .sl-heading-wrapper divs.
|
|
169
|
+
remove(tree, (_node) => {
|
|
170
|
+
const node = _node as RootContent;
|
|
171
|
+
return matches('a.sl-anchor-link', node);
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
})
|
|
175
|
+
.use(rehypeRemark)
|
|
176
|
+
.use(remarkGfm)
|
|
177
|
+
.use(remarkStringify, {
|
|
178
|
+
emphasis: '*',
|
|
179
|
+
strong: '*',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
/** Render a content collection entry to HTML and back to Markdown to support rendering and simplifying MDX components */
|
|
183
|
+
export async function entryToSimpleMarkdown(
|
|
184
|
+
entry: CollectionEntry<'docs'>,
|
|
185
|
+
context: APIContext,
|
|
186
|
+
shouldMinify: boolean = false,
|
|
187
|
+
) {
|
|
188
|
+
const { rawContent } = starlightLllmsTxtContext;
|
|
189
|
+
|
|
190
|
+
// Skip processing if the `rawContent` option is enabled.
|
|
191
|
+
// This helps with UI framework components (we don't support renderers for these yet)
|
|
192
|
+
// and speeds up processing for large sites.
|
|
193
|
+
if (rawContent) {
|
|
194
|
+
// Return the raw body content directly.
|
|
195
|
+
return entry.body || '';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { Content } = await render(entry);
|
|
199
|
+
const html = await astroContainer.renderToString(Content, context);
|
|
200
|
+
const file = await htmlToMarkdownPipeline.process({
|
|
201
|
+
value: html,
|
|
202
|
+
data: { starlightLlmsTxt: { minify: shouldMinify } },
|
|
203
|
+
});
|
|
204
|
+
let markdown = String(file).trim();
|
|
205
|
+
if (shouldMinify && minify.whitespace) {
|
|
206
|
+
markdown = markdown.replace(/\s+/g, ' ');
|
|
207
|
+
}
|
|
208
|
+
return markdown;
|
|
209
|
+
}
|
package/env.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// <reference types="../../docs/.astro/types.d.ts" />
|
|
2
|
+
|
|
3
|
+
declare module 'virtual:starlight-llms-txt/context' {
|
|
4
|
+
export const starlightLllmsTxtContext: import('./types').ProjectContext;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
declare module 'vfile' {
|
|
8
|
+
interface DataMap {
|
|
9
|
+
starlightLlmsTxt: {
|
|
10
|
+
minify: boolean;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface FederatedSite {
|
|
2
|
+
label: string;
|
|
3
|
+
url: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
category?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FederatedSiteCategory {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderSiteList(sites: FederatedSite[]): string[] {
|
|
15
|
+
return sites.map((site) => {
|
|
16
|
+
const desc = site.description ? `: ${site.description}` : '';
|
|
17
|
+
return `- [${site.label}](${site.url})${desc}`;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function renderFederatedSites(sites: FederatedSite[], categories?: FederatedSiteCategory[]): string {
|
|
22
|
+
if (sites.length === 0) return '';
|
|
23
|
+
|
|
24
|
+
if (!categories || categories.length === 0) {
|
|
25
|
+
const lines: string[] = ['## Federated Sites', ''];
|
|
26
|
+
lines.push(...renderSiteList(sites));
|
|
27
|
+
return lines.join('\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const sections: string[] = [];
|
|
31
|
+
const sitesByCategory = new Map<string, FederatedSite[]>();
|
|
32
|
+
|
|
33
|
+
for (const site of sites) {
|
|
34
|
+
const key = site.category || '__uncategorized__';
|
|
35
|
+
const list = sitesByCategory.get(key) || [];
|
|
36
|
+
list.push(site);
|
|
37
|
+
sitesByCategory.set(key, list);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const cat of categories) {
|
|
41
|
+
const catSites = sitesByCategory.get(cat.id);
|
|
42
|
+
if (!catSites || catSites.length === 0) continue;
|
|
43
|
+
const lines: string[] = [`## ${cat.label}`];
|
|
44
|
+
if (cat.description) lines.push(cat.description);
|
|
45
|
+
lines.push('');
|
|
46
|
+
lines.push(...renderSiteList(catSites));
|
|
47
|
+
sections.push(lines.join('\n'));
|
|
48
|
+
sitesByCategory.delete(cat.id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const uncategorized = sitesByCategory.get('__uncategorized__') || [];
|
|
52
|
+
const remaining = Array.from(sitesByCategory.entries())
|
|
53
|
+
.filter(([key]) => key !== '__uncategorized__')
|
|
54
|
+
.flatMap(([, s]) => s);
|
|
55
|
+
const otherSites = [...remaining, ...uncategorized];
|
|
56
|
+
|
|
57
|
+
if (otherSites.length > 0) {
|
|
58
|
+
const lines: string[] = ['## Other', ''];
|
|
59
|
+
lines.push(...renderSiteList(otherSites));
|
|
60
|
+
sections.push(lines.join('\n'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return sections.join('\n\n');
|
|
64
|
+
}
|
package/generator.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { CollectionEntry } from 'astro:content';
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import { starlightLllmsTxtContext } from 'virtual:starlight-llms-txt/context';
|
|
4
|
+
import type { APIContext } from 'astro';
|
|
5
|
+
import micromatch from 'micromatch';
|
|
6
|
+
import { entryToSimpleMarkdown } from './entryToSimpleMarkdown';
|
|
7
|
+
import { defaultLang, isDefaultLocale, isLocale } from './utils';
|
|
8
|
+
|
|
9
|
+
/** Collator to compare two strings in the default language. */
|
|
10
|
+
const collator = new Intl.Collator(defaultLang);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates a single plaintext Markdown document from the full website content.
|
|
14
|
+
*/
|
|
15
|
+
export async function generateLlmsTxt(
|
|
16
|
+
context: APIContext,
|
|
17
|
+
{
|
|
18
|
+
minify,
|
|
19
|
+
description,
|
|
20
|
+
exclude,
|
|
21
|
+
include,
|
|
22
|
+
locale,
|
|
23
|
+
}: {
|
|
24
|
+
/** Generate a smaller file to fit within smaller context windows. */
|
|
25
|
+
minify: boolean;
|
|
26
|
+
/** Description of the document being generated. Prepended to output inside `<SYSTEM>` tags. */
|
|
27
|
+
description: string | undefined;
|
|
28
|
+
exclude?: string[] | undefined;
|
|
29
|
+
include?: string[] | undefined;
|
|
30
|
+
locale?: string | undefined;
|
|
31
|
+
},
|
|
32
|
+
): Promise<string> {
|
|
33
|
+
const docFilter = locale
|
|
34
|
+
? (doc: { id: string; data: { draft?: boolean } }) =>
|
|
35
|
+
isLocale(doc as CollectionEntry<'docs'>, locale) && !doc.data.draft
|
|
36
|
+
: (doc: { id: string; data: { draft?: boolean } }) =>
|
|
37
|
+
isDefaultLocale(doc as CollectionEntry<'docs'>) && !doc.data.draft;
|
|
38
|
+
let docs = await getCollection('docs', docFilter);
|
|
39
|
+
if (include) {
|
|
40
|
+
docs = docs.filter((doc) => micromatch.isMatch(doc.id, include));
|
|
41
|
+
}
|
|
42
|
+
if (exclude) {
|
|
43
|
+
docs = docs.filter((doc) => !micromatch.isMatch(doc.id, exclude));
|
|
44
|
+
}
|
|
45
|
+
const { promote, demote, pageSeparator } = starlightLllmsTxtContext;
|
|
46
|
+
/** Processes page IDs by prepending underscores to influence the sorting order. */
|
|
47
|
+
const prioritizePages = (id: string) => {
|
|
48
|
+
// Match the page ID against the patterns listed in the `promote` and `demote`
|
|
49
|
+
// config options and return the index of the first match. If a page matches
|
|
50
|
+
// a `demote` pattern, we don't check `promote` as demotions take precedence.
|
|
51
|
+
const demoted = demote.findIndex((expr) => micromatch.isMatch(id, expr));
|
|
52
|
+
const promoted = demoted > -1 ? -1 : promote.findIndex((expr) => micromatch.isMatch(id, expr));
|
|
53
|
+
// Calculate the number of underscores to prefix the page ID with
|
|
54
|
+
// to influence the sorting order. The more underscores, the earlier
|
|
55
|
+
// the page will appear in the list. The amount of underscores added by
|
|
56
|
+
// a pattern is determined by the respective array length and the match index.
|
|
57
|
+
const prefixLength = (promoted > -1 ? promote.length - promoted : 0) + demote.length - demoted - 1;
|
|
58
|
+
return '_'.repeat(prefixLength) + id;
|
|
59
|
+
};
|
|
60
|
+
docs.sort((a, b) => collator.compare(prioritizePages(a.id), prioritizePages(b.id)));
|
|
61
|
+
const segments: string[] = [];
|
|
62
|
+
for (const doc of docs) {
|
|
63
|
+
const docSegments = [`# ${doc.data.hero?.title || doc.data.title}`];
|
|
64
|
+
const description = doc.data.hero?.tagline || doc.data.description;
|
|
65
|
+
if (description) docSegments.push(`> ${description}`);
|
|
66
|
+
docSegments.push(await entryToSimpleMarkdown(doc, context, minify));
|
|
67
|
+
segments.push(docSegments.join('\n\n'));
|
|
68
|
+
}
|
|
69
|
+
if (description) {
|
|
70
|
+
segments.unshift(`<SYSTEM>${description}</SYSTEM>`);
|
|
71
|
+
}
|
|
72
|
+
return segments.join(pageSeparator);
|
|
73
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { StarlightPlugin } from '@astrojs/starlight/types';
|
|
2
|
+
import { AstroError } from 'astro/errors';
|
|
3
|
+
import type { ProjectContext, StarlightLllmsTextOptions } from './types';
|
|
4
|
+
|
|
5
|
+
export default function starlightLlmsTxt(opts: StarlightLllmsTextOptions = {}): StarlightPlugin {
|
|
6
|
+
return {
|
|
7
|
+
name: 'starlight-llms-txt',
|
|
8
|
+
hooks: {
|
|
9
|
+
setup({ astroConfig, addIntegration, config }) {
|
|
10
|
+
if (!astroConfig.site) {
|
|
11
|
+
throw new AstroError(
|
|
12
|
+
'`site` not set in Astro configuration',
|
|
13
|
+
'The `starlight-llms-txt` plugin requires setting `site` in your Astro configuration file.',
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
addIntegration({
|
|
17
|
+
name: 'starlight-llms-txt',
|
|
18
|
+
hooks: {
|
|
19
|
+
'astro:config:setup'({ injectRoute, updateConfig }) {
|
|
20
|
+
injectRoute({
|
|
21
|
+
entrypoint: new URL('./llms.txt.ts', import.meta.url),
|
|
22
|
+
pattern: '/llms.txt',
|
|
23
|
+
prerender: true,
|
|
24
|
+
});
|
|
25
|
+
injectRoute({
|
|
26
|
+
entrypoint: new URL('./llms-full.txt.ts', import.meta.url),
|
|
27
|
+
pattern: '/llms-full.txt',
|
|
28
|
+
prerender: true,
|
|
29
|
+
});
|
|
30
|
+
injectRoute({
|
|
31
|
+
entrypoint: new URL('./llms-small.txt.ts', import.meta.url),
|
|
32
|
+
pattern: '/llms-small.txt',
|
|
33
|
+
prerender: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
injectRoute({
|
|
37
|
+
entrypoint: new URL('./llms-locale-full.txt.ts', import.meta.url),
|
|
38
|
+
pattern: '/[locale]/llms-full.txt',
|
|
39
|
+
prerender: true,
|
|
40
|
+
});
|
|
41
|
+
injectRoute({
|
|
42
|
+
entrypoint: new URL('./llms-locale-small.txt.ts', import.meta.url),
|
|
43
|
+
pattern: '/[locale]/llms-small.txt',
|
|
44
|
+
prerender: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const tieredHierarchy = opts.tieredHierarchy ?? true;
|
|
48
|
+
if (tieredHierarchy) {
|
|
49
|
+
injectRoute({
|
|
50
|
+
entrypoint: new URL('./llms-tiered.txt.ts', import.meta.url),
|
|
51
|
+
pattern: '/_llms-txt/[...path].txt',
|
|
52
|
+
prerender: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const projectContext: ProjectContext = {
|
|
57
|
+
base: astroConfig.base,
|
|
58
|
+
title: opts.projectName ?? config.title,
|
|
59
|
+
description: opts.description ?? config.description,
|
|
60
|
+
details: opts.details,
|
|
61
|
+
optionalLinks: opts.optionalLinks ?? [],
|
|
62
|
+
minify: opts.minify ?? {},
|
|
63
|
+
promote: opts.promote ?? ['index*'],
|
|
64
|
+
demote: opts.demote ?? [],
|
|
65
|
+
exclude: opts.exclude ?? [],
|
|
66
|
+
defaultLocale: config.defaultLocale,
|
|
67
|
+
locales: config.locales,
|
|
68
|
+
pageSeparator: opts.pageSeparator ?? '\n\n',
|
|
69
|
+
rawContent: opts.rawContent ?? false,
|
|
70
|
+
sidebarNav: opts.sidebarNav ?? false,
|
|
71
|
+
tieredHierarchy,
|
|
72
|
+
federatedSites: opts.federatedSites ?? [],
|
|
73
|
+
federatedSiteCategories: opts.federatedSiteCategories ?? [],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const modules = {
|
|
77
|
+
'virtual:starlight-llms-txt/context': `export const starlightLllmsTxtContext = ${JSON.stringify(
|
|
78
|
+
projectContext,
|
|
79
|
+
)}`,
|
|
80
|
+
};
|
|
81
|
+
const resolutionMap = Object.fromEntries(
|
|
82
|
+
(Object.keys(modules) as (keyof typeof modules)[]).map((key) => [resolveVirtualModuleId(key), key]),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
updateConfig({
|
|
86
|
+
vite: {
|
|
87
|
+
plugins: [
|
|
88
|
+
{
|
|
89
|
+
name: 'vite-plugin-starlight-llms-text',
|
|
90
|
+
resolveId(id): string | undefined {
|
|
91
|
+
if (id in modules) return resolveVirtualModuleId(id);
|
|
92
|
+
},
|
|
93
|
+
load(id): string | undefined {
|
|
94
|
+
const resolution = resolutionMap[id];
|
|
95
|
+
if (resolution) return modules[resolution];
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
|
|
110
|
+
return `\0${id}`;
|
|
111
|
+
}
|
package/llms-full.txt.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { generateLlmsTxt } from './generator';
|
|
3
|
+
import { getSiteTitle } from './utils';
|
|
4
|
+
|
|
5
|
+
// Explicitly set this to prerender so it works the same way for sites in `server` mode.
|
|
6
|
+
|
|
7
|
+
export const prerender = true;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Route that generates a single plaintext Markdown document from the full website content.
|
|
11
|
+
*/
|
|
12
|
+
export const GET: APIRoute = async (context) => {
|
|
13
|
+
const body = await generateLlmsTxt(context, {
|
|
14
|
+
minify: false,
|
|
15
|
+
description: `This is the full developer documentation for ${getSiteTitle()}`,
|
|
16
|
+
});
|
|
17
|
+
return new Response(body);
|
|
18
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { APIRoute, GetStaticPaths } from 'astro';
|
|
2
|
+
import { generateLlmsTxt } from './generator';
|
|
3
|
+
import { getLocaleKeys, getSiteTitle } from './utils';
|
|
4
|
+
|
|
5
|
+
export const prerender = true;
|
|
6
|
+
|
|
7
|
+
export const getStaticPaths: GetStaticPaths = () => {
|
|
8
|
+
return getLocaleKeys().map((locale) => ({ params: { locale } }));
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const GET: APIRoute = async (context) => {
|
|
12
|
+
const locale = context.params.locale as string;
|
|
13
|
+
const body = await generateLlmsTxt(context, {
|
|
14
|
+
minify: false,
|
|
15
|
+
description: `This is the full developer documentation for ${getSiteTitle()} (${locale})`,
|
|
16
|
+
locale,
|
|
17
|
+
});
|
|
18
|
+
return new Response(body);
|
|
19
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { starlightLllmsTxtContext } from 'virtual:starlight-llms-txt/context';
|
|
2
|
+
import type { APIRoute, GetStaticPaths } from 'astro';
|
|
3
|
+
import { generateLlmsTxt } from './generator';
|
|
4
|
+
import { getLocaleKeys, getSiteTitle } from './utils';
|
|
5
|
+
|
|
6
|
+
export const prerender = true;
|
|
7
|
+
|
|
8
|
+
export const getStaticPaths: GetStaticPaths = () => {
|
|
9
|
+
return getLocaleKeys().map((locale) => ({ params: { locale } }));
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const GET: APIRoute = async (context) => {
|
|
13
|
+
const locale = context.params.locale as string;
|
|
14
|
+
const body = await generateLlmsTxt(context, {
|
|
15
|
+
minify: true,
|
|
16
|
+
description: `This is the abridged developer documentation for ${getSiteTitle()} (${locale})`,
|
|
17
|
+
exclude: starlightLllmsTxtContext.exclude,
|
|
18
|
+
locale,
|
|
19
|
+
});
|
|
20
|
+
return new Response(body);
|
|
21
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { starlightLllmsTxtContext } from 'virtual:starlight-llms-txt/context';
|
|
2
|
+
import type { APIRoute } from 'astro';
|
|
3
|
+
import { generateLlmsTxt } from './generator';
|
|
4
|
+
import { getSiteTitle } from './utils';
|
|
5
|
+
|
|
6
|
+
// Explicitly set this to prerender so it works the same way for sites in `server` mode.
|
|
7
|
+
|
|
8
|
+
export const prerender = true;
|
|
9
|
+
/**
|
|
10
|
+
* Route that generates a single plaintext Markdown document from the full website content.
|
|
11
|
+
*/
|
|
12
|
+
export const GET: APIRoute = async (context) => {
|
|
13
|
+
const body = await generateLlmsTxt(context, {
|
|
14
|
+
minify: true,
|
|
15
|
+
description: `This is the abridged developer documentation for ${getSiteTitle()}`,
|
|
16
|
+
exclude: starlightLllmsTxtContext.exclude,
|
|
17
|
+
});
|
|
18
|
+
return new Response(body);
|
|
19
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type CollectionEntry, getCollection } from 'astro:content';
|
|
2
|
+
import { starlightLllmsTxtContext } from 'virtual:starlight-llms-txt/context';
|
|
3
|
+
import type { APIRoute, GetStaticPaths, InferGetStaticParamsType, InferGetStaticPropsType } from 'astro';
|
|
4
|
+
import { entryToSimpleMarkdown } from './entryToSimpleMarkdown';
|
|
5
|
+
import { buildTierTree, type DirectoryNode, getAllTierPaths, type LeafNode } from './tier-tree';
|
|
6
|
+
import { renderDirectoryIndex, renderLeafContent } from './tier-tree-render';
|
|
7
|
+
import { isDefaultLocale } from './utils';
|
|
8
|
+
|
|
9
|
+
export const prerender = true;
|
|
10
|
+
|
|
11
|
+
let cachedTree: DirectoryNode | undefined;
|
|
12
|
+
|
|
13
|
+
async function getTree(): Promise<DirectoryNode> {
|
|
14
|
+
if (cachedTree) return cachedTree;
|
|
15
|
+
const docs = await getCollection('docs', (doc) => isDefaultLocale(doc) && !doc.data.draft);
|
|
16
|
+
cachedTree = buildTierTree(docs, {
|
|
17
|
+
promote: starlightLllmsTxtContext.promote,
|
|
18
|
+
demote: starlightLllmsTxtContext.demote,
|
|
19
|
+
});
|
|
20
|
+
return cachedTree;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findNode(root: DirectoryNode, pathSegments: string[]): DirectoryNode | LeafNode | undefined {
|
|
24
|
+
let current: DirectoryNode = root;
|
|
25
|
+
for (let i = 0; i < pathSegments.length; i++) {
|
|
26
|
+
const seg = pathSegments[i] ?? '';
|
|
27
|
+
const child = current.children.get(seg);
|
|
28
|
+
if (!child) return undefined;
|
|
29
|
+
if (i === pathSegments.length - 1) return child;
|
|
30
|
+
if (child.type !== 'directory') return undefined;
|
|
31
|
+
current = child;
|
|
32
|
+
}
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const getStaticPaths = (async () => {
|
|
37
|
+
const tree = await getTree();
|
|
38
|
+
const tierPaths = getAllTierPaths(tree);
|
|
39
|
+
return tierPaths.map((tp) => ({
|
|
40
|
+
params: { path: tp.path },
|
|
41
|
+
props: { tierType: tp.type },
|
|
42
|
+
}));
|
|
43
|
+
}) satisfies GetStaticPaths;
|
|
44
|
+
|
|
45
|
+
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
|
46
|
+
type Params = InferGetStaticParamsType<typeof getStaticPaths>;
|
|
47
|
+
|
|
48
|
+
export const GET: APIRoute<Props, Params> = async (context) => {
|
|
49
|
+
const tree = await getTree();
|
|
50
|
+
const pathStr = context.params.path;
|
|
51
|
+
const segments = pathStr.split('/');
|
|
52
|
+
const node = findNode(tree, segments);
|
|
53
|
+
|
|
54
|
+
if (!node) {
|
|
55
|
+
return new Response('Not found', { status: 404 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const site = new URL(
|
|
59
|
+
starlightLllmsTxtContext.base.endsWith('/') ? starlightLllmsTxtContext.base : `${starlightLllmsTxtContext.base}/`,
|
|
60
|
+
context.site,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (node.type === 'directory') {
|
|
64
|
+
return new Response(renderDirectoryIndex(node, site));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const renderedContent = await entryToSimpleMarkdown(node.entry as unknown as CollectionEntry<'docs'>, context, false);
|
|
68
|
+
return new Response(renderLeafContent(node, renderedContent));
|
|
69
|
+
};
|
package/llms.txt.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getCollection } from 'astro:content';
|
|
2
|
+
import { starlightLllmsTxtContext } from 'virtual:starlight-llms-txt/context';
|
|
3
|
+
import type { APIRoute } from 'astro';
|
|
4
|
+
import { renderFederatedSites } from './federated-sites';
|
|
5
|
+
import { buildSectionTree, renderSectionTree } from './sidebar-nav';
|
|
6
|
+
import { ensureTrailingSlash, getSiteTitle, isDefaultLocale } from './utils';
|
|
7
|
+
|
|
8
|
+
export const prerender = true;
|
|
9
|
+
|
|
10
|
+
export const GET: APIRoute = async (context) => {
|
|
11
|
+
const title = getSiteTitle();
|
|
12
|
+
const description = starlightLllmsTxtContext.description ? `> ${starlightLllmsTxtContext.description}` : '';
|
|
13
|
+
const site = new URL(ensureTrailingSlash(starlightLllmsTxtContext.base), context.site);
|
|
14
|
+
const llmsFullLink = new URL('./llms-full.txt', site);
|
|
15
|
+
const llmsSmallLink = new URL('./llms-small.txt', site);
|
|
16
|
+
|
|
17
|
+
const segments = [`# ${title}`];
|
|
18
|
+
if (description) segments.push(description);
|
|
19
|
+
if (starlightLllmsTxtContext.details) segments.push(starlightLllmsTxtContext.details);
|
|
20
|
+
|
|
21
|
+
segments.push(`## Documentation Sets`);
|
|
22
|
+
segments.push(
|
|
23
|
+
[
|
|
24
|
+
`- [Abridged documentation](${llmsSmallLink}): a compact version of the documentation for ${getSiteTitle()}, with non-essential content removed`,
|
|
25
|
+
`- [Complete documentation](${llmsFullLink}): the full documentation for ${getSiteTitle()}`,
|
|
26
|
+
].join('\n'),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (starlightLllmsTxtContext.sidebarNav) {
|
|
30
|
+
const docs = await getCollection('docs', (doc) => isDefaultLocale(doc) && !doc.data.draft);
|
|
31
|
+
const tree = buildSectionTree(docs, starlightLllmsTxtContext.promote, starlightLllmsTxtContext.demote);
|
|
32
|
+
const rendered = renderSectionTree(tree, site);
|
|
33
|
+
if (rendered) segments.push(rendered);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
const rendered = renderFederatedSites(
|
|
38
|
+
starlightLllmsTxtContext.federatedSites,
|
|
39
|
+
starlightLllmsTxtContext.federatedSiteCategories,
|
|
40
|
+
);
|
|
41
|
+
if (rendered) segments.push(rendered);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
segments.push(`## Notes`);
|
|
45
|
+
segments.push(`- The complete documentation includes all content from the official documentation
|
|
46
|
+
- The content is automatically generated from the same source as the official documentation`);
|
|
47
|
+
|
|
48
|
+
if (starlightLllmsTxtContext.optionalLinks.length > 0) {
|
|
49
|
+
segments.push('## Optional');
|
|
50
|
+
segments.push(
|
|
51
|
+
starlightLllmsTxtContext.optionalLinks
|
|
52
|
+
.map((link) => `- [${link.label}](${link.url})${link.description ? `: ${link.description}` : ''}`)
|
|
53
|
+
.join('\n'),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return new Response(`${segments.join('\n\n')}\n`);
|
|
58
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@f5-sales-demo/starlight-llms-txt",
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Generate llms.txt files to train large language models on your Starlight documentation website",
|
|
6
|
+
"author": "f5-sales-demo (fork of delucis/starlight-llms-txt)",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"*.ts",
|
|
13
|
+
"!*.test.ts",
|
|
14
|
+
"!vitest.config.ts",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@astrojs/mdx": "^5.0.3",
|
|
19
|
+
"@types/hast": "^3.0.4",
|
|
20
|
+
"@types/micromatch": "^4.0.10",
|
|
21
|
+
"github-slugger": "^2.0.0",
|
|
22
|
+
"hast-util-select": "^6.0.4",
|
|
23
|
+
"micromatch": "^4.0.8",
|
|
24
|
+
"rehype-parse": "^9.0.1",
|
|
25
|
+
"rehype-remark": "^10.0.1",
|
|
26
|
+
"remark-gfm": "^4.0.1",
|
|
27
|
+
"remark-stringify": "^11.0.0",
|
|
28
|
+
"unified": "^11.0.5",
|
|
29
|
+
"unist-util-remove": "^4.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@astrojs/starlight": "^0.38.3",
|
|
33
|
+
"astro": "^6.1.5",
|
|
34
|
+
"vitest": "^3.2.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@astrojs/starlight": ">=0.38.0",
|
|
42
|
+
"astro": ">=6.0.0"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://f5-sales-demo.github.io/starlight-llms-txt/",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/f5-sales-demo/starlight-llms-txt.git",
|
|
51
|
+
"directory": "packages/starlight-llms-txt"
|
|
52
|
+
},
|
|
53
|
+
"bugs": "https://github.com/f5-sales-demo/starlight-llms-txt/issues",
|
|
54
|
+
"keywords": [
|
|
55
|
+
"llms.txt",
|
|
56
|
+
"withastro",
|
|
57
|
+
"starlight"
|
|
58
|
+
]
|
|
59
|
+
}
|
package/sidebar-nav.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { buildTierTree, type DirectoryNode } from './tier-tree';
|
|
2
|
+
|
|
3
|
+
type DocLike = {
|
|
4
|
+
id: string;
|
|
5
|
+
data: {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
draft?: boolean;
|
|
9
|
+
sidebar?: { order?: number };
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function buildSectionTree(docs: DocLike[], promote: string[] = [], demote: string[] = []): DirectoryNode {
|
|
14
|
+
return buildTierTree(docs, { promote, demote });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderSectionTree(tree: DirectoryNode, site: URL): string {
|
|
18
|
+
if (tree.children.size === 0) return '';
|
|
19
|
+
|
|
20
|
+
const lines: string[] = ['## Sections', ''];
|
|
21
|
+
|
|
22
|
+
for (const [, child] of tree.children) {
|
|
23
|
+
const descSuffix = child.meta.description ? `: ${child.meta.description}` : '';
|
|
24
|
+
const url = new URL(`./_llms-txt/${child.slug}.txt`, site);
|
|
25
|
+
lines.push(`- [${child.meta.title}](${url})${descSuffix}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return lines.join('\n');
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { DirectoryNode, LeafNode } from './tier-tree';
|
|
2
|
+
|
|
3
|
+
export function renderDirectoryIndex(node: DirectoryNode, site: URL): string {
|
|
4
|
+
const segments: string[] = [];
|
|
5
|
+
|
|
6
|
+
segments.push(`# ${node.meta.title}`);
|
|
7
|
+
if (node.meta.description) {
|
|
8
|
+
segments.push(`> ${node.meta.description}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
segments.push('## Contents');
|
|
12
|
+
|
|
13
|
+
const lines: string[] = [];
|
|
14
|
+
for (const [, child] of node.children) {
|
|
15
|
+
const childPath = child.slug;
|
|
16
|
+
const url = new URL(`./_llms-txt/${childPath}.txt`, site);
|
|
17
|
+
const title = child.meta.title;
|
|
18
|
+
const desc = child.meta.description ? `: ${child.meta.description}` : '';
|
|
19
|
+
lines.push(`- [${title}](${url})${desc}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
segments.push(lines.join('\n'));
|
|
23
|
+
|
|
24
|
+
return segments.join('\n\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function renderLeafContent(leaf: LeafNode, renderedContent?: string): string {
|
|
28
|
+
const segments: string[] = [];
|
|
29
|
+
|
|
30
|
+
const title = leaf.entry.data.hero?.title || leaf.entry.data.title;
|
|
31
|
+
segments.push(`# ${title}`);
|
|
32
|
+
|
|
33
|
+
const description = leaf.entry.data.hero?.tagline || leaf.entry.data.description;
|
|
34
|
+
if (description) {
|
|
35
|
+
segments.push(`> ${description}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (renderedContent !== undefined) {
|
|
39
|
+
segments.push(renderedContent);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return segments.join('\n\n');
|
|
43
|
+
}
|
package/tier-tree.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// packages/starlight-llms-txt/tier-tree.ts
|
|
2
|
+
import micromatch from 'micromatch';
|
|
3
|
+
|
|
4
|
+
export interface DirectoryNode {
|
|
5
|
+
type: 'directory';
|
|
6
|
+
slug: string;
|
|
7
|
+
segment: string;
|
|
8
|
+
meta: { title: string; description?: string };
|
|
9
|
+
children: Map<string, DirectoryNode | LeafNode>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LeafNode {
|
|
13
|
+
type: 'leaf';
|
|
14
|
+
slug: string;
|
|
15
|
+
segment: string;
|
|
16
|
+
meta: { title: string; description?: string };
|
|
17
|
+
entry: {
|
|
18
|
+
id: string;
|
|
19
|
+
data: {
|
|
20
|
+
title: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
draft?: boolean;
|
|
23
|
+
sidebar?: { order?: number };
|
|
24
|
+
hero?: { title?: string; tagline?: string };
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type TierNode = DirectoryNode | LeafNode;
|
|
30
|
+
|
|
31
|
+
export interface TierTreeOptions {
|
|
32
|
+
promote?: string[];
|
|
33
|
+
demote?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function titleCase(segment: string): string {
|
|
37
|
+
return segment
|
|
38
|
+
.split(/[-_]/)
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
41
|
+
.join(' ');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sortKey(id: string, promote: string[], demote: string[]): string {
|
|
45
|
+
const demoted = demote.findIndex((expr) => micromatch.isMatch(id, expr));
|
|
46
|
+
const promoted = demoted > -1 ? -1 : promote.findIndex((expr) => micromatch.isMatch(id, expr));
|
|
47
|
+
const prefixLength = (promoted > -1 ? promote.length - promoted : 0) + demote.length - demoted - 1;
|
|
48
|
+
return '_'.repeat(prefixLength) + id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type DocLike = {
|
|
52
|
+
id: string;
|
|
53
|
+
data: {
|
|
54
|
+
title: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
draft?: boolean;
|
|
57
|
+
sidebar?: { order?: number };
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export function buildTierTree(entries: DocLike[], options: TierTreeOptions = {}): DirectoryNode {
|
|
62
|
+
const { promote = [], demote = [] } = options;
|
|
63
|
+
|
|
64
|
+
const filtered = entries.filter((e) => !e.data.draft);
|
|
65
|
+
|
|
66
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
67
|
+
const keyA = sortKey(a.id, promote, demote);
|
|
68
|
+
const keyB = sortKey(b.id, promote, demote);
|
|
69
|
+
if (keyA !== keyB) return keyA.localeCompare(keyB);
|
|
70
|
+
const orderA = a.data.sidebar?.order ?? Infinity;
|
|
71
|
+
const orderB = b.data.sidebar?.order ?? Infinity;
|
|
72
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
73
|
+
return a.data.title.localeCompare(b.data.title);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const root: DirectoryNode = {
|
|
77
|
+
type: 'directory',
|
|
78
|
+
slug: '',
|
|
79
|
+
segment: '',
|
|
80
|
+
meta: { title: '' },
|
|
81
|
+
children: new Map(),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const entry of sorted) {
|
|
85
|
+
const segments = entry.id.split('/');
|
|
86
|
+
|
|
87
|
+
if (segments.length === 1) {
|
|
88
|
+
const seg0 = segments[0];
|
|
89
|
+
if (!seg0) continue;
|
|
90
|
+
root.children.set(seg0, {
|
|
91
|
+
type: 'leaf',
|
|
92
|
+
slug: entry.id,
|
|
93
|
+
segment: seg0,
|
|
94
|
+
meta: { title: entry.data.title, description: entry.data.description },
|
|
95
|
+
entry,
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let current = root;
|
|
101
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
102
|
+
const seg = segments[i];
|
|
103
|
+
if (!seg) continue;
|
|
104
|
+
const existingChild = current.children.get(seg);
|
|
105
|
+
if (!existingChild) {
|
|
106
|
+
const slugPath = segments.slice(0, i + 1).join('/');
|
|
107
|
+
const dirNode: DirectoryNode = {
|
|
108
|
+
type: 'directory',
|
|
109
|
+
slug: slugPath,
|
|
110
|
+
segment: seg,
|
|
111
|
+
meta: { title: titleCase(seg) },
|
|
112
|
+
children: new Map(),
|
|
113
|
+
};
|
|
114
|
+
current.children.set(seg, dirNode);
|
|
115
|
+
current = dirNode;
|
|
116
|
+
} else if (existingChild.type === 'leaf') {
|
|
117
|
+
const slugPath = segments.slice(0, i + 1).join('/');
|
|
118
|
+
const dirNode: DirectoryNode = {
|
|
119
|
+
type: 'directory',
|
|
120
|
+
slug: slugPath,
|
|
121
|
+
segment: seg,
|
|
122
|
+
meta: { title: existingChild.meta.title, description: existingChild.meta.description },
|
|
123
|
+
children: new Map(),
|
|
124
|
+
};
|
|
125
|
+
dirNode.children.set('index', {
|
|
126
|
+
...existingChild,
|
|
127
|
+
slug: `${slugPath}/index`,
|
|
128
|
+
segment: 'index',
|
|
129
|
+
});
|
|
130
|
+
current.children.set(seg, dirNode);
|
|
131
|
+
current = dirNode;
|
|
132
|
+
} else {
|
|
133
|
+
current = existingChild;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const leafSegment = segments[segments.length - 1];
|
|
138
|
+
if (!leafSegment) continue;
|
|
139
|
+
const leaf: LeafNode = {
|
|
140
|
+
type: 'leaf',
|
|
141
|
+
slug: entry.id,
|
|
142
|
+
segment: leafSegment,
|
|
143
|
+
meta: { title: entry.data.title, description: entry.data.description },
|
|
144
|
+
entry,
|
|
145
|
+
};
|
|
146
|
+
current.children.set(leafSegment, leaf);
|
|
147
|
+
|
|
148
|
+
if (leafSegment === 'index') {
|
|
149
|
+
current.meta.title = entry.data.title;
|
|
150
|
+
current.meta.description = entry.data.description;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pruneEmptyDirectories(root);
|
|
155
|
+
|
|
156
|
+
return root;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function pruneEmptyDirectories(node: DirectoryNode): boolean {
|
|
160
|
+
for (const [key, child] of node.children) {
|
|
161
|
+
if (child.type === 'directory') {
|
|
162
|
+
const isEmpty = pruneEmptyDirectories(child);
|
|
163
|
+
if (isEmpty) node.children.delete(key);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return node.children.size === 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface TierPath {
|
|
170
|
+
path: string;
|
|
171
|
+
type: 'directory' | 'leaf';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function getAllTierPaths(root: DirectoryNode): TierPath[] {
|
|
175
|
+
const paths: TierPath[] = [];
|
|
176
|
+
|
|
177
|
+
function walk(node: DirectoryNode): void {
|
|
178
|
+
for (const [, child] of node.children) {
|
|
179
|
+
if (child.type === 'directory') {
|
|
180
|
+
paths.push({ path: child.slug, type: 'directory' });
|
|
181
|
+
walk(child);
|
|
182
|
+
} else {
|
|
183
|
+
paths.push({ path: child.slug, type: 'leaf' });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
walk(root);
|
|
189
|
+
return paths;
|
|
190
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { StarlightUserConfig } from '@astrojs/starlight/types';
|
|
2
|
+
import type { AstroConfig } from 'astro';
|
|
3
|
+
|
|
4
|
+
interface FederatedSiteCategoryUserConfig {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProjectContext {
|
|
11
|
+
base: AstroConfig['base'];
|
|
12
|
+
defaultLocale: StarlightUserConfig['defaultLocale'];
|
|
13
|
+
locales: StarlightUserConfig['locales'];
|
|
14
|
+
title: StarlightUserConfig['title'];
|
|
15
|
+
description: StarlightUserConfig['description'];
|
|
16
|
+
details: StarlightLllmsTextOptions['details'];
|
|
17
|
+
optionalLinks: NonNullable<StarlightLllmsTextOptions['optionalLinks']>;
|
|
18
|
+
minify: NonNullable<StarlightLllmsTextOptions['minify']>;
|
|
19
|
+
promote: NonNullable<StarlightLllmsTextOptions['promote']>;
|
|
20
|
+
demote: NonNullable<StarlightLllmsTextOptions['demote']>;
|
|
21
|
+
exclude: NonNullable<StarlightLllmsTextOptions['exclude']>;
|
|
22
|
+
pageSeparator: NonNullable<StarlightLllmsTextOptions['pageSeparator']>;
|
|
23
|
+
rawContent: NonNullable<StarlightLllmsTextOptions['rawContent']>;
|
|
24
|
+
sidebarNav: NonNullable<StarlightLllmsTextOptions['sidebarNav']>;
|
|
25
|
+
tieredHierarchy: NonNullable<StarlightLllmsTextOptions['tieredHierarchy']>;
|
|
26
|
+
federatedSites: NonNullable<StarlightLllmsTextOptions['federatedSites']>;
|
|
27
|
+
federatedSiteCategories: NonNullable<StarlightLllmsTextOptions['federatedSiteCategories']>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StarlightLllmsTextOptions {
|
|
31
|
+
projectName?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
details?: string;
|
|
34
|
+
optionalLinks?: Array<{ label: string; url: string; description?: string }>;
|
|
35
|
+
minify?: {
|
|
36
|
+
note?: boolean;
|
|
37
|
+
tip?: boolean;
|
|
38
|
+
caution?: boolean;
|
|
39
|
+
danger?: boolean;
|
|
40
|
+
details?: boolean;
|
|
41
|
+
whitespace?: boolean;
|
|
42
|
+
customSelectors?: string[];
|
|
43
|
+
};
|
|
44
|
+
promote?: string[];
|
|
45
|
+
demote?: string[];
|
|
46
|
+
exclude?: string[];
|
|
47
|
+
pageSeparator?: string;
|
|
48
|
+
rawContent?: boolean;
|
|
49
|
+
sidebarNav?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* When enabled, auto-generates a tiered `.txt` hierarchy under `/_llms-txt/`.
|
|
52
|
+
* @default true
|
|
53
|
+
*/
|
|
54
|
+
tieredHierarchy?: boolean;
|
|
55
|
+
federatedSites?: Array<{
|
|
56
|
+
label: string;
|
|
57
|
+
url: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
category?: string;
|
|
60
|
+
}>;
|
|
61
|
+
federatedSiteCategories?: Array<FederatedSiteCategoryUserConfig>;
|
|
62
|
+
}
|
package/utils.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CollectionEntry } from 'astro:content';
|
|
2
|
+
import { starlightLllmsTxtContext } from 'virtual:starlight-llms-txt/context';
|
|
3
|
+
|
|
4
|
+
const { defaultLocale, locales, title } = starlightLllmsTxtContext;
|
|
5
|
+
export const defaultLang = (defaultLocale === 'root' ? locales?.root?.lang : defaultLocale) || 'en';
|
|
6
|
+
|
|
7
|
+
/** Get the site title from the Starlight config. */
|
|
8
|
+
export function getSiteTitle(): string {
|
|
9
|
+
return typeof title === 'string' ? title : (title[defaultLang] as string);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const localeKeys = Object.keys(locales || {}).filter((key) => key !== 'root' && key !== defaultLang);
|
|
13
|
+
const startsWithLocaleRE = new RegExp(`^(${localeKeys.join('|')})/`);
|
|
14
|
+
|
|
15
|
+
/** Check if a content collection entry is part of the default locale or not. */
|
|
16
|
+
export function isDefaultLocale(doc: CollectionEntry<'docs'>): boolean {
|
|
17
|
+
return !(localeKeys.includes(doc.id) || startsWithLocaleRE.test(doc.id));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Check if a content collection entry belongs to a specific locale. */
|
|
21
|
+
export function isLocale(doc: CollectionEntry<'docs'>, targetLocale: string): boolean {
|
|
22
|
+
if (targetLocale === defaultLang || targetLocale === 'root') return isDefaultLocale(doc);
|
|
23
|
+
return doc.id === targetLocale || doc.id.startsWith(`${targetLocale}/`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Get all non-default locale keys. */
|
|
27
|
+
export function getLocaleKeys(): string[] {
|
|
28
|
+
return localeKeys;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Append a `/` to the passed string if it doesn’t already end with one. */
|
|
32
|
+
export function ensureTrailingSlash(path: string) {
|
|
33
|
+
return path.at(-1) === '/' ? path : `${path}/`;
|
|
34
|
+
}
|