@life-and-dev/mdsite 0.2.1 → 0.3.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.
@@ -0,0 +1,177 @@
1
+ import path from 'node:path'
2
+ import os from 'node:os'
3
+ import fs from 'node:fs'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
7
+
8
+ import { generateFavicons, generateWebManifest, resolveFaviconSource } from './generate-favicons.js'
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
+ const DEFAULT_FAVICON_PATH = path.resolve(__dirname, '..', 'assets', 'default-favicon.svg')
12
+
13
+ describe('generate-favicons', () => {
14
+ let tmpDir: string
15
+
16
+ beforeEach(() => {
17
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mdsite-favicon-'))
18
+ })
19
+
20
+ afterEach(() => {
21
+ fs.rmSync(tmpDir, { recursive: true, force: true })
22
+ })
23
+
24
+ describe('resolveFaviconSource', () => {
25
+ it('falls back to the bundled default favicon when favicon is an empty string', () => {
26
+ const result = resolveFaviconSource(tmpDir, '')
27
+
28
+ expect(result).not.toBeNull()
29
+ expect(result!.isDefault).toBe(true)
30
+ expect(path.basename(result!.sourcePath)).toBe('default-favicon.svg')
31
+ expect(fs.existsSync(result!.sourcePath)).toBe(true)
32
+ })
33
+
34
+ it('falls back to the bundled default favicon when favicon is only whitespace', () => {
35
+ const result = resolveFaviconSource(tmpDir, ' ')
36
+
37
+ expect(result).not.toBeNull()
38
+ expect(result!.isDefault).toBe(true)
39
+ expect(path.basename(result!.sourcePath)).toBe('default-favicon.svg')
40
+ })
41
+
42
+ it('uses the configured source when the favicon file exists under the content dir', () => {
43
+ const relPath = 'favicon.svg'
44
+ const absPath = path.join(tmpDir, relPath)
45
+ const customSvg =
46
+ '<?xml version="1.0" encoding="UTF-8"?>' +
47
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">' +
48
+ '<rect width="16" height="16" fill="red"/></svg>'
49
+ fs.writeFileSync(absPath, customSvg, 'utf8')
50
+
51
+ const result = resolveFaviconSource(tmpDir, relPath)
52
+
53
+ expect(result).not.toBeNull()
54
+ expect(result!.isDefault).toBe(false)
55
+ expect(result!.sourcePath).toBe(path.resolve(tmpDir, relPath))
56
+ })
57
+
58
+ it('falls back to the bundled default favicon when the configured file does not exist', () => {
59
+ const result = resolveFaviconSource(tmpDir, 'does-not-exist.svg')
60
+
61
+ expect(result).not.toBeNull()
62
+ expect(result!.isDefault).toBe(true)
63
+ expect(path.basename(result!.sourcePath)).toBe('default-favicon.svg')
64
+ expect(fs.existsSync(result!.sourcePath)).toBe(true)
65
+ })
66
+ })
67
+
68
+ describe('generateFavicons', () => {
69
+ it('uses the bundled default favicon and writes all expected assets when config.favicon is empty', async () => {
70
+ const outputDir = path.join(tmpDir, 'output')
71
+
72
+ const ok = await generateFavicons({
73
+ contentDir: tmpDir,
74
+ config: { favicon: '' },
75
+ outputDir,
76
+ })
77
+
78
+ expect(ok).toBe(true)
79
+
80
+ const expectedFiles = [
81
+ 'favicon.svg',
82
+ 'favicon.ico',
83
+ 'apple-touch-icon.png',
84
+ 'icon-192.png',
85
+ 'icon-512.png',
86
+ ]
87
+ for (const file of expectedFiles) {
88
+ const filePath = path.join(outputDir, file)
89
+ expect(fs.existsSync(filePath)).toBe(true)
90
+ expect(fs.statSync(filePath).size).toBeGreaterThan(0)
91
+ }
92
+
93
+ // The default source svg is copied verbatim as favicon.svg
94
+ const defaultSvgContent = fs.readFileSync(DEFAULT_FAVICON_PATH, 'utf8')
95
+ const writtenSvgContent = fs.readFileSync(path.join(outputDir, 'favicon.svg'), 'utf8')
96
+ expect(writtenSvgContent).toBe(defaultSvgContent)
97
+ })
98
+
99
+ it('uses the configured custom source svg over the bundled default', async () => {
100
+ const outputDir = path.join(tmpDir, 'output')
101
+ const customSvg =
102
+ '<?xml version="1.0" encoding="UTF-8"?>' +
103
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">' +
104
+ '<rect width="16" height="16" fill="blue"/></svg>'
105
+ fs.writeFileSync(path.join(tmpDir, 'favicon.svg'), customSvg, 'utf8')
106
+
107
+ const ok = await generateFavicons({
108
+ contentDir: tmpDir,
109
+ config: { favicon: 'favicon.svg' },
110
+ outputDir,
111
+ })
112
+
113
+ expect(ok).toBe(true)
114
+
115
+ const writtenSvgContent = fs.readFileSync(path.join(outputDir, 'favicon.svg'), 'utf8')
116
+ expect(writtenSvgContent).toBe(customSvg)
117
+
118
+ // And it is NOT the bundled default
119
+ const defaultSvgContent = fs.readFileSync(DEFAULT_FAVICON_PATH, 'utf8')
120
+ expect(writtenSvgContent).not.toBe(defaultSvgContent)
121
+ })
122
+ })
123
+
124
+ describe('generateWebManifest', () => {
125
+ it('writes manifest with theme colors from config when name and themes are provided', async () => {
126
+ const outputDir = path.join(tmpDir, 'output')
127
+
128
+ await generateWebManifest({
129
+ name: 'My Site',
130
+ themes: {
131
+ light: { colors: { primary: '#abcdef', surface: '#fedcba' } },
132
+ dark: { colors: {} },
133
+ },
134
+ outputDir,
135
+ })
136
+
137
+ const manifestPath = path.join(outputDir, 'site.webmanifest')
138
+ expect(fs.existsSync(manifestPath)).toBe(true)
139
+ const written = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
140
+ expect(written).toEqual({
141
+ name: 'My Site',
142
+ short_name: 'My Site',
143
+ icons: [
144
+ { src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
145
+ { src: 'icon-512.png', sizes: '512x512', type: 'image/png' },
146
+ ],
147
+ theme_color: '#abcdef',
148
+ background_color: '#fedcba',
149
+ display: 'standalone',
150
+ })
151
+ })
152
+
153
+ it('falls back to safe defaults when themes are missing colors', async () => {
154
+ const outputDir = path.join(tmpDir, 'output')
155
+
156
+ await generateWebManifest({
157
+ name: 'No Themes',
158
+ themes: { light: { colors: {} }, dark: { colors: {} } },
159
+ outputDir,
160
+ })
161
+
162
+ const written = JSON.parse(fs.readFileSync(path.join(outputDir, 'site.webmanifest'), 'utf8'))
163
+ expect(written.theme_color).toBe('#000000')
164
+ expect(written.background_color).toBe('#ffffff')
165
+ })
166
+
167
+ it('falls back to safe defaults when themes are not provided at all', async () => {
168
+ const outputDir = path.join(tmpDir, 'output')
169
+
170
+ await generateWebManifest({ name: 'No Themes', outputDir })
171
+
172
+ const written = JSON.parse(fs.readFileSync(path.join(outputDir, 'site.webmanifest'), 'utf8'))
173
+ expect(written.theme_color).toBe('#000000')
174
+ expect(written.background_color).toBe('#ffffff')
175
+ })
176
+ })
177
+ })
@@ -3,12 +3,14 @@
3
3
  import sharp from 'sharp'
4
4
  import fs from 'fs-extra'
5
5
  import path from 'path'
6
- import { fileURLToPath } from 'url'
7
- import { loadMdsiteConfigSync, resolveMdsiteConfigPath } from '../utils/mdsite-config.js'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { loadMdsiteConfigSync, resolveMdsiteConfigPath, type MdsiteConfig } from '../utils/mdsite-config.js'
8
8
 
9
9
  const __filename = fileURLToPath(import.meta.url)
10
10
  const __dirname = path.dirname(__filename)
11
11
 
12
+ const DEFAULT_FAVICON_PATH = fileURLToPath(new URL('../assets/default-favicon.svg', import.meta.url))
13
+
12
14
  const FAVICON_SIZES = {
13
15
  ico: [16, 32],
14
16
  appleTouchIcon: 180,
@@ -16,28 +18,56 @@ const FAVICON_SIZES = {
16
18
  pwaIcon512: 512
17
19
  } as const
18
20
 
21
+ export function resolveFaviconSource(
22
+ contentDir: string,
23
+ favicon: string,
24
+ ): { sourcePath: string; isDefault: boolean } | null {
25
+ if (typeof favicon === 'string' && favicon.trim().length > 0) {
26
+ const candidate = path.resolve(contentDir, favicon)
27
+ if (fs.pathExistsSync(candidate)) {
28
+ return { sourcePath: candidate, isDefault: false }
29
+ }
30
+ }
31
+
32
+ if (fs.pathExistsSync(DEFAULT_FAVICON_PATH)) {
33
+ return { sourcePath: DEFAULT_FAVICON_PATH, isDefault: true }
34
+ }
35
+
36
+ return null
37
+ }
38
+
39
+ export interface GenerateFaviconsOptions {
40
+ contentDir?: string
41
+ config?: { favicon?: string; site?: { name?: string } }
42
+ outputDir?: string
43
+ }
44
+
19
45
  /**
20
46
  * Generate favicons from the active mdsite config
21
47
  */
22
- export async function generateFavicons(): Promise<boolean> {
23
- const { contentDir, config } = loadMdsiteConfigSync()
48
+ export async function generateFavicons(options: GenerateFaviconsOptions = {}): Promise<boolean> {
49
+ const resolved = options.contentDir && options.config
50
+ ? { contentDir: options.contentDir, config: options.config }
51
+ : loadMdsiteConfigSync()
52
+ const { contentDir, config } = resolved
53
+ const siteName = (config as { site?: { name?: string } }).site?.name ?? 'site'
24
54
 
25
- if (!config.favicon || !config.favicon.trim()) {
26
- console.warn(`āš ļø No favicon source configured (config.favicon is empty). Skipping favicon generation.`)
27
- return false
28
- }
55
+ const resolvedSource = resolveFaviconSource(contentDir, config.favicon ?? '')
29
56
 
30
- const sourcePath = path.resolve(contentDir, config.favicon)
31
-
32
- if (!await fs.pathExists(sourcePath)) {
33
- console.error(`āŒ Favicon source not found: ${sourcePath}`)
57
+ if (!resolvedSource) {
58
+ console.error('āŒ No favicon source available (configured source missing AND bundled default not found).')
34
59
  return false
35
60
  }
36
61
 
37
- const publicDir = path.resolve(__dirname, '..', 'public')
62
+ const { sourcePath } = resolvedSource
63
+ const publicDir = options.outputDir ?? path.resolve(__dirname, '..', 'public')
38
64
  await fs.ensureDir(publicDir)
39
65
 
40
- console.log(`šŸŽØ Generating favicons for site: ${config.site.name}`)
66
+ if (resolvedSource.isDefault) {
67
+ console.log('ā„¹ļø No favicon source configured (config.favicon empty or file not found). Using bundled default favicon.')
68
+ }
69
+
70
+ console.log(`šŸŽØ Generating favicons for site: ${siteName}`)
41
71
  console.log(` Source: ${sourcePath}`)
42
72
  console.log(` Output: ${publicDir}`)
43
73
 
@@ -92,25 +122,38 @@ export async function generateFavicons(): Promise<boolean> {
92
122
  .toFile(icon512Path)
93
123
  console.log(` āœ“ PWA Icon 512: icon-512.png`)
94
124
 
95
- console.log(`āœ… Favicons generated successfully for ${config.site.name}\n`)
125
+ console.log(`āœ… Favicons generated successfully for ${siteName}\n`)
96
126
  return true
97
127
  } catch (error) {
98
- console.error(`āŒ Failed to generate favicons for ${config.site.name}:`, error)
128
+ console.error(`āŒ Failed to generate favicons for ${siteName}:`, error)
99
129
  return false
100
130
  }
101
131
  }
102
132
 
133
+ export interface GenerateWebManifestOptions {
134
+ name?: string
135
+ themes?: MdsiteConfig['themes']
136
+ outputDir?: string
137
+ }
138
+
103
139
  /**
104
- * Generate web manifest for PWA support
140
+ * Generate web manifest for PWA support.
141
+ * Theme/background colors are taken from the light theme in mdsite.yml
142
+ * to match the existing `<meta name="theme-color" media="(prefers-color-scheme: light)">` tag.
143
+ * The web manifest spec only supports a single `theme_color`, so light values are used.
105
144
  */
106
- export async function generateWebManifest(name: string) {
107
- const projectRoot = path.resolve(__dirname, '..')
108
- const publicDir = path.join(projectRoot, 'public')
109
- const manifestPath = path.join(publicDir, 'site.webmanifest')
145
+ export async function generateWebManifest(options: GenerateWebManifestOptions = {}): Promise<void> {
146
+ const siteName = options.name ?? 'site'
147
+ const themeColor = options.themes?.light?.colors?.primary ?? '#000000'
148
+ const backgroundColor = options.themes?.light?.colors?.surface ?? '#ffffff'
149
+
150
+ const outputDir = options.outputDir ?? path.resolve(__dirname, '..', 'public')
151
+ await fs.ensureDir(outputDir)
152
+ const manifestPath = path.join(outputDir, 'site.webmanifest')
110
153
 
111
154
  const manifest = {
112
- name: name,
113
- short_name: name,
155
+ name: siteName,
156
+ short_name: siteName,
114
157
  icons: [
115
158
  {
116
159
  src: 'icon-192.png',
@@ -123,13 +166,13 @@ export async function generateWebManifest(name: string) {
123
166
  type: 'image/png'
124
167
  }
125
168
  ],
126
- theme_color: '#ffffff',
127
- background_color: '#ffffff',
169
+ theme_color: themeColor,
170
+ background_color: backgroundColor,
128
171
  display: 'standalone'
129
172
  }
130
173
 
131
174
  await fs.writeJson(manifestPath, manifest, { spaces: 2 })
132
- console.log(`šŸ“± Web manifest generated: site.webmanifest\n`)
175
+ console.log(`šŸ“± Web manifest generated: site.webmanifest (theme_color=${themeColor})\n`)
133
176
  }
134
177
 
135
178
  /**
@@ -153,7 +196,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
153
196
  ; (async () => {
154
197
  const success = await generateFavicons()
155
198
  if (success) {
156
- await generateWebManifest(config.site.name)
199
+ await generateWebManifest({ name: config.site.name, themes: config.themes })
157
200
  } else {
158
201
  process.exit(1)
159
202
  }
@@ -87,7 +87,20 @@ describe('renderer hooks orchestration', () => {
87
87
 
88
88
  resolveMdsiteConfigPathMock.mockReturnValue('/renderer/mdsite.yml')
89
89
  loadMdsiteConfigSyncMock.mockReturnValue({
90
- config: { site: { name: 'Docs' } },
90
+ config: {
91
+ site: { name: 'Docs' },
92
+ themes: {
93
+ light: {
94
+ colors: {
95
+ primary: '#abcdef',
96
+ surface: '#fedcba',
97
+ },
98
+ },
99
+ dark: {
100
+ colors: {},
101
+ },
102
+ },
103
+ },
91
104
  configPath: '/renderer/mdsite.yml',
92
105
  contentDir: '/renderer/docs',
93
106
  })
@@ -117,7 +130,13 @@ describe('renderer hooks orchestration', () => {
117
130
  contentPath: '/input/docs',
118
131
  })
119
132
  expect(runtime).toEqual({
120
- config: { site: { name: 'Docs' } },
133
+ config: {
134
+ site: { name: 'Docs' },
135
+ themes: {
136
+ light: { colors: { primary: '#abcdef', surface: '#fedcba' } },
137
+ dark: { colors: {} },
138
+ },
139
+ },
121
140
  configPath: '/renderer/mdsite.yml',
122
141
  contentDir: '/renderer/docs',
123
142
  rootDir: '/renderer',
@@ -233,6 +252,15 @@ describe('renderer hooks orchestration', () => {
233
252
  expect(syncContentMock).not.toHaveBeenCalled()
234
253
  expect(buildContentDataMock).not.toHaveBeenCalled()
235
254
  expect(generateFaviconsMock).not.toHaveBeenCalled()
255
+ expect(generateWebManifestMock).toHaveBeenCalledTimes(1)
256
+ expect(generateWebManifestMock).toHaveBeenCalledWith(expect.objectContaining({
257
+ name: 'Docs',
258
+ themes: expect.objectContaining({
259
+ light: expect.objectContaining({
260
+ colors: expect.objectContaining({ primary: '#abcdef' }),
261
+ }),
262
+ }),
263
+ }))
236
264
  expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
237
265
  expect(runtime.contentDir).toBe('/renderer/docs')
238
266
  })
@@ -243,6 +271,7 @@ describe('renderer hooks orchestration', () => {
243
271
  expect(rmMock).not.toHaveBeenCalled()
244
272
  expect(startWatcherMock).toHaveBeenCalledTimes(1)
245
273
  expect(syncContentMock).not.toHaveBeenCalled()
274
+ expect(generateWebManifestMock).toHaveBeenCalledTimes(1)
246
275
  })
247
276
 
248
277
  it('runs non-dev setup hooks in orchestration order and publishes favicon assets on success', async () => {
@@ -251,7 +280,14 @@ describe('renderer hooks orchestration', () => {
251
280
  expect(syncContentMock).toHaveBeenCalledTimes(1)
252
281
  expect(buildContentDataMock).toHaveBeenCalledTimes(1)
253
282
  expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
254
- expect(generateWebManifestMock).toHaveBeenCalledWith('Docs')
283
+ expect(generateWebManifestMock).toHaveBeenCalledWith(expect.objectContaining({
284
+ name: 'Docs',
285
+ themes: expect.objectContaining({
286
+ light: expect.objectContaining({
287
+ colors: expect.objectContaining({ primary: '#abcdef' }),
288
+ }),
289
+ }),
290
+ }))
255
291
  expect(syncContentMock.mock.invocationCallOrder[0]).toBeLessThan(buildContentDataMock.mock.invocationCallOrder[0])
256
292
  expect(buildContentDataMock.mock.invocationCallOrder[0]).toBeLessThan(generateFaviconsMock.mock.invocationCallOrder[0])
257
293
  expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
@@ -263,7 +299,14 @@ describe('renderer hooks orchestration', () => {
263
299
  expect(syncContentMock).toHaveBeenCalledTimes(1)
264
300
  expect(buildContentDataMock).toHaveBeenCalledTimes(1)
265
301
  expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
266
- expect(generateWebManifestMock).toHaveBeenCalledWith('Docs')
302
+ expect(generateWebManifestMock).toHaveBeenCalledWith(expect.objectContaining({
303
+ name: 'Docs',
304
+ themes: expect.objectContaining({
305
+ light: expect.objectContaining({
306
+ colors: expect.objectContaining({ primary: '#abcdef' }),
307
+ }),
308
+ }),
309
+ }))
267
310
  expect(startWatcherMock).not.toHaveBeenCalled()
268
311
  expect(rmMock).not.toHaveBeenCalled()
269
312
  expect(process.env.MDSITE_RENDERER_ORCHESTRATED).toBe('1')
@@ -282,7 +325,7 @@ describe('renderer hooks orchestration', () => {
282
325
  it('runs build fallback hooks without publishing favicon assets when generation reports no output', async () => {
283
326
  generateFaviconsMock.mockResolvedValue(false)
284
327
 
285
- await runBuildFallbackHooks('Docs')
328
+ await runBuildFallbackHooks({ site: { name: 'Docs' }, themes: { light: { colors: {} }, dark: { colors: {} } } } as any)
286
329
 
287
330
  expect(buildContentDataMock).toHaveBeenCalledTimes(1)
288
331
  expect(generateFaviconsMock).toHaveBeenCalledTimes(1)
@@ -5,7 +5,7 @@ import YAML from 'yaml'
5
5
  import { buildContentData } from './generate-indices.js'
6
6
  import { generateFavicons, generateWebManifest } from './generate-favicons.js'
7
7
  import { startWatcher, syncContent } from './sync-content.js'
8
- import { loadMdsiteConfigSync, resolveMdsiteConfigPath } from '../utils/mdsite-config.js'
8
+ import { loadMdsiteConfigSync, resolveMdsiteConfigPath, type MdsiteConfig } from '../utils/mdsite-config.js'
9
9
 
10
10
  export interface RendererRuntime {
11
11
  config: ReturnType<typeof loadMdsiteConfigSync>['config']
@@ -69,7 +69,7 @@ export async function runSetupHooks(mode: 'setup' | 'build' | 'generate' | 'dev'
69
69
  if (!options.cached) {
70
70
  await fs.promises.rm(path.join(rootDir, '.data'), { recursive: true, force: true })
71
71
  }
72
-
72
+ await generateDevManifestAssets(runtime.config)
73
73
  process.env.MDSITE_RENDERER_ORCHESTRATED = '1'
74
74
  await startWatcher()
75
75
  return runtime
@@ -78,28 +78,32 @@ export async function runSetupHooks(mode: 'setup' | 'build' | 'generate' | 'dev'
78
78
  await syncContent()
79
79
  console.log(`\nšŸ”Ø Generating navigation and search index...`)
80
80
  await buildContentData()
81
- await generateFaviconAssets(runtime.config.site.name)
81
+ await generateFaviconAssets(runtime.config)
82
82
  process.env.MDSITE_RENDERER_ORCHESTRATED = '1'
83
83
 
84
84
  return runtime
85
85
  }
86
86
 
87
- export async function runBuildFallbackHooks(siteName: string): Promise<void> {
87
+ export async function runBuildFallbackHooks(config: MdsiteConfig): Promise<void> {
88
88
  console.log(`\nšŸ”Ø Generating navigation and search index...`)
89
89
  await buildContentData()
90
- await generateFaviconAssets(siteName)
90
+ await generateFaviconAssets(config)
91
91
  }
92
92
 
93
- async function generateFaviconAssets(siteName: string): Promise<void> {
93
+ async function generateFaviconAssets(config: MdsiteConfig): Promise<void> {
94
94
  console.log(`\nšŸŽØ Generating favicons for build...`)
95
95
  const success = await generateFavicons()
96
96
 
97
97
  if (success) {
98
- await generateWebManifest(siteName)
99
- console.log(`āœ… Favicons ready for ${siteName}\n`)
98
+ await generateWebManifest({ name: config.site.name, themes: config.themes })
99
+ console.log(`āœ… Favicons ready for ${config.site.name}\n`)
100
100
  }
101
101
  }
102
102
 
103
+ async function generateDevManifestAssets(config: MdsiteConfig): Promise<void> {
104
+ await generateWebManifest({ name: config.site.name, themes: config.themes })
105
+ }
106
+
103
107
  function ensureLegacyCompatibilityConfig(rootDir: string): string | undefined {
104
108
  const legacyConfigPath = path.join(rootDir, 'content.config.yml')
105
109
 
@@ -164,7 +164,8 @@ export function resolveContentDir(options: {
164
164
 
165
165
  if (resolvedConfigPath) {
166
166
  const configDir = path.dirname(resolvedConfigPath);
167
- return typeof rawConfig.content?.path === 'string' ? path.resolve(configDir, rawConfig.content.path) : configDir;
167
+ const contentPath = resolveContentConfigPath(rawConfig.content);
168
+ return contentPath ? path.resolve(configDir, contentPath) : configDir;
168
169
  }
169
170
 
170
171
  if (options.searchFrom) {
@@ -174,6 +175,24 @@ export function resolveContentDir(options: {
174
175
  return process.cwd();
175
176
  }
176
177
 
178
+ /**
179
+ * Extract the content path from either the shorthand string form
180
+ * (`content: docs`) or the explicit object form (`content:\n path: docs`).
181
+ * Returns undefined when no usable path is configured.
182
+ */
183
+ function resolveContentConfigPath(rawContent: unknown): string | undefined {
184
+ if (typeof rawContent === 'string') {
185
+ const trimmed = rawContent.trim();
186
+ return trimmed.length > 0 ? trimmed : undefined;
187
+ }
188
+
189
+ if (rawContent && typeof rawContent === 'object' && typeof (rawContent as { path?: unknown }).path === 'string') {
190
+ return (rawContent as { path: string }).path;
191
+ }
192
+
193
+ return undefined;
194
+ }
195
+
177
196
  function createDefaultMdsiteConfig(siteName: string): MdsiteConfig {
178
197
  return {
179
198
  favicon: '',
@@ -204,6 +223,7 @@ function createDefaultMdsiteConfig(siteName: string): MdsiteConfig {
204
223
 
205
224
  function normalizeMdsiteConfig(rawConfig: Record<string, any>, contentDir: string): MdsiteConfig {
206
225
  const fallbackConfig = createDefaultMdsiteConfig(path.basename(contentDir) || 'Site');
226
+ const contentPath = resolveContentConfigPath(rawConfig.content);
207
227
 
208
228
  return {
209
229
  favicon: typeof rawConfig.favicon === 'string' ? rawConfig.favicon : fallbackConfig.favicon,
@@ -211,7 +231,7 @@ function normalizeMdsiteConfig(rawConfig: Record<string, any>, contentDir: strin
211
231
  bibleTooltips: rawConfig.features?.bibleTooltips ?? fallbackConfig.features.bibleTooltips,
212
232
  sourceEdit: rawConfig.features?.sourceEdit ?? fallbackConfig.features.sourceEdit
213
233
  },
214
- content: typeof rawConfig.content?.path === 'string' ? { path: rawConfig.content.path } : fallbackConfig.content,
234
+ content: contentPath ? { path: contentPath } : fallbackConfig.content,
215
235
  menu: Array.isArray(rawConfig.menu) ? rawConfig.menu : fallbackConfig.menu,
216
236
  server: {
217
237
  output: typeof rawConfig.server?.output === 'string' ? rawConfig.server.output : fallbackConfig.server.output,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@life-and-dev/mdsite",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Local-first CLI that orchestrates mdsite-nuxt",