@life-and-dev/mdsite 0.5.3 → 0.7.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.
Files changed (70) hide show
  1. package/README.md +29 -37
  2. package/dist/commands/clean.d.ts +1 -0
  3. package/dist/commands/clean.js +70 -0
  4. package/dist/commands/clean.js.map +1 -0
  5. package/dist/commands/commands.test.js +157 -75
  6. package/dist/commands/commands.test.js.map +1 -1
  7. package/dist/commands/generate.js +5 -4
  8. package/dist/commands/generate.js.map +1 -1
  9. package/dist/commands/init.js +5 -64
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/commands/prepare.js +2 -14
  12. package/dist/commands/prepare.js.map +1 -1
  13. package/dist/commands/prepare.test.js +26 -24
  14. package/dist/commands/prepare.test.js.map +1 -1
  15. package/dist/commands/preview.js +21 -21
  16. package/dist/commands/preview.js.map +1 -1
  17. package/dist/commands/start.js +13 -11
  18. package/dist/commands/start.js.map +1 -1
  19. package/dist/commands/stop.js +7 -4
  20. package/dist/commands/stop.js.map +1 -1
  21. package/dist/commands/workflows.test.js +42 -56
  22. package/dist/commands/workflows.test.js.map +1 -1
  23. package/dist/config/default-mdsite-config.js +7 -8
  24. package/dist/config/default-mdsite-config.js.map +1 -1
  25. package/dist/config/default-mdsite-config.test.js +7 -8
  26. package/dist/config/default-mdsite-config.test.js.map +1 -1
  27. package/dist/config/mdsite-config.d.ts +46 -10
  28. package/dist/config/mdsite-config.js +46 -24
  29. package/dist/config/mdsite-config.js.map +1 -1
  30. package/dist/config/mdsite-config.test.js +55 -50
  31. package/dist/config/mdsite-config.test.js.map +1 -1
  32. package/dist/index.js +8 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.test.js +13 -0
  35. package/dist/index.test.js.map +1 -1
  36. package/dist/process/child-process.d.ts +4 -0
  37. package/dist/process/child-process.js +33 -1
  38. package/dist/process/child-process.js.map +1 -1
  39. package/dist/process/child-process.test.js +41 -5
  40. package/dist/process/child-process.test.js.map +1 -1
  41. package/dist/process/runtime-state.d.ts +13 -5
  42. package/dist/process/runtime-state.js +25 -13
  43. package/dist/process/runtime-state.js.map +1 -1
  44. package/dist/process/runtime-state.test.js +10 -10
  45. package/dist/process/runtime-state.test.js.map +1 -1
  46. package/dist/renderer/mdsite-nuxt.d.ts +28 -3
  47. package/dist/renderer/mdsite-nuxt.js +32 -27
  48. package/dist/renderer/mdsite-nuxt.js.map +1 -1
  49. package/dist/renderer/mdsite-nuxt.test.js +40 -39
  50. package/dist/renderer/mdsite-nuxt.test.js.map +1 -1
  51. package/mdsite-nuxt/app/components/AppFooter.vue +84 -22
  52. package/mdsite-nuxt/app/composables/useFooter.test.ts +54 -0
  53. package/mdsite-nuxt/app/composables/useFooter.ts +48 -31
  54. package/mdsite-nuxt/app/composables/useSiteConfig.test.ts +13 -87
  55. package/mdsite-nuxt/app/composables/useSiteConfig.ts +7 -26
  56. package/mdsite-nuxt/app/composables/useSourceEdit.test.ts +103 -0
  57. package/mdsite-nuxt/app/composables/useSourceEdit.ts +39 -51
  58. package/mdsite-nuxt/app/layouts/default.vue +10 -3
  59. package/mdsite-nuxt/nuxt.config.ts +22 -15
  60. package/mdsite-nuxt/scripts/generate-favicons.test.ts +3 -3
  61. package/mdsite-nuxt/scripts/generate-favicons.ts +4 -4
  62. package/mdsite-nuxt/scripts/generate-indices.test.ts +71 -10
  63. package/mdsite-nuxt/scripts/generate-indices.ts +161 -27
  64. package/mdsite-nuxt/scripts/renderer-hooks.test.ts +0 -91
  65. package/mdsite-nuxt/scripts/renderer-hooks.ts +1 -50
  66. package/mdsite-nuxt/scripts/start.test.ts +0 -1
  67. package/mdsite-nuxt/scripts/start.ts +0 -1
  68. package/mdsite-nuxt/utils/mdsite-config.ts +86 -41
  69. package/package.json +1 -1
  70. package/mdsite-nuxt/example.config.yml +0 -67
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Unit tests for the pure `mapSiteConfig` helper extracted from
3
- * `useSiteConfig`. The `sourceEdit` Edit-on-GitHub link in `AppBar` and
4
- * `AppFooter` is only rendered when `getEditUrl()` produces a URL, which
5
- * in turn requires `contentGitRepo` to be populated from `server.repo`.
6
- * These tests pin that mapping down so a future refactor cannot regress
7
- * it back to the empty-string default.
3
+ * `useSiteConfig`. The `sourceEdit` Edit-link in `AppBar` and `AppFooter`
4
+ * is only rendered when `getEditUrl()` produces a URL, which in turn
5
+ * requires `features.sourceEdit` to be a non-empty URL prefix. These
6
+ * tests pin that mapping down so a future refactor cannot regress it
7
+ * back to a boolean flag or break the empty-string default.
8
8
  */
9
9
 
10
10
  import { describe, expect, it } from 'vitest'
@@ -17,13 +17,10 @@ describe('mapSiteConfig', () => {
17
17
  expect(result).toEqual({
18
18
  siteName: '',
19
19
  siteCanonical: '',
20
- contentGitRepo: '',
21
- contentGitBranch: 'main',
22
- contentGitPath: '.',
23
20
  contentPath: '.',
24
21
  features: {
25
22
  bibleTooltips: false,
26
- sourceEdit: false
23
+ sourceEdit: ''
27
24
  },
28
25
  themeColorLight: '#000000',
29
26
  themeColorDark: '#ffffff'
@@ -39,76 +36,29 @@ describe('mapSiteConfig', () => {
39
36
  expect(result.siteCanonical).toBe('https://example.test')
40
37
  })
41
38
 
42
- describe('contentGitRepo (Edit on GitHub source)', () => {
43
- it('reads server.repo into contentGitRepo', () => {
44
- const result = mapSiteConfig({
45
- server: { repo: 'https://github.com/life-and-dev/mdsite' }
46
- }, undefined)
47
-
48
- expect(result.contentGitRepo).toBe('https://github.com/life-and-dev/mdsite')
49
- })
50
-
51
- it('defaults to empty string when server.repo is missing', () => {
52
- const result = mapSiteConfig({ server: {} }, undefined)
53
-
54
- expect(result.contentGitRepo).toBe('')
55
- })
56
-
57
- it('defaults to empty string when server is missing entirely', () => {
58
- const result = mapSiteConfig({}, undefined)
59
-
60
- expect(result.contentGitRepo).toBe('')
61
- })
62
- })
63
-
64
- describe('contentGitBranch (Edit on GitHub source)', () => {
65
- it('defaults to "main" when server.gitBranch is missing', () => {
66
- expect(mapSiteConfig({}, undefined).contentGitBranch).toBe('main')
67
- })
68
-
69
- it('defaults to "main" when server is missing entirely', () => {
70
- expect(mapSiteConfig({ server: {} }, undefined).contentGitBranch).toBe('main')
71
- })
72
-
73
- it('reads server.gitBranch into contentGitBranch', () => {
74
- const result = mapSiteConfig({
75
- server: { gitBranch: 'develop' }
76
- }, undefined)
77
-
78
- expect(result.contentGitBranch).toBe('develop')
79
- })
80
-
81
- it('treats an empty string server.gitBranch as missing and falls back to "main"', () => {
82
- // Whitespace-only branches are normalised away upstream in
83
- // `utils/mdsite-config.ts` `normalizeMdsiteConfig`; the mapper here
84
- // only falls back on the empty string, not on whitespace.
85
- expect(mapSiteConfig({ server: { gitBranch: '' } }, undefined).contentGitBranch).toBe('main')
86
- })
87
- })
88
-
89
39
  describe('features', () => {
90
- it('defaults both feature flags to false when features is missing', () => {
40
+ it('defaults sourceEdit to "" and bibleTooltips to false when features is missing', () => {
91
41
  const result = mapSiteConfig({}, undefined)
92
42
 
93
43
  expect(result.features.bibleTooltips).toBe(false)
94
- expect(result.features.sourceEdit).toBe(false)
44
+ expect(result.features.sourceEdit).toBe('')
95
45
  })
96
46
 
97
- it('reads sourceEdit from features.sourceEdit', () => {
47
+ it('reads sourceEdit (URL prefix) from features.sourceEdit', () => {
98
48
  const result = mapSiteConfig({
99
- features: { sourceEdit: true, bibleTooltips: false }
49
+ features: { sourceEdit: 'https://github.com/org/repo/edit/main/', bibleTooltips: false }
100
50
  }, undefined)
101
51
 
102
- expect(result.features.sourceEdit).toBe(true)
52
+ expect(result.features.sourceEdit).toBe('https://github.com/org/repo/edit/main/')
103
53
  expect(result.features.bibleTooltips).toBe(false)
104
54
  })
105
55
 
106
56
  it('reads bibleTooltips from features.bibleTooltips', () => {
107
57
  const result = mapSiteConfig({
108
- features: { sourceEdit: false, bibleTooltips: true }
58
+ features: { sourceEdit: '', bibleTooltips: true }
109
59
  }, undefined)
110
60
 
111
- expect(result.features.sourceEdit).toBe(false)
61
+ expect(result.features.sourceEdit).toBe('')
112
62
  expect(result.features.bibleTooltips).toBe(true)
113
63
  })
114
64
  })
@@ -139,28 +89,4 @@ describe('mapSiteConfig', () => {
139
89
  expect(mapSiteConfig({}, undefined).contentPath).toBe('.')
140
90
  expect(mapSiteConfig({}, '').contentPath).toBe('.')
141
91
  })
142
-
143
- describe('contentGitPath (Edit on GitHub source)', () => {
144
- it('defaults to "." when contentGitPath is undefined', () => {
145
- // Preserves the historical cwd-relative fallback so existing
146
- // callers/tests that omit the third arg keep working.
147
- expect(mapSiteConfig({}, undefined).contentGitPath).toBe('.')
148
- expect(mapSiteConfig({}, undefined, undefined).contentGitPath).toBe('.')
149
- })
150
-
151
- it('defaults to "." when contentGitPath is an empty string', () => {
152
- // Empty string is treated as missing so an absent
153
- // runtimeConfig.public.contentGitPath falls back cleanly.
154
- expect(mapSiteConfig({}, undefined, '').contentGitPath).toBe('.')
155
- })
156
-
157
- it('passes an absolute contentGitPath through unchanged', () => {
158
- // The renderer supplies `path.dirname(mdsite.configPath)` here so
159
- // `relative(contentGitPath, contentPath)` is cwd-independent and
160
- // identical on server and client (no hydration mismatch).
161
- expect(
162
- mapSiteConfig({}, '/home/user/site/docs', '/home/user/site').contentGitPath
163
- ).toBe('/home/user/site')
164
- })
165
- })
166
92
  })
@@ -9,13 +9,9 @@ export interface RawSiteConfig {
9
9
  name?: string
10
10
  canonical?: string
11
11
  }
12
- server?: {
13
- repo?: string
14
- gitBranch?: string
15
- }
16
12
  features?: {
17
13
  bibleTooltips?: boolean
18
- sourceEdit?: boolean
14
+ sourceEdit?: string
19
15
  }
20
16
  themes?: {
21
17
  light?: { colors?: { primary?: string } }
@@ -26,13 +22,10 @@ export interface RawSiteConfig {
26
22
  export interface SiteConfig {
27
23
  siteName: string
28
24
  siteCanonical: string
29
- contentGitRepo: string
30
- contentGitBranch: string
31
- contentGitPath: string
32
25
  contentPath: string
33
26
  features: {
34
27
  bibleTooltips: boolean
35
- sourceEdit: boolean
28
+ sourceEdit: string
36
29
  }
37
30
  themeColorLight: string
38
31
  themeColorDark: string
@@ -41,33 +34,22 @@ export interface SiteConfig {
41
34
  /**
42
35
  * Pure helper that maps the raw runtime `siteConfig` object into the shape
43
36
  * the renderer actually consumes. Extracted from `useSiteConfig` so the
44
- * field mapping (notably `contentGitRepo` `server.repo`, which is what
45
- * powers the Edit on GitHub link in `AppBar` and `AppFooter`) can be unit
46
- * tested independently of the Nuxt runtime.
37
+ * field mapping can be unit tested independently of the Nuxt runtime.
47
38
  *
48
- * `contentGitPath` must be an absolute path when supplied (the renderer
49
- * sets it from `path.dirname(mdsite.configPath)` in `nuxt.config.ts`).
50
- * Leaving it undefined preserves the historical cwd-relative `'.'`
51
- * default, which is only correct when the renderer's cwd equals the git
52
- * repo root — pass an absolute path to make `useSourceEdit`'s
53
- * `relative(contentGitPath, contentPath)` computation deterministic
54
- * across server and client and avoid hydration mismatches.
39
+ * `sourceEdit` is a user-supplied URL prefix used by `useSourceEdit` to
40
+ * build per-page Edit links; an empty string disables the link.
55
41
  */
56
42
  export function mapSiteConfig(
57
43
  siteConfig: RawSiteConfig | undefined,
58
44
  contentPath: string | undefined,
59
- contentGitPath: string | undefined = '.',
60
45
  ): SiteConfig {
61
46
  return {
62
47
  siteName: siteConfig?.site?.name || '',
63
48
  siteCanonical: siteConfig?.site?.canonical || '',
64
- contentGitRepo: siteConfig?.server?.repo || '',
65
- contentGitBranch: siteConfig?.server?.gitBranch || 'main',
66
- contentGitPath: contentGitPath || '.',
67
49
  contentPath: contentPath || '.',
68
50
  features: {
69
51
  bibleTooltips: siteConfig?.features?.bibleTooltips ?? false,
70
- sourceEdit: siteConfig?.features?.sourceEdit ?? false
52
+ sourceEdit: siteConfig?.features?.sourceEdit ?? ''
71
53
  },
72
54
  themeColorLight: siteConfig?.themes?.light?.colors?.primary || '#000000',
73
55
  themeColorDark: siteConfig?.themes?.dark?.colors?.primary || '#ffffff'
@@ -83,7 +65,6 @@ export function useSiteConfig(): SiteConfig {
83
65
 
84
66
  return mapSiteConfig(
85
67
  siteConfig,
86
- config.public.contentPath,
87
- config.public.contentGitPath as string | undefined
68
+ config.public.contentPath
88
69
  )
89
70
  }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Unit tests for `buildEditUrl`, the pure helper extracted from
3
+ * `useSourceEdit`. The composable itself depends on Nuxt's auto-imported
4
+ * `useRoute`/`useSiteConfig`, so the testable surface is the pure
5
+ * function. The slug-derivation logic in `mapSiteConfig` is already
6
+ * pinned by `useSiteConfig.test.ts`; these tests pin the URL-building
7
+ * behavior — most importantly that a user-supplied `source-edit` URL
8
+ * without a trailing slash still produces a valid path with exactly
9
+ * one separator (no `…/blob/mainindex.md` bug, no duplicate slashes).
10
+ */
11
+
12
+ import { describe, expect, it } from 'vitest'
13
+ import { buildEditUrl } from './useSourceEdit'
14
+
15
+ describe('buildEditUrl', () => {
16
+ describe('disabled prefix', () => {
17
+ it('returns undefined when prefix is empty', () => {
18
+ expect(buildEditUrl('', '/features/source-edit')).toBeUndefined()
19
+ })
20
+
21
+ it('returns undefined when prefix is whitespace-only', () => {
22
+ expect(buildEditUrl(' ', '/features/source-edit')).toBeUndefined()
23
+ })
24
+
25
+ it('does not depend on route path when prefix is empty', () => {
26
+ expect(buildEditUrl('', '/')).toBeUndefined()
27
+ expect(buildEditUrl('', '')).toBeUndefined()
28
+ })
29
+ })
30
+
31
+ describe('separator normalization', () => {
32
+ const nestedPath = '/features/source-edit'
33
+
34
+ it('inserts a single "/" when prefix has no trailing slash', () => {
35
+ // Regression: previously produced `…/blob/mainfeatures/source-edit.md`.
36
+ expect(buildEditUrl('https://github.com/org/repo/blob/main', nestedPath))
37
+ .toBe('https://github.com/org/repo/blob/main/features/source-edit.md')
38
+ })
39
+
40
+ it('keeps exactly one "/" when prefix already has a trailing slash', () => {
41
+ expect(buildEditUrl('https://github.com/org/repo/blob/main/', nestedPath))
42
+ .toBe('https://github.com/org/repo/blob/main/features/source-edit.md')
43
+ })
44
+
45
+ it('collapses multiple trailing slashes to one', () => {
46
+ expect(buildEditUrl('https://github.com/org/repo/blob/main///', nestedPath))
47
+ .toBe('https://github.com/org/repo/blob/main/features/source-edit.md')
48
+ expect(buildEditUrl('https://github.com/org/repo/blob/main//', nestedPath))
49
+ .toBe('https://github.com/org/repo/blob/main/features/source-edit.md')
50
+ })
51
+
52
+ it('does not double-slash when prefix path component already ends with slash', () => {
53
+ // Defensive: ensures normalize strips every trailing slash, not just
54
+ // the last one.
55
+ expect(buildEditUrl('https://github.com/org/repo/blob/main//', '/index'))
56
+ .toBe('https://github.com/org/repo/blob/main/index.md')
57
+ })
58
+ })
59
+
60
+ describe('route path handling', () => {
61
+ const prefix = 'https://github.com/org/repo/blob/main'
62
+
63
+ it('maps the root path "/" to index.md', () => {
64
+ expect(buildEditUrl(prefix, '/')).toBe(`${prefix}/index.md`)
65
+ })
66
+
67
+ it('maps an empty path to index.md', () => {
68
+ expect(buildEditUrl(prefix, '')).toBe(`${prefix}/index.md`)
69
+ })
70
+
71
+ it('strips a leading slash from nested paths', () => {
72
+ expect(buildEditUrl(prefix, '/features/source-edit'))
73
+ .toBe(`${prefix}/features/source-edit.md`)
74
+ })
75
+
76
+ it('passes nested paths without leading slash through unchanged', () => {
77
+ expect(buildEditUrl(prefix, 'features/source-edit'))
78
+ .toBe(`${prefix}/features/source-edit.md`)
79
+ })
80
+
81
+ it('handles single-segment nested paths', () => {
82
+ expect(buildEditUrl(prefix, '/menu'))
83
+ .toBe(`${prefix}/menu.md`)
84
+ })
85
+ })
86
+
87
+ describe('combined normalization + route handling', () => {
88
+ it('produces the documented URL for the docs site root with no trailing slash', () => {
89
+ // The exact case from the bug report: `mdsite.yml:6` has no trailing
90
+ // slash and the docs root resolves to `index.md`.
91
+ expect(buildEditUrl('https://github.com/life-and-dev/mdsite/blob/main', '/'))
92
+ .toBe('https://github.com/life-and-dev/mdsite/blob/main/index.md')
93
+ })
94
+
95
+ it('produces the same URL whether or not the user adds a trailing slash', () => {
96
+ const a = buildEditUrl('https://github.com/org/repo/blob/main', '/features/source-edit')
97
+ const b = buildEditUrl('https://github.com/org/repo/blob/main/', '/features/source-edit')
98
+ const c = buildEditUrl('https://github.com/org/repo/blob/main///', '/features/source-edit')
99
+ expect(a).toBe(b)
100
+ expect(b).toBe(c)
101
+ })
102
+ })
103
+ })
@@ -1,66 +1,54 @@
1
- import { relative, normalize, posix } from 'pathe'
1
+ /**
2
+ * Generate source edit URL for the current page.
3
+ *
4
+ * `siteConfig.features.sourceEdit` is a user-supplied URL prefix (e.g.
5
+ * `https://github.com/org/repo/edit/main`) to which the renderer's
6
+ * per-route content path (with a `.md` suffix) is appended. The prefix is
7
+ * normalized so the user may include or omit a trailing slash — any
8
+ * number of trailing slashes is collapsed to exactly one. An empty or
9
+ * whitespace-only prefix disables the link. The user is in control of
10
+ * the full URL — no provider-specific validation is performed.
11
+ */
2
12
 
3
13
  /**
4
- * Generate source edit URL for the current page
14
+ * Build a source-edit URL from a user-supplied prefix and a route path.
15
+ *
16
+ * Exported for unit testing. Behavior:
17
+ * - Returns `undefined` when `prefix` is empty or whitespace-only.
18
+ * - Strips every trailing `/` from `prefix`, then inserts exactly one `/`
19
+ * between the prefix and the content path. This means a prefix with no
20
+ * trailing slash (`…/blob/main`) and a prefix with one (`…/blob/main/`)
21
+ * both produce the same result, and a prefix with several
22
+ * (`…/blob/main///`) does not yield duplicate slashes.
23
+ * - The route `/` and an empty path both map to `index.md`.
24
+ *
25
+ * @param prefix URL prefix from `features.source-edit`
26
+ * @param routePath Current route path (e.g. `/features/source-edit`)
27
+ * @returns Full edit URL, or `undefined` when disabled
5
28
  */
6
- export function useSourceEdit() {
7
- const route = useRoute()
8
- const siteConfig = useSiteConfig()
29
+ export function buildEditUrl(prefix: string, routePath: string): string | undefined {
30
+ if (!prefix || prefix.trim() === '') {
31
+ return undefined
32
+ }
9
33
 
10
- /**
11
- * Calculate the subdirectory within the git repo where content is located
12
- */
13
- function getContentSubdirectory(): string {
14
- const gitPath = normalize(siteConfig.contentGitPath)
15
- const contentPath = normalize(siteConfig.contentPath)
34
+ const contentPath = !routePath || routePath === '/'
35
+ ? 'index'
36
+ : (routePath.startsWith('/') ? routePath.slice(1) : routePath)
16
37
 
17
- // Calculate relative path from git root to content directory
18
- const subdir = relative(gitPath, contentPath)
38
+ const normalizedPrefix = prefix.replace(/\/+$/, '')
39
+ return `${normalizedPrefix}/${contentPath}.md`
40
+ }
19
41
 
20
- // Convert to posix path for GitHub URLs (forward slashes)
21
- return subdir ? posix.normalize(subdir) : ''
22
- }
42
+ export function useSourceEdit() {
43
+ const route = useRoute()
44
+ const siteConfig = useSiteConfig()
23
45
 
24
46
  /**
25
47
  * Generate source edit URL for current route
26
48
  * @returns source edit URL or undefined if not enabled or not a content page
27
49
  */
28
50
  function getEditUrl(): string | undefined {
29
- const path = route.path
30
- const repo = siteConfig.contentGitRepo
31
-
32
- // Check if feature is enabled
33
- if (!siteConfig.features.sourceEdit) {
34
- return undefined
35
- }
36
-
37
- // Only render if repo starts with "https://github.com" (current supported provider)
38
- if (!repo || !repo.startsWith('https://github.com')) {
39
- return undefined
40
- }
41
-
42
- var contentPath: string;
43
-
44
- // Home page path
45
- if (!path || path === '/') {
46
- contentPath = 'index'
47
- }
48
-
49
- // Convert route path to content file path
50
- // Example: /church/evolution/312-constantine → https://github.com/life-and-dev/church/blob/main/evolution/312-constantine.md
51
- else {
52
- contentPath = path.startsWith('/') ? path.slice(1) : path
53
- }
54
-
55
- // Get the subdirectory within the git repo
56
- const subdir = getContentSubdirectory()
57
-
58
- // Build the full path: subdirectory + content file path
59
- const fullPath = subdir
60
- ? `${subdir}/${contentPath}.md`
61
- : `${contentPath}.md`
62
-
63
- return `${repo}/blob/${siteConfig.contentGitBranch}/${fullPath}`
51
+ return buildEditUrl(siteConfig.features.sourceEdit, route.path)
64
52
  }
65
53
 
66
54
  return {
@@ -35,6 +35,12 @@
35
35
  <slot />
36
36
  </div>
37
37
  </v-container>
38
+ <!--
39
+ Footer sits inside the main column (not as a fixed bottom bar) so it
40
+ scrolls with the article and only appears once the user has reached
41
+ the end of the page.
42
+ -->
43
+ <AppFooter />
38
44
  </v-main>
39
45
 
40
46
  <!-- Right Sidebar (Table of Contents) -->
@@ -110,11 +116,12 @@
110
116
  <slot />
111
117
  </div>
112
118
  </v-container>
119
+ <!--
120
+ In-flow footer for mobile: scrolls with the article content.
121
+ -->
122
+ <AppFooter />
113
123
  </v-main>
114
124
  </div>
115
-
116
- <!-- Footer Bar (always visible on all layouts) -->
117
- <AppFooter />
118
125
  </div>
119
126
  </template>
120
127
 
@@ -9,17 +9,17 @@ import { loadMdsiteConfigSync } from './utils/mdsite-config'
9
9
  const mdsite = loadMdsiteConfigSync()
10
10
  const siteConfig = mdsite.config
11
11
  const appBaseURL = process.env.NUXT_APP_BASE_URL || '/'
12
- // The git repo root, used by `useSourceEdit` to compute the
13
- // repo-relative path to a content file (so the Edit-on-GitHub URL
14
- // points at `…/blob/<branch>/<subdir>/<file>.md` rather than a
15
- // cwd-relative or absolute filesystem path). We default to the
16
- // directory containing `mdsite.yml` because that is the conventional
17
- // git root for an mdsite content dir; falling back to the content
18
- // dir itself covers the case where `mdsite.yml` is missing (legacy
19
- // `content.config.yml` layouts loaded through `renderer-hooks`).
20
- const contentGitPath = mdsite.configPath
21
- ? path.dirname(mdsite.configPath)
22
- : mdsite.contentDir
12
+
13
+ // mdsite is a static site generator. SSR is only needed at build time so
14
+ // `nuxi generate` can pre-render every route to HTML. In `dev` and
15
+ // `preview` mode SSR uses a per-request fork worker that pulls the full
16
+ // Nuxt + Vuetify + @nuxt/content + mermaid + sharp pipeline and OOMs
17
+ // (Worker terminated JS heap out of memory) on memory-constrained
18
+ // hosts. Keep SSR on for build/generate, off for dev/preview so the
19
+ // dev server renders client-side only and `mdsite generate` still
20
+ // produces per-route static HTML.
21
+ const isSsrNuxtCommand = process.argv.includes('build') || process.argv.includes('generate')
22
+ const ssrEnabled = isSsrNuxtCommand
23
23
 
24
24
  export default defineNuxtConfig({
25
25
  compatibilityDate: '2025-07-15',
@@ -29,7 +29,6 @@ export default defineNuxtConfig({
29
29
  public: {
30
30
  contentDomain: path.basename(mdsite.contentDir),
31
31
  contentPath: mdsite.contentDir,
32
- contentGitPath,
33
32
  // `mdsite.config` is a valid `MdsiteConfig` at runtime, but
34
33
  // Nuxt's runtime-config type generator collapses every complex
35
34
  // field of `siteConfig` to a degenerate shape — `menu` becomes
@@ -58,10 +57,18 @@ export default defineNuxtConfig({
58
57
  },
59
58
 
60
59
  nitro: {
61
- preset: 'static' // Pure static preset - no SPA fallbacks
60
+ preset: 'static', // Pure static preset - no SPA fallbacks
61
+ // The `mdsite` CLI sets `MDSITE_NITRO_OUTPUT_DIR` in dev mode so the
62
+ // build output lands in the content directory's `<paths.build>/.output/`
63
+ // rather than inside the renderer source (e.g. the `mdsite-nuxt/`
64
+ // submodule). The default `.output` is kept for direct use of the
65
+ // renderer (e.g. running `nuxt generate` by hand for renderer dev).
66
+ output: {
67
+ dir: process.env.MDSITE_NITRO_OUTPUT_DIR || '.output'
68
+ }
62
69
  },
63
70
 
64
- ssr: true,
71
+ ssr: ssrEnabled,
65
72
 
66
73
  css: [
67
74
  '~/assets/css/markdown.css',
@@ -186,7 +193,7 @@ export default defineNuxtConfig({
186
193
  },
187
194
 
188
195
  'build:before': async () => {
189
- if (process.argv.includes('prepare') || !mdsite.configPath || process.env.MDSITE_RENDERER_ORCHESTRATED === '1') {
196
+ if (process.argv.includes('prepare') || !mdsite.configPath) {
190
197
  return
191
198
  }
192
199
 
@@ -66,12 +66,12 @@ describe('generate-favicons', () => {
66
66
  })
67
67
 
68
68
  describe('generateFavicons', () => {
69
- it('uses the bundled default favicon and writes all expected assets when config.favicon is empty', async () => {
69
+ it('uses the bundled default favicon and writes all expected assets when site.favicon is empty', async () => {
70
70
  const outputDir = path.join(tmpDir, 'output')
71
71
 
72
72
  const ok = await generateFavicons({
73
73
  contentDir: tmpDir,
74
- config: { favicon: '' },
74
+ config: { site: { favicon: '' } },
75
75
  outputDir,
76
76
  })
77
77
 
@@ -106,7 +106,7 @@ describe('generate-favicons', () => {
106
106
 
107
107
  const ok = await generateFavicons({
108
108
  contentDir: tmpDir,
109
- config: { favicon: 'favicon.svg' },
109
+ config: { site: { favicon: 'favicon.svg' } },
110
110
  outputDir,
111
111
  })
112
112
 
@@ -38,7 +38,7 @@ export function resolveFaviconSource(
38
38
 
39
39
  export interface GenerateFaviconsOptions {
40
40
  contentDir?: string
41
- config?: { favicon?: string; site?: { name?: string } }
41
+ config?: { site?: { favicon?: string; name?: string } }
42
42
  outputDir?: string
43
43
  }
44
44
 
@@ -50,9 +50,9 @@ export async function generateFavicons(options: GenerateFaviconsOptions = {}): P
50
50
  ? { contentDir: options.contentDir, config: options.config }
51
51
  : loadMdsiteConfigSync()
52
52
  const { contentDir, config } = resolved
53
- const siteName = (config as { site?: { name?: string } }).site?.name ?? 'site'
53
+ const siteName = config.site?.name ?? 'site'
54
54
 
55
- const resolvedSource = resolveFaviconSource(contentDir, config.favicon ?? '')
55
+ const resolvedSource = resolveFaviconSource(contentDir, config.site?.favicon ?? '')
56
56
 
57
57
  if (!resolvedSource) {
58
58
  console.error('❌ No favicon source available (configured source missing AND bundled default not found).')
@@ -64,7 +64,7 @@ export async function generateFavicons(options: GenerateFaviconsOptions = {}): P
64
64
  await fs.ensureDir(publicDir)
65
65
 
66
66
  if (resolvedSource.isDefault) {
67
- console.log('ℹ️ No favicon source configured (config.favicon empty or file not found). Using bundled default favicon.')
67
+ console.log('ℹ️ No favicon source configured (site.favicon empty or file not found). Using bundled default favicon.')
68
68
  }
69
69
 
70
70
  console.log(`🎨 Generating favicons for site: ${siteName}`)