@docsector/docsector-reader 0.7.2 → 0.8.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
@@ -18,13 +18,20 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
18
18
 
19
19
  ---
20
20
 
21
+ ## 🤖 AI-Friendly Features
22
+
23
+ - 📋 **Copy Page** — One-click button copies the current page as raw Markdown, ready to paste into LLMs
24
+ - 📄 **View as Markdown** — Open any page as plain text by appending `.md` to the URL, with locale support (`?lang=`)
25
+ - 🤖 **Open in ChatGPT / Claude** — One-click links to open the current page directly in ChatGPT or Claude for Q&A
26
+ - 🤖 **LLM Bot Detection** — Automatically serves raw Markdown to known AI crawlers (GPTBot, ClaudeBot, PerplexityBot, GrokBot, and others)
27
+
28
+ ---
29
+
21
30
  ## ✨ Features
22
31
 
23
32
  - 📝 **Markdown Rendering** — Write docs in Markdown, rendered with syntax highlighting (Prism.js)
24
33
  - 🧩 **Mermaid Diagrams** — Native support for fenced ` ```mermaid ` blocks, with automatic dark/light theme switching
25
34
  - 🚨 **GitHub-Style Alerts** — Native support for `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, and `[!CAUTION]`
26
- - 🤖 **AI-Friendly** — "Copy page" button copies raw Markdown for LLMs; "View as Markdown" opens any page as plain text via `.md` URL suffix
27
- - 📅 **Last Updated Date** — Automatic per-page "last updated" date from git commit history, locale-formatted
28
35
  - 🌍 **Internationalization (i18n)** — Multi-language support with HJSON locale files and per-page translations
29
36
  - 🌗 **Dark/Light Mode** — Automatic theme switching with Quasar Dark Plugin
30
37
  - 🔗 **Anchor Navigation** — Right-side Table of Contents tree with scroll tracking
@@ -32,6 +39,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
32
39
  - 📱 **Responsive** — Mobile-friendly with collapsible sidebar and drawers
33
40
  - 🏷️ **Status Badges** — Mark pages as `done`, `draft`, or `empty` with visual indicators
34
41
  - ✏️ **Edit on GitHub** — Direct links to edit pages on your repository
42
+ - 📅 **Last Updated Date** — Automatic per-page "last updated" date from git commit history, locale-formatted
35
43
  - 📊 **Translation Progress** — Automatic translation percentage based on header coverage
36
44
  - ⚙️ **Single Config File** — Customize branding, links, and languages via `docsector.config.js`
37
45
 
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.7.2'
26
+ const VERSION = '0.8.0'
27
27
 
28
28
  const HELP = `
29
29
  Docsector Reader v${VERSION}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "0.7.2",
3
+ "version": "0.8.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",
@@ -3,7 +3,7 @@ import { ref, computed } from 'vue'
3
3
  import { useRoute } from 'vue-router'
4
4
  import { useStore } from 'vuex'
5
5
  import { useI18n } from 'vue-i18n'
6
- import { copyToClipboard, openURL } from 'quasar'
6
+ import { copyToClipboard } from 'quasar'
7
7
 
8
8
  import gitDates from 'virtual:docsector-git-dates'
9
9
 
@@ -13,6 +13,9 @@ const { t, locale } = useI18n()
13
13
 
14
14
  const copied = ref(false)
15
15
 
16
+ const openaiIcon = `img:data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23888" d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z"/></svg>')}`
17
+ const claudeIcon = `img:data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" fill="%23888" fill-rule="evenodd" viewBox="0 0 24 24"><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"/></svg>')}`
18
+
16
19
  const subpage = computed(() => {
17
20
  const rel = store.state.page.relative
18
21
  return rel ? rel.replace(/^\//, '') : 'overview'
@@ -56,6 +59,19 @@ const markdownURL = computed(() => {
56
59
  return `${route.path}.md`
57
60
  })
58
61
 
62
+ const fullMarkdownURL = computed(() => {
63
+ return `${window.location.origin}${route.path}.md`
64
+ })
65
+
66
+ const chatgptURL = computed(() => {
67
+ const prompt = `Read ${fullMarkdownURL.value} and answer questions about the content.`
68
+ return `https://chat.openai.com/?q=${encodeURIComponent(prompt)}`
69
+ })
70
+ const claudeURL = computed(() => {
71
+ const prompt = `Read ${fullMarkdownURL.value} and answer questions about the content.`
72
+ return `https://claude.ai/new?q=${encodeURIComponent(prompt)}`
73
+ })
74
+
59
75
  const copyPage = () => {
60
76
  if (!rawMarkdown.value) return
61
77
 
@@ -64,16 +80,12 @@ const copyPage = () => {
64
80
  setTimeout(() => { copied.value = false }, 2000)
65
81
  })
66
82
  }
67
-
68
- const viewAsMarkdown = () => {
69
- openURL(markdownURL.value)
70
- }
71
83
  </script>
72
84
 
73
85
  <template>
74
86
  <div class="d-page-bar">
75
87
  <span v-if="formattedDate" class="d-page-bar__date">
76
- {{ t('page.lastUpdated') }}: {{ formattedDate }}
88
+ {{ t('page.lastUpdated') }}: <br class="d-page-bar__date-break"> {{ formattedDate }}
77
89
  </span>
78
90
  <span v-else class="d-page-bar__date"></span>
79
91
 
@@ -98,7 +110,7 @@ const viewAsMarkdown = () => {
98
110
  </q-item-section>
99
111
  </q-item>
100
112
 
101
- <q-item clickable v-close-popup @click="viewAsMarkdown" class="q-py-sm">
113
+ <q-item clickable v-close-popup :href="markdownURL" target="_blank" class="q-py-sm">
102
114
  <q-item-section avatar>
103
115
  <q-icon name="description" />
104
116
  </q-item-section>
@@ -110,6 +122,34 @@ const viewAsMarkdown = () => {
110
122
  <q-icon name="open_in_new" size="xs" />
111
123
  </q-item-section>
112
124
  </q-item>
125
+
126
+ <q-separator />
127
+
128
+ <q-item clickable v-close-popup :href="chatgptURL" target="_blank" class="q-py-sm">
129
+ <q-item-section avatar>
130
+ <q-icon :name="openaiIcon" size="xs" />
131
+ </q-item-section>
132
+ <q-item-section>
133
+ <q-item-label>{{ t('page.openInChatGPT') }}</q-item-label>
134
+ <q-item-label caption>{{ t('page.openInChatGPTCaption') }}</q-item-label>
135
+ </q-item-section>
136
+ <q-item-section side>
137
+ <q-icon name="open_in_new" size="xs" />
138
+ </q-item-section>
139
+ </q-item>
140
+
141
+ <q-item clickable v-close-popup :href="claudeURL" target="_blank" class="q-py-sm">
142
+ <q-item-section avatar>
143
+ <q-icon :name="claudeIcon" size="xs" />
144
+ </q-item-section>
145
+ <q-item-section>
146
+ <q-item-label>{{ t('page.openInClaude') }}</q-item-label>
147
+ <q-item-label caption>{{ t('page.openInClaudeCaption') }}</q-item-label>
148
+ </q-item-section>
149
+ <q-item-section side>
150
+ <q-icon name="open_in_new" size="xs" />
151
+ </q-item-section>
152
+ </q-item>
113
153
  </q-list>
114
154
  </q-btn-dropdown>
115
155
  </div>
@@ -126,10 +166,17 @@ const viewAsMarkdown = () => {
126
166
  font-size: 0.8rem
127
167
  opacity: 0.6
128
168
 
169
+ &__date-break
170
+ display: none
171
+
129
172
  &__actions
130
173
  font-size: 0.75rem
131
174
 
132
175
  body.body--dark
133
176
  .d-page-bar__date
134
177
  color: rgba(255, 255, 255, 0.7)
178
+
179
+ @media (max-width: 376px)
180
+ .d-page-bar__date-break
181
+ display: block
135
182
  </style>
@@ -29,7 +29,11 @@ const engineDefaults = {
29
29
  copyPageCaption: 'Copy page as Markdown for LLMs',
30
30
  copied: 'Copied!',
31
31
  viewAsMarkdown: 'View as Markdown',
32
- viewAsMarkdownCaption: 'View this page as plain text'
32
+ viewAsMarkdownCaption: 'View this page as plain text',
33
+ openInChatGPT: 'Open in ChatGPT',
34
+ openInChatGPTCaption: 'Ask ChatGPT about this page',
35
+ openInClaude: 'Open in Claude',
36
+ openInClaudeCaption: 'Ask Claude about this page'
33
37
  }
34
38
  },
35
39
  'pt-BR': {
@@ -39,7 +43,11 @@ const engineDefaults = {
39
43
  copyPageCaption: 'Copiar página como Markdown para LLMs',
40
44
  copied: 'Copiado!',
41
45
  viewAsMarkdown: 'Ver como Markdown',
42
- viewAsMarkdownCaption: 'Ver esta página como texto simples'
46
+ viewAsMarkdownCaption: 'Ver esta página como texto simples',
47
+ openInChatGPT: 'Abrir no ChatGPT',
48
+ openInChatGPTCaption: 'Pergunte ao ChatGPT sobre esta página',
49
+ openInClaude: 'Abrir no Claude',
50
+ openInClaudeCaption: 'Pergunte ao Claude sobre esta página'
43
51
  }
44
52
  }
45
53
  }
@@ -36,7 +36,11 @@
36
36
  copyPageCaption: 'Copy page as Markdown for LLMs',
37
37
  copied: 'Copied!',
38
38
  viewAsMarkdown: 'View as Markdown',
39
- viewAsMarkdownCaption: 'View this page as plain text'
39
+ viewAsMarkdownCaption: 'View this page as plain text',
40
+ openInChatGPT: 'Open in ChatGPT',
41
+ openInChatGPTCaption: 'Ask ChatGPT about this page',
42
+ openInClaude: 'Open in Claude',
43
+ openInClaudeCaption: 'Ask Claude about this page'
40
44
  },
41
45
 
42
46
  menu: {
@@ -35,7 +35,11 @@
35
35
  copyPageCaption: 'Copiar página como Markdown para LLMs',
36
36
  copied: 'Copiado!',
37
37
  viewAsMarkdown: 'Ver como Markdown',
38
- viewAsMarkdownCaption: 'Ver esta página como texto simples'
38
+ viewAsMarkdownCaption: 'Ver esta página como texto simples',
39
+ openInChatGPT: 'Abrir no ChatGPT',
40
+ openInChatGPTCaption: 'Pergunte ao ChatGPT sobre esta página',
41
+ openInClaude: 'Abrir no Claude',
42
+ openInClaudeCaption: 'Pergunte ao Claude sobre esta página'
39
43
  },
40
44
 
41
45
  menu: {
@@ -331,6 +331,9 @@ function createMarkdownEndpointPlugin (projectRoot) {
331
331
  return null
332
332
  }
333
333
 
334
+ // LLM bot user-agent patterns
335
+ const LLM_BOT_PATTERN = /GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-User|Claude-SearchBot|anthropic-ai|Google-Extended|Gemini-Deep-Research|PerplexityBot|Perplexity-User|Bytespider|CCBot|Meta-ExternalAgent|FacebookBot|Amazonbot|Applebot-Extended|cohere-ai|DuckAssistBot|GrokBot|AI2Bot|YouBot|PetalBot/i
336
+
334
337
  return {
335
338
  name: 'docsector-markdown-endpoint',
336
339
 
@@ -349,15 +352,35 @@ function createMarkdownEndpointPlugin (projectRoot) {
349
352
 
350
353
  server.middlewares.use((req, res, next) => {
351
354
  const url = new URL(req.url, 'http://localhost')
352
- if (!url.pathname.endsWith('.md')) return next()
353
355
 
354
- const lang = url.searchParams.get('lang') || defaultLang
355
- const file = resolveMarkdownFile(url.pathname, lang)
356
- if (!file) return next()
356
+ // Explicit .md request
357
+ if (url.pathname.endsWith('.md')) {
358
+ const lang = url.searchParams.get('lang') || defaultLang
359
+ const file = resolveMarkdownFile(url.pathname, lang)
360
+ if (!file) return next()
361
+
362
+ const content = readFileSync(file, 'utf-8')
363
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8')
364
+ res.end(content)
365
+ return
366
+ }
367
+
368
+ // Auto-serve markdown to LLM bot crawlers
369
+ const ua = req.headers['user-agent'] || ''
370
+ if (LLM_BOT_PATTERN.test(ua)) {
371
+ const lang = url.searchParams.get('lang') || defaultLang
372
+ // Try appending /overview as the default subpage
373
+ const mdPath = url.pathname.replace(/\/$/, '') + '/overview.md'
374
+ const file = resolveMarkdownFile(mdPath, lang)
375
+ if (file) {
376
+ const content = readFileSync(file, 'utf-8')
377
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8')
378
+ res.end(content)
379
+ return
380
+ }
381
+ }
357
382
 
358
- const content = readFileSync(file, 'utf-8')
359
- res.setHeader('Content-Type', 'text/plain; charset=utf-8')
360
- res.end(content)
383
+ next()
361
384
  })
362
385
  },
363
386