@docsector/docsector-reader 1.6.0 → 1.7.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
@@ -41,6 +41,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
41
41
  ## ✨ Features
42
42
 
43
43
  - 📝 **Markdown Rendering** — Write docs in Markdown, rendered with syntax highlighting (Prism.js)
44
+ - 🧱 **Raw HTML in Markdown** — Renders inline and block HTML tags inside markdown sections (including homepage remote README content)
44
45
  - 🧩 **Mermaid Diagrams** — Native support for fenced ` ```mermaid ` blocks, with automatic dark/light theme switching
45
46
  - 🚨 **GitHub-Style Alerts** — Native support for `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, and `[!CAUTION]`
46
47
  - 🌍 **Internationalization (i18n)** — Multi-language support with HJSON locale files and per-page translations
@@ -58,6 +59,8 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
58
59
  - 🔐 **Web Bot Auth** — Can publish a signed HTTP message signatures directory and includes helpers to sign outbound bot requests
59
60
  - 🧭 **Content Signals** — Injects `Content-Signal` policy in `robots.txt` with deterministic, idempotent build output
60
61
  - 🏠 **Markdown Home at Root** — Homepage is rendered from `src/pages/Homepage.{lang}.md` directly at `/`
62
+ - 🌍 **Remote README as Home** — Optional build-time remote README source for homepage with automatic local fallback
63
+ - 🧬 **Scaffolded Homepage Override Wiring** — New consumer projects automatically wire `virtual:docsector-homepage-override` into i18n message building
61
64
  - 🧭 **Quick Links Custom Element** — Use `<d-quick-links>` and `<d-quick-link>` in Markdown to render rich home navigation cards
62
65
  - 🗂️ **API Catalog Well-Known** — Auto-generates `/.well-known/api-catalog` as Linkset JSON for machine-readable API discovery
63
66
  - ⚙️ **Single Config File** — Customize branding, links, and languages via `docsector.config.js`
@@ -333,6 +336,39 @@ Set any target to `null` or `false` to disable that relation.
333
336
 
334
337
  ---
335
338
 
339
+ ## 🏠 Remote README as Home
340
+
341
+ You can configure Docsector Reader to use a remote README as homepage content.
342
+
343
+ - Fetch happens at build-time.
344
+ - The same README content is used for all configured languages.
345
+ - If fetch fails, it falls back to local `src/pages/Homepage.{lang}.md` by default.
346
+
347
+ ### Configure
348
+
349
+ ```javascript
350
+ export default {
351
+ // ...other config
352
+
353
+ homePage: {
354
+ source: 'remote-readme',
355
+ remoteReadmeUrl: 'https://raw.githubusercontent.com/your-org/your-repo/main/README.md',
356
+ timeoutMs: 8000,
357
+ fallbackToLocal: true
358
+ }
359
+ }
360
+ ```
361
+
362
+ ### Validate
363
+
364
+ ```bash
365
+ npx docsector build
366
+ cat dist/spa/homepage.md
367
+ cat dist/spa/homepage.en-US.md
368
+ ```
369
+
370
+ ---
371
+
336
372
  ## 🔐 Web Bot Auth
337
373
 
338
374
  Docsector Reader can publish a signed Web Bot Auth directory at:
@@ -733,6 +769,7 @@ Consumer projects use the `buildMessages` helper from the engine:
733
769
 
734
770
  ```javascript
735
771
  import { buildMessages } from '@docsector/docsector-reader/i18n'
772
+ import homePageOverride from 'virtual:docsector-homepage-override'
736
773
 
737
774
  const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
738
775
  const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
@@ -740,7 +777,7 @@ const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?r
740
777
  import boot from 'pages/boot'
741
778
  import pages from 'pages'
742
779
 
743
- export default buildMessages({ langModules, mdModules, pages, boot })
780
+ export default buildMessages({ langModules, mdModules, pages, boot, homePageOverride })
744
781
  ```
745
782
 
746
783
  ### Language files
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.6.0'
26
+ const VERSION = '1.7.1'
27
27
 
28
28
  const HELP = `
29
29
  Docsector Reader v${VERSION}
@@ -165,6 +165,16 @@ export default {
165
165
  // }
166
166
  // },
167
167
 
168
+ // @ Home page source (optional)
169
+ // Use a remote README.md as homepage content at build-time.
170
+ // Falls back to local src/pages/Homepage.{lang}.md on fetch failure by default.
171
+ // homePage: {
172
+ // source: 'remote-readme', // 'local' | 'remote-readme'
173
+ // remoteReadmeUrl: 'https://raw.githubusercontent.com/your-org/your-repo/main/README.md',
174
+ // timeoutMs: 8000,
175
+ // fallbackToLocal: true
176
+ // },
177
+
168
178
  // @ Homepage Link headers for agent discovery (optional)
169
179
  // linkHeaders: {
170
180
  // enabled: true,
@@ -254,6 +264,7 @@ const TEMPLATE_CSS_STUB = `\
254
264
  const TEMPLATE_I18N_INDEX = `\
255
265
  // @ Import i18n message builder from Docsector Reader
256
266
  import { buildMessages } from '@docsector/docsector-reader/i18n'
267
+ import homePageOverride from 'virtual:docsector-homepage-override'
257
268
 
258
269
  // @ Import language HJSON files (Vite-compatible eager import)
259
270
  const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
@@ -264,7 +275,7 @@ const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?r
264
275
  import boot from 'pages/boot'
265
276
  import pages from 'pages'
266
277
 
267
- export default buildMessages({ langModules, mdModules, pages, boot })
278
+ export default buildMessages({ langModules, mdModules, pages, boot, homePageOverride })
268
279
  `
269
280
 
270
281
  const TEMPLATE_I18N_HJSON = `\
@@ -43,6 +43,14 @@ export default {
43
43
  toolSuffix: 'docsector'
44
44
  },
45
45
 
46
+ // @ Home page source
47
+ homePage: {
48
+ source: 'remote-readme',
49
+ remoteReadmeUrl: 'https://raw.githubusercontent.com/docsector/docsector-reader/main/README.md',
50
+ timeoutMs: 8000,
51
+ fallbackToLocal: true
52
+ },
53
+
46
54
  // @ Languages
47
55
  languages: [
48
56
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "1.6.0",
3
+ "version": "1.7.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",
@@ -71,6 +71,13 @@ const rawMarkdown = computed(() => {
71
71
 
72
72
  const markdownURL = computed(() => {
73
73
  if (store.state.page.base === 'home') {
74
+ const homePage = docsectorConfig.homePage || {}
75
+ const isRemoteHome = homePage.source === 'remote-readme' && typeof homePage.remoteReadmeUrl === 'string' && homePage.remoteReadmeUrl.length > 0
76
+
77
+ if (isRemoteHome) {
78
+ return homePage.remoteReadmeUrl
79
+ }
80
+
74
81
  return `/Homepage.${locale.value}.md`
75
82
  }
76
83
 
@@ -87,12 +94,27 @@ const fullMarkdownURL = computed(() => {
87
94
  return `${window.location.origin}${path}.md`
88
95
  })
89
96
 
97
+ const chatSourceURL = computed(() => {
98
+ if (store.state.page.base !== 'home') {
99
+ return fullMarkdownURL.value
100
+ }
101
+
102
+ const homePage = docsectorConfig.homePage || {}
103
+ const isRemoteHome = homePage.source === 'remote-readme' && typeof homePage.remoteReadmeUrl === 'string' && homePage.remoteReadmeUrl.length > 0
104
+
105
+ if (isRemoteHome) {
106
+ return `${window.location.origin}/`
107
+ }
108
+
109
+ return fullMarkdownURL.value
110
+ })
111
+
90
112
  const chatgptURL = computed(() => {
91
- const prompt = `Read ${fullMarkdownURL.value} and answer questions about the content.`
113
+ const prompt = `Read ${chatSourceURL.value} and answer questions about the content.`
92
114
  return `https://chat.openai.com/?q=${encodeURIComponent(prompt)}`
93
115
  })
94
116
  const claudeURL = computed(() => {
95
- const prompt = `Read ${fullMarkdownURL.value} and answer questions about the content.`
117
+ const prompt = `Read ${chatSourceURL.value} and answer questions about the content.`
96
118
  return `https://claude.ai/new?q=${encodeURIComponent(prompt)}`
97
119
  })
98
120
 
@@ -157,11 +157,19 @@ const next = computed(() => {
157
157
 
158
158
  return ''
159
159
  })
160
+
161
+ const hideRemoteHomeFooterMeta = computed(() => {
162
+ const homePage = docsectorConfig.homePage || {}
163
+ const isRemoteHome = homePage.source === 'remote-readme' && typeof homePage.remoteReadmeUrl === 'string' && homePage.remoteReadmeUrl.length > 0
164
+ const isHomePage = route.path === '/' || store.state.page.base === 'home'
165
+
166
+ return isRemoteHome && isHomePage
167
+ })
160
168
  </script>
161
169
 
162
170
  <template>
163
171
  <div id="d-page-meta">
164
- <div class="row justify-between q-mt-lg">
172
+ <div v-if="!hideRemoteHomeFooterMeta" class="row justify-between q-mt-lg">
165
173
  <div id="d-page-edit" class="col">
166
174
  <q-btn dense no-caps text-color="black" :color="color" @click="openURL(URL)" aria-label="Edit page on Github">
167
175
  <q-icon class="q-mr-xs" name="fab fa-github" size="20px" />
@@ -130,16 +130,21 @@ const tokenized = computed(() => {
130
130
 
131
131
  const { source: sourceWithQuickLinks, quickLinksMap } = extractQuickLinksBlocks(normalizedSource)
132
132
 
133
- const Markdown = new MarkdownIt()
133
+ const Markdown = new MarkdownIt({
134
+ // Home remote README may contain raw HTML blocks (badges, centered headers, etc.)
135
+ html: true
136
+ })
134
137
  Markdown.use(attrs, {
135
138
  leftDelimiter: ':',
136
139
  rightDelimiter: ';',
137
140
  allowedAttributes: ['filename']
138
141
  })
139
142
 
140
- // Use a plain inline renderer to avoid markdown-it-attrs edge cases
141
- // when rendering isolated inline fragments.
142
- const MarkdownInline = new MarkdownIt()
143
+ // Keep inline rendering aligned with block parsing so raw HTML inline
144
+ // fragments (e.g. <b>, <a>) are rendered instead of escaped.
145
+ const MarkdownInline = new MarkdownIt({
146
+ html: true
147
+ })
143
148
 
144
149
  const markdownEnv = {}
145
150
 
@@ -323,6 +328,12 @@ const tokenized = computed(() => {
323
328
  })
324
329
  break
325
330
  }
331
+ case 'html_block':
332
+ tokens.push({
333
+ tag: 'html',
334
+ content: element.content
335
+ })
336
+ break
326
337
  }
327
338
  } else if (level === 1) {
328
339
  const parent = tokens[tokens.length - 1]
@@ -370,6 +381,10 @@ const tokenized = computed(() => {
370
381
  case 'inline':
371
382
  parent.content += element.content
372
383
  break
384
+ case 'html_inline':
385
+ case 'html_block':
386
+ parent.content += element.content
387
+ break
373
388
 
374
389
  case 'list_item_close':
375
390
  parent.content += '</li>'
@@ -450,6 +465,11 @@ const tokenized = computed(() => {
450
465
  <table v-html="token.content"></table>
451
466
  </div>
452
467
 
468
+ <div
469
+ v-else-if="token.tag === 'html'"
470
+ v-html="token.content"
471
+ ></div>
472
+
453
473
  <p
454
474
  v-else-if="token.tag === 'p'"
455
475
  v-html="token.content"
package/src/css/app.sass CHANGED
@@ -1,7 +1,7 @@
1
1
  /* --- Docsector Reader --- */
2
2
  @font-face
3
3
  font-family: "Fira Code Nerd Font"
4
- src: url('/fonts/FiraCodeNerdFont-Regular.ttf') format('truetype')
4
+ src: local('Fira Code Nerd Font'), local('FiraCode Nerd Font')
5
5
  font-weight: normal
6
6
  font-style: normal
7
7
  // * General
@@ -117,9 +117,10 @@ export function filter (source) {
117
117
  * @param {Object} options.pages - Page registry from pages/index.js
118
118
  * @param {Object} options.boot - Boot meta from pages/boot.js
119
119
  * @param {string[]} [options.langs] - Language codes to process (auto-detected from langModules if omitted)
120
+ * @param {Object<string,string>} [options.homePageOverride] - Optional per-language Home markdown override
120
121
  * @returns {Object} Complete i18n messages object keyed by locale
121
122
  */
122
- export function buildMessages ({ langModules, mdModules, pages, boot, langs }) {
123
+ export function buildMessages ({ langModules, mdModules, pages, boot, langs, homePageOverride = {} }) {
123
124
  // Auto-detect languages from HJSON files if not provided
124
125
  if (!langs) {
125
126
  langs = Object.keys(langModules).map(key => {
@@ -145,6 +146,11 @@ export function buildMessages ({ langModules, mdModules, pages, boot, langs }) {
145
146
  }
146
147
 
147
148
  function loadHomepage (lang) {
149
+ const override = homePageOverride?.[lang] ?? homePageOverride?.['en-US']
150
+ if (typeof override === 'string' && override.length > 0) {
151
+ return filter(override)
152
+ }
153
+
148
154
  const key = `../pages/Homepage.${lang}.md`
149
155
  const fallbackKey = '../pages/Homepage.en-US.md'
150
156
 
@@ -159,6 +165,23 @@ export function buildMessages ({ langModules, mdModules, pages, boot, langs }) {
159
165
  }
160
166
 
161
167
  function extractHeadingFromHomepage (lang) {
168
+ const override = homePageOverride?.[lang] ?? homePageOverride?.['en-US']
169
+ if (typeof override === 'string' && override.length > 0) {
170
+ const htmlHeadingMatch = override.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i)
171
+ if (htmlHeadingMatch) {
172
+ const htmlHeading = htmlHeadingMatch[1]
173
+ .replace(/<[^>]+>/g, ' ')
174
+ .replace(/\s+/g, ' ')
175
+ .trim()
176
+ if (htmlHeading) {
177
+ return htmlHeading
178
+ }
179
+ }
180
+
181
+ const overrideMatch = override.match(/^#\s+(.+)$/m)
182
+ return overrideMatch ? overrideMatch[1].trim() : ''
183
+ }
184
+
162
185
  const key = `../pages/Homepage.${lang}.md`
163
186
  const fallbackKey = '../pages/Homepage.en-US.md'
164
187
 
package/src/i18n/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // @ Import i18n message builder
2
2
  import { buildMessages } from './helpers'
3
+ import homePageOverride from 'virtual:docsector-homepage-override'
3
4
 
4
5
  // @ Import language HJSON files (Vite-compatible eager import)
5
6
  const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
@@ -10,4 +11,4 @@ const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?r
10
11
  import boot from 'pages/boot'
11
12
  import pages from 'pages'
12
13
 
13
- export default buildMessages({ langModules, mdModules, pages, boot })
14
+ export default buildMessages({ langModules, mdModules, pages, boot, homePageOverride })
package/src/index.js CHANGED
@@ -98,6 +98,11 @@
98
98
  * @param {boolean} [config.webMcp.tools.getPage=true] - Enables tool get_page
99
99
  * @param {boolean} [config.webMcp.tools.navigateTo=true] - Enables tool navigate_to
100
100
  * @param {boolean} [config.webMcp.tools.copyCurrentPage=true] - Enables tool copy_current_page
101
+ * @param {Object} [config.homePage] - Home page content source settings
102
+ * @param {'local'|'remote-readme'} [config.homePage.source='local'] - Source strategy for home page markdown
103
+ * @param {string|null} [config.homePage.remoteReadmeUrl=null] - Absolute URL of remote README markdown when source is remote-readme
104
+ * @param {number} [config.homePage.timeoutMs=8000] - Timeout in milliseconds for remote README fetch during build
105
+ * @param {boolean} [config.homePage.fallbackToLocal=true] - Fallback to local Homepage.{lang}.md when remote fetch fails
101
106
  * @returns {Object} Resolved Docsector configuration
102
107
  */
103
108
  export function createDocsector (config = {}) {
@@ -217,6 +222,14 @@ export function createDocsector (config = {}) {
217
222
  copyCurrentPage: true,
218
223
  ...(config.webMcp?.tools || {})
219
224
  }
225
+ },
226
+
227
+ homePage: {
228
+ source: 'local',
229
+ remoteReadmeUrl: null,
230
+ timeoutMs: 8000,
231
+ fallbackToLocal: true,
232
+ ...config.homePage
220
233
  }
221
234
  }
222
235
  }
@@ -141,7 +141,6 @@ function createPrerenderMetaPlugin (projectRoot) {
141
141
  async closeBundle () {
142
142
  const distDir = resolve(projectRoot, 'dist', 'spa')
143
143
  const baseHtmlPath = resolve(distDir, 'index.html')
144
-
145
144
  if (!existsSync(baseHtmlPath)) return
146
145
 
147
146
  const baseHtml = readFileSync(baseHtmlPath, 'utf-8')
@@ -421,6 +420,148 @@ function createGitDatesPlugin (projectRoot) {
421
420
  }
422
421
  }
423
422
 
423
+ function getHomePageConfig (config = {}) {
424
+ const homePage = config.homePage || {}
425
+ return {
426
+ source: homePage.source || 'local',
427
+ remoteReadmeUrl: homePage.remoteReadmeUrl || null,
428
+ timeoutMs: Number.isFinite(homePage.timeoutMs)
429
+ ? Math.max(1000, Number(homePage.timeoutMs))
430
+ : 8000,
431
+ fallbackToLocal: homePage.fallbackToLocal !== false
432
+ }
433
+ }
434
+
435
+ function getConfiguredLanguages (config = {}) {
436
+ const defaultLang = config.defaultLanguage || config.languages?.[0]?.value || 'en-US'
437
+ const languageValues = config.languages?.map(language => language.value).filter(Boolean) || []
438
+ const langs = [...new Set([defaultLang, ...languageValues])]
439
+ return { defaultLang, langs }
440
+ }
441
+
442
+ async function fetchRemoteMarkdown (url, timeoutMs = 8000) {
443
+ const controller = new AbortController()
444
+ const timeout = setTimeout(() => controller.abort(), timeoutMs)
445
+
446
+ try {
447
+ const response = await fetch(url, {
448
+ headers: {
449
+ Accept: 'text/markdown, text/plain;q=0.9, */*;q=0.8'
450
+ },
451
+ signal: controller.signal
452
+ })
453
+
454
+ if (!response.ok) {
455
+ throw new Error(`Remote README request failed with status ${response.status}`)
456
+ }
457
+
458
+ return await response.text()
459
+ } finally {
460
+ clearTimeout(timeout)
461
+ }
462
+ }
463
+
464
+ async function resolveHomePageSources (projectRoot, config = {}, options = {}) {
465
+ const { logPrefix = '[docsector]' } = options
466
+ const pagesDir = resolve(projectRoot, 'src', 'pages')
467
+ const { defaultLang, langs } = getConfiguredLanguages(config)
468
+ const homePageConfig = getHomePageConfig(config)
469
+
470
+ const byLang = {}
471
+ let mode = 'local'
472
+
473
+ if (homePageConfig.source === 'remote-readme' && homePageConfig.remoteReadmeUrl) {
474
+ try {
475
+ const remote = await fetchRemoteMarkdown(homePageConfig.remoteReadmeUrl, homePageConfig.timeoutMs)
476
+ for (const lang of langs) {
477
+ byLang[lang] = remote
478
+ }
479
+ mode = 'remote-readme'
480
+ console.log(`\x1b[36m${logPrefix}\x1b[0m Loaded remote README for home page`)
481
+ return { mode, byLang, defaultLang, langs }
482
+ } catch (error) {
483
+ const reason = error?.message || String(error)
484
+ console.warn(`${logPrefix} Failed to load remote README for home page: ${reason}`)
485
+
486
+ if (!homePageConfig.fallbackToLocal) {
487
+ throw error
488
+ }
489
+ }
490
+ }
491
+
492
+ for (const lang of langs) {
493
+ const homepage = resolve(pagesDir, `Homepage.${lang}.md`)
494
+ if (existsSync(homepage)) {
495
+ byLang[lang] = readFileSync(homepage, 'utf-8')
496
+ continue
497
+ }
498
+
499
+ const fallback = resolve(pagesDir, `Homepage.${defaultLang}.md`)
500
+ if (existsSync(fallback)) {
501
+ byLang[lang] = readFileSync(fallback, 'utf-8')
502
+ }
503
+ }
504
+
505
+ return { mode, byLang, defaultLang, langs }
506
+ }
507
+
508
+ function createHomePageOverridePlugin (projectRoot) {
509
+ const virtualId = 'virtual:docsector-homepage-override'
510
+ const resolvedId = '\0' + virtualId
511
+ let byLang = null
512
+ let loadPromise = null
513
+
514
+ const ensureSources = async () => {
515
+ if (byLang) return byLang
516
+ if (!loadPromise) {
517
+ loadPromise = (async () => {
518
+ const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
519
+ const { default: config } = await import(configUrl)
520
+ const sources = await resolveHomePageSources(projectRoot, config, { logPrefix: '[docsector]' })
521
+ byLang = sources.byLang
522
+ return byLang
523
+ })().finally(() => {
524
+ loadPromise = null
525
+ })
526
+ }
527
+
528
+ return loadPromise
529
+ }
530
+
531
+ return {
532
+ name: 'docsector-homepage-override',
533
+ resolveId (id) {
534
+ if (id === virtualId) return resolvedId
535
+ },
536
+ async buildStart () {
537
+ await ensureSources()
538
+ },
539
+ configureServer () {
540
+ ensureSources().catch((error) => {
541
+ console.warn(`[docsector] Could not prepare home page override: ${error?.message || String(error)}`)
542
+ })
543
+ },
544
+ async load (id) {
545
+ if (id === resolvedId) {
546
+ await ensureSources()
547
+ return `export default ${JSON.stringify(byLang || {})}`
548
+ }
549
+
550
+ await ensureSources()
551
+ if (!byLang) return null
552
+
553
+ const match = id.match(/Homepage\.([A-Za-z0-9-]+)\.md\?raw(?:$|&)/)
554
+ if (!match) return null
555
+
556
+ const lang = match[1]
557
+ const content = byLang[lang]
558
+ if (typeof content !== 'string') return null
559
+
560
+ return `export default ${JSON.stringify(content)}`
561
+ }
562
+ }
563
+ }
564
+
424
565
  /**
425
566
  * Create a Vite plugin that serves raw Markdown content for `.md` suffixed URLs.
426
567
  *
@@ -496,32 +637,63 @@ function createMarkdownEndpointPlugin (projectRoot) {
496
637
  name: 'docsector-markdown-endpoint',
497
638
 
498
639
  configureServer (server) {
499
- // Read default language from config
500
640
  let defaultLang = 'en-US'
501
641
  let markdownNegotiationEnabled = true
502
642
  let markdownAgentFallback = true
503
- try {
504
- const configPath = resolve(projectRoot, 'docsector.config.js')
505
- if (existsSync(configPath)) {
506
- // Dynamic import in dev — we read it synchronously via a simple approach
507
- const configContent = readFileSync(configPath, 'utf-8')
508
- const match = configContent.match(/defaultLanguage\s*:\s*['"]([^'"]+)['"]/)
509
- if (match) defaultLang = match[1]
510
-
511
- const enabledMatch = configContent.match(/markdownNegotiation\s*:\s*\{[\s\S]*?enabled\s*:\s*(true|false)/)
512
- if (enabledMatch) markdownNegotiationEnabled = enabledMatch[1] === 'true'
513
-
514
- const fallbackMatch = configContent.match(/markdownNegotiation\s*:\s*\{[\s\S]*?agentFallback\s*:\s*(true|false)/)
515
- if (fallbackMatch) markdownAgentFallback = fallbackMatch[1] === 'true'
643
+ let homepageByLang = null
644
+
645
+ const configReady = (async () => {
646
+ try {
647
+ const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
648
+ const { default: config } = await import(configUrl)
649
+
650
+ defaultLang = config.defaultLanguage || config.languages?.[0]?.value || 'en-US'
651
+ const markdownNegotiationConfig = config.markdownNegotiation || {}
652
+ markdownNegotiationEnabled = markdownNegotiationConfig.enabled !== false
653
+ markdownAgentFallback = markdownNegotiationConfig.agentFallback !== false
654
+
655
+ const sources = await resolveHomePageSources(projectRoot, config, { logPrefix: '[docsector]' })
656
+ homepageByLang = sources.byLang
657
+ } catch (error) {
658
+ console.warn(`[docsector] Could not load config for markdown endpoint: ${error?.message || String(error)}`)
516
659
  }
517
- } catch { /* use fallback */ }
660
+ })()
661
+
662
+ server.middlewares.use(async (req, res, next) => {
663
+ await configReady
518
664
 
519
- server.middlewares.use((req, res, next) => {
520
665
  const url = new URL(req.url, 'http://localhost')
521
666
  const accept = (req.headers.accept || '').toLowerCase()
522
667
  const wantsMarkdown = accept.includes('text/markdown')
523
668
  const lang = url.searchParams.get('lang') || defaultLang
524
669
 
670
+ const homepagePath = url.pathname === '/' || url.pathname === '/index.html'
671
+ const remoteHomepage = homepageByLang?.[lang] || homepageByLang?.[defaultLang] || null
672
+
673
+ const homepageMarkdownMatch = url.pathname.match(/^\/homepage(?:\.([A-Za-z0-9-]+))?\.md$/i)
674
+ if (homepageMarkdownMatch) {
675
+ const requestedLang = homepageMarkdownMatch[1] || lang
676
+ const homepageMarkdown = homepageByLang?.[requestedLang] || homepageByLang?.[defaultLang] || null
677
+
678
+ if (typeof homepageMarkdown === 'string' && homepageMarkdown.length > 0) {
679
+ res.setHeader('Content-Type', 'text/markdown; charset=utf-8')
680
+ res.setHeader('Vary', 'Accept')
681
+ res.setHeader('x-markdown-tokens', String(estimateMarkdownTokens(homepageMarkdown)))
682
+ res.end(homepageMarkdown)
683
+ return
684
+ }
685
+ }
686
+
687
+ if (homepagePath && typeof remoteHomepage === 'string' && remoteHomepage.length > 0) {
688
+ if ((markdownNegotiationEnabled && wantsMarkdown) || (markdownAgentFallback && LLM_BOT_PATTERN.test(req.headers['user-agent'] || ''))) {
689
+ res.setHeader('Content-Type', 'text/markdown; charset=utf-8')
690
+ res.setHeader('Vary', 'Accept')
691
+ res.setHeader('x-markdown-tokens', String(estimateMarkdownTokens(remoteHomepage)))
692
+ res.end(remoteHomepage)
693
+ return
694
+ }
695
+ }
696
+
525
697
  // Explicit .md request
526
698
  if (url.pathname.endsWith('.md')) {
527
699
  const file = resolveMarkdownFile(url.pathname, lang)
@@ -620,16 +792,14 @@ function createMarkdownBuildPlugin (projectRoot) {
620
792
  }
621
793
 
622
794
  // Generate homepage markdown files so root content can be negotiated in production
623
- const languageValues = config.languages?.map(language => language.value).filter(Boolean) || []
624
- const allLangs = [...new Set([defaultLang, ...languageValues])]
795
+ const homepageSources = await resolveHomePageSources(projectRoot, config, { logPrefix: '[docsector]' })
625
796
  let homepageCount = 0
626
- for (const lang of allLangs) {
627
- const homepageSrc = resolve(pagesDir, `Homepage.${lang}.md`)
628
- if (!existsSync(homepageSrc)) continue
797
+ for (const lang of homepageSources.langs) {
798
+ const homepageContent = homepageSources.byLang?.[lang]
799
+ if (typeof homepageContent !== 'string' || homepageContent.length === 0) continue
629
800
 
630
- const homepageContent = readFileSync(homepageSrc, 'utf-8')
631
801
  writeFileSync(resolve(distDir, `homepage.${lang}.md`), homepageContent)
632
- if (lang === defaultLang) {
802
+ if (lang === homepageSources.defaultLang) {
633
803
  writeFileSync(resolve(distDir, 'homepage.md'), homepageContent)
634
804
  }
635
805
  homepageCount++
@@ -1705,6 +1875,7 @@ export function createQuasarConfig (options = {}) {
1705
1875
 
1706
1876
  vitePlugins: [
1707
1877
  createHjsonPlugin(),
1878
+ createHomePageOverridePlugin(projectRoot),
1708
1879
  createGitDatesPlugin(projectRoot),
1709
1880
  createMarkdownEndpointPlugin(projectRoot),
1710
1881
  createMarkdownBuildPlugin(projectRoot),