@life-and-dev/mdsite 0.2.1 → 0.2.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.
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
</template>
|
|
16
16
|
|
|
17
17
|
<script setup lang="ts">
|
|
18
|
+
import { withBasePath } from '../../utils/base-url'
|
|
18
19
|
const route = useRoute()
|
|
19
20
|
|
|
20
21
|
// Query content using Nuxt Content v3 API
|
|
@@ -29,6 +30,7 @@ const { data: page, pending } = await useAsyncData(
|
|
|
29
30
|
const siteConfig = useSiteConfig()
|
|
30
31
|
const title = page.value?.title || 'Page'
|
|
31
32
|
const description = page.value?.description || ''
|
|
33
|
+
const appBaseURL = useRuntimeConfig().app.baseURL
|
|
32
34
|
|
|
33
35
|
useHead(() => ({
|
|
34
36
|
title,
|
|
@@ -42,9 +44,9 @@ useHead(() => ({
|
|
|
42
44
|
// Open Graph
|
|
43
45
|
{ property: 'og:title', content: title },
|
|
44
46
|
{ property: 'og:description', content: description },
|
|
45
|
-
{ property: 'og:url', content: `${siteConfig.siteCanonical}${route.path}` },
|
|
47
|
+
{ property: 'og:url', content: siteConfig.siteCanonical ? `${siteConfig.siteCanonical}${route.path}` : withBasePath(route.path, appBaseURL) },
|
|
46
48
|
{ property: 'og:type', content: 'article' },
|
|
47
|
-
{ property: 'og:image', content: '/icon-512.png' }
|
|
49
|
+
{ property: 'og:image', content: withBasePath('/icon-512.png', appBaseURL) }
|
|
48
50
|
],
|
|
49
51
|
link: [
|
|
50
52
|
...(siteConfig.siteCanonical ? [{ rel: 'canonical', href: `${siteConfig.siteCanonical}${route.path}` }] : [])
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
</template>
|
|
16
16
|
|
|
17
17
|
<script setup lang="ts">
|
|
18
|
+
import { withBasePath } from '../../utils/base-url'
|
|
18
19
|
// Query home page content using Nuxt Content v3 API
|
|
19
20
|
// server: true = Only query during SSR/prerendering, never on client
|
|
20
21
|
// This prevents 3.5MB database download - client uses prerendered HTML
|
|
@@ -27,6 +28,7 @@ const { data: page, pending } = await useAsyncData(
|
|
|
27
28
|
const siteConfig = useSiteConfig()
|
|
28
29
|
const title = page.value?.title || siteConfig.siteName || 'Home'
|
|
29
30
|
const description = page.value?.description || ''
|
|
31
|
+
const appBaseURL = useRuntimeConfig().app.baseURL
|
|
30
32
|
|
|
31
33
|
useHead(() => ({
|
|
32
34
|
title,
|
|
@@ -40,9 +42,9 @@ useHead(() => ({
|
|
|
40
42
|
// Open Graph
|
|
41
43
|
{ property: 'og:title', content: title },
|
|
42
44
|
{ property: 'og:description', content: description },
|
|
43
|
-
{ property: 'og:url', content: siteConfig.siteCanonical },
|
|
45
|
+
{ property: 'og:url', content: siteConfig.siteCanonical || withBasePath('/', appBaseURL) },
|
|
44
46
|
{ property: 'og:type', content: 'website' },
|
|
45
|
-
{ property: 'og:image', content: '/icon-512.png' }
|
|
47
|
+
{ property: 'og:image', content: withBasePath('/icon-512.png', appBaseURL) }
|
|
46
48
|
],
|
|
47
49
|
link: [
|
|
48
50
|
...(siteConfig.siteCanonical ? [{ rel: 'canonical', href: siteConfig.siteCanonical }] : [])
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
3
|
+
|
|
4
|
+
<svg
|
|
5
|
+
width="348.061"
|
|
6
|
+
height="340"
|
|
7
|
+
viewBox="0 0 34.8061 34"
|
|
8
|
+
version="1.1"
|
|
9
|
+
id="svg1"
|
|
10
|
+
inkscape:version="1.4.3 (1:1.4.3+202512261035+0d15f75042)"
|
|
11
|
+
sodipodi:docname="logo.svg"
|
|
12
|
+
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
13
|
+
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
14
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
xmlns:svg="http://www.w3.org/2000/svg">
|
|
17
|
+
<sodipodi:namedview
|
|
18
|
+
id="namedview1"
|
|
19
|
+
pagecolor="#505050"
|
|
20
|
+
bordercolor="#eeeeee"
|
|
21
|
+
borderopacity="1"
|
|
22
|
+
inkscape:showpageshadow="0"
|
|
23
|
+
inkscape:pageopacity="0"
|
|
24
|
+
inkscape:pagecheckerboard="0"
|
|
25
|
+
inkscape:deskcolor="#505050"
|
|
26
|
+
inkscape:document-units="px"
|
|
27
|
+
showgrid="true"
|
|
28
|
+
inkscape:zoom="2.9179688"
|
|
29
|
+
inkscape:cx="181.11914"
|
|
30
|
+
inkscape:cy="144.27844"
|
|
31
|
+
inkscape:window-width="3032"
|
|
32
|
+
inkscape:window-height="1696"
|
|
33
|
+
inkscape:window-x="40"
|
|
34
|
+
inkscape:window-y="0"
|
|
35
|
+
inkscape:window-maximized="1"
|
|
36
|
+
inkscape:current-layer="layer1">
|
|
37
|
+
<inkscape:grid
|
|
38
|
+
id="grid1"
|
|
39
|
+
units="px"
|
|
40
|
+
originx="1.2100008"
|
|
41
|
+
originy="1"
|
|
42
|
+
spacingx="1"
|
|
43
|
+
spacingy="1"
|
|
44
|
+
empcolor="#0099e5"
|
|
45
|
+
empopacity="0.30196078"
|
|
46
|
+
color="#0099e5"
|
|
47
|
+
opacity="0.14901961"
|
|
48
|
+
empspacing="5"
|
|
49
|
+
enabled="true"
|
|
50
|
+
visible="true" />
|
|
51
|
+
</sodipodi:namedview>
|
|
52
|
+
<defs
|
|
53
|
+
id="defs1">
|
|
54
|
+
<linearGradient
|
|
55
|
+
id="linearGradient12"
|
|
56
|
+
inkscape:collect="always">
|
|
57
|
+
<stop
|
|
58
|
+
style="stop-color:#000000;stop-opacity:1;"
|
|
59
|
+
offset="0"
|
|
60
|
+
id="stop13" />
|
|
61
|
+
<stop
|
|
62
|
+
style="stop-color:#000000;stop-opacity:0;"
|
|
63
|
+
offset="1"
|
|
64
|
+
id="stop14" />
|
|
65
|
+
</linearGradient>
|
|
66
|
+
<linearGradient
|
|
67
|
+
id="linearGradient4"
|
|
68
|
+
inkscape:collect="always">
|
|
69
|
+
<stop
|
|
70
|
+
style="stop-color:#ffffff;stop-opacity:1;"
|
|
71
|
+
offset="0.49735323"
|
|
72
|
+
id="stop5" />
|
|
73
|
+
<stop
|
|
74
|
+
style="stop-color:#ffffff;stop-opacity:0.75119454;"
|
|
75
|
+
offset="1"
|
|
76
|
+
id="stop6" />
|
|
77
|
+
</linearGradient>
|
|
78
|
+
<linearGradient
|
|
79
|
+
id="linearGradient3"
|
|
80
|
+
inkscape:collect="always">
|
|
81
|
+
<stop
|
|
82
|
+
style="stop-color:#f9f9f9;stop-opacity:1;"
|
|
83
|
+
offset="0.49600685"
|
|
84
|
+
id="stop3" />
|
|
85
|
+
<stop
|
|
86
|
+
style="stop-color:#ffffff;stop-opacity:0.7457487;"
|
|
87
|
+
offset="1"
|
|
88
|
+
id="stop4" />
|
|
89
|
+
</linearGradient>
|
|
90
|
+
<radialGradient
|
|
91
|
+
inkscape:collect="always"
|
|
92
|
+
xlink:href="#linearGradient3"
|
|
93
|
+
id="radialGradient4"
|
|
94
|
+
cx="10.5"
|
|
95
|
+
cy="16"
|
|
96
|
+
fx="10.5"
|
|
97
|
+
fy="16"
|
|
98
|
+
r="10.5"
|
|
99
|
+
gradientTransform="matrix(1,0,0,1.3333333,0,-5.3333333)"
|
|
100
|
+
gradientUnits="userSpaceOnUse" />
|
|
101
|
+
<radialGradient
|
|
102
|
+
inkscape:collect="always"
|
|
103
|
+
xlink:href="#linearGradient4"
|
|
104
|
+
id="radialGradient6"
|
|
105
|
+
cx="24.514727"
|
|
106
|
+
cy="12"
|
|
107
|
+
fx="24.514727"
|
|
108
|
+
fy="12"
|
|
109
|
+
r="7.5147257"
|
|
110
|
+
gradientTransform="matrix(1,0,0,1.3307206,0,-3.9686472)"
|
|
111
|
+
gradientUnits="userSpaceOnUse" />
|
|
112
|
+
<radialGradient
|
|
113
|
+
inkscape:collect="always"
|
|
114
|
+
xlink:href="#linearGradient12"
|
|
115
|
+
id="radialGradient14"
|
|
116
|
+
cx="16"
|
|
117
|
+
cy="16"
|
|
118
|
+
fx="16"
|
|
119
|
+
fy="16"
|
|
120
|
+
r="16"
|
|
121
|
+
gradientUnits="userSpaceOnUse"
|
|
122
|
+
gradientTransform="matrix(1.0624999,0,0,1.0624999,0.21000074,0)" />
|
|
123
|
+
<filter
|
|
124
|
+
style="color-interpolation-filters:sRGB"
|
|
125
|
+
inkscape:label="Drop Shadow"
|
|
126
|
+
id="filter66"
|
|
127
|
+
x="-0.057619048"
|
|
128
|
+
y="-0.055841193"
|
|
129
|
+
width="1.132074"
|
|
130
|
+
height="1.1384681">
|
|
131
|
+
<feFlood
|
|
132
|
+
result="flood"
|
|
133
|
+
in="SourceGraphic"
|
|
134
|
+
flood-opacity="0.498039"
|
|
135
|
+
flood-color="rgb(0,0,0)"
|
|
136
|
+
id="feFlood65" />
|
|
137
|
+
<feGaussianBlur
|
|
138
|
+
result="blur"
|
|
139
|
+
in="SourceGraphic"
|
|
140
|
+
stdDeviation="0.400000"
|
|
141
|
+
id="feGaussianBlur65" />
|
|
142
|
+
<feOffset
|
|
143
|
+
result="offset"
|
|
144
|
+
in="blur"
|
|
145
|
+
dx="0.000000"
|
|
146
|
+
dy="1.000000"
|
|
147
|
+
id="feOffset65" />
|
|
148
|
+
<feComposite
|
|
149
|
+
result="comp1"
|
|
150
|
+
operator="in"
|
|
151
|
+
in="flood"
|
|
152
|
+
in2="offset"
|
|
153
|
+
id="feComposite65" />
|
|
154
|
+
<feComposite
|
|
155
|
+
result="comp2"
|
|
156
|
+
operator="over"
|
|
157
|
+
in="SourceGraphic"
|
|
158
|
+
in2="comp1"
|
|
159
|
+
id="feComposite66" />
|
|
160
|
+
</filter>
|
|
161
|
+
<filter
|
|
162
|
+
style="color-interpolation-filters:sRGB"
|
|
163
|
+
inkscape:label="Drop Shadow"
|
|
164
|
+
id="filter68"
|
|
165
|
+
x="-0.10403264"
|
|
166
|
+
y="-0.060500001"
|
|
167
|
+
width="1.2082745"
|
|
168
|
+
height="1.171">
|
|
169
|
+
<feFlood
|
|
170
|
+
result="flood"
|
|
171
|
+
in="SourceGraphic"
|
|
172
|
+
flood-opacity="0.498039"
|
|
173
|
+
flood-color="rgb(0,0,0)"
|
|
174
|
+
id="feFlood66" />
|
|
175
|
+
<feGaussianBlur
|
|
176
|
+
result="blur"
|
|
177
|
+
in="SourceGraphic"
|
|
178
|
+
stdDeviation="0.400000"
|
|
179
|
+
id="feGaussianBlur66" />
|
|
180
|
+
<feOffset
|
|
181
|
+
result="offset"
|
|
182
|
+
in="blur"
|
|
183
|
+
dx="0.000000"
|
|
184
|
+
dy="1.000000"
|
|
185
|
+
id="feOffset66" />
|
|
186
|
+
<feComposite
|
|
187
|
+
result="comp1"
|
|
188
|
+
operator="in"
|
|
189
|
+
in="flood"
|
|
190
|
+
in2="offset"
|
|
191
|
+
id="feComposite67" />
|
|
192
|
+
<feComposite
|
|
193
|
+
result="comp2"
|
|
194
|
+
operator="over"
|
|
195
|
+
in="SourceGraphic"
|
|
196
|
+
in2="comp1"
|
|
197
|
+
id="feComposite68" />
|
|
198
|
+
</filter>
|
|
199
|
+
</defs>
|
|
200
|
+
<rect
|
|
201
|
+
style="display:inline;fill:url(#radialGradient14);stroke:none;stroke-width:1.0625"
|
|
202
|
+
id="rect2"
|
|
203
|
+
width="34"
|
|
204
|
+
height="34"
|
|
205
|
+
x="0.21000074"
|
|
206
|
+
y="0" />
|
|
207
|
+
<g
|
|
208
|
+
inkscape:label="Layer 1"
|
|
209
|
+
inkscape:groupmode="layer"
|
|
210
|
+
id="layer1"
|
|
211
|
+
transform="translate(1.21,1)">
|
|
212
|
+
<path
|
|
213
|
+
style="fill:url(#radialGradient4);stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter66)"
|
|
214
|
+
d="M 0,22 V 2 l 8,8 8,-8 0.03029,21 H 21 l -7,7 -7,-7 h 5.030291 L 12,11 8,15 4,11 v 11 z"
|
|
215
|
+
id="path1"
|
|
216
|
+
sodipodi:nodetypes="cccccccccccccc" />
|
|
217
|
+
<path
|
|
218
|
+
style="fill:url(#radialGradient6);stroke:#000000;stroke-width:0.5;stroke-linecap:square;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter68)"
|
|
219
|
+
d="m 21,18 -4,4 h 11 l 4,-4 V 14 L 28,10 H 23 L 21,8 23,6 h 5 L 32.029451,2 H 21.05087 L 17,5.8781794 V 10.014726 L 21,14 h 5 l 2,2 -2,2 z"
|
|
220
|
+
id="path2"
|
|
221
|
+
sodipodi:nodetypes="ccccccccccccccccccc" />
|
|
222
|
+
</g>
|
|
223
|
+
</svg>
|
|
@@ -0,0 +1,123 @@
|
|
|
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, 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
|
+
})
|
|
@@ -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'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import { loadMdsiteConfigSync, resolveMdsiteConfigPath } 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
62
|
+
const { sourcePath } = resolvedSource
|
|
63
|
+
const publicDir = options.outputDir ?? path.resolve(__dirname, '..', 'public')
|
|
38
64
|
await fs.ensureDir(publicDir)
|
|
39
65
|
|
|
40
|
-
|
|
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,10 +122,10 @@ 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 ${
|
|
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 ${
|
|
128
|
+
console.error(`❌ Failed to generate favicons for ${siteName}:`, error)
|
|
99
129
|
return false
|
|
100
130
|
}
|
|
101
131
|
}
|