@docsector/docsector-reader 1.2.4 β†’ 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/README.md CHANGED
@@ -28,6 +28,7 @@ Transform Markdown content into beautiful, navigable documentation sites β€” wit
28
28
  - πŸ€– **LLM Bot Detection** β€” Automatically serves raw Markdown to known AI crawlers (GPTBot, ClaudeBot, PerplexityBot, GrokBot, and others)
29
29
  - πŸ—ΊοΈ **Sitemap Generation** β€” Automatic `sitemap.xml` generation at build time with all page URLs (requires `siteUrl` in config)
30
30
  - πŸ€– **AI-Friendly robots.txt** β€” Scaffold includes a `robots.txt` explicitly allowing 23 AI crawlers (GPTBot, ClaudeBot, PerplexityBot, GrokBot, etc.)
31
+ - 🧭 **Content Signals** β€” Optional `Content-Signal` directive for declaring AI usage policy (`ai-train`, `search`, `ai-input`) in `robots.txt`
31
32
  - πŸ”— **Homepage Link Headers** β€” Auto-generated `Link` response headers for agent discovery (`api-catalog`, `service-doc`, `service-desc`, `describedby`) per RFC 8288 / RFC 9727
32
33
  - πŸ”Œ **MCP Server** β€” Auto-generated [MCP](https://modelcontextprotocol.io) server at `/mcp` for AI assistant integration (Claude Desktop, VS Code, etc.)
33
34
  - πŸ“„ **llms.txt / llms-full.txt** β€” Auto-generated [llms.txt](https://llmstxt.org) index and full-content file for LLM discovery (requires `siteUrl` in config)
@@ -46,10 +47,12 @@ Transform Markdown content into beautiful, navigable documentation sites β€” wit
46
47
  - πŸ“± **Responsive** β€” Mobile-friendly with collapsible sidebar and drawers
47
48
  - 🏷️ **Status Badges** β€” Mark pages as `done`, `draft`, or `empty` with visual indicators
48
49
  - ✏️ **Edit on GitHub** β€” Direct links to edit pages on your repository
50
+ - 🧭 **Robust Edit Link Mapping** β€” Normalizes route paths (including trailing slashes) into `page.subpage.locale.md` source files for reliable GitHub edit URLs
49
51
  - πŸ“… **Last Updated Date** β€” Automatic per-page "last updated" date from git commit history, locale-formatted
50
52
  - πŸ“Š **Translation Progress** β€” Automatic translation percentage based on header coverage
51
53
  - 🧠 **Markdown Negotiation** β€” Responds with Markdown when clients send `Accept: text/markdown`, while keeping HTML as browser default
52
54
  - πŸ” **Web Bot Auth** β€” Can publish a signed HTTP message signatures directory and includes helpers to sign outbound bot requests
55
+ - 🧭 **Content Signals** β€” Injects `Content-Signal` policy in `robots.txt` with deterministic, idempotent build output
53
56
  - 🏠 **Markdown Home at Root** β€” Homepage is rendered from `src/pages/Homepage.{lang}.md` directly at `/`
54
57
  - 🧭 **Quick Links Custom Element** β€” Use `<d-quick-links>` and `<d-quick-link>` in Markdown to render rich home navigation cards
55
58
  - πŸ—‚οΈ **API Catalog Well-Known** β€” Auto-generates `/.well-known/api-catalog` as Linkset JSON for machine-readable API discovery
@@ -280,6 +283,56 @@ Check `checks.discoverability.linkHeaders.status` equals `"pass"`.
280
283
 
281
284
  ---
282
285
 
286
+ ## 🧭 Content Signals
287
+
288
+ Docsector Reader can declare AI usage preferences in `robots.txt` via `Content-Signal`.
289
+
290
+ When enabled, build output ensures a deterministic directive format:
291
+
292
+ - `Content-Signal: ai-train=..., search=..., ai-input=...`
293
+
294
+ ### Configure
295
+
296
+ ```javascript
297
+ export default {
298
+ // ...other config
299
+
300
+ contentSignals: {
301
+ enabled: true,
302
+ aiTrain: 'yes',
303
+ search: 'yes',
304
+ aiInput: 'yes',
305
+ userAgent: '*',
306
+ applyToAllBlocks: false
307
+ }
308
+ }
309
+ ```
310
+
311
+ Notes:
312
+
313
+ - `aiTrain`, `search`, and `aiInput` accept `yes` / `no` (or booleans).
314
+ - Default scope is only `User-agent: *`.
315
+ - Build patch is idempotent: repeated builds do not duplicate `Content-Signal` lines.
316
+
317
+ ### Validate
318
+
319
+ ```bash
320
+ npx docsector build
321
+ cat dist/spa/robots.txt
322
+ ```
323
+
324
+ Optional external validation:
325
+
326
+ ```bash
327
+ curl -X POST https://isitagentready.com/api/scan \
328
+ -H 'Content-Type: application/json' \
329
+ -d '{"url":"https://YOUR-SITE.com"}'
330
+ ```
331
+
332
+ Check `checks.botAccessControl.contentSignals.status` equals `"pass"`.
333
+
334
+ ---
335
+
283
336
  ## οΏ½πŸš€ Quick Start
284
337
 
285
338
  ### πŸ“¦ Install
@@ -434,6 +487,15 @@ export default {
434
487
  agentFallback: true
435
488
  },
436
489
 
490
+ contentSignals: {
491
+ enabled: true,
492
+ aiTrain: 'yes',
493
+ search: 'yes',
494
+ aiInput: 'yes',
495
+ userAgent: '*',
496
+ applyToAllBlocks: false
497
+ },
498
+
437
499
  languages: [
438
500
  { image: '/images/flags/united-states-of-america.png', label: 'English (US)', value: 'en-US' },
439
501
  { 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 = '1.2.4'
26
+ const VERSION = '1.3.1'
27
27
 
28
28
  const HELP = `
29
29
  Docsector Reader v${VERSION}
@@ -170,6 +170,17 @@ export default {
170
170
  // signatureLabel: 'sig1'
171
171
  // },
172
172
 
173
+ // @ Content Signals (optional)
174
+ // Declares AI usage preferences in robots.txt.
175
+ // contentSignals: {
176
+ // enabled: true,
177
+ // aiTrain: 'yes',
178
+ // search: 'yes',
179
+ // aiInput: 'yes',
180
+ // userAgent: '*',
181
+ // applyToAllBlocks: false
182
+ // },
183
+
173
184
  // @ Languages
174
185
  languages: [
175
186
  {
@@ -763,6 +774,7 @@ const TEMPLATE_MARKDOWNLINT = `\
763
774
  const TEMPLATE_ROBOTS_TXT = `\
764
775
  User-agent: *
765
776
  Allow: /
777
+ Content-Signal: ai-train=yes, search=yes, ai-input=yes
766
778
 
767
779
  # Explicitly allow AI crawlers
768
780
  # OpenAI
@@ -24,7 +24,7 @@ export default {
24
24
  discussions: 'https://github.com/docsector/docsector-reader/discussions',
25
25
  chat: null, // e.g., Discord/Slack invite URL
26
26
  email: null, // e.g., 'mailto:contact@example.com'
27
- changelog: '/changelog',
27
+ changelog: 'https://github.com/docsector/docsector-reader/releases',
28
28
  roadmap: null, // e.g., external roadmap URL
29
29
  sponsor: null, // e.g., GitHub Sponsors URL
30
30
  explore: null // e.g., URL to explore related repos
@@ -58,5 +58,8 @@ export default {
58
58
  ],
59
59
 
60
60
  // @ Default language
61
- defaultLanguage: 'en-US'
61
+ defaultLanguage: 'en-US',
62
+ contentSignals: {
63
+ enabled: true
64
+ }
62
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "1.2.4",
3
+ "version": "1.3.1",
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",
@@ -12,11 +12,34 @@ const route = useRoute()
12
12
  const router = useRouter()
13
13
  const { t, locale, availableLocales, te, tm } = useI18n()
14
14
 
15
- const base = docsectorConfig.github?.editBaseUrl || ''
15
+ function normalizeEditBaseUrl (url = '') {
16
+ const normalized = String(url).trim().replace(/\/+$/, '')
17
+ return normalized.replace(/(github\.com\/[^/]+\/[^/]+)\/(blob|tree)\//, '$1/edit/')
18
+ }
19
+
20
+ const base = normalizeEditBaseUrl(docsectorConfig.github?.editBaseUrl || '')
21
+
22
+ function routePathToSourcePath (path = '') {
23
+ const cleanPath = String(path)
24
+ .replace(/\/index\.html$/, '')
25
+ .replace(/\/+$/, '')
26
+
27
+ if (cleanPath === '' || cleanPath === '/') {
28
+ return '/Homepage'
29
+ }
30
+
31
+ const segments = cleanPath.replace(/^\//, '').split('/').filter(Boolean)
32
+ if (segments.length < 2) {
33
+ return `/${segments.join('/')}`
34
+ }
35
+
36
+ const subpage = segments.pop()
37
+ return `/${segments.join('/')}.${subpage}`
38
+ }
16
39
 
17
40
  const status = computed(() => route.meta.status)
18
41
  const URL = computed(() => {
19
- const path = route.path.replace(/\/([^/]*)$/, '.$1')
42
+ const path = routePathToSourcePath(route.path)
20
43
  return `${base}${path}.${locale.value}.md`
21
44
  })
22
45
  const color = computed(() => {
package/src/index.js CHANGED
@@ -66,6 +66,13 @@
66
66
  * @param {string|null} [config.webBotAuth.keyId=null] - Optional static fallback key identifier when env var is absent
67
67
  * @param {number} [config.webBotAuth.signatureMaxAge=300] - Signature validity window in seconds for directory responses
68
68
  * @param {string} [config.webBotAuth.signatureLabel='sig1'] - Signature label used in Signature and Signature-Input headers
69
+ * @param {Object} [config.contentSignals] - Content Signals policy for robots.txt
70
+ * @param {boolean} [config.contentSignals.enabled=false] - Enables Content-Signal injection in robots.txt during build
71
+ * @param {'yes'|'no'|boolean} [config.contentSignals.aiTrain='yes'] - Permission for AI model training consumption
72
+ * @param {'yes'|'no'|boolean} [config.contentSignals.search='yes'] - Permission for AI search indexing/discovery consumption
73
+ * @param {'yes'|'no'|boolean} [config.contentSignals.aiInput='yes'] - Permission for AI input/inference-time consumption
74
+ * @param {string} [config.contentSignals.userAgent='*'] - Target User-agent block for directive injection
75
+ * @param {boolean} [config.contentSignals.applyToAllBlocks=false] - When true, applies directive to every User-agent block
69
76
  * @returns {Object} Resolved Docsector configuration
70
77
  */
71
78
  export function createDocsector (config = {}) {
@@ -139,6 +146,16 @@ export function createDocsector (config = {}) {
139
146
  signatureMaxAge: 300,
140
147
  signatureLabel: 'sig1',
141
148
  ...config.webBotAuth
149
+ },
150
+
151
+ contentSignals: {
152
+ enabled: false,
153
+ aiTrain: 'yes',
154
+ search: 'yes',
155
+ aiInput: 'yes',
156
+ userAgent: '*',
157
+ applyToAllBlocks: false,
158
+ ...config.contentSignals
142
159
  }
143
160
  }
144
161
  }
@@ -753,6 +753,8 @@ function createMarkdownBuildPlugin (projectRoot) {
753
753
  ? Math.max(30, Number(webBotAuthConfig.signatureMaxAge))
754
754
  : 300
755
755
  const webBotAuthSignatureLabel = webBotAuthConfig.signatureLabel || 'sig1'
756
+ const contentSignalsConfig = config.contentSignals || {}
757
+ const contentSignalsEnabled = contentSignalsConfig.enabled === true
756
758
 
757
759
  if (markdownNegotiationEnabled || webBotAuthEnabled) {
758
760
  const functionsDir = resolve(projectRoot, 'functions')
@@ -1001,6 +1003,25 @@ export async function onRequest (context) {
1001
1003
  writeFileSync(resolve(functionsDir, '_middleware.js'), middlewareCode)
1002
1004
  console.log(`\x1b[36m[docsector]\x1b[0m Generated runtime middleware at functions/_middleware.js`)
1003
1005
  }
1006
+
1007
+ if (contentSignalsEnabled) {
1008
+ const robotsPath = resolve(distDir, 'robots.txt')
1009
+ const existingRobots = existsSync(robotsPath)
1010
+ ? readFileSync(robotsPath, 'utf-8')
1011
+ : 'User-agent: *\nAllow: /\n'
1012
+ const contentSignalLine = buildContentSignalLine(contentSignalsConfig)
1013
+ const patchedRobots = applyContentSignalsToRobots(existingRobots, {
1014
+ contentSignalLine,
1015
+ userAgent: contentSignalsConfig.userAgent || '*',
1016
+ applyToAllBlocks: contentSignalsConfig.applyToAllBlocks === true
1017
+ })
1018
+
1019
+ if (patchedRobots !== existingRobots) {
1020
+ writeFileSync(robotsPath, patchedRobots)
1021
+ console.log(`\x1b[36m[docsector]\x1b[0m Updated robots.txt with Content-Signal policy`)
1022
+ }
1023
+ }
1024
+
1004
1025
  console.log(`\x1b[36m[docsector]\x1b[0m Added _headers rule for .md files`)
1005
1026
 
1006
1027
  // Add homepage Link headers for agent discovery (RFC 8288 / RFC 9727)
@@ -1274,6 +1295,87 @@ export async function onRequest (context) {
1274
1295
  }
1275
1296
  }
1276
1297
 
1298
+ function normalizeContentSignalValue (value, fallback = 'yes') {
1299
+ if (typeof value === 'boolean') return value ? 'yes' : 'no'
1300
+ if (typeof value === 'string') {
1301
+ const normalized = value.trim().toLowerCase()
1302
+ if (normalized === 'yes' || normalized === 'no') return normalized
1303
+ }
1304
+ return fallback
1305
+ }
1306
+
1307
+ function buildContentSignalLine (contentSignalsConfig = {}) {
1308
+ const aiTrain = normalizeContentSignalValue(contentSignalsConfig.aiTrain, 'yes')
1309
+ const search = normalizeContentSignalValue(contentSignalsConfig.search, 'yes')
1310
+ const aiInput = normalizeContentSignalValue(contentSignalsConfig.aiInput, 'yes')
1311
+ return `Content-Signal: ai-train=${aiTrain}, search=${search}, ai-input=${aiInput}`
1312
+ }
1313
+
1314
+ function ensureContentSignalInBlock (blockLines, contentSignalLine) {
1315
+ const cleanedBlock = blockLines.filter((line, index) => {
1316
+ if (index === 0) return true
1317
+ return !/^\s*Content-Signal\s*:/i.test(line)
1318
+ })
1319
+
1320
+ let insertAt = cleanedBlock.findIndex(line => /^\s*Allow\s*:/i.test(line))
1321
+ if (insertAt === -1) {
1322
+ insertAt = 0
1323
+ }
1324
+
1325
+ cleanedBlock.splice(insertAt + 1, 0, contentSignalLine)
1326
+ return cleanedBlock
1327
+ }
1328
+
1329
+ function applyContentSignalsToRobots (robotsContent, { contentSignalLine, userAgent = '*', applyToAllBlocks = false }) {
1330
+ const input = typeof robotsContent === 'string' ? robotsContent : ''
1331
+ const lines = input.replace(/\r\n/g, '\n').split('\n')
1332
+ const userAgentIndexes = []
1333
+
1334
+ for (let i = 0; i < lines.length; i++) {
1335
+ if (/^\s*User-agent\s*:/i.test(lines[i])) {
1336
+ userAgentIndexes.push(i)
1337
+ }
1338
+ }
1339
+
1340
+ if (userAgentIndexes.length === 0) {
1341
+ const scaffold = [
1342
+ `User-agent: ${userAgent}`,
1343
+ 'Allow: /',
1344
+ contentSignalLine,
1345
+ ''
1346
+ ].join('\n')
1347
+ return scaffold.endsWith('\n') ? scaffold : scaffold + '\n'
1348
+ }
1349
+
1350
+ const targetIndexes = []
1351
+ for (const startIndex of userAgentIndexes) {
1352
+ const uaValue = lines[startIndex].split(':').slice(1).join(':').trim()
1353
+ if (applyToAllBlocks || uaValue.toLowerCase() === String(userAgent).toLowerCase()) {
1354
+ targetIndexes.push(startIndex)
1355
+ }
1356
+ }
1357
+
1358
+ if (targetIndexes.length === 0) {
1359
+ targetIndexes.push(userAgentIndexes[0])
1360
+ }
1361
+
1362
+ let updated = []
1363
+ let cursor = 0
1364
+
1365
+ for (const startIndex of targetIndexes) {
1366
+ const nextUserAgent = userAgentIndexes.find(index => index > startIndex)
1367
+ const endIndex = nextUserAgent === undefined ? lines.length : nextUserAgent
1368
+
1369
+ updated = updated.concat(lines.slice(cursor, startIndex))
1370
+ const currentBlock = lines.slice(startIndex, endIndex)
1371
+ updated = updated.concat(ensureContentSignalInBlock(currentBlock, contentSignalLine))
1372
+ cursor = endIndex
1373
+ }
1374
+
1375
+ updated = updated.concat(lines.slice(cursor))
1376
+ return updated.join('\n').replace(/\n+$/g, '') + '\n'
1377
+ }
1378
+
1277
1379
  /**
1278
1380
  * Create a complete Quasar configuration for a docsector-reader consumer project.
1279
1381
  *