@docsector/docsector-reader 0.13.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +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
+ - 🔗 **Homepage Link Headers** — Auto-generated `Link` response headers for agent discovery (`api-catalog`, `service-doc`, `service-desc`, `describedby`) per RFC 8288 / RFC 9727
30
30
  - 🔌 **MCP Server** — Auto-generated [MCP](https://modelcontextprotocol.io) server at `/mcp` for AI assistant integration (Claude Desktop, VS Code, etc.)
31
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)
32
32
 
@@ -46,6 +46,9 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
46
46
  - ✏️ **Edit on GitHub** — Direct links to edit pages on your repository
47
47
  - 📅 **Last Updated Date** — Automatic per-page "last updated" date from git commit history, locale-formatted
48
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
51
+ - 🗂️ **API Catalog Well-Known** — Auto-generates `/.well-known/api-catalog` as Linkset JSON for machine-readable API discovery
49
52
  - ⚙️ **Single Config File** — Customize branding, links, and languages via `docsector.config.js`
50
53
 
51
54
  ---
@@ -160,6 +163,7 @@ Docsector Reader adds homepage `Link` response headers at build time for agent d
160
163
 
161
164
  Default relations emitted on homepage (`/` and `/index.html`):
162
165
 
166
+ - `rel="api-catalog"` → `</.well-known/api-catalog>`
163
167
  - `rel="service-doc"` → `</>`
164
168
  - `rel="service-desc"` → `</mcp>` (only when `mcp` is enabled)
165
169
  - `rel="describedby"` → `</llms.txt>` (only when `siteUrl` is configured, i.e. `llms.txt` is generated)
@@ -167,6 +171,7 @@ Default relations emitted on homepage (`/` and `/index.html`):
167
171
  Generated in:
168
172
 
169
173
  - `dist/spa/_headers`
174
+ - `dist/spa/.well-known/api-catalog` (Linkset JSON)
170
175
 
171
176
  ### Optional configuration
172
177
 
@@ -176,9 +181,19 @@ export default {
176
181
 
177
182
  linkHeaders: {
178
183
  enabled: true,
184
+ apiCatalog: '/.well-known/api-catalog',
179
185
  serviceDoc: '/',
180
186
  serviceDesc: '/mcp',
181
187
  describedBy: '/llms.txt'
188
+ },
189
+
190
+ apiCatalog: {
191
+ enabled: true,
192
+ path: '/.well-known/api-catalog',
193
+ items: [
194
+ '/mcp',
195
+ 'https://api.example.com/openapi.json'
196
+ ]
182
197
  }
183
198
  }
184
199
  ```
@@ -190,6 +205,7 @@ Set any target to `null` or `false` to disable that relation.
190
205
  ```bash
191
206
  npx docsector build
192
207
  cat dist/spa/_headers
208
+ cat dist/spa/.well-known/api-catalog
193
209
  ```
194
210
 
195
211
  Or scan discoverability:
@@ -341,11 +357,18 @@ export default {
341
357
 
342
358
  linkHeaders: {
343
359
  enabled: true,
360
+ apiCatalog: '/.well-known/api-catalog',
344
361
  serviceDoc: '/',
345
362
  serviceDesc: '/mcp',
346
363
  describedBy: '/llms.txt'
347
364
  },
348
365
 
366
+ apiCatalog: {
367
+ enabled: true,
368
+ path: '/.well-known/api-catalog',
369
+ items: []
370
+ },
371
+
349
372
  languages: [
350
373
  { image: '/images/flags/united-states-of-america.png', label: 'English (US)', value: 'en-US' },
351
374
  { 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.13.0'
26
+ const VERSION = '1.1.0'
27
27
 
28
28
  const HELP = `
29
29
  Docsector Reader v${VERSION}
@@ -136,11 +136,19 @@ export default {
136
136
  // @ Homepage Link headers for agent discovery (optional)
137
137
  // linkHeaders: {
138
138
  // enabled: true,
139
+ // apiCatalog: '/.well-known/api-catalog',
139
140
  // serviceDoc: '/',
140
141
  // serviceDesc: '/mcp',
141
142
  // describedBy: '/llms.txt'
142
143
  // },
143
144
 
145
+ // @ API catalog artifact (RFC 9727) (optional)
146
+ // apiCatalog: {
147
+ // enabled: true,
148
+ // path: '/.well-known/api-catalog',
149
+ // items: ['/mcp']
150
+ // },
151
+
144
152
  // @ Languages
145
153
  languages: [
146
154
  {
@@ -341,6 +349,22 @@ export default {
341
349
  }
342
350
  `
343
351
 
352
+ const TEMPLATE_HOMEPAGE_MD = `\
353
+ # Welcome to Docsector Reader
354
+
355
+ Docsector Reader is a markdown-first documentation engine.
356
+
357
+ ## Quick Links
358
+
359
+ - [Getting Started](/guide/getting-started/overview/)
360
+ - [Configuration](/guide/configuration/overview/)
361
+ - [Pages and Routing](/guide/pages-and-routing/overview/)
362
+
363
+ ## About
364
+
365
+ - Repository: [docsector/docsector-reader](https://github.com/docsector/docsector-reader)
366
+ `
367
+
344
368
  const TEMPLATE_BOOT_PAGE = `\
345
369
  <template>
346
370
  <q-page-container>
@@ -703,6 +727,18 @@ npm-debug.log*
703
727
  .thumbs.db
704
728
  `
705
729
 
730
+ const TEMPLATE_MARKDOWNLINT = `\
731
+ {
732
+ "MD013": false,
733
+ "MD033": {
734
+ "allowed_elements": [
735
+ "d-quick-links",
736
+ "d-quick-link"
737
+ ]
738
+ }
739
+ }
740
+ `
741
+
706
742
  const TEMPLATE_ROBOTS_TXT = `\
707
743
  User-agent: *
708
744
  Allow: /
@@ -822,9 +858,11 @@ Here's an overview of the project files:
822
858
  | --- | --- |
823
859
  | \`docsector.config.js\` | Branding, links, languages, and GitHub config |
824
860
  | \`quasar.config.js\` | Quasar/Vite build configuration (via factory) |
861
+ | \`.markdownlint.json\` | Markdown lint rules (allows Docsector custom tags) |
825
862
  | \`src/pages/index.js\` | Page registry — defines all documentation pages |
826
863
  | \`src/pages/boot.js\` | Boot metadata for the home page |
827
- | \`src/pages/@/\` | Special pages (Home, 404) |
864
+ | \`src/pages/Homepage.en-US.md\` | Home page content in Markdown |
865
+ | \`src/pages/404Page.vue\` | Not found page |
828
866
  | \`src/pages/guide/\` | Guide pages (Markdown files) |
829
867
  | \`src/i18n/languages/\` | Translation files (HJSON format) |
830
868
  | \`src/css/app.sass\` | Custom styles |
@@ -927,7 +965,6 @@ function initProject (name) {
927
965
  'src/i18n',
928
966
  'src/i18n/languages',
929
967
  'src/pages',
930
- 'src/pages/@',
931
968
  'src/pages/guide',
932
969
  'public',
933
970
  'public/images',
@@ -944,6 +981,7 @@ function initProject (name) {
944
981
  ['package.json', getTemplatePackageJson(name)],
945
982
  ['quasar.config.js', TEMPLATE_QUASAR_CONFIG],
946
983
  ['docsector.config.js', TEMPLATE_DOCSECTOR_CONFIG],
984
+ ['.markdownlint.json', TEMPLATE_MARKDOWNLINT],
947
985
  ['index.html', TEMPLATE_INDEX_HTML],
948
986
  ['postcss.config.cjs', TEMPLATE_POSTCSS],
949
987
  ['.gitignore', TEMPLATE_GITIGNORE],
@@ -953,8 +991,8 @@ function initProject (name) {
953
991
  ['src/i18n/languages/en-US.hjson', TEMPLATE_I18N_HJSON],
954
992
  ['src/pages/index.js', TEMPLATE_PAGES_INDEX],
955
993
  ['src/pages/boot.js', TEMPLATE_PAGES_BOOT],
956
- ['src/pages/@/BootPage.vue', TEMPLATE_BOOT_PAGE],
957
- ['src/pages/@/404Page.vue', TEMPLATE_404_PAGE],
994
+ ['src/pages/Homepage.en-US.md', TEMPLATE_HOMEPAGE_MD],
995
+ ['src/pages/404Page.vue', TEMPLATE_404_PAGE],
958
996
  ['src/pages/guide/getting-started.overview.en-US.md', TEMPLATE_GETTING_STARTED_MD]
959
997
  ]
960
998
 
@@ -972,6 +1010,7 @@ function initProject (name) {
972
1010
  console.log(` ${name}/`)
973
1011
  console.log(' ├── docsector.config.js')
974
1012
  console.log(' ├── quasar.config.js')
1013
+ console.log(' ├── .markdownlint.json')
975
1014
  console.log(' ├── package.json')
976
1015
  console.log(' ├── index.html')
977
1016
  console.log(' ├── postcss.config.cjs')
@@ -990,9 +1029,8 @@ function initProject (name) {
990
1029
  console.log(' └── pages/')
991
1030
  console.log(' ├── index.js')
992
1031
  console.log(' ├── boot.js')
993
- console.log(' ├── @/')
994
- console.log(' ├── BootPage.vue')
995
- console.log(' │ └── 404Page.vue')
1032
+ console.log(' ├── Homepage.en-US.md')
1033
+ console.log(' ├── 404Page.vue')
996
1034
  console.log(' └── guide/')
997
1035
  console.log(' └── getting-started.overview.en-US.md')
998
1036
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "0.13.0",
3
+ "version": "1.1.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(/&#125;/g, '}')
63
129
  .replace(/&amp;/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(normalizedSource, markdownEnv)
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>
@@ -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
@@ -47,8 +47,13 @@
47
47
  * @param {Object} [config.linkHeaders] - Homepage Link headers for agent discovery
48
48
  * @param {boolean} [config.linkHeaders.enabled=true] - Enables homepage Link headers generation
49
49
  * @param {string|null|false} [config.linkHeaders.serviceDoc='/'] - Target URI for rel="service-doc"
50
+ * @param {string|null|false} [config.linkHeaders.apiCatalog='/.well-known/api-catalog'] - Target URI for rel="api-catalog"
50
51
  * @param {string|null|false} [config.linkHeaders.serviceDesc='/mcp'] - Target URI for rel="service-desc" (only emitted when MCP is enabled)
51
52
  * @param {string|null|false} [config.linkHeaders.describedBy='/llms.txt'] - Target URI for rel="describedby" (only emitted when llms.txt is generated)
53
+ * @param {Object} [config.apiCatalog] - API catalog generation settings
54
+ * @param {boolean} [config.apiCatalog.enabled=true] - Enables generation of API catalog artifact
55
+ * @param {string} [config.apiCatalog.path='/.well-known/api-catalog'] - Output URI path for API catalog artifact
56
+ * @param {Array<string|{href: string}>} [config.apiCatalog.items=[]] - Additional API endpoint links to include as item relations
52
57
  * @returns {Object} Resolved Docsector configuration
53
58
  */
54
59
  export function createDocsector (config = {}) {
@@ -93,9 +98,17 @@ export function createDocsector (config = {}) {
93
98
  linkHeaders: {
94
99
  enabled: true,
95
100
  serviceDoc: '/',
101
+ apiCatalog: '/.well-known/api-catalog',
96
102
  serviceDesc: '/mcp',
97
103
  describedBy: '/llms.txt',
98
104
  ...config.linkHeaders
105
+ },
106
+
107
+ apiCatalog: {
108
+ enabled: true,
109
+ path: '/.well-known/api-catalog',
110
+ items: [],
111
+ ...config.apiCatalog
99
112
  }
100
113
  }
101
114
  }
@@ -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
@@ -679,6 +679,13 @@ function createMarkdownBuildPlugin (projectRoot) {
679
679
  homepageLinks.push({ rel: 'service-doc', href: serviceDocHref })
680
680
  }
681
681
 
682
+ const apiCatalogHref = linkHeadersConfig.apiCatalog === undefined
683
+ ? '/.well-known/api-catalog'
684
+ : linkHeadersConfig.apiCatalog
685
+ if (apiCatalogHref) {
686
+ homepageLinks.push({ rel: 'api-catalog', href: apiCatalogHref })
687
+ }
688
+
682
689
  const serviceDescHref = linkHeadersConfig.serviceDesc === undefined
683
690
  ? '/mcp'
684
691
  : linkHeadersConfig.serviceDesc
@@ -694,19 +701,106 @@ function createMarkdownBuildPlugin (projectRoot) {
694
701
  }
695
702
 
696
703
  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
704
  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"')
705
+ const missingHomepageLinks = homepageLinks.filter(({ rel }) => !currentHeaders.includes(`rel="${rel}"`))
706
+
707
+ if (missingHomepageLinks.length > 0) {
708
+ const linkLines = missingHomepageLinks
709
+ .map(({ rel, href }) => ` Link: <${href}>; rel="${rel}"`)
710
+ .join('\n')
711
+ const homepageRule = ['/', '/index.html']
712
+ .map(path => `${path}\n${linkLines}`)
713
+ .join('\n\n') + '\n'
706
714
 
707
- if (!hasAgentLinks) {
708
715
  writeFileSync(headersPath, currentHeaders.trimEnd() + '\n\n' + homepageRule)
709
- console.log(`\x1b[36m[docsector]\x1b[0m Added homepage Link headers for agent discovery`)
716
+ console.log(`\x1b[36m[docsector]\x1b[0m Added homepage Link headers for agent discovery (${missingHomepageLinks.length} relation(s))`)
717
+ }
718
+ }
719
+
720
+ // Generate /.well-known/api-catalog Linkset document (RFC 9727)
721
+ const apiCatalogConfig = config.apiCatalog || {}
722
+ const apiCatalogEnabled = apiCatalogConfig.enabled !== false
723
+ const apiCatalogPath = (apiCatalogConfig.path || apiCatalogHref || '/.well-known/api-catalog')
724
+
725
+ const toUrl = (href) => {
726
+ if (!href) return null
727
+ if (/^https?:\/\//i.test(href)) return href
728
+ const normalizedHref = href.startsWith('/') ? href : `/${href}`
729
+ return siteUrl ? `${siteUrl}${normalizedHref}` : normalizedHref
730
+ }
731
+
732
+ const normalizeLocalPath = (href) => {
733
+ if (!href || /^https?:\/\//i.test(href)) return null
734
+ const path = href.startsWith('/') ? href.slice(1) : href
735
+ return path || null
736
+ }
737
+
738
+ if (apiCatalogEnabled && apiCatalogPath) {
739
+ const catalogDistPath = normalizeLocalPath(apiCatalogPath)
740
+
741
+ if (catalogDistPath) {
742
+ const catalogHref = apiCatalogPath.startsWith('/') ? apiCatalogPath : `/${apiCatalogPath}`
743
+ const catalogEntry = {
744
+ anchor: toUrl(catalogHref)
745
+ }
746
+
747
+ const catalogServiceDoc = toUrl(serviceDocHref)
748
+ if (catalogServiceDoc) {
749
+ catalogEntry['service-doc'] = [{ href: catalogServiceDoc }]
750
+ }
751
+
752
+ const catalogServiceDesc = config.mcp ? toUrl(serviceDescHref) : null
753
+ if (catalogServiceDesc) {
754
+ catalogEntry['service-desc'] = [{ href: catalogServiceDesc }]
755
+ }
756
+
757
+ const catalogDescribedBy = siteUrl ? toUrl(describedByHref) : null
758
+ if (catalogDescribedBy) {
759
+ catalogEntry.describedby = [{ href: catalogDescribedBy }]
760
+ }
761
+
762
+ const customItems = Array.isArray(apiCatalogConfig.items)
763
+ ? apiCatalogConfig.items
764
+ : []
765
+ const itemHrefs = new Set()
766
+
767
+ if (catalogServiceDesc) {
768
+ itemHrefs.add(catalogServiceDesc)
769
+ }
770
+
771
+ for (const item of customItems) {
772
+ if (typeof item === 'string') {
773
+ const href = toUrl(item)
774
+ if (href) itemHrefs.add(href)
775
+ continue
776
+ }
777
+
778
+ if (item && typeof item === 'object' && typeof item.href === 'string') {
779
+ const href = toUrl(item.href)
780
+ if (href) itemHrefs.add(href)
781
+ }
782
+ }
783
+
784
+ if (itemHrefs.size > 0) {
785
+ catalogEntry.item = [...itemHrefs].map(href => ({ href }))
786
+ }
787
+
788
+ const catalogDir = resolve(distDir, catalogDistPath, '..')
789
+ mkdirSync(catalogDir, { recursive: true })
790
+ writeFileSync(
791
+ resolve(distDir, catalogDistPath),
792
+ JSON.stringify({ linkset: [catalogEntry] }, null, 2) + '\n'
793
+ )
794
+ console.log(`\x1b[36m[docsector]\x1b[0m Generated ${catalogHref}`)
795
+
796
+ const headersWithLinks = readFileSync(headersPath, 'utf-8')
797
+ if (!headersWithLinks.includes(catalogHref)) {
798
+ const apiCatalogHeaders = `${catalogHref}\n Content-Type: application/linkset+json; profile=\"https://www.rfc-editor.org/info/rfc9727\"\n`
799
+ writeFileSync(headersPath, headersWithLinks.trimEnd() + '\n\n' + apiCatalogHeaders)
800
+ console.log(`\x1b[36m[docsector]\x1b[0m Added _headers rule for ${catalogHref}`)
801
+ }
802
+ } else {
803
+ console.warn(`\x1b[33m[docsector]\x1b[0m Skipped API catalog generation: path must be a local URI path, got \"${apiCatalogPath}\"`)
710
804
  }
711
805
  }
712
806
  }
@@ -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('pages/@/BootPage.vue'),
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/@/404Page.vue')
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].path
16
- const secondRoutePath = routeMatched[1].path
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 !== '/') {
@@ -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 &amp; 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