@docsector/docsector-reader 0.4.1 → 0.5.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/index.html CHANGED
@@ -9,6 +9,18 @@
9
9
  <meta name="msapplication-tap-highlight" content="no">
10
10
  <meta name="viewport" content="user-scalable=yes, initial-scale=1, maximum-scale=5, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
11
11
 
12
+ <!-- Open Graph -->
13
+ <meta property="og:title" content="<%= productName %>">
14
+ <meta property="og:description" content="<%= productDescription %>">
15
+ <meta property="og:type" content="website">
16
+ <meta property="og:image" content="">
17
+
18
+ <!-- Twitter Card -->
19
+ <meta name="twitter:card" content="summary">
20
+ <meta name="twitter:title" content="<%= productName %>">
21
+ <meta name="twitter:description" content="<%= productDescription %>">
22
+ <meta name="twitter:image" content="">
23
+
12
24
  <link rel="icon" type="image/png" sizes="128x128" href="images/icons/favicon-128.png">
13
25
  <link rel="icon" type="image/png" sizes="32x32" href="images/icons/favicon-32.png">
14
26
  <link rel="icon" type="image/png" sizes="16x16" href="images/icons/favicon-16.png">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "0.4.1",
3
+ "version": "0.5.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",
@@ -32,6 +32,7 @@ import { ref, computed } from 'vue'
32
32
  import { useRoute, useRouter } from 'vue-router'
33
33
  import { useStore } from 'vuex'
34
34
  import { useI18n } from 'vue-i18n'
35
+ import { useMeta } from 'quasar'
35
36
 
36
37
  import DMenu from '../components/DMenu.vue'
37
38
  import docsectorConfig from 'docsector.config.js'
@@ -43,7 +44,7 @@ const branding = docsectorConfig.branding || {}
43
44
  const route = useRoute()
44
45
  const router = useRouter()
45
46
  const store = useStore()
46
- const { t } = useI18n()
47
+ const { t, locale } = useI18n()
47
48
 
48
49
  const layout = ref({
49
50
  menu: false
@@ -61,6 +62,34 @@ const headerTitleText = computed(() => {
61
62
  }
62
63
  })
63
64
 
65
+ // @ Dynamic page title & meta tags
66
+ const pageTitle = computed(() => {
67
+ const data = route.matched[0]?.meta?.data
68
+ if (data) {
69
+ const langData = data[locale.value] || data['en-US'] || Object.values(data)[0]
70
+ return langData?.title || ''
71
+ }
72
+ return ''
73
+ })
74
+
75
+ useMeta(() => {
76
+ const title = pageTitle.value
77
+ ? `${pageTitle.value} — ${branding.name || ''}`
78
+ : branding.name || ''
79
+
80
+ const image = branding.logo || ''
81
+
82
+ return {
83
+ title,
84
+ meta: {
85
+ ogTitle: { property: 'og:title', content: title },
86
+ ogType: { property: 'og:type', content: 'article' },
87
+ ogImage: { property: 'og:image', content: image },
88
+ twitterImage: { name: 'twitter:image', content: image }
89
+ }
90
+ }
91
+ })
92
+
64
93
  function toogleMenu () {
65
94
  layout.value.menu = !layout.value.menu
66
95
  }
@@ -23,9 +23,10 @@
23
23
  * @param {Function} [options.extendViteConf] - Additional Vite config extension
24
24
  */
25
25
 
26
- import { readFileSync, existsSync, rmSync } from 'fs'
26
+ import { readFileSync, existsSync, rmSync, mkdirSync, writeFileSync } from 'fs'
27
27
  import { createHash } from 'crypto'
28
28
  import { resolve } from 'path'
29
+ import { pathToFileURL } from 'url'
29
30
  import HJSON from 'hjson'
30
31
 
31
32
  /**
@@ -116,6 +117,95 @@ function createHjsonPlugin () {
116
117
  }
117
118
  }
118
119
 
120
+ /**
121
+ * Create a Vite plugin that pre-renders route-specific index.html files at build
122
+ * time with correct <title> and Open Graph / Twitter Card meta tags.
123
+ *
124
+ * Why: SPA builds produce a single index.html with a generic title. Search engine
125
+ * crawlers and social media link previews read the static HTML without executing
126
+ * JavaScript, so they always see the same generic meta tags regardless of the URL.
127
+ *
128
+ * How: After Vite writes the bundle (closeBundle hook), the plugin dynamically
129
+ * imports the consumer's pages registry and docsector config, then for each route
130
+ * creates a directory with an index.html copy whose <title> and meta tags reflect
131
+ * the page's actual title. Cloudflare Pages (and any static host) serves these
132
+ * route-specific files automatically.
133
+ *
134
+ * Zero external dependencies — no Puppeteer or headless browser required.
135
+ */
136
+ function createPrerenderMetaPlugin (projectRoot) {
137
+ return {
138
+ name: 'docsector-prerender-meta',
139
+ apply: 'build',
140
+ async closeBundle () {
141
+ const distDir = resolve(projectRoot, 'dist', 'spa')
142
+ const baseHtmlPath = resolve(distDir, 'index.html')
143
+
144
+ if (!existsSync(baseHtmlPath)) return
145
+
146
+ const baseHtml = readFileSync(baseHtmlPath, 'utf-8')
147
+
148
+ // Dynamic import pages registry and docsector config
149
+ const pagesUrl = pathToFileURL(resolve(projectRoot, 'src', 'pages', 'index.js')).href
150
+ const configUrl = pathToFileURL(resolve(projectRoot, 'docsector.config.js')).href
151
+
152
+ const { default: pages } = await import(pagesUrl)
153
+ const { default: config } = await import(configUrl)
154
+
155
+ const brandingName = config.branding?.name || ''
156
+ const brandingLogo = config.branding?.logo || ''
157
+ const defaultLang = config.defaultLanguage || config.languages?.[0]?.value || 'en-US'
158
+
159
+ let count = 0
160
+
161
+ for (const [pagePath, page] of Object.entries(pages)) {
162
+ if (page.config === null) continue
163
+
164
+ const type = page.config.type ?? 'manual'
165
+ const title = page.data?.[defaultLang]?.title || ''
166
+ const fullTitle = title
167
+ ? `${title} — ${brandingName}`
168
+ : brandingName
169
+
170
+ // Each page can have sub-routes: overview, showcase, vs
171
+ const subpages = ['overview']
172
+ if (page.config.subpages?.showcase) subpages.push('showcase')
173
+ if (page.config.subpages?.vs) subpages.push('vs')
174
+
175
+ for (const subpage of subpages) {
176
+ const routePath = `${type}${pagePath}/${subpage}`
177
+
178
+ const html = baseHtml
179
+ .replace(/<title>[^<]*<\/title>/, () => `<title>${fullTitle}</title>`)
180
+ .replace(
181
+ /(<meta\s+property="?og:title"?\s+content=")[^"]*"/,
182
+ (_, p1) => `${p1}${fullTitle}"`
183
+ )
184
+ .replace(
185
+ /(<meta\s+property="?og:image"?\s+content=")[^"]*"/,
186
+ (_, p1) => `${p1}${brandingLogo}"`
187
+ )
188
+ .replace(
189
+ /(<meta\s+name="?twitter:title"?\s+content=")[^"]*"/,
190
+ (_, p1) => `${p1}${fullTitle}"`
191
+ )
192
+ .replace(
193
+ /(<meta\s+name="?twitter:image"?\s+content=")[^"]*"/,
194
+ (_, p1) => `${p1}${brandingLogo}"`
195
+ )
196
+
197
+ const dir = resolve(distDir, routePath)
198
+ mkdirSync(dir, { recursive: true })
199
+ writeFileSync(resolve(dir, 'index.html'), html)
200
+ count++
201
+ }
202
+ }
203
+
204
+ console.log(`\x1b[36m[docsector]\x1b[0m Pre-rendered meta tags for ${count} routes`)
205
+ }
206
+ }
207
+ }
208
+
119
209
  /**
120
210
  * Create a complete Quasar configuration for a docsector-reader consumer project.
121
211
  *
@@ -187,6 +277,7 @@ export function createQuasarConfig (options = {}) {
187
277
  vitePlugins: [
188
278
  createHjsonPlugin(),
189
279
  createPagesWatchPlugin(projectRoot),
280
+ createPrerenderMetaPlugin(projectRoot),
190
281
  ...vitePlugins
191
282
  ],
192
283
 
@@ -327,7 +418,7 @@ export function createQuasarConfig (options = {}) {
327
418
  config: {},
328
419
  lang: 'en-US',
329
420
  plugins: [
330
- 'LocalStorage', 'SessionStorage'
421
+ 'Meta', 'LocalStorage', 'SessionStorage'
331
422
  ]
332
423
  },
333
424
 
@@ -48,6 +48,7 @@ for (const [path, page] of Object.entries(pages)) {
48
48
  component: () => import('layouts/DefaultLayout.vue'),
49
49
  meta: {
50
50
  ...config,
51
+ data: page.data,
51
52
  type: topPage
52
53
  },
53
54
  children