@life-and-dev/mdsite 0.0.3

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 (133) hide show
  1. package/README.md +228 -0
  2. package/bin/mdsite.js +3 -0
  3. package/dist/commands/commands.test.d.ts +1 -0
  4. package/dist/commands/commands.test.js +302 -0
  5. package/dist/commands/commands.test.js.map +1 -0
  6. package/dist/commands/generate.d.ts +1 -0
  7. package/dist/commands/generate.js +20 -0
  8. package/dist/commands/generate.js.map +1 -0
  9. package/dist/commands/init.d.ts +1 -0
  10. package/dist/commands/init.js +16 -0
  11. package/dist/commands/init.js.map +1 -0
  12. package/dist/commands/prepare.d.ts +1 -0
  13. package/dist/commands/prepare.js +111 -0
  14. package/dist/commands/prepare.js.map +1 -0
  15. package/dist/commands/prepare.test.d.ts +1 -0
  16. package/dist/commands/prepare.test.js +131 -0
  17. package/dist/commands/prepare.test.js.map +1 -0
  18. package/dist/commands/preview.d.ts +5 -0
  19. package/dist/commands/preview.js +62 -0
  20. package/dist/commands/preview.js.map +1 -0
  21. package/dist/commands/start.d.ts +5 -0
  22. package/dist/commands/start.js +50 -0
  23. package/dist/commands/start.js.map +1 -0
  24. package/dist/commands/stop.d.ts +1 -0
  25. package/dist/commands/stop.js +26 -0
  26. package/dist/commands/stop.js.map +1 -0
  27. package/dist/commands/workflows.test.d.ts +1 -0
  28. package/dist/commands/workflows.test.js +183 -0
  29. package/dist/commands/workflows.test.js.map +1 -0
  30. package/dist/config/default-mdsite-config.d.ts +2 -0
  31. package/dist/config/default-mdsite-config.js +82 -0
  32. package/dist/config/default-mdsite-config.js.map +1 -0
  33. package/dist/config/default-mdsite-config.test.d.ts +1 -0
  34. package/dist/config/default-mdsite-config.test.js +42 -0
  35. package/dist/config/default-mdsite-config.test.js.map +1 -0
  36. package/dist/config/mdsite-config.d.ts +42 -0
  37. package/dist/config/mdsite-config.js +74 -0
  38. package/dist/config/mdsite-config.js.map +1 -0
  39. package/dist/config/mdsite-config.test.d.ts +1 -0
  40. package/dist/config/mdsite-config.test.js +115 -0
  41. package/dist/config/mdsite-config.test.js.map +1 -0
  42. package/dist/config/menu.d.ts +3 -0
  43. package/dist/config/menu.js +58 -0
  44. package/dist/config/menu.js.map +1 -0
  45. package/dist/config/menu.test.d.ts +1 -0
  46. package/dist/config/menu.test.js +53 -0
  47. package/dist/config/menu.test.js.map +1 -0
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +99 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/index.test.d.ts +1 -0
  52. package/dist/index.test.js +133 -0
  53. package/dist/index.test.js.map +1 -0
  54. package/dist/process/child-process.d.ts +9 -0
  55. package/dist/process/child-process.js +142 -0
  56. package/dist/process/child-process.js.map +1 -0
  57. package/dist/process/child-process.test.d.ts +1 -0
  58. package/dist/process/child-process.test.js +131 -0
  59. package/dist/process/child-process.test.js.map +1 -0
  60. package/dist/process/runtime-state.d.ts +16 -0
  61. package/dist/process/runtime-state.js +42 -0
  62. package/dist/process/runtime-state.js.map +1 -0
  63. package/dist/process/runtime-state.test.d.ts +1 -0
  64. package/dist/process/runtime-state.test.js +57 -0
  65. package/dist/process/runtime-state.test.js.map +1 -0
  66. package/dist/renderer/mdsite-nuxt.d.ts +22 -0
  67. package/dist/renderer/mdsite-nuxt.js +194 -0
  68. package/dist/renderer/mdsite-nuxt.js.map +1 -0
  69. package/dist/renderer/mdsite-nuxt.test.d.ts +1 -0
  70. package/dist/renderer/mdsite-nuxt.test.js +226 -0
  71. package/dist/renderer/mdsite-nuxt.test.js.map +1 -0
  72. package/mdsite-nuxt/app/app.vue +7 -0
  73. package/mdsite-nuxt/app/assets/css/bible-tooltips.css +87 -0
  74. package/mdsite-nuxt/app/assets/css/markdown.css +195 -0
  75. package/mdsite-nuxt/app/assets/css/print.css +133 -0
  76. package/mdsite-nuxt/app/components/AppBar.vue +138 -0
  77. package/mdsite-nuxt/app/components/AppFooter.vue +88 -0
  78. package/mdsite-nuxt/app/components/AppNavigation.vue +162 -0
  79. package/mdsite-nuxt/app/components/AppTableOfContents.vue +149 -0
  80. package/mdsite-nuxt/app/components/BreadcrumbNav.vue +139 -0
  81. package/mdsite-nuxt/app/components/NavigationTree.vue +169 -0
  82. package/mdsite-nuxt/app/components/SearchBox.vue +212 -0
  83. package/mdsite-nuxt/app/components/TocItem.vue +75 -0
  84. package/mdsite-nuxt/app/components/TreeNode.vue +264 -0
  85. package/mdsite-nuxt/app/components/content/MarkdownAlert.vue +42 -0
  86. package/mdsite-nuxt/app/components/content/Mermaid.vue +103 -0
  87. package/mdsite-nuxt/app/components/content/ProseA.vue +57 -0
  88. package/mdsite-nuxt/app/components/content/ProseBlockquote.vue +14 -0
  89. package/mdsite-nuxt/app/components/content/ProsePre.vue +49 -0
  90. package/mdsite-nuxt/app/components/content/ProseTable.vue +143 -0
  91. package/mdsite-nuxt/app/composables/useAppTheme.ts +89 -0
  92. package/mdsite-nuxt/app/composables/useBibleTooltips.ts +21 -0
  93. package/mdsite-nuxt/app/composables/useBreadcrumbs.ts +70 -0
  94. package/mdsite-nuxt/app/composables/useContentPostProcessing.ts +89 -0
  95. package/mdsite-nuxt/app/composables/useNavigationTree.ts +174 -0
  96. package/mdsite-nuxt/app/composables/useSearchIndex.ts +47 -0
  97. package/mdsite-nuxt/app/composables/useSearchRelevance.test.ts +255 -0
  98. package/mdsite-nuxt/app/composables/useSearchRelevance.ts +207 -0
  99. package/mdsite-nuxt/app/composables/useSidebarState.ts +62 -0
  100. package/mdsite-nuxt/app/composables/useSiteConfig.ts +37 -0
  101. package/mdsite-nuxt/app/composables/useSourceEdit.ts +69 -0
  102. package/mdsite-nuxt/app/composables/useTableOfContents.ts +141 -0
  103. package/mdsite-nuxt/app/composables/useTableParser.ts +106 -0
  104. package/mdsite-nuxt/app/composables/useTooltipConfig.ts +20 -0
  105. package/mdsite-nuxt/app/config/themes.ts +17 -0
  106. package/mdsite-nuxt/app/layouts/default.vue +376 -0
  107. package/mdsite-nuxt/app/pages/[...slug].vue +56 -0
  108. package/mdsite-nuxt/app/pages/index.vue +54 -0
  109. package/mdsite-nuxt/app/plugins/bible-tooltips.client.ts +401 -0
  110. package/mdsite-nuxt/app/plugins/bible-tooltips.test.ts +284 -0
  111. package/mdsite-nuxt/app/types/content.d.ts +10 -0
  112. package/mdsite-nuxt/app/types/table.ts +57 -0
  113. package/mdsite-nuxt/app/utils/bible-book-names.ts +37 -0
  114. package/mdsite-nuxt/app/utils/bible-verse-utils.test.ts +148 -0
  115. package/mdsite-nuxt/app/utils/bible-verse-utils.ts +236 -0
  116. package/mdsite-nuxt/content.config.ts +18 -0
  117. package/mdsite-nuxt/example.config.yml +67 -0
  118. package/mdsite-nuxt/nuxt.config.ts +247 -0
  119. package/mdsite-nuxt/package-lock.json +15922 -0
  120. package/mdsite-nuxt/package.json +50 -0
  121. package/mdsite-nuxt/playwright.config.js +47 -0
  122. package/mdsite-nuxt/public/robots.txt +2 -0
  123. package/mdsite-nuxt/scripts/generate-favicons.ts +192 -0
  124. package/mdsite-nuxt/scripts/generate-indices.ts +508 -0
  125. package/mdsite-nuxt/scripts/renderer-hooks.test.ts +277 -0
  126. package/mdsite-nuxt/scripts/renderer-hooks.ts +141 -0
  127. package/mdsite-nuxt/scripts/start.test.ts +164 -0
  128. package/mdsite-nuxt/scripts/start.ts +55 -0
  129. package/mdsite-nuxt/scripts/sync-content.ts +504 -0
  130. package/mdsite-nuxt/tsconfig.json +18 -0
  131. package/mdsite-nuxt/utils/mdsite-config.ts +240 -0
  132. package/mdsite-nuxt/vitest.config.ts +7 -0
  133. package/package.json +36 -0
@@ -0,0 +1,376 @@
1
+ <template>
2
+ <div class="layout-wrapper">
3
+ <!-- App Bar -->
4
+ <AppBar
5
+ :sidebars-visible="mdAndUp && sidebarsVisible"
6
+ @toggle-menu="handleToggleMenu"
7
+ />
8
+
9
+ <!-- Desktop Layout (md and up) -->
10
+ <div v-if="mdAndUp" class="desktop-wrapper">
11
+ <div class="desktop-layout">
12
+ <!-- Left Sidebar (Navigation) -->
13
+ <v-navigation-drawer
14
+ v-model="sidebarsVisible"
15
+ :permanent="!isPrinting"
16
+ absolute
17
+ location="left"
18
+ width="320"
19
+ :touchless="true"
20
+ :scrim="false"
21
+ :disableRouteWatcher="true"
22
+ class="desktop-drawer-left"
23
+ >
24
+ <AppNavigation
25
+ :show-search="true"
26
+ @select="handleNavSelect"
27
+ />
28
+ </v-navigation-drawer>
29
+
30
+ <!-- Center Content Column (flexible) -->
31
+ <v-main class="content-area">
32
+ <v-container :fluid="isPrinting" :class="{ 'print-force-full': isPrinting }">
33
+ <div ref="desktopContentContainer">
34
+ <slot />
35
+ </div>
36
+ </v-container>
37
+ </v-main>
38
+
39
+ <!-- Right Sidebar (Table of Contents) -->
40
+ <v-navigation-drawer
41
+ v-if="shouldShowTOC"
42
+ v-model="sidebarsVisible"
43
+ :permanent="!isPrinting"
44
+ absolute
45
+ location="right"
46
+ width="320"
47
+ :touchless="true"
48
+ :scrim="false"
49
+ :disableRouteWatcher="true"
50
+ class="desktop-drawer-right"
51
+ >
52
+ <AppTableOfContents
53
+ :toc-items="tocItems"
54
+ :active-id="activeHeadingId"
55
+ />
56
+ </v-navigation-drawer>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Mobile Layout (sm and below) -->
61
+ <div v-else class="mobile-layout">
62
+ <!-- Mobile Drawer -->
63
+ <v-navigation-drawer
64
+ v-model="drawerOpen"
65
+ temporary
66
+ location="left"
67
+ width="320"
68
+ >
69
+ <div class="drawer-content">
70
+ <!-- Search Box -->
71
+ <SearchBox
72
+ @select="handleMobileSelection"
73
+ />
74
+
75
+ <v-divider class="my-2" />
76
+
77
+ <!-- "On This Page" Section (mobile only) -->
78
+ <v-expansion-panels v-if="shouldShowTOC" flat>
79
+ <v-expansion-panel color="surface-rail">
80
+ <v-expansion-panel-title class="toc-panel-title">
81
+ On This Page
82
+ </v-expansion-panel-title>
83
+ <v-expansion-panel-text class="bg-surface-rail">
84
+ <AppTableOfContents
85
+ :toc-items="tocItems"
86
+ :active-id="activeHeadingId"
87
+ :show-header="false"
88
+ @item-click="handleMobileSelection"
89
+ />
90
+ </v-expansion-panel-text>
91
+ </v-expansion-panel>
92
+ </v-expansion-panels>
93
+
94
+ <v-divider v-if="shouldShowTOC" class="my-2" />
95
+
96
+ <!-- Navigation Tree -->
97
+ <AppNavigation
98
+ :show-search="false"
99
+ @select="handleMobileSelection"
100
+ />
101
+ </div>
102
+ </v-navigation-drawer>
103
+
104
+ <!-- Full-width Content -->
105
+ <v-main class="content-area-mobile">
106
+ <v-container>
107
+ <div ref="mobileContentContainer">
108
+ <slot />
109
+ </div>
110
+ </v-container>
111
+ </v-main>
112
+ </div>
113
+
114
+ <!-- Footer Bar (always visible on all layouts) -->
115
+ <AppFooter />
116
+ </div>
117
+ </template>
118
+
119
+ <script setup lang="ts">
120
+ import { useTableOfContents } from '~/composables/useTableOfContents'
121
+
122
+ const { mdAndUp } = useDisplay()
123
+ const route = useRoute()
124
+
125
+ // Table of Contents state
126
+ const desktopContentContainer = ref<HTMLElement>()
127
+ const mobileContentContainer = ref<HTMLElement>()
128
+ const { tocItems, activeId: activeHeadingId, shouldShowTOC, generateTOC } = useTableOfContents()
129
+
130
+ // Provide TOC generation function to child pages
131
+ provide('generateTOC', () => {
132
+ const container = mdAndUp.value ? desktopContentContainer.value : mobileContentContainer.value
133
+ if (container) {
134
+ generateTOC(container)
135
+ }
136
+ })
137
+
138
+ // Sidebar state - initialize based on screen size
139
+ const sidebarsVisible = ref(mdAndUp.value)
140
+ const drawerOpen = ref(false)
141
+
142
+ // Update sidebars visibility when screen size changes
143
+ watch(mdAndUp, (newValue) => {
144
+ sidebarsVisible.value = newValue
145
+ if (!newValue) {
146
+ drawerOpen.value = false
147
+ }
148
+ })
149
+
150
+ // Print handling - close sidebars to allow content to expand natively
151
+ // Print handling
152
+ const isPrinting = ref(false)
153
+ let previousSidebarState = true
154
+
155
+ const handlePrint = async () => {
156
+ previousSidebarState = sidebarsVisible.value
157
+
158
+ // 1. Enter print mode
159
+ isPrinting.value = true
160
+ sidebarsVisible.value = false
161
+
162
+ // 2. Wait for Vue to update the DOM (including style recalculations)
163
+ await nextTick()
164
+ // Small delay to ensure layout shifts have completed visually for the browser's print capture
165
+ // 500ms to accommodate the 0.3s sidebar transition
166
+ await new Promise(resolve => setTimeout(resolve, 500))
167
+
168
+ // 3. Print
169
+ window.print()
170
+
171
+ // 4. Cleanup after print dialog closes
172
+ isPrinting.value = false
173
+ sidebarsVisible.value = previousSidebarState
174
+ }
175
+
176
+ // Provide to children (like AppBar)
177
+ provide('triggerPrint', handlePrint)
178
+
179
+ // Also listen for Ctrl+P (native browser print)
180
+ // Note: This might still have a race condition on some browsers since we can't "pause" the event.
181
+ // The button click approach (triggerPrint) is preferred.
182
+ const onBeforePrint = () => {
183
+ if (!isPrinting.value) { // Only run if not already triggered by our helper
184
+ previousSidebarState = sidebarsVisible.value
185
+ isPrinting.value = true
186
+ sidebarsVisible.value = false
187
+ }
188
+ }
189
+
190
+ const onAfterPrint = () => {
191
+ // Only restore if we weren't triggered by the helper (which handles its own restore)
192
+ // Or just safe to always restore if we are in print mode
193
+ if (isPrinting.value) {
194
+ isPrinting.value = false
195
+ sidebarsVisible.value = previousSidebarState
196
+ }
197
+ }
198
+
199
+ onMounted(() => {
200
+ if (import.meta.client) {
201
+ window.addEventListener('beforeprint', onBeforePrint)
202
+ window.addEventListener('afterprint', onAfterPrint)
203
+ }
204
+ })
205
+
206
+ onUnmounted(() => {
207
+ if (import.meta.client) {
208
+ window.removeEventListener('beforeprint', onBeforePrint)
209
+ window.removeEventListener('afterprint', onAfterPrint)
210
+ }
211
+ })
212
+
213
+ /**
214
+ * Handle toggle menu (hamburger click)
215
+ */
216
+ function handleToggleMenu() {
217
+ if (mdAndUp.value) {
218
+ // Desktop: toggle both sidebars
219
+ sidebarsVisible.value = !sidebarsVisible.value
220
+ } else {
221
+ // Mobile: toggle drawer
222
+ drawerOpen.value = !drawerOpen.value
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Handle navigation selection (desktop)
228
+ */
229
+ function handleNavSelect(path: string) {
230
+ // Don't auto-close on desktop
231
+ navigateTo(path)
232
+ }
233
+
234
+ /**
235
+ * Handle any selection in mobile drawer (navigation, search, or TOC)
236
+ * Auto-closes drawer after selection
237
+ */
238
+ function handleMobileSelection(path?: string) {
239
+ drawerOpen.value = false
240
+ if (path) {
241
+ navigateTo(path)
242
+ }
243
+ }
244
+
245
+ /**
246
+ * NOTE: We do NOT clear TOC on route change here
247
+ * The page component's useContentPostProcessing handles clearing and regenerating
248
+ * Clearing here causes a race condition where we clear AFTER the page has already generated TOC
249
+ */
250
+ </script>
251
+
252
+ <style>
253
+ /* Global: Prevent horizontal scroll on body */
254
+ body {
255
+ overflow-x: hidden;
256
+ max-width: 100vw;
257
+ }
258
+
259
+ /* CSS Variables - must be unscoped */
260
+ :root {
261
+ --app-bar-height: 56px;
262
+ --footer-height: 56px;
263
+ --sidebar-transition: 0.3s ease;
264
+ }
265
+
266
+ /* Print styles - hide navigation elements */
267
+ /* Print styles - hide navigation elements */
268
+ @media print {
269
+ .v-navigation-drawer,
270
+ .v-app-bar,
271
+ .v-footer {
272
+ display: none !important;
273
+ }
274
+
275
+ /* Reset layout constraints - Force block to kill flexbox gaps */
276
+ .desktop-layout,
277
+ .v-application,
278
+ .v-main,
279
+ .content-area {
280
+ display: block !important;
281
+ width: 100% !important;
282
+ max-width: none !important;
283
+ margin: 0 !important;
284
+ padding: 0 !important;
285
+ /* Force reset Vuetify layout variables to ensure no space is reserved */
286
+ --v-layout-left: 0px !important;
287
+ --v-layout-right: 0px !important;
288
+ --v-layout-top: 0px !important;
289
+ --v-layout-bottom: 0px !important;
290
+ }
291
+
292
+ /* Ensure container takes full width - High Specificity to override scoped styles */
293
+ .v-application .v-container,
294
+ .v-container.print-force-full,
295
+ .v-container {
296
+ max-width: none !important;
297
+ width: 100% !important;
298
+ padding: 0 16px !important;
299
+ margin: 0 !important;
300
+ flex: 0 0 100% !important;
301
+ }
302
+ }
303
+ </style>
304
+
305
+ <style scoped>
306
+
307
+ /* Desktop Wrapper - clips overflow for slide animations */
308
+ .desktop-wrapper {
309
+ width: 100%;
310
+ max-width: 100vw;
311
+ overflow-x: hidden;
312
+ overflow-y: visible;
313
+ position: relative;
314
+ }
315
+
316
+ /* Desktop Layout - full height flex container */
317
+ .desktop-layout {
318
+ display: flex;
319
+ min-height: 100vh;
320
+ width: 100%;
321
+ position: relative;
322
+ }
323
+
324
+ /* Desktop drawers - fixed positioning with independent scrolling */
325
+ .desktop-layout :deep(.v-navigation-drawer.desktop-drawer-left),
326
+ .desktop-layout :deep(.v-navigation-drawer.desktop-drawer-right) {
327
+ position: fixed !important;
328
+ top: 0 !important;
329
+ bottom: 0 !important;
330
+ left: auto !important;
331
+ right: auto !important;
332
+ height: 100vh !important;
333
+ z-index: 1100 !important;
334
+ overflow-y: auto;
335
+ }
336
+
337
+ .desktop-layout :deep(.v-navigation-drawer.desktop-drawer-left) {
338
+ left: 0 !important;
339
+ }
340
+
341
+ .desktop-layout :deep(.v-navigation-drawer.desktop-drawer-right) {
342
+ right: 0 !important;
343
+ }
344
+
345
+ /* Content area - Vuetify handles spacing automatically via --v-layout-* vars */
346
+ .content-area {
347
+ flex: 1;
348
+ min-width: 0;
349
+ /* Remove custom margins - Vuetify calculates based on drawer width */
350
+ margin: 0 !important;
351
+ }
352
+
353
+ .content-area-mobile {
354
+ width: 100%;
355
+ overflow-y: auto;
356
+ }
357
+
358
+ .drawer-content {
359
+ height: 100%;
360
+ display: flex;
361
+ flex-direction: column;
362
+ overflow-y: auto;
363
+ }
364
+
365
+ .toc-panel-title {
366
+ font-weight: 600;
367
+ font-size: 0.875rem;
368
+ }
369
+
370
+ /* Ensure proper spacing for container - Screen only */
371
+ @media screen {
372
+ :deep(.v-container) {
373
+ max-width: 1200px;
374
+ }
375
+ }
376
+ </style>
@@ -0,0 +1,56 @@
1
+ <template>
2
+ <article>
3
+ <div v-if="pending" class="text-center py-8">
4
+ <v-progress-circular indeterminate color="primary"></v-progress-circular>
5
+ </div>
6
+ <div v-else-if="!page">
7
+ <v-alert type="error">Page not found</v-alert>
8
+ </div>
9
+ <div v-else>
10
+ <div class="content-body">
11
+ <ContentRenderer :value="page" />
12
+ </div>
13
+ </div>
14
+ </article>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ const route = useRoute()
19
+
20
+ // Query content using Nuxt Content v3 API
21
+ // server: true = Only query during SSR/prerendering, never on client
22
+ // This prevents 3.5MB database download - client uses prerendered HTML
23
+ const { data: page, pending } = await useAsyncData(
24
+ `content-${route.path}`,
25
+ () => queryCollection('content').path(route.path).first(),
26
+ { server: true }
27
+ )
28
+
29
+ const siteConfig = useSiteConfig()
30
+ const title = page.value?.title || 'Page'
31
+ const description = page.value?.description || ''
32
+
33
+ useHead(() => ({
34
+ title,
35
+ htmlAttrs: { lang: 'en' },
36
+ meta: [
37
+ { name: 'description', content: description },
38
+ { name: 'keywords', content: page.value?.keywords?.join(', ') || '' },
39
+ { name: 'robots', content: 'index, follow' },
40
+ { name: 'theme-color', content: siteConfig.themeColorLight, media: '(prefers-color-scheme: light)' },
41
+ { name: 'theme-color', content: siteConfig.themeColorDark, media: '(prefers-color-scheme: dark)' },
42
+ // Open Graph
43
+ { property: 'og:title', content: title },
44
+ { property: 'og:description', content: description },
45
+ { property: 'og:url', content: `${siteConfig.siteCanonical}${route.path}` },
46
+ { property: 'og:type', content: 'article' },
47
+ { property: 'og:image', content: '/icon-512.png' }
48
+ ],
49
+ link: [
50
+ ...(siteConfig.siteCanonical ? [{ rel: 'canonical', href: `${siteConfig.siteCanonical}${route.path}` }] : [])
51
+ ]
52
+ }))
53
+
54
+ // Post-process content: Bible tooltips + TOC generation
55
+ useContentPostProcessing(page)
56
+ </script>
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <article>
3
+ <div v-if="pending" class="text-center py-8">
4
+ <v-progress-circular indeterminate color="primary"></v-progress-circular>
5
+ </div>
6
+ <div v-else-if="!page">
7
+ <v-alert type="error">Home page not found</v-alert>
8
+ </div>
9
+ <div v-else>
10
+ <div class="content-body">
11
+ <ContentRenderer :value="page" />
12
+ </div>
13
+ </div>
14
+ </article>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ // Query home page content using Nuxt Content v3 API
19
+ // server: true = Only query during SSR/prerendering, never on client
20
+ // This prevents 3.5MB database download - client uses prerendered HTML
21
+ const { data: page, pending } = await useAsyncData(
22
+ 'content-home',
23
+ () => queryCollection('content').path('/').first(),
24
+ { server: true } // true = SSR/prerender only
25
+ )
26
+
27
+ const siteConfig = useSiteConfig()
28
+ const title = page.value?.title || siteConfig.siteName || 'Home'
29
+ const description = page.value?.description || ''
30
+
31
+ useHead(() => ({
32
+ title,
33
+ htmlAttrs: { lang: 'en' },
34
+ meta: [
35
+ { name: 'description', content: description },
36
+ { name: 'keywords', content: page.value?.keywords?.join(', ') || '' },
37
+ { name: 'robots', content: 'index, follow' },
38
+ { name: 'theme-color', content: siteConfig.themeColorLight, media: '(prefers-color-scheme: light)' },
39
+ { name: 'theme-color', content: siteConfig.themeColorDark, media: '(prefers-color-scheme: dark)' },
40
+ // Open Graph
41
+ { property: 'og:title', content: title },
42
+ { property: 'og:description', content: description },
43
+ { property: 'og:url', content: siteConfig.siteCanonical },
44
+ { property: 'og:type', content: 'website' },
45
+ { property: 'og:image', content: '/icon-512.png' }
46
+ ],
47
+ link: [
48
+ ...(siteConfig.siteCanonical ? [{ rel: 'canonical', href: siteConfig.siteCanonical }] : [])
49
+ ]
50
+ }))
51
+
52
+ // Post-process content: like Bible tooltips + TOC generation
53
+ useContentPostProcessing(page)
54
+ </script>