@docsector/docsector-reader 4.1.0 → 4.3.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.
Files changed (27) hide show
  1. package/README.md +16 -6
  2. package/bin/docsector.js +1 -1
  3. package/docsector.config.js +15 -0
  4. package/package.json +1 -1
  5. package/public/.well-known/agent-skills/docsector-documentation-authoring/SKILL.md +118 -0
  6. package/public/.well-known/agent-skills/docsector-documentation-authoring/references/authoring-patterns.md +98 -0
  7. package/public/.well-known/agent-skills/docsector-documentation-authoring/references/block-catalog.md +321 -0
  8. package/public/.well-known/agent-skills/docsector-documentation-authoring/references/mcp-webmcp.md +90 -0
  9. package/public/.well-known/agent-skills/docsector-documentation-authoring/references/page-structure.md +101 -0
  10. package/public/api/manual/http-client.json +91 -0
  11. package/public/quasar-api/QSeparator.json +39 -0
  12. package/src/components/DBlockApi.vue +634 -0
  13. package/src/components/DBlockApiEntry.js +623 -0
  14. package/src/components/DBlockCodeExample.vue +22 -0
  15. package/src/components/DBlockSourceCode.vue +2 -4
  16. package/src/components/DMenu.vue +70 -25
  17. package/src/components/DPageTokens.vue +8 -0
  18. package/src/components/api-block-model.js +326 -0
  19. package/src/components/page-section-tokens.js +53 -1
  20. package/src/components/source-code-lines.js +17 -0
  21. package/src/pages/manual/basic/agent-skills.overview.en-US.md +77 -0
  22. package/src/pages/manual/basic/agent-skills.overview.pt-BR.md +77 -0
  23. package/src/pages/manual/content/blocks/api-reference.overview.en-US.md +40 -0
  24. package/src/pages/manual/content/blocks/api-reference.overview.pt-BR.md +40 -0
  25. package/src/pages/manual/content/blocks/api-reference.showcase.en-US.md +33 -0
  26. package/src/pages/manual/content/blocks/api-reference.showcase.pt-BR.md +33 -0
  27. package/src/pages/manual.index.js +57 -0
@@ -22,6 +22,8 @@ const term = ref(null)
22
22
  const founds = ref(false)
23
23
  const items = ref([])
24
24
  const scrolling = ref(null)
25
+ const isMenuHovered = ref(false)
26
+ const pendingScroll = ref(false)
25
27
 
26
28
  const subpage = computed(() => {
27
29
  const parent = $route.matched[0]?.path
@@ -312,41 +314,79 @@ const getMenuItemHeaderLabel = (meta) => {
312
314
  return label // String raw
313
315
  }
314
316
 
317
+ const executeScrollToActiveMenuItem = () => {
318
+ const menu = document.getElementById('menu')
319
+ if (!menu) {
320
+ return
321
+ }
322
+
323
+ const menuItemActive = (menu.getElementsByClassName('q-router-link--active'))[0]
324
+ if (!menuItemActive || typeof menuItemActive !== 'object') {
325
+ return
326
+ }
327
+
328
+ const offsetTop1 = menuItemActive.closest('.menu-list-expansion')?.offsetTop ?? 0
329
+ const offsetTop2 = menuItemActive.offsetTop
330
+
331
+ const innerHeightBy2 = window.innerHeight / 2
332
+
333
+ const searchBarHeight = 50
334
+ let expansionHeaderHeight = 0
335
+ if (offsetTop1 > 0) {
336
+ expansionHeaderHeight = 45
337
+ }
338
+ const fixedHeight = searchBarHeight + expansionHeaderHeight
339
+
340
+ const target = scroll.getScrollTarget(menuItemActive)
341
+ const offset = (offsetTop1 + offsetTop2) - innerHeightBy2 + fixedHeight
342
+ const duration = 300
343
+
344
+ if (offset > 0) {
345
+ scroll.setVerticalScrollPosition(target, offset, duration)
346
+ }
347
+ }
348
+
349
+ const flushPendingMenuScroll = () => {
350
+ if (!pendingScroll.value || isMenuHovered.value) {
351
+ return
352
+ }
353
+
354
+ if (scrolling.value) {
355
+ clearTimeout(scrolling.value)
356
+ scrolling.value = null
357
+ }
358
+
359
+ pendingScroll.value = false
360
+ executeScrollToActiveMenuItem()
361
+ }
362
+
315
363
  const scrollToActiveMenuItem = () => {
364
+ pendingScroll.value = true
365
+
316
366
  if (scrolling.value) {
317
367
  clearTimeout(scrolling.value)
368
+ scrolling.value = null
369
+ }
370
+
371
+ if (isMenuHovered.value) {
372
+ return
318
373
  }
319
374
 
320
375
  scrolling.value = setTimeout(() => {
321
- const menu = document.getElementById('menu')
322
- if (menu) {
323
- const menuItemActive = (menu.getElementsByClassName('q-router-link--active'))[0]
324
- if (menuItemActive && typeof menuItemActive === 'object') {
325
- const offsetTop1 = menuItemActive.closest('.menu-list-expansion')?.offsetTop ?? 0
326
- const offsetTop2 = menuItemActive.offsetTop
327
-
328
- const innerHeightBy2 = window.innerHeight / 2
329
-
330
- const searchBarHeight = 50
331
- let expansionHeaderHeight = 0
332
- if (offsetTop1 > 0) {
333
- expansionHeaderHeight = 45
334
- }
335
- const fixedHeight = searchBarHeight + expansionHeaderHeight
336
-
337
- const target = scroll.getScrollTarget(menuItemActive)
338
- const offset = (offsetTop1 + offsetTop2) - innerHeightBy2 + fixedHeight
339
- const duration = 300
340
-
341
- if (offset > 0) {
342
- scroll.setVerticalScrollPosition(target, offset, duration)
343
- }
344
- }
345
- }
346
376
  scrolling.value = null
377
+ flushPendingMenuScroll()
347
378
  }, 1500)
348
379
  }
349
380
 
381
+ const handleMenuMouseEnter = () => {
382
+ isMenuHovered.value = true
383
+ }
384
+
385
+ const handleMenuMouseLeave = () => {
386
+ isMenuHovered.value = false
387
+ flushPendingMenuScroll()
388
+ }
389
+
350
390
  onMounted(() => {
351
391
  scrollToActiveMenuItem()
352
392
 
@@ -361,6 +401,9 @@ onBeforeUnmount(() => {
361
401
  if (scrolling.value) {
362
402
  clearTimeout(scrolling.value)
363
403
  }
404
+
405
+ isMenuHovered.value = false
406
+ pendingScroll.value = false
364
407
  })
365
408
 
366
409
  const buildMenuItems = () => {
@@ -428,6 +471,8 @@ watch([currentBookId, activeVersionId], rebuildItems)
428
471
  </transition>
429
472
 
430
473
  <q-scroll-area id="menu"
474
+ @mouseenter="handleMenuMouseEnter"
475
+ @mouseleave="handleMenuMouseLeave"
431
476
  :visible="true"
432
477
  :class="$q.dark.isActive ? '' : 'bg-grey-2'"
433
478
  >
@@ -35,6 +35,7 @@ import DBlockTimeline from './DBlockTimeline.vue'
35
35
  import DBlockExpandable from './DBlockExpandable.vue'
36
36
  import DBlockStepper from './DBlockStepper.vue'
37
37
  import DBlockCodeExample from './DBlockCodeExample.vue'
38
+ import DBlockApi from './DBlockApi.vue'
38
39
  </script>
39
40
 
40
41
  <template>
@@ -149,6 +150,13 @@ import DBlockCodeExample from './DBlockCodeExample.vue'
149
150
  :height="token.height"
150
151
  />
151
152
 
153
+ <d-block-api
154
+ v-else-if="token.tag === 'api'"
155
+ :src="token.src"
156
+ :title="token.title"
157
+ :page-link="token.pageLink"
158
+ />
159
+
152
160
  <d-block-mermaid-diagram
153
161
  v-else-if="token.tag === 'mermaid'"
154
162
  :content="token.content"
@@ -0,0 +1,326 @@
1
+ const defaultInnerTabName = '__default'
2
+ const fallbackCategoryName = 'general'
3
+
4
+ const isPlainObject = (value) => {
5
+ return Object.prototype.toString.call(value) === '[object Object]'
6
+ }
7
+
8
+ const isSupportedTopLevelSection = (value) => {
9
+ return typeof value === 'string' || isPlainObject(value)
10
+ }
11
+
12
+ const getEntryCategories = (entry = {}) => {
13
+ const raw = String(entry?.category || '').trim()
14
+
15
+ if (raw === '') {
16
+ return [fallbackCategoryName]
17
+ }
18
+
19
+ const groups = raw
20
+ .split('|')
21
+ .map((value) => value.trim())
22
+ .filter(Boolean)
23
+
24
+ return groups.length === 0 ? [fallbackCategoryName] : groups
25
+ }
26
+
27
+ const pruneInternalEntries = (value) => {
28
+ if (Array.isArray(value)) {
29
+ return value
30
+ .map((entry) => pruneInternalEntries(entry))
31
+ .filter((entry) => entry !== undefined)
32
+ }
33
+
34
+ if (!isPlainObject(value)) {
35
+ return value
36
+ }
37
+
38
+ if (value.internal === true) {
39
+ return undefined
40
+ }
41
+
42
+ const acc = {}
43
+
44
+ Object.entries(value).forEach(([key, entryValue]) => {
45
+ if (key === 'internal') {
46
+ return
47
+ }
48
+
49
+ const nextValue = pruneInternalEntries(entryValue)
50
+
51
+ if (nextValue !== undefined) {
52
+ acc[key] = nextValue
53
+ }
54
+ })
55
+
56
+ return acc
57
+ }
58
+
59
+ const getApiSourceName = (sourceName = '') => {
60
+ const normalized = String(sourceName || '')
61
+ .split('?')[0]
62
+ .split('#')[0]
63
+ .replace(/\\/g, '/')
64
+ const fileName = normalized.split('/').filter(Boolean).pop() || ''
65
+
66
+ return fileName.replace(/\.json$/i, '') || 'API'
67
+ }
68
+
69
+ const isEmptySingleEntry = (value) => {
70
+ if (value === undefined || value === null) {
71
+ return true
72
+ }
73
+
74
+ if (typeof value === 'string') {
75
+ return value.trim() === ''
76
+ }
77
+
78
+ if (Array.isArray(value)) {
79
+ return value.length === 0
80
+ }
81
+
82
+ if (isPlainObject(value)) {
83
+ return Object.keys(value).length === 0
84
+ }
85
+
86
+ return false
87
+ }
88
+
89
+ export const normalizeApiDocsLink = (value = '') => {
90
+ const normalized = String(value || '').trim()
91
+
92
+ if (normalized === '') {
93
+ return ''
94
+ }
95
+
96
+ return normalized
97
+ .replace(/^https:\/\/v[\d]+\.quasar\.dev/i, '')
98
+ .replace(/^https:\/\/quasar\.dev/i, '')
99
+ }
100
+
101
+ export const getPropsCategories = (props = {}) => {
102
+ const acc = new Set()
103
+
104
+ Object.values(props || {}).forEach((value) => {
105
+ if (value !== undefined && value !== null) {
106
+ getEntryCategories(value).forEach((groupKey) => {
107
+ acc.add(groupKey)
108
+ })
109
+ }
110
+ })
111
+
112
+ return acc.size === 1 ? [defaultInnerTabName] : [...acc].sort()
113
+ }
114
+
115
+ export const getInnerTabs = (api = {}, tabs = []) => {
116
+ const acc = {}
117
+
118
+ tabs.forEach((tab) => {
119
+ acc[tab] = tab === 'props' && isPlainObject(api[tab])
120
+ ? getPropsCategories(api[tab])
121
+ : [defaultInnerTabName]
122
+ })
123
+
124
+ return acc
125
+ }
126
+
127
+ export const parseApi = (api = {}, tabs = [], innerTabs = {}) => {
128
+ const acc = {}
129
+
130
+ tabs.forEach((tab) => {
131
+ const apiValue = api[tab]
132
+
133
+ if (innerTabs[tab]?.length > 1 && isPlainObject(apiValue)) {
134
+ const inner = {}
135
+
136
+ innerTabs[tab].forEach((subTab) => {
137
+ inner[subTab] = {}
138
+ })
139
+
140
+ Object.entries(apiValue).forEach(([key, value]) => {
141
+ if (value === undefined || value === null) {
142
+ return
143
+ }
144
+
145
+ getEntryCategories(value).forEach((groupKey) => {
146
+ if (inner[groupKey] !== undefined) {
147
+ inner[groupKey][key] = value
148
+ }
149
+ })
150
+ })
151
+
152
+ acc[tab] = inner
153
+ return
154
+ }
155
+
156
+ acc[tab] = {
157
+ [defaultInnerTabName]: apiValue ?? {}
158
+ }
159
+ })
160
+
161
+ return acc
162
+ }
163
+
164
+ const passesFilter = (filter, name, desc) => {
165
+ const normalizedFilter = String(filter || '').trim().toLowerCase()
166
+
167
+ if (normalizedFilter === '') {
168
+ return true
169
+ }
170
+
171
+ return (
172
+ String(name || '').toLowerCase().includes(normalizedFilter) ||
173
+ String(desc || '').toLowerCase().includes(normalizedFilter)
174
+ )
175
+ }
176
+
177
+ export const getFilteredApi = (parsedApi = {}, filter = '', tabs = [], innerTabs = {}) => {
178
+ const normalizedFilter = String(filter || '').trim().toLowerCase()
179
+
180
+ if (normalizedFilter === '') {
181
+ return parsedApi
182
+ }
183
+
184
+ const acc = {}
185
+
186
+ tabs.forEach((tab) => {
187
+ if (tab === 'injection') {
188
+ const name = parsedApi?.[tab]?.[defaultInnerTabName]
189
+
190
+ acc[tab] = {
191
+ [defaultInnerTabName]: passesFilter(normalizedFilter, name, '') ? name : {}
192
+ }
193
+ return
194
+ }
195
+
196
+ if (tab === 'quasarConfOptions') {
197
+ const api = parsedApi?.[tab]?.[defaultInnerTabName] || {}
198
+ const result = {
199
+ ...api,
200
+ definition: {}
201
+ }
202
+
203
+ Object.entries(api.definition || {}).forEach(([name, entry]) => {
204
+ if (passesFilter(normalizedFilter, name, entry?.desc)) {
205
+ result.definition[name] = entry
206
+ }
207
+ })
208
+
209
+ acc[tab] = {
210
+ [defaultInnerTabName]: Object.keys(result.definition).length === 0 && !passesFilter(normalizedFilter, api.propName, '')
211
+ ? {}
212
+ : result
213
+ }
214
+ return
215
+ }
216
+
217
+ const tabApi = parsedApi?.[tab] || {}
218
+ const tabCategories = innerTabs[tab] || [defaultInnerTabName]
219
+
220
+ acc[tab] = {}
221
+
222
+ tabCategories.forEach((category) => {
223
+ const subTabs = {}
224
+ const categoryEntries = tabApi[category] || {}
225
+
226
+ Object.entries(categoryEntries).forEach(([name, entry]) => {
227
+ if (passesFilter(normalizedFilter, name, entry?.desc)) {
228
+ subTabs[name] = entry
229
+ }
230
+ })
231
+
232
+ acc[tab][category] = subTabs
233
+ })
234
+ })
235
+
236
+ return acc
237
+ }
238
+
239
+ export const getApiCount = (parsedApi = {}, tabs = [], innerTabs = {}) => {
240
+ const acc = {}
241
+
242
+ tabs.forEach((tab) => {
243
+ const tabApi = parsedApi?.[tab] || {}
244
+ const tabCategories = innerTabs[tab] || [defaultInnerTabName]
245
+
246
+ if (['value', 'arg', 'injection'].includes(tab)) {
247
+ const value = tabApi[tabCategories[0]]
248
+
249
+ acc[tab] = {
250
+ overall: isEmptySingleEntry(value) ? 0 : 1
251
+ }
252
+ return
253
+ }
254
+
255
+ if (tab === 'quasarConfOptions') {
256
+ const api = tabApi[tabCategories[0]] || {}
257
+
258
+ acc[tab] = {
259
+ overall: Object.keys(api).length === 0
260
+ ? 0
261
+ : api.definition === undefined
262
+ ? 1
263
+ : Object.keys(api.definition || {}).length
264
+ }
265
+ return
266
+ }
267
+
268
+ const nextValue = {
269
+ overall: 0,
270
+ category: {}
271
+ }
272
+
273
+ tabCategories.forEach((category) => {
274
+ const count = Object.keys(tabApi[category] || {}).length
275
+
276
+ nextValue.category[category] = count
277
+ nextValue.overall += count
278
+ })
279
+
280
+ acc[tab] = nextValue
281
+ })
282
+
283
+ return acc
284
+ }
285
+
286
+ export const createApiBlockModel = (sourceName = '', apiDocument = {}) => {
287
+ const rawDocument = isPlainObject(apiDocument) ? apiDocument : {}
288
+ const {
289
+ type: _type,
290
+ behavior: _behavior,
291
+ meta,
292
+ addedIn: _addedIn,
293
+ internal: _internalSection,
294
+ ...apiSectionsRaw
295
+ } = rawDocument
296
+ const apiSections = {}
297
+
298
+ Object.entries(apiSectionsRaw).forEach(([sectionName, sectionValue]) => {
299
+ const sanitizedValue = pruneInternalEntries(sectionValue)
300
+
301
+ if (sanitizedValue !== undefined && isSupportedTopLevelSection(sanitizedValue)) {
302
+ apiSections[sectionName] = sanitizedValue
303
+ }
304
+ })
305
+
306
+ const tabs = Object.keys(apiSections)
307
+ const innerTabs = getInnerTabs(apiSections, tabs)
308
+ const api = parseApi(apiSections, tabs, innerTabs)
309
+ const sourceLabel = getApiSourceName(sourceName)
310
+ const docsUrl = String(meta?.docsUrl || '').trim()
311
+
312
+ return {
313
+ sourceLabel,
314
+ title: `${sourceLabel} API`,
315
+ docsUrl,
316
+ docsLink: normalizeApiDocsLink(docsUrl),
317
+ tabs,
318
+ innerTabs,
319
+ api,
320
+ nothingToShow: tabs.length === 0
321
+ }
322
+ }
323
+
324
+ export {
325
+ defaultInnerTabName
326
+ }
@@ -21,6 +21,7 @@ const EXPANDABLE_MARKER_PREFIX = '@@DOCSECTOR_EXPANDABLE_'
21
21
  const FILE_MARKER_PREFIX = '@@DOCSECTOR_FILE_'
22
22
  const EMBEDDED_URL_MARKER_PREFIX = '@@DOCSECTOR_EMBEDDED_URL_'
23
23
  const CODE_EXAMPLE_MARKER_PREFIX = '@@DOCSECTOR_CODE_EXAMPLE_'
24
+ const API_BLOCK_MARKER_PREFIX = '@@DOCSECTOR_API_BLOCK_'
24
25
  const CODE_SEGMENT_MARKER_PREFIX = '@@DOCSECTOR_CODE_SEGMENT_'
25
26
  const MATH_KATEX_OPTIONS = {
26
27
  throwOnError: false,
@@ -576,6 +577,43 @@ const extractCodeExampleBlocks = (source = '') => {
576
577
  }
577
578
  }
578
579
 
580
+ const extractApiBlocks = (source = '') => {
581
+ const map = new Map()
582
+ let index = 0
583
+
584
+ const replaceBlock = (match, rawAttrs) => {
585
+ const attrs = parseCustomTagAttributes(rawAttrs)
586
+ const src = decodeHtmlEntities(attrs.src || '').trim()
587
+
588
+ if (!src) {
589
+ return match
590
+ }
591
+
592
+ const marker = `${API_BLOCK_MARKER_PREFIX}${index}@@`
593
+ index++
594
+
595
+ map.set(marker, {
596
+ src,
597
+ title: decodeHtmlEntities(attrs.title || '').trim(),
598
+ pageLink: parseBooleanAttribute(attrs['page-link'], false)
599
+ })
600
+
601
+ return `\n${marker}\n`
602
+ }
603
+
604
+ const replacedSelfClosing = String(source).replace(/<d-block-api\b([^>]*)\/\s*>/gi, (match, rawAttrs) => {
605
+ return replaceBlock(match, rawAttrs)
606
+ })
607
+ const replaced = replacedSelfClosing.replace(/<d-block-api\b([^>]*)>([\s\S]*?)<\/d-block-api>/gi, (match, rawAttrs) => {
608
+ return replaceBlock(match, rawAttrs)
609
+ })
610
+
611
+ return {
612
+ source: replaced,
613
+ apiBlockMap: map
614
+ }
615
+ }
616
+
579
617
  const parseFenceAttributes = (raw = '') => {
580
618
  const parsed = {}
581
619
  const pattern = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s;]+))/g
@@ -931,6 +969,7 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
931
969
  const { source: sourceWithFiles, fileMap } = extractFileBlocks(sourceWithQuickLinks)
932
970
  const { source: sourceWithEmbeddedUrls, embeddedUrlMap } = extractEmbeddedUrlBlocks(sourceWithFiles)
933
971
  const { source: sourceWithCodeExamples, codeExampleMap } = extractCodeExampleBlocks(sourceWithEmbeddedUrls)
972
+ const { source: sourceWithApiBlocks, apiBlockMap } = extractApiBlocks(sourceWithCodeExamples)
934
973
 
935
974
  fileMap.forEach((data, marker) => {
936
975
  fileMap.set(marker, {
@@ -956,7 +995,7 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
956
995
  const markdown = createMarkdownBlockParser()
957
996
  const markdownInline = createMarkdownInlineParser()
958
997
  const markdownEnv = {}
959
- const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithCodeExamples, codeSegmentsMap), markdownEnv)
998
+ const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithApiBlocks, codeSegmentsMap), markdownEnv)
960
999
  const tokens = []
961
1000
 
962
1001
  let level = 0
@@ -1234,6 +1273,19 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
1234
1273
  break
1235
1274
  }
1236
1275
 
1276
+ if (apiBlockMap.has(element.content.trim())) {
1277
+ const data = apiBlockMap.get(element.content.trim())
1278
+
1279
+ tokens.push({
1280
+ tag: 'api',
1281
+ map: element.map,
1282
+ src: data.src,
1283
+ title: data.title,
1284
+ pageLink: data.pageLink
1285
+ })
1286
+ break
1287
+ }
1288
+
1237
1289
  if (tag === 'p') {
1238
1290
  const imageToken = parseStandaloneImageToken(element.content)
1239
1291
 
@@ -0,0 +1,17 @@
1
+ const normalizeLineBreaks = (text = '') => String(text || '').replace(/\r\n/g, '\n')
2
+
3
+ export const countRenderedCodeLines = (text = '') => {
4
+ const normalized = normalizeLineBreaks(text)
5
+
6
+ if (normalized === '') {
7
+ return 0
8
+ }
9
+
10
+ const withoutTerminalBreak = normalized.replace(/\n$/, '')
11
+
12
+ if (withoutTerminalBreak === '') {
13
+ return 1
14
+ }
15
+
16
+ return withoutTerminalBreak.split('\n').length
17
+ }
@@ -0,0 +1,77 @@
1
+ ## Overview
2
+
3
+ Docsector Reader ships a public authoring skill for AI agents that need to understand how Docsector documentation works.
4
+
5
+ The skill is a `SKILL.md` file with companion references. It explains Docsector Markdown conventions, every documented block, page structure, asset paths, authoring patterns, MCP lookup, and WebMCP browser tools.
6
+
7
+ ## Public URLs
8
+
9
+ The built-in skill is published as a static artifact:
10
+
11
+ ```text
12
+ /.well-known/agent-skills/docsector-documentation-authoring/SKILL.md
13
+ ```
14
+
15
+ The discovery index points agents to the same file and includes a SHA-256 digest:
16
+
17
+ ```text
18
+ /.well-known/agent-skills/index.json
19
+ ```
20
+
21
+ ## What the Skill Contains
22
+
23
+ <d-block-quick-links title="Skill references">
24
+ <d-block-quick-link
25
+ title="SKILL.md"
26
+ description="The compact workflow agents load first"
27
+ href="/.well-known/agent-skills/docsector-documentation-authoring/SKILL.md"
28
+ />
29
+ <d-block-quick-link
30
+ title="Block catalog"
31
+ description="All Docsector blocks, syntax, and when-to-use guidance"
32
+ href="/.well-known/agent-skills/docsector-documentation-authoring/references/block-catalog.md"
33
+ />
34
+ <d-block-quick-link
35
+ title="Page structure"
36
+ description="Page files, locales, assets, examples, and API JSON conventions"
37
+ href="/.well-known/agent-skills/docsector-documentation-authoring/references/page-structure.md"
38
+ />
39
+ <d-block-quick-link
40
+ title="MCP and WebMCP"
41
+ description="How agents can search, fetch, navigate, and copy live docs"
42
+ href="/.well-known/agent-skills/docsector-documentation-authoring/references/mcp-webmcp.md"
43
+ />
44
+ </d-block-quick-links>
45
+
46
+ ## Local and Published Copies
47
+
48
+ Docsector keeps two synchronized copies of the same skill:
49
+
50
+ - `.github/skills/docsector-documentation-authoring/` for repository-local assistants such as GitHub Copilot in VS Code.
51
+ - `public/.well-known/agent-skills/docsector-documentation-authoring/` for the built documentation site.
52
+
53
+ During build, Docsector copies the public artifact into `dist/spa/.well-known/agent-skills/` and generates the discovery index.
54
+
55
+ ## How Agents Should Use It
56
+
57
+ Agents should load `SKILL.md` first, then open reference files only when a task needs more detail.
58
+
59
+ For current page examples, agents can combine the skill with Docsector's MCP tools:
60
+
61
+ ```text
62
+ search_docsector
63
+ get_page_docsector
64
+ ```
65
+
66
+ Browser agents can also use WebMCP tools when `navigator.modelContext` is available:
67
+
68
+ - `docs.search_docs`
69
+ - `docs.get_page`
70
+ - `docs.navigate_to`
71
+ - `docs.copy_current_page`
72
+
73
+ ## When to Publish Your Own Skill
74
+
75
+ Publish a project-specific skill when your documentation has domain rules that are not covered by the built-in Docsector authoring skill.
76
+
77
+ Use Docsector's `agentSkills` config to expose the skill through `/.well-known/agent-skills/index.json`, and keep the artifact under `public/.well-known/agent-skills/...` when you want Docsector to compute the digest automatically.