@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.
- package/README.md +29 -37
- package/dist/commands/clean.d.ts +1 -0
- package/dist/commands/clean.js +70 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/commands.test.js +157 -75
- package/dist/commands/commands.test.js.map +1 -1
- package/dist/commands/generate.js +5 -4
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.js +5 -64
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/prepare.js +2 -14
- package/dist/commands/prepare.js.map +1 -1
- package/dist/commands/prepare.test.js +26 -24
- package/dist/commands/prepare.test.js.map +1 -1
- package/dist/commands/preview.js +21 -21
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/start.js +13 -11
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/stop.js +7 -4
- package/dist/commands/stop.js.map +1 -1
- package/dist/commands/workflows.test.js +42 -56
- package/dist/commands/workflows.test.js.map +1 -1
- package/dist/config/default-mdsite-config.js +7 -8
- package/dist/config/default-mdsite-config.js.map +1 -1
- package/dist/config/default-mdsite-config.test.js +7 -8
- package/dist/config/default-mdsite-config.test.js.map +1 -1
- package/dist/config/mdsite-config.d.ts +46 -10
- package/dist/config/mdsite-config.js +46 -24
- package/dist/config/mdsite-config.js.map +1 -1
- package/dist/config/mdsite-config.test.js +55 -50
- package/dist/config/mdsite-config.test.js.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +13 -0
- package/dist/index.test.js.map +1 -1
- package/dist/process/child-process.d.ts +4 -0
- package/dist/process/child-process.js +33 -1
- package/dist/process/child-process.js.map +1 -1
- package/dist/process/child-process.test.js +41 -5
- package/dist/process/child-process.test.js.map +1 -1
- package/dist/process/runtime-state.d.ts +13 -5
- package/dist/process/runtime-state.js +25 -13
- package/dist/process/runtime-state.js.map +1 -1
- package/dist/process/runtime-state.test.js +10 -10
- package/dist/process/runtime-state.test.js.map +1 -1
- package/dist/renderer/mdsite-nuxt.d.ts +28 -3
- package/dist/renderer/mdsite-nuxt.js +32 -27
- package/dist/renderer/mdsite-nuxt.js.map +1 -1
- package/dist/renderer/mdsite-nuxt.test.js +40 -39
- package/dist/renderer/mdsite-nuxt.test.js.map +1 -1
- package/mdsite-nuxt/app/components/AppFooter.vue +84 -22
- package/mdsite-nuxt/app/composables/useFooter.test.ts +54 -0
- package/mdsite-nuxt/app/composables/useFooter.ts +48 -31
- package/mdsite-nuxt/app/composables/useSiteConfig.test.ts +13 -87
- package/mdsite-nuxt/app/composables/useSiteConfig.ts +7 -26
- package/mdsite-nuxt/app/composables/useSourceEdit.test.ts +103 -0
- package/mdsite-nuxt/app/composables/useSourceEdit.ts +39 -51
- package/mdsite-nuxt/app/layouts/default.vue +10 -3
- package/mdsite-nuxt/nuxt.config.ts +22 -15
- package/mdsite-nuxt/scripts/generate-favicons.test.ts +3 -3
- package/mdsite-nuxt/scripts/generate-favicons.ts +4 -4
- package/mdsite-nuxt/scripts/generate-indices.test.ts +71 -10
- package/mdsite-nuxt/scripts/generate-indices.ts +161 -27
- package/mdsite-nuxt/scripts/renderer-hooks.test.ts +0 -91
- package/mdsite-nuxt/scripts/renderer-hooks.ts +1 -50
- package/mdsite-nuxt/scripts/start.test.ts +0 -1
- package/mdsite-nuxt/scripts/start.ts +0 -1
- package/mdsite-nuxt/utils/mdsite-config.ts +86 -41
- package/package.json +1 -1
- 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-
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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:
|
|
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
|
|
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(
|
|
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:
|
|
49
|
+
features: { sourceEdit: 'https://github.com/org/repo/edit/main/', bibleTooltips: false }
|
|
100
50
|
}, undefined)
|
|
101
51
|
|
|
102
|
-
expect(result.features.sourceEdit).toBe(
|
|
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:
|
|
58
|
+
features: { sourceEdit: '', bibleTooltips: true }
|
|
109
59
|
}, undefined)
|
|
110
60
|
|
|
111
|
-
expect(result.features.sourceEdit).toBe(
|
|
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?:
|
|
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:
|
|
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
|
|
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
|
-
* `
|
|
49
|
-
*
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
29
|
+
export function buildEditUrl(prefix: string, routePath: string): string | undefined {
|
|
30
|
+
if (!prefix || prefix.trim() === '') {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
9
33
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
18
|
-
|
|
38
|
+
const normalizedPrefix = prefix.replace(/\/+$/, '')
|
|
39
|
+
return `${normalizedPrefix}/${contentPath}.md`
|
|
40
|
+
}
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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:
|
|
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
|
|
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
|
|
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;
|
|
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 =
|
|
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 (
|
|
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}`)
|