@docsector/docsector-reader 0.12.3 → 1.0.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 +60 -0
- package/bin/docsector.js +46 -8
- package/package.json +1 -1
- package/src/components/DPageBar.vue +8 -0
- package/src/components/DPageSection.vue +86 -1
- package/src/components/DQuickLinks.vue +98 -0
- package/src/i18n/helpers.js +52 -0
- package/src/index.js +14 -1
- package/src/pages/Homepage.en-US.md +17 -0
- package/src/pages/Homepage.pt-BR.md +17 -0
- package/src/quasar.factory.js +46 -0
- package/src/router/routes.js +22 -3
- package/src/store/App.js +3 -3
- package/src/pages/@/BootPage.vue +0 -106
- /package/src/pages/{@/404Page.vue → 404Page.vue} +0 -0
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
26
26
|
- 🤖 **LLM Bot Detection** — Automatically serves raw Markdown to known AI crawlers (GPTBot, ClaudeBot, PerplexityBot, GrokBot, and others)
|
|
27
27
|
- 🗺️ **Sitemap Generation** — Automatic `sitemap.xml` generation at build time with all page URLs (requires `siteUrl` in config)
|
|
28
28
|
- 🤖 **AI-Friendly robots.txt** — Scaffold includes a `robots.txt` explicitly allowing 23 AI crawlers (GPTBot, ClaudeBot, PerplexityBot, GrokBot, etc.)
|
|
29
|
+
- 🔗 **Homepage Link Headers** — Auto-generated `Link` response headers for agent discovery (`service-doc`, `service-desc`, `describedby`) per RFC 8288 / RFC 9727
|
|
29
30
|
- 🔌 **MCP Server** — Auto-generated [MCP](https://modelcontextprotocol.io) server at `/mcp` for AI assistant integration (Claude Desktop, VS Code, etc.)
|
|
30
31
|
- 📄 **llms.txt / llms-full.txt** — Auto-generated [llms.txt](https://llmstxt.org) index and full-content file for LLM discovery (requires `siteUrl` in config)
|
|
31
32
|
|
|
@@ -45,6 +46,8 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
45
46
|
- ✏️ **Edit on GitHub** — Direct links to edit pages on your repository
|
|
46
47
|
- 📅 **Last Updated Date** — Automatic per-page "last updated" date from git commit history, locale-formatted
|
|
47
48
|
- 📊 **Translation Progress** — Automatic translation percentage based on header coverage
|
|
49
|
+
- 🏠 **Markdown Home at Root** — Homepage is rendered from `src/pages/Homepage.{lang}.md` directly at `/`
|
|
50
|
+
- 🧭 **Quick Links Custom Element** — Use `<d-quick-links>` and `<d-quick-link>` in Markdown to render rich home navigation cards
|
|
48
51
|
- ⚙️ **Single Config File** — Customize branding, links, and languages via `docsector.config.js`
|
|
49
52
|
|
|
50
53
|
---
|
|
@@ -153,6 +156,56 @@ export default {
|
|
|
153
156
|
|
|
154
157
|
---
|
|
155
158
|
|
|
159
|
+
## 🔗 Link Headers (Agent Discovery)
|
|
160
|
+
|
|
161
|
+
Docsector Reader adds homepage `Link` response headers at build time for agent discovery, following [RFC 8288](https://www.rfc-editor.org/rfc/rfc8288) and [RFC 9727](https://www.rfc-editor.org/rfc/rfc9727#section-3).
|
|
162
|
+
|
|
163
|
+
Default relations emitted on homepage (`/` and `/index.html`):
|
|
164
|
+
|
|
165
|
+
- `rel="service-doc"` → `</>`
|
|
166
|
+
- `rel="service-desc"` → `</mcp>` (only when `mcp` is enabled)
|
|
167
|
+
- `rel="describedby"` → `</llms.txt>` (only when `siteUrl` is configured, i.e. `llms.txt` is generated)
|
|
168
|
+
|
|
169
|
+
Generated in:
|
|
170
|
+
|
|
171
|
+
- `dist/spa/_headers`
|
|
172
|
+
|
|
173
|
+
### Optional configuration
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
export default {
|
|
177
|
+
// ...other config
|
|
178
|
+
|
|
179
|
+
linkHeaders: {
|
|
180
|
+
enabled: true,
|
|
181
|
+
serviceDoc: '/',
|
|
182
|
+
serviceDesc: '/mcp',
|
|
183
|
+
describedBy: '/llms.txt'
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Set any target to `null` or `false` to disable that relation.
|
|
189
|
+
|
|
190
|
+
### Validate
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
npx docsector build
|
|
194
|
+
cat dist/spa/_headers
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Or scan discoverability:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
curl -X POST https://isitagentready.com/api/scan \
|
|
201
|
+
-H 'Content-Type: application/json' \
|
|
202
|
+
-d '{"url":"https://YOUR-SITE.com"}'
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Check `checks.discoverability.linkHeaders.status` equals `"pass"`.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
156
209
|
## �🚀 Quick Start
|
|
157
210
|
|
|
158
211
|
### 📦 Install
|
|
@@ -288,6 +341,13 @@ export default {
|
|
|
288
341
|
editBaseUrl: 'https://github.com/org/repo/edit/main/src/pages'
|
|
289
342
|
},
|
|
290
343
|
|
|
344
|
+
linkHeaders: {
|
|
345
|
+
enabled: true,
|
|
346
|
+
serviceDoc: '/',
|
|
347
|
+
serviceDesc: '/mcp',
|
|
348
|
+
describedBy: '/llms.txt'
|
|
349
|
+
},
|
|
350
|
+
|
|
291
351
|
languages: [
|
|
292
352
|
{ image: '/images/flags/united-states-of-america.png', label: 'English (US)', value: 'en-US' },
|
|
293
353
|
{ image: '/images/flags/brazil.png', label: 'Português (BR)', value: 'pt-BR' }
|
package/bin/docsector.js
CHANGED
|
@@ -23,7 +23,7 @@ const packageRoot = resolve(__dirname, '..')
|
|
|
23
23
|
const args = process.argv.slice(2)
|
|
24
24
|
const command = args[0]
|
|
25
25
|
|
|
26
|
-
const VERSION = '0.
|
|
26
|
+
const VERSION = '1.0.0'
|
|
27
27
|
|
|
28
28
|
const HELP = `
|
|
29
29
|
Docsector Reader v${VERSION}
|
|
@@ -133,6 +133,14 @@ export default {
|
|
|
133
133
|
// toolSuffix: 'my_docs'
|
|
134
134
|
// },
|
|
135
135
|
|
|
136
|
+
// @ Homepage Link headers for agent discovery (optional)
|
|
137
|
+
// linkHeaders: {
|
|
138
|
+
// enabled: true,
|
|
139
|
+
// serviceDoc: '/',
|
|
140
|
+
// serviceDesc: '/mcp',
|
|
141
|
+
// describedBy: '/llms.txt'
|
|
142
|
+
// },
|
|
143
|
+
|
|
136
144
|
// @ Languages
|
|
137
145
|
languages: [
|
|
138
146
|
{
|
|
@@ -333,6 +341,22 @@ export default {
|
|
|
333
341
|
}
|
|
334
342
|
`
|
|
335
343
|
|
|
344
|
+
const TEMPLATE_HOMEPAGE_MD = `\
|
|
345
|
+
# Welcome to Docsector Reader
|
|
346
|
+
|
|
347
|
+
Docsector Reader is a markdown-first documentation engine.
|
|
348
|
+
|
|
349
|
+
## Quick Links
|
|
350
|
+
|
|
351
|
+
- [Getting Started](/guide/getting-started/overview/)
|
|
352
|
+
- [Configuration](/guide/configuration/overview/)
|
|
353
|
+
- [Pages and Routing](/guide/pages-and-routing/overview/)
|
|
354
|
+
|
|
355
|
+
## About
|
|
356
|
+
|
|
357
|
+
- Repository: [docsector/docsector-reader](https://github.com/docsector/docsector-reader)
|
|
358
|
+
`
|
|
359
|
+
|
|
336
360
|
const TEMPLATE_BOOT_PAGE = `\
|
|
337
361
|
<template>
|
|
338
362
|
<q-page-container>
|
|
@@ -695,6 +719,18 @@ npm-debug.log*
|
|
|
695
719
|
.thumbs.db
|
|
696
720
|
`
|
|
697
721
|
|
|
722
|
+
const TEMPLATE_MARKDOWNLINT = `\
|
|
723
|
+
{
|
|
724
|
+
"MD013": false,
|
|
725
|
+
"MD033": {
|
|
726
|
+
"allowed_elements": [
|
|
727
|
+
"d-quick-links",
|
|
728
|
+
"d-quick-link"
|
|
729
|
+
]
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
`
|
|
733
|
+
|
|
698
734
|
const TEMPLATE_ROBOTS_TXT = `\
|
|
699
735
|
User-agent: *
|
|
700
736
|
Allow: /
|
|
@@ -814,9 +850,11 @@ Here's an overview of the project files:
|
|
|
814
850
|
| --- | --- |
|
|
815
851
|
| \`docsector.config.js\` | Branding, links, languages, and GitHub config |
|
|
816
852
|
| \`quasar.config.js\` | Quasar/Vite build configuration (via factory) |
|
|
853
|
+
| \`.markdownlint.json\` | Markdown lint rules (allows Docsector custom tags) |
|
|
817
854
|
| \`src/pages/index.js\` | Page registry — defines all documentation pages |
|
|
818
855
|
| \`src/pages/boot.js\` | Boot metadata for the home page |
|
|
819
|
-
| \`src/pages
|
|
856
|
+
| \`src/pages/Homepage.en-US.md\` | Home page content in Markdown |
|
|
857
|
+
| \`src/pages/404Page.vue\` | Not found page |
|
|
820
858
|
| \`src/pages/guide/\` | Guide pages (Markdown files) |
|
|
821
859
|
| \`src/i18n/languages/\` | Translation files (HJSON format) |
|
|
822
860
|
| \`src/css/app.sass\` | Custom styles |
|
|
@@ -919,7 +957,6 @@ function initProject (name) {
|
|
|
919
957
|
'src/i18n',
|
|
920
958
|
'src/i18n/languages',
|
|
921
959
|
'src/pages',
|
|
922
|
-
'src/pages/@',
|
|
923
960
|
'src/pages/guide',
|
|
924
961
|
'public',
|
|
925
962
|
'public/images',
|
|
@@ -936,6 +973,7 @@ function initProject (name) {
|
|
|
936
973
|
['package.json', getTemplatePackageJson(name)],
|
|
937
974
|
['quasar.config.js', TEMPLATE_QUASAR_CONFIG],
|
|
938
975
|
['docsector.config.js', TEMPLATE_DOCSECTOR_CONFIG],
|
|
976
|
+
['.markdownlint.json', TEMPLATE_MARKDOWNLINT],
|
|
939
977
|
['index.html', TEMPLATE_INDEX_HTML],
|
|
940
978
|
['postcss.config.cjs', TEMPLATE_POSTCSS],
|
|
941
979
|
['.gitignore', TEMPLATE_GITIGNORE],
|
|
@@ -945,8 +983,8 @@ function initProject (name) {
|
|
|
945
983
|
['src/i18n/languages/en-US.hjson', TEMPLATE_I18N_HJSON],
|
|
946
984
|
['src/pages/index.js', TEMPLATE_PAGES_INDEX],
|
|
947
985
|
['src/pages/boot.js', TEMPLATE_PAGES_BOOT],
|
|
948
|
-
['src/pages
|
|
949
|
-
['src/pages
|
|
986
|
+
['src/pages/Homepage.en-US.md', TEMPLATE_HOMEPAGE_MD],
|
|
987
|
+
['src/pages/404Page.vue', TEMPLATE_404_PAGE],
|
|
950
988
|
['src/pages/guide/getting-started.overview.en-US.md', TEMPLATE_GETTING_STARTED_MD]
|
|
951
989
|
]
|
|
952
990
|
|
|
@@ -964,6 +1002,7 @@ function initProject (name) {
|
|
|
964
1002
|
console.log(` ${name}/`)
|
|
965
1003
|
console.log(' ├── docsector.config.js')
|
|
966
1004
|
console.log(' ├── quasar.config.js')
|
|
1005
|
+
console.log(' ├── .markdownlint.json')
|
|
967
1006
|
console.log(' ├── package.json')
|
|
968
1007
|
console.log(' ├── index.html')
|
|
969
1008
|
console.log(' ├── postcss.config.cjs')
|
|
@@ -982,9 +1021,8 @@ function initProject (name) {
|
|
|
982
1021
|
console.log(' └── pages/')
|
|
983
1022
|
console.log(' ├── index.js')
|
|
984
1023
|
console.log(' ├── boot.js')
|
|
985
|
-
console.log(' ├──
|
|
986
|
-
console.log('
|
|
987
|
-
console.log(' │ └── 404Page.vue')
|
|
1024
|
+
console.log(' ├── Homepage.en-US.md')
|
|
1025
|
+
console.log(' ├── 404Page.vue')
|
|
988
1026
|
console.log(' └── guide/')
|
|
989
1027
|
console.log(' └── getting-started.overview.en-US.md')
|
|
990
1028
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docsector/docsector-reader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "A documentation rendering engine built with Vue 3, Quasar v2 and Vite. Transform Markdown into beautiful, navigable documentation sites.",
|
|
5
5
|
"productName": "Docsector Reader",
|
|
6
6
|
"author": "Rodrigo de Araujo Vieira",
|
|
@@ -70,11 +70,19 @@ const rawMarkdown = computed(() => {
|
|
|
70
70
|
})
|
|
71
71
|
|
|
72
72
|
const markdownURL = computed(() => {
|
|
73
|
+
if (store.state.page.base === 'home') {
|
|
74
|
+
return `/Homepage.${locale.value}.md`
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
const path = route.path.replace(/\/+$/, '')
|
|
74
78
|
return `${path}.md`
|
|
75
79
|
})
|
|
76
80
|
|
|
77
81
|
const fullMarkdownURL = computed(() => {
|
|
82
|
+
if (store.state.page.base === 'home') {
|
|
83
|
+
return `${window.location.origin}/Homepage.${locale.value}.md`
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
const path = route.path.replace(/\/+$/, '')
|
|
79
87
|
return `${window.location.origin}${path}.md`
|
|
80
88
|
})
|
|
@@ -13,6 +13,7 @@ import DH6 from './DH6.vue'
|
|
|
13
13
|
import DPageSourceCode from './DPageSourceCode.vue'
|
|
14
14
|
import DMermaidDiagram from './DMermaidDiagram.vue'
|
|
15
15
|
import DPageBlockquote from './DPageBlockquote.vue'
|
|
16
|
+
import DQuickLinks from './DQuickLinks.vue'
|
|
16
17
|
|
|
17
18
|
const props = defineProps({
|
|
18
19
|
id: {
|
|
@@ -49,6 +50,71 @@ const parseAlertMarker = (rawContent = '') => {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
const QUICK_LINKS_MARKER_PREFIX = '@@DOCSECTOR_QUICK_LINKS_'
|
|
54
|
+
|
|
55
|
+
const parseTagAttributes = (raw = '') => {
|
|
56
|
+
const attrs = {}
|
|
57
|
+
const pattern = /(\w+)\s*=\s*"([^"]*)"|(\w+)\s*=\s*'([^']*)'/g
|
|
58
|
+
|
|
59
|
+
let match = pattern.exec(raw)
|
|
60
|
+
while (match !== null) {
|
|
61
|
+
const key = match[1] || match[3]
|
|
62
|
+
const value = match[2] || match[4] || ''
|
|
63
|
+
attrs[key] = value
|
|
64
|
+
match = pattern.exec(raw)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return attrs
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const extractQuickLinksBlocks = (source = '') => {
|
|
71
|
+
const map = new Map()
|
|
72
|
+
let index = 0
|
|
73
|
+
|
|
74
|
+
const blockPattern = /<d-quick-links\b([^>]*)>([\s\S]*?)<\/d-quick-links>/gi
|
|
75
|
+
const replaced = String(source).replace(blockPattern, (_, blockAttrsRaw, inner) => {
|
|
76
|
+
const blockAttrs = parseTagAttributes(blockAttrsRaw)
|
|
77
|
+
const items = []
|
|
78
|
+
const itemPattern = /<d-quick-link\b([^>]*)\/?\s*>/gi
|
|
79
|
+
|
|
80
|
+
let itemMatch = itemPattern.exec(inner)
|
|
81
|
+
while (itemMatch !== null) {
|
|
82
|
+
const attrs = parseTagAttributes(itemMatch[1])
|
|
83
|
+
const title = attrs.title || ''
|
|
84
|
+
const description = attrs.description || ''
|
|
85
|
+
const to = attrs.to || ''
|
|
86
|
+
const href = attrs.href || ''
|
|
87
|
+
|
|
88
|
+
if (title && description && (to || href)) {
|
|
89
|
+
items.push({
|
|
90
|
+
icon: attrs.icon || 'link',
|
|
91
|
+
title,
|
|
92
|
+
description,
|
|
93
|
+
to,
|
|
94
|
+
href
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
itemMatch = itemPattern.exec(inner)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const marker = `${QUICK_LINKS_MARKER_PREFIX}${index}@@`
|
|
102
|
+
index++
|
|
103
|
+
|
|
104
|
+
map.set(marker, {
|
|
105
|
+
title: blockAttrs.title || '',
|
|
106
|
+
items
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return `\n${marker}\n`
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
source: replaced,
|
|
114
|
+
quickLinksMap: map
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
52
118
|
const tokenized = computed(() => {
|
|
53
119
|
const absolute = store.state.i18n.absolute
|
|
54
120
|
|
|
@@ -62,6 +128,8 @@ const tokenized = computed(() => {
|
|
|
62
128
|
.replace(/}/g, '}')
|
|
63
129
|
.replace(/&/g, '&')
|
|
64
130
|
|
|
131
|
+
const { source: sourceWithQuickLinks, quickLinksMap } = extractQuickLinksBlocks(normalizedSource)
|
|
132
|
+
|
|
65
133
|
const Markdown = new MarkdownIt()
|
|
66
134
|
Markdown.use(attrs, {
|
|
67
135
|
leftDelimiter: ':',
|
|
@@ -75,7 +143,7 @@ const tokenized = computed(() => {
|
|
|
75
143
|
|
|
76
144
|
const markdownEnv = {}
|
|
77
145
|
|
|
78
|
-
const parsed = Markdown.parse(
|
|
146
|
+
const parsed = Markdown.parse(sourceWithQuickLinks, markdownEnv)
|
|
79
147
|
|
|
80
148
|
// @ map
|
|
81
149
|
const tokens = []
|
|
@@ -213,6 +281,17 @@ const tokenized = computed(() => {
|
|
|
213
281
|
// Push
|
|
214
282
|
switch (element.type) {
|
|
215
283
|
case 'inline':
|
|
284
|
+
if (quickLinksMap.has(element.content.trim())) {
|
|
285
|
+
const data = quickLinksMap.get(element.content.trim())
|
|
286
|
+
|
|
287
|
+
tokens.push({
|
|
288
|
+
tag: 'quick-links',
|
|
289
|
+
title: data.title,
|
|
290
|
+
items: data.items
|
|
291
|
+
})
|
|
292
|
+
break
|
|
293
|
+
}
|
|
294
|
+
|
|
216
295
|
tokens.push({
|
|
217
296
|
tag,
|
|
218
297
|
map: element.map,
|
|
@@ -395,6 +474,12 @@ const tokenized = computed(() => {
|
|
|
395
474
|
v-else-if="token.tag === 'mermaid'"
|
|
396
475
|
:content="token.content"
|
|
397
476
|
/>
|
|
477
|
+
|
|
478
|
+
<d-quick-links
|
|
479
|
+
v-else-if="token.tag === 'quick-links'"
|
|
480
|
+
:title="token.title"
|
|
481
|
+
:items="token.items"
|
|
482
|
+
/>
|
|
398
483
|
</template>
|
|
399
484
|
</section>
|
|
400
485
|
</template>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
items: {
|
|
4
|
+
type: Array,
|
|
5
|
+
default: () => []
|
|
6
|
+
},
|
|
7
|
+
title: {
|
|
8
|
+
type: String,
|
|
9
|
+
default: ''
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const isExternal = (item) => {
|
|
14
|
+
const href = item?.href || ''
|
|
15
|
+
return /^https?:\/\//i.test(href)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const toTarget = (item) => {
|
|
19
|
+
if (item?.to) return item.to
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const hrefTarget = (item) => {
|
|
24
|
+
if (item?.href) return item.href
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const relValue = (item) => {
|
|
29
|
+
if (isExternal(item)) return 'noopener noreferrer'
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const targetValue = (item) => {
|
|
34
|
+
if (isExternal(item)) return '_blank'
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="d-quick-links">
|
|
41
|
+
<h3 v-if="title" class="d-quick-links__title">{{ title }}</h3>
|
|
42
|
+
|
|
43
|
+
<q-list bordered separator class="d-quick-links__list rounded-borders">
|
|
44
|
+
<q-item
|
|
45
|
+
v-for="(item, index) in props.items"
|
|
46
|
+
:key="index"
|
|
47
|
+
clickable
|
|
48
|
+
:to="toTarget(item)"
|
|
49
|
+
:href="hrefTarget(item)"
|
|
50
|
+
:target="targetValue(item)"
|
|
51
|
+
:rel="relValue(item)"
|
|
52
|
+
>
|
|
53
|
+
<q-item-section avatar>
|
|
54
|
+
<q-icon :name="item.icon || 'link'" color="primary" />
|
|
55
|
+
</q-item-section>
|
|
56
|
+
|
|
57
|
+
<q-item-section>
|
|
58
|
+
<q-item-label class="d-quick-links__label">{{ item.title }}</q-item-label>
|
|
59
|
+
<q-item-label caption class="d-quick-links__caption">{{ item.description }}</q-item-label>
|
|
60
|
+
</q-item-section>
|
|
61
|
+
|
|
62
|
+
<q-item-section side>
|
|
63
|
+
<q-icon name="chevron_right" />
|
|
64
|
+
</q-item-section>
|
|
65
|
+
</q-item>
|
|
66
|
+
</q-list>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<style lang="sass" scoped>
|
|
71
|
+
.d-quick-links
|
|
72
|
+
margin: 0 auto
|
|
73
|
+
|
|
74
|
+
.d-quick-links__title
|
|
75
|
+
text-align: center
|
|
76
|
+
margin: 0 0 12px
|
|
77
|
+
|
|
78
|
+
.d-quick-links__list
|
|
79
|
+
border-color: rgba(255, 255, 255, 0.16)
|
|
80
|
+
|
|
81
|
+
.q-item
|
|
82
|
+
min-height: 58px
|
|
83
|
+
|
|
84
|
+
.d-quick-links__label
|
|
85
|
+
font-weight: 700
|
|
86
|
+
|
|
87
|
+
.d-quick-links__caption
|
|
88
|
+
font-size: 0.97rem
|
|
89
|
+
color: #4d5563
|
|
90
|
+
opacity: 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
:global(body.body--dark) .d-quick-links__list
|
|
94
|
+
border-color: rgba(255, 255, 255, 0.16)
|
|
95
|
+
|
|
96
|
+
:global(body.body--dark) .d-quick-links__caption
|
|
97
|
+
color: rgba(255, 255, 255, 0.9)
|
|
98
|
+
</style>
|
package/src/i18n/helpers.js
CHANGED
|
@@ -144,6 +144,38 @@ export function buildMessages ({ langModules, mdModules, pages, boot, langs }) {
|
|
|
144
144
|
return source
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
function loadHomepage (lang) {
|
|
148
|
+
const key = `../pages/Homepage.${lang}.md`
|
|
149
|
+
const fallbackKey = '../pages/Homepage.en-US.md'
|
|
150
|
+
|
|
151
|
+
const content = mdModules[key] ?? mdModules[fallbackKey]
|
|
152
|
+
if (!content) {
|
|
153
|
+
console.warn(`[i18n] Missing homepage markdown: ${key}`)
|
|
154
|
+
return ''
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const source = filter(typeof content === 'string' ? content : String(content))
|
|
158
|
+
return source
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function extractHeadingFromHomepage (lang) {
|
|
162
|
+
const key = `../pages/Homepage.${lang}.md`
|
|
163
|
+
const fallbackKey = '../pages/Homepage.en-US.md'
|
|
164
|
+
|
|
165
|
+
const content = mdModules[key] ?? mdModules[fallbackKey]
|
|
166
|
+
if (!content) {
|
|
167
|
+
return ''
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const raw = typeof content === 'string' ? content : String(content)
|
|
171
|
+
const match = raw.match(/^#\s+(.+)$/m)
|
|
172
|
+
if (!match) {
|
|
173
|
+
return ''
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return match[1].trim()
|
|
177
|
+
}
|
|
178
|
+
|
|
147
179
|
// @ Iterate langs
|
|
148
180
|
for (const lang of langs) {
|
|
149
181
|
// Load HJSON language file
|
|
@@ -155,6 +187,26 @@ export function buildMessages ({ langModules, mdModules, pages, boot, langs }) {
|
|
|
155
187
|
deepMerge(i18n[lang], engineDefaults[lang])
|
|
156
188
|
}
|
|
157
189
|
|
|
190
|
+
// @ Homepage markdown in root route
|
|
191
|
+
if (i18n[lang]._ === undefined) {
|
|
192
|
+
i18n[lang]._ = {}
|
|
193
|
+
}
|
|
194
|
+
if (i18n[lang]._.home === undefined) {
|
|
195
|
+
i18n[lang]._.home = {}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const homepageHeading = extractHeadingFromHomepage(lang)
|
|
199
|
+
i18n[lang]._.home._ = homepageHeading || i18n[lang]._.home._ || i18n[lang].menu?.home || 'Home'
|
|
200
|
+
|
|
201
|
+
if (i18n[lang]._.home.overview === undefined) {
|
|
202
|
+
i18n[lang]._.home.overview = {}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const homeMeta = boot?.meta?.[lang] || boot?.meta?.['en-US'] || {}
|
|
206
|
+
i18n[lang]._.home.overview._translations = homeMeta?.overview?._translations
|
|
207
|
+
i18n[lang]._.home.overview._sections = homeMeta?.overview?._sections
|
|
208
|
+
i18n[lang]._.home.overview.source = loadHomepage(lang)
|
|
209
|
+
|
|
158
210
|
// @ Iterate pages
|
|
159
211
|
for (const [key, page] of Object.entries(pages)) {
|
|
160
212
|
const path = key.slice(1)
|
package/src/index.js
CHANGED
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
* @param {Object} [config.mcp] - MCP (Model Context Protocol) server settings
|
|
45
45
|
* @param {string} config.mcp.serverName - Server name for MCP identification (e.g. 'my-docs')
|
|
46
46
|
* @param {string} config.mcp.toolSuffix - Suffix for tool names (e.g. 'my_docs' → search_my_docs)
|
|
47
|
+
* @param {Object} [config.linkHeaders] - Homepage Link headers for agent discovery
|
|
48
|
+
* @param {boolean} [config.linkHeaders.enabled=true] - Enables homepage Link headers generation
|
|
49
|
+
* @param {string|null|false} [config.linkHeaders.serviceDoc='/'] - Target URI for rel="service-doc"
|
|
50
|
+
* @param {string|null|false} [config.linkHeaders.serviceDesc='/mcp'] - Target URI for rel="service-desc" (only emitted when MCP is enabled)
|
|
51
|
+
* @param {string|null|false} [config.linkHeaders.describedBy='/llms.txt'] - Target URI for rel="describedby" (only emitted when llms.txt is generated)
|
|
47
52
|
* @returns {Object} Resolved Docsector configuration
|
|
48
53
|
*/
|
|
49
54
|
export function createDocsector (config = {}) {
|
|
@@ -83,7 +88,15 @@ export function createDocsector (config = {}) {
|
|
|
83
88
|
|
|
84
89
|
defaultLanguage: config.defaultLanguage || 'en-US',
|
|
85
90
|
|
|
86
|
-
mcp: config.mcp || null
|
|
91
|
+
mcp: config.mcp || null,
|
|
92
|
+
|
|
93
|
+
linkHeaders: {
|
|
94
|
+
enabled: true,
|
|
95
|
+
serviceDoc: '/',
|
|
96
|
+
serviceDesc: '/mcp',
|
|
97
|
+
describedBy: '/llms.txt',
|
|
98
|
+
...config.linkHeaders
|
|
99
|
+
}
|
|
87
100
|
}
|
|
88
101
|
}
|
|
89
102
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Docsector Reader
|
|
2
|
+
|
|
3
|
+
Docsector Reader is a documentation rendering engine built with Vue 3, Quasar v2 and Vite.
|
|
4
|
+
|
|
5
|
+
## Quick Links
|
|
6
|
+
|
|
7
|
+
- [Getting Started](/guide/getting-started/overview/)
|
|
8
|
+
- [Configuration](/guide/configuration/overview/)
|
|
9
|
+
- [Pages and Routing](/guide/pages-and-routing/overview/)
|
|
10
|
+
- [Components](/manual/components/d-page/overview/)
|
|
11
|
+
- [Composables](/manual/composables/use-navigator/overview/)
|
|
12
|
+
- [Store Modules](/manual/store/modules/overview/)
|
|
13
|
+
|
|
14
|
+
## About
|
|
15
|
+
|
|
16
|
+
- Repository: [docsector/docsector-reader](https://github.com/docsector/docsector-reader)
|
|
17
|
+
- Focus: markdown-first docs with i18n, menu navigation and static deployment
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Docsector Reader
|
|
2
|
+
|
|
3
|
+
Docsector Reader e um motor de documentacao construído com Vue 3, Quasar v2 e Vite.
|
|
4
|
+
|
|
5
|
+
## Links rapidos
|
|
6
|
+
|
|
7
|
+
- [Comecando](/guide/getting-started/overview/)
|
|
8
|
+
- [Configuracao](/guide/configuration/overview/)
|
|
9
|
+
- [Paginas e Rotas](/guide/pages-and-routing/overview/)
|
|
10
|
+
- [Componentes](/manual/components/d-page/overview/)
|
|
11
|
+
- [Composables](/manual/composables/use-navigator/overview/)
|
|
12
|
+
- [Modulos de Store](/manual/store/modules/overview/)
|
|
13
|
+
|
|
14
|
+
## Sobre
|
|
15
|
+
|
|
16
|
+
- Repositorio: [docsector/docsector-reader](https://github.com/docsector/docsector-reader)
|
|
17
|
+
- Foco: docs markdown-first com i18n, menu de navegacao e deploy estatico
|
package/src/quasar.factory.js
CHANGED
|
@@ -665,6 +665,52 @@ function createMarkdownBuildPlugin (projectRoot) {
|
|
|
665
665
|
}
|
|
666
666
|
console.log(`\x1b[36m[docsector]\x1b[0m Added _headers rule for .md files`)
|
|
667
667
|
|
|
668
|
+
// Add homepage Link headers for agent discovery (RFC 8288 / RFC 9727)
|
|
669
|
+
const linkHeadersConfig = config.linkHeaders || {}
|
|
670
|
+
const linkHeadersEnabled = linkHeadersConfig.enabled !== false
|
|
671
|
+
|
|
672
|
+
if (linkHeadersEnabled) {
|
|
673
|
+
const homepageLinks = []
|
|
674
|
+
|
|
675
|
+
const serviceDocHref = linkHeadersConfig.serviceDoc === undefined
|
|
676
|
+
? '/'
|
|
677
|
+
: linkHeadersConfig.serviceDoc
|
|
678
|
+
if (serviceDocHref) {
|
|
679
|
+
homepageLinks.push({ rel: 'service-doc', href: serviceDocHref })
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const serviceDescHref = linkHeadersConfig.serviceDesc === undefined
|
|
683
|
+
? '/mcp'
|
|
684
|
+
: linkHeadersConfig.serviceDesc
|
|
685
|
+
if (config.mcp && serviceDescHref) {
|
|
686
|
+
homepageLinks.push({ rel: 'service-desc', href: serviceDescHref })
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const describedByHref = linkHeadersConfig.describedBy === undefined
|
|
690
|
+
? '/llms.txt'
|
|
691
|
+
: linkHeadersConfig.describedBy
|
|
692
|
+
if (siteUrl && describedByHref) {
|
|
693
|
+
homepageLinks.push({ rel: 'describedby', href: describedByHref })
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (homepageLinks.length > 0) {
|
|
697
|
+
const linkLines = homepageLinks.map(({ rel, href }) => ` Link: <${href}>; rel="${rel}"`).join('\n')
|
|
698
|
+
const homepageRule = ['/','/index.html']
|
|
699
|
+
.map(path => `${path}\n${linkLines}`)
|
|
700
|
+
.join('\n\n') + '\n'
|
|
701
|
+
|
|
702
|
+
const currentHeaders = readFileSync(headersPath, 'utf-8')
|
|
703
|
+
const hasAgentLinks = currentHeaders.includes('rel="service-doc"')
|
|
704
|
+
|| currentHeaders.includes('rel="service-desc"')
|
|
705
|
+
|| currentHeaders.includes('rel="describedby"')
|
|
706
|
+
|
|
707
|
+
if (!hasAgentLinks) {
|
|
708
|
+
writeFileSync(headersPath, currentHeaders.trimEnd() + '\n\n' + homepageRule)
|
|
709
|
+
console.log(`\x1b[36m[docsector]\x1b[0m Added homepage Link headers for agent discovery`)
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
668
714
|
// Generate MCP server if configured
|
|
669
715
|
if (config.mcp) {
|
|
670
716
|
const mcpConfig = config.mcp
|
package/src/router/routes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pages from 'pages'
|
|
2
|
+
import boot from 'pages/boot'
|
|
2
3
|
|
|
3
4
|
const pagesRoutes = []
|
|
4
5
|
for (const [path, page] of Object.entries(pages)) {
|
|
@@ -59,9 +60,27 @@ const routes = [
|
|
|
59
60
|
...pagesRoutes,
|
|
60
61
|
|
|
61
62
|
{
|
|
62
|
-
path: '/',
|
|
63
|
+
path: '/home',
|
|
64
|
+
alias: '/',
|
|
63
65
|
component: () => import('layouts/DefaultLayout.vue'),
|
|
64
66
|
meta: {
|
|
67
|
+
icon: 'home',
|
|
68
|
+
menu: {},
|
|
69
|
+
status: 'done',
|
|
70
|
+
type: 'home',
|
|
71
|
+
subpages: {
|
|
72
|
+
showcase: false,
|
|
73
|
+
vs: false
|
|
74
|
+
},
|
|
75
|
+
data: {
|
|
76
|
+
'en-US': {
|
|
77
|
+
title: 'Home'
|
|
78
|
+
},
|
|
79
|
+
'pt-BR': {
|
|
80
|
+
title: 'Pagina inicial'
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
meta: boot.meta,
|
|
65
84
|
layouts: {
|
|
66
85
|
footer: false,
|
|
67
86
|
submenu: false
|
|
@@ -71,7 +90,7 @@ const routes = [
|
|
|
71
90
|
children: [
|
|
72
91
|
{
|
|
73
92
|
path: '',
|
|
74
|
-
component: () => import('
|
|
93
|
+
component: () => import('components/DSubpage.vue'),
|
|
75
94
|
meta: {
|
|
76
95
|
icon: 'home',
|
|
77
96
|
menu: 'home'
|
|
@@ -89,7 +108,7 @@ const routes = [
|
|
|
89
108
|
children: [
|
|
90
109
|
{
|
|
91
110
|
path: '',
|
|
92
|
-
component: () => import('pages
|
|
111
|
+
component: () => import('pages/404Page.vue')
|
|
93
112
|
}
|
|
94
113
|
]
|
|
95
114
|
}
|
package/src/store/App.js
CHANGED
|
@@ -12,10 +12,10 @@ export default {
|
|
|
12
12
|
commit('page/resetNodes', null, { root: true })
|
|
13
13
|
|
|
14
14
|
// Route
|
|
15
|
-
const firstRoutePath = routeMatched[0]
|
|
16
|
-
const secondRoutePath = routeMatched[1]
|
|
15
|
+
const firstRoutePath = routeMatched[0]?.path || ''
|
|
16
|
+
const secondRoutePath = routeMatched[1]?.path || ''
|
|
17
17
|
|
|
18
|
-
const base = firstRoutePath.substr(1)
|
|
18
|
+
const base = firstRoutePath === '/' ? 'home' : firstRoutePath.substr(1)
|
|
19
19
|
let relative = secondRoutePath.substr(firstRoutePath.length)
|
|
20
20
|
|
|
21
21
|
if (relative !== '/') {
|
package/src/pages/@/BootPage.vue
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<q-page-container>
|
|
3
|
-
<q-page class="content">
|
|
4
|
-
<div class="text-center q-pa-xs q-pt-md">
|
|
5
|
-
<h1>Docsector Reader</h1>
|
|
6
|
-
|
|
7
|
-
<p class="caption">
|
|
8
|
-
{{ $t('_.home.texts[0]') }}
|
|
9
|
-
<a :href="projectUrl" target="_blank">{{ projectName }}</a>!
|
|
10
|
-
</p>
|
|
11
|
-
<hr />
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
|
-
<div class="q-pa-md" style="max-width: 700px; margin: 0 auto;">
|
|
15
|
-
<h3 class="text-center q-mb-md">Quick Links</h3>
|
|
16
|
-
|
|
17
|
-
<q-list bordered separator class="rounded-borders">
|
|
18
|
-
<q-item clickable to="/guide/getting-started/overview/">
|
|
19
|
-
<q-item-section avatar>
|
|
20
|
-
<q-icon name="flag" color="primary" />
|
|
21
|
-
</q-item-section>
|
|
22
|
-
<q-item-section>
|
|
23
|
-
<q-item-label>Getting Started</q-item-label>
|
|
24
|
-
<q-item-label caption>Installation, setup, and project structure</q-item-label>
|
|
25
|
-
</q-item-section>
|
|
26
|
-
<q-item-section side>
|
|
27
|
-
<q-icon name="chevron_right" />
|
|
28
|
-
</q-item-section>
|
|
29
|
-
</q-item>
|
|
30
|
-
|
|
31
|
-
<q-item clickable to="/guide/configuration/overview/">
|
|
32
|
-
<q-item-section avatar>
|
|
33
|
-
<q-icon name="tune" color="primary" />
|
|
34
|
-
</q-item-section>
|
|
35
|
-
<q-item-section>
|
|
36
|
-
<q-item-label>Configuration</q-item-label>
|
|
37
|
-
<q-item-label caption>docsector.config.js reference</q-item-label>
|
|
38
|
-
</q-item-section>
|
|
39
|
-
<q-item-section side>
|
|
40
|
-
<q-icon name="chevron_right" />
|
|
41
|
-
</q-item-section>
|
|
42
|
-
</q-item>
|
|
43
|
-
|
|
44
|
-
<q-item clickable to="/guide/pages-and-routing/overview/">
|
|
45
|
-
<q-item-section avatar>
|
|
46
|
-
<q-icon name="route" color="primary" />
|
|
47
|
-
</q-item-section>
|
|
48
|
-
<q-item-section>
|
|
49
|
-
<q-item-label>Pages & Routing</q-item-label>
|
|
50
|
-
<q-item-label caption>Page registry and route generation</q-item-label>
|
|
51
|
-
</q-item-section>
|
|
52
|
-
<q-item-section side>
|
|
53
|
-
<q-icon name="chevron_right" />
|
|
54
|
-
</q-item-section>
|
|
55
|
-
</q-item>
|
|
56
|
-
|
|
57
|
-
<q-item clickable to="/manual/components/d-page/overview/">
|
|
58
|
-
<q-item-section avatar>
|
|
59
|
-
<q-icon name="widgets" color="secondary" />
|
|
60
|
-
</q-item-section>
|
|
61
|
-
<q-item-section>
|
|
62
|
-
<q-item-label>Components</q-item-label>
|
|
63
|
-
<q-item-label caption>DPage, DPageSection, DH1–DH6, DMenu, and more</q-item-label>
|
|
64
|
-
</q-item-section>
|
|
65
|
-
<q-item-section side>
|
|
66
|
-
<q-icon name="chevron_right" />
|
|
67
|
-
</q-item-section>
|
|
68
|
-
</q-item>
|
|
69
|
-
|
|
70
|
-
<q-item clickable to="/manual/composables/use-navigator/overview/">
|
|
71
|
-
<q-item-section avatar>
|
|
72
|
-
<q-icon name="navigation" color="accent" />
|
|
73
|
-
</q-item-section>
|
|
74
|
-
<q-item-section>
|
|
75
|
-
<q-item-label>Composables</q-item-label>
|
|
76
|
-
<q-item-label caption>useNavigator — anchor navigation and ToC</q-item-label>
|
|
77
|
-
</q-item-section>
|
|
78
|
-
<q-item-section side>
|
|
79
|
-
<q-icon name="chevron_right" />
|
|
80
|
-
</q-item-section>
|
|
81
|
-
</q-item>
|
|
82
|
-
|
|
83
|
-
<q-item clickable to="/manual/store/modules/overview/">
|
|
84
|
-
<q-item-section avatar>
|
|
85
|
-
<q-icon name="storage" color="deep-orange" />
|
|
86
|
-
</q-item-section>
|
|
87
|
-
<q-item-section>
|
|
88
|
-
<q-item-label>Vuex Store</q-item-label>
|
|
89
|
-
<q-item-label caption>App, I18n, Page, Layout, Settings modules</q-item-label>
|
|
90
|
-
</q-item-section>
|
|
91
|
-
<q-item-section side>
|
|
92
|
-
<q-icon name="chevron_right" />
|
|
93
|
-
</q-item-section>
|
|
94
|
-
</q-item>
|
|
95
|
-
</q-list>
|
|
96
|
-
</div>
|
|
97
|
-
</q-page>
|
|
98
|
-
</q-page-container>
|
|
99
|
-
</template>
|
|
100
|
-
|
|
101
|
-
<script setup>
|
|
102
|
-
import docsectorConfig from 'docsector.config.js'
|
|
103
|
-
|
|
104
|
-
const projectName = docsectorConfig.branding?.name || 'My Project'
|
|
105
|
-
const projectUrl = docsectorConfig.links?.github || '#'
|
|
106
|
-
</script>
|
|
File without changes
|