slidev-workspace 0.4.2 → 0.5.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/dist/cli.js CHANGED
@@ -80,6 +80,8 @@ function getSlideFrontmatterByPath(slideDir, slideName) {
80
80
  const frontmatter = parse(frontmatterYaml);
81
81
  const sourceBasename = basename(slideDir);
82
82
  const slideId = `${sourceBasename}/${slideName}`;
83
+ const ogImagePath = join$1(slideDir, slideName, "og-image.png");
84
+ const hasOgImage = existsSync$1(ogImagePath);
83
85
  return {
84
86
  id: slideId,
85
87
  path: slideName,
@@ -87,7 +89,8 @@ function getSlideFrontmatterByPath(slideDir, slideName) {
87
89
  sourceDir: slideDir,
88
90
  frontmatter,
89
91
  content: content.replace(frontmatterMatch[0], ""),
90
- baseUrl: config.baseUrl
92
+ baseUrl: config.baseUrl,
93
+ hasOgImage
91
94
  };
92
95
  } catch (error) {
93
96
  console.error(`Error parsing frontmatter for ${slideName} in ${slideDir}:`, error);
package/dist/index.d.ts CHANGED
@@ -29,6 +29,8 @@ interface SlideInfo {
29
29
  content: string;
30
30
  /** The base URL of the slide, which is defined in slidev-workspace.yml */
31
31
  baseUrl: string;
32
+ /** Whether og-image.png exists in the slide directory */
33
+ hasOgImage: boolean;
32
34
  }
33
35
  interface SlideData {
34
36
  title: string;
@@ -43,6 +45,7 @@ interface SlideData {
43
45
  }
44
46
  //#endregion
45
47
  //#region src/preview/composables/useSlides.d.ts
48
+
46
49
  declare function useSlides(): {
47
50
  slides: vue0.ComputedRef<SlideData[]>;
48
51
  slidesCount: vue0.ComputedRef<number>;
package/dist/index.js CHANGED
@@ -45,37 +45,48 @@ function isUrl(str) {
45
45
  }
46
46
  }
47
47
  /**
48
- * Resolve background image path.
49
- * If the background is not a URL, construct the full path using slide.path as the base.
48
+ * Resolve image URL with fallback priority:
49
+ * 1. og-image.png (if it exists in the slides root directory)
50
+ * 2. seoMeta.ogImage (explicit og-image config)
51
+ * 3. background (background image)
52
+ * 4. default cover image (https://cover.sli.dev)
50
53
  *
51
- * Example (development mode):
52
- * {
53
- * background: "/bg1.jpg",
54
- * slidePath: "/slides-presentation-1",
55
- * baseUrl: "/slidev-workspace-starter/",
56
- * domain: "http://localhost:3001" // domain of the current server
57
- * }
58
- * returns: "http://localhost:3001/slides-presentation-1/bg1.jpg"
54
+ * Example (development mode with og-image.png):
55
+ * returns: "http://localhost:3001/og-image.png"
59
56
  *
60
- * Example (production mode):
61
- * {
62
- * background: "/bg1.jpg",
63
- * slidePath: "/slides-presentation-1",
64
- * baseUrl: "/slidev-workspace-starter/",
65
- * domain: "https://my-slides.com" // domain of the current server
66
- * }
67
- * returns: "https://my-slides.com/slidev-workspace-starter/slides-presentation-1/bg1.jpg"
57
+ * Example (production mode with og-image.png):
58
+ * returns: "https://my-slides.com/slidev-workspace-starter/og-image.png"
68
59
  */
69
- function resolveBackgroundPath(params) {
70
- const { background, slidePath, domain, baseUrl } = params;
71
- if (!background) return "";
72
- if (isUrl(background)) return background;
73
- try {
74
- return IS_DEVELOPMENT ? new URL(pathJoin(slidePath, background), domain).href : new URL(pathJoin(baseUrl, slidePath, background), domain).href;
60
+ function resolveImageUrl(slide, domain) {
61
+ const { hasOgImage, path: slidePath, baseUrl, frontmatter } = slide;
62
+ const seoOgImage = frontmatter.seoMeta?.ogImage;
63
+ const background = frontmatter.background;
64
+ if (hasOgImage) try {
65
+ const path = IS_DEVELOPMENT ? "/og-image.png" : pathJoin(baseUrl, "og-image.png");
66
+ return new URL(path, domain).href;
75
67
  } catch (error) {
76
- console.error("Failed to resolve background path:", error);
77
- return background;
68
+ console.error("Failed to resolve og-image.png path:", error);
69
+ return "https://cover.sli.dev";
70
+ }
71
+ if (seoOgImage) {
72
+ if (isUrl(seoOgImage)) return seoOgImage;
73
+ try {
74
+ return IS_DEVELOPMENT ? new URL(seoOgImage, domain).href : new URL(pathJoin(baseUrl, slidePath, seoOgImage), domain).href;
75
+ } catch (error) {
76
+ console.error("Failed to resolve seoMeta.ogImage path:", error);
77
+ return "https://cover.sli.dev";
78
+ }
79
+ }
80
+ if (background) {
81
+ if (isUrl(background)) return background;
82
+ try {
83
+ return IS_DEVELOPMENT ? new URL(background, domain).href : new URL(pathJoin(baseUrl, slidePath, background), domain).href;
84
+ } catch (error) {
85
+ console.error("Failed to resolve background path:", error);
86
+ return "https://cover.sli.dev";
87
+ }
78
88
  }
89
+ return "https://cover.sli.dev";
79
90
  }
80
91
  function useSlides() {
81
92
  const slidesData = ref([]);
@@ -97,13 +108,8 @@ function useSlides() {
97
108
  return slidesData.value.map((slide, index) => {
98
109
  const port = 3001 + index;
99
110
  const devServerUrl = `http://localhost:${port}`;
100
- const background = slide.frontmatter.seoMeta?.ogImage || slide.frontmatter.background;
101
- const imageUrl = background ? resolveBackgroundPath({
102
- background: slide.frontmatter.background,
103
- slidePath: slide.path,
104
- baseUrl: slide.baseUrl,
105
- domain: IS_DEVELOPMENT ? devServerUrl : window.location.origin
106
- }) : "https://cover.sli.dev";
111
+ const domain = IS_DEVELOPMENT ? devServerUrl : window.location.origin;
112
+ const imageUrl = resolveImageUrl(slide, domain);
107
113
  return {
108
114
  title: slide.frontmatter.title || slide.path,
109
115
  url: IS_DEVELOPMENT ? devServerUrl : slide.path,
@@ -71,6 +71,8 @@ function getSlideFrontmatterByPath(slideDir, slideName) {
71
71
  const frontmatter = parse(frontmatterYaml);
72
72
  const sourceBasename = basename(slideDir);
73
73
  const slideId = `${sourceBasename}/${slideName}`;
74
+ const ogImagePath = join(slideDir, slideName, "og-image.png");
75
+ const hasOgImage = existsSync(ogImagePath);
74
76
  return {
75
77
  id: slideId,
76
78
  path: slideName,
@@ -78,7 +80,8 @@ function getSlideFrontmatterByPath(slideDir, slideName) {
78
80
  sourceDir: slideDir,
79
81
  frontmatter,
80
82
  content: content.replace(frontmatterMatch[0], ""),
81
- baseUrl: config.baseUrl
83
+ baseUrl: config.baseUrl,
84
+ hasOgImage
82
85
  };
83
86
  } catch (error) {
84
87
  console.error(`Error parsing frontmatter for ${slideName} in ${slideDir}:`, error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidev-workspace",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "A workspace tool for managing multiple Slidev presentations with API-based content management",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -5,11 +5,26 @@
5
5
  @click="$emit('click')"
6
6
  >
7
7
  <div class="relative overflow-hidden rounded-t-lg">
8
+ <Skeleton
9
+ v-if="isLoading && image"
10
+ class="w-full h-48 rounded-b-none"
11
+ />
8
12
  <img
9
- :src="image || '/placeholder.svg'"
13
+ v-if="image"
14
+ ref="imageRef"
15
+ v-show="!isLoading"
16
+ :src="image"
10
17
  :alt="title"
11
18
  class="w-full h-48 object-cover group-hover:scale-105 transition-transform duration-200"
19
+ @load="onImageLoad"
20
+ @error="onImageError"
12
21
  />
22
+ <div
23
+ v-if="!image"
24
+ class="w-full h-48 bg-muted flex items-center justify-center"
25
+ >
26
+ <span class="text-muted-foreground">No Image</span>
27
+ </div>
13
28
  </div>
14
29
 
15
30
  <CardHeader class="pb-2">
@@ -58,6 +73,7 @@
58
73
  </template>
59
74
 
60
75
  <script setup lang="ts">
76
+ import { ref, useTemplateRef } from "vue";
61
77
  import { Calendar, User } from "lucide-vue-next";
62
78
 
63
79
  import {
@@ -67,8 +83,9 @@ import {
67
83
  CardHeader,
68
84
  CardTitle,
69
85
  } from "@/components/ui/card";
86
+ import { Skeleton } from "@/components/ui/skeleton";
70
87
 
71
- defineProps<{
88
+ const props = defineProps<{
72
89
  title: string;
73
90
  image?: string;
74
91
  description?: string;
@@ -82,4 +99,29 @@ defineProps<{
82
99
  defineEmits<{
83
100
  click: [];
84
101
  }>();
102
+
103
+ const imageRef = useTemplateRef<HTMLImageElement>("imageRef");
104
+ const isLoading = ref(true);
105
+ const retryCount = ref(0);
106
+
107
+ const MAX_RETRIES = 5;
108
+ const RETRY_DELAY = 1000; // 1 second
109
+
110
+ const onImageLoad = () => {
111
+ isLoading.value = false;
112
+ };
113
+
114
+ const onImageError = () => {
115
+ if (retryCount.value < MAX_RETRIES) {
116
+ retryCount.value++;
117
+
118
+ setTimeout(() => {
119
+ if (imageRef.value && props.image) {
120
+ imageRef.value.src = props.image;
121
+ }
122
+ }, RETRY_DELAY);
123
+ } else {
124
+ isLoading.value = false;
125
+ }
126
+ };
85
127
  </script>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ interface SkeletonProps {
6
+ class?: HTMLAttributes["class"];
7
+ }
8
+
9
+ const props = defineProps<SkeletonProps>();
10
+ </script>
11
+
12
+ <template>
13
+ <div
14
+ data-slot="skeleton"
15
+ :class="cn('animate-pulse rounded-md bg-primary/10', props.class)"
16
+ />
17
+ </template>
@@ -0,0 +1 @@
1
+ export { default as Skeleton } from "./Skeleton.vue";
@@ -18,6 +18,7 @@ const mockSlidesData: SlideInfo[] = [
18
18
  },
19
19
  content: "# Slide content",
20
20
  baseUrl: "/slidev-workspace-starter/",
21
+ hasOgImage: true,
21
22
  },
22
23
  {
23
24
  id: "slide-2",
@@ -34,6 +35,7 @@ const mockSlidesData: SlideInfo[] = [
34
35
  },
35
36
  content: "# Another slide",
36
37
  baseUrl: "/slidev-workspace-starter/",
38
+ hasOgImage: false,
37
39
  },
38
40
  {
39
41
  id: "slide-3",
@@ -43,6 +45,7 @@ const mockSlidesData: SlideInfo[] = [
43
45
  frontmatter: {},
44
46
  content: "# Minimal slide",
45
47
  baseUrl: "/slidev-workspace-starter/",
48
+ hasOgImage: false,
46
49
  },
47
50
  ];
48
51
 
@@ -7,6 +7,317 @@ import {
7
7
  afterEach,
8
8
  beforeAll,
9
9
  } from "vitest";
10
+ import type { SlideInfo } from "../../types/slide";
11
+
12
+ /**
13
+ * Helper function to create a mock SlideInfo object
14
+ */
15
+ function createMockSlide(overrides: Partial<SlideInfo> = {}): SlideInfo {
16
+ return {
17
+ path: "/slides-presentation-1/",
18
+ baseUrl: "/slidev-workspace-starter",
19
+ hasOgImage: false,
20
+ frontmatter: {
21
+ title: "Test Slide",
22
+ author: "Test Author",
23
+ date: "2024-01-01",
24
+ },
25
+ ...overrides,
26
+ } as SlideInfo;
27
+ }
28
+
29
+ describe("resolveImageUrl (Development Mode)", () => {
30
+ beforeAll(async () => {
31
+ vi.resetModules();
32
+ vi.doMock("../constants/env", () => ({
33
+ IS_DEVELOPMENT: true,
34
+ }));
35
+ });
36
+
37
+ afterEach(() => {
38
+ vi.restoreAllMocks();
39
+ });
40
+
41
+ it("should return og-image.png URL when hasOgImage is true in development", async () => {
42
+ const { resolveImageUrl } = await import("./useSlides");
43
+ const slide = createMockSlide({ hasOgImage: true });
44
+ const domain = "http://localhost:3001";
45
+
46
+ const result = resolveImageUrl(slide, domain);
47
+
48
+ expect(result).toBe("http://localhost:3001/og-image.png");
49
+ });
50
+
51
+ it("should return seoMeta.ogImage when it's an absolute URL", async () => {
52
+ const { resolveImageUrl } = await import("./useSlides");
53
+ const slide = createMockSlide({
54
+ hasOgImage: false,
55
+ frontmatter: {
56
+ title: "Test",
57
+ seoMeta: {
58
+ ogImage: "https://example.com/image.jpg",
59
+ },
60
+ },
61
+ });
62
+ const domain = "http://localhost:3001";
63
+
64
+ const result = resolveImageUrl(slide, domain);
65
+
66
+ expect(result).toBe("https://example.com/image.jpg");
67
+ });
68
+
69
+ it("should resolve relative seoMeta.ogImage URL in development", async () => {
70
+ const { resolveImageUrl } = await import("./useSlides");
71
+ const slide = createMockSlide({
72
+ hasOgImage: false,
73
+ path: "/slides-presentation-1/",
74
+ frontmatter: {
75
+ title: "Test",
76
+ seoMeta: {
77
+ ogImage: "custom-image.jpg",
78
+ },
79
+ },
80
+ });
81
+ const domain = "http://localhost:3001";
82
+
83
+ const result = resolveImageUrl(slide, domain);
84
+
85
+ expect(result).toBe("http://localhost:3001/custom-image.jpg");
86
+ });
87
+
88
+ it("should return absolute background URL unchanged", async () => {
89
+ const { resolveImageUrl } = await import("./useSlides");
90
+ const slide = createMockSlide({
91
+ hasOgImage: false,
92
+ frontmatter: {
93
+ title: "Test",
94
+ background: "https://example.com/bg.jpg",
95
+ },
96
+ });
97
+ const domain = "http://localhost:3001";
98
+
99
+ const result = resolveImageUrl(slide, domain);
100
+
101
+ expect(result).toBe("https://example.com/bg.jpg");
102
+ });
103
+
104
+ it("should resolve relative background URL in development", async () => {
105
+ const { resolveImageUrl } = await import("./useSlides");
106
+ const slide = createMockSlide({
107
+ hasOgImage: false,
108
+ path: "/slides-presentation-1/",
109
+ frontmatter: {
110
+ title: "Test",
111
+ background: "background.jpg",
112
+ },
113
+ });
114
+ const domain = "http://localhost:3001";
115
+
116
+ const result = resolveImageUrl(slide, domain);
117
+
118
+ expect(result).toBe("http://localhost:3001/background.jpg");
119
+ });
120
+
121
+ it("should return default cover when no image sources provided", async () => {
122
+ const { resolveImageUrl } = await import("./useSlides");
123
+ const slide = createMockSlide({
124
+ hasOgImage: false,
125
+ frontmatter: {
126
+ title: "Test",
127
+ },
128
+ });
129
+ const domain = "http://localhost:3001";
130
+
131
+ const result = resolveImageUrl(slide, domain);
132
+
133
+ expect(result).toBe("https://cover.sli.dev");
134
+ });
135
+
136
+ it("should prioritize og-image.png over seoMeta.ogImage", async () => {
137
+ const { resolveImageUrl } = await import("./useSlides");
138
+ const slide = createMockSlide({
139
+ hasOgImage: true,
140
+ frontmatter: {
141
+ title: "Test",
142
+ seoMeta: {
143
+ ogImage: "https://example.com/image.jpg",
144
+ },
145
+ },
146
+ });
147
+ const domain = "http://localhost:3001";
148
+
149
+ const result = resolveImageUrl(slide, domain);
150
+
151
+ expect(result).toBe("http://localhost:3001/og-image.png");
152
+ expect(result).not.toBe("https://example.com/image.jpg");
153
+ });
154
+
155
+ it("should prioritize seoMeta.ogImage over background", async () => {
156
+ const { resolveImageUrl } = await import("./useSlides");
157
+ const slide = createMockSlide({
158
+ hasOgImage: false,
159
+ frontmatter: {
160
+ title: "Test",
161
+ seoMeta: {
162
+ ogImage: "https://example.com/image.jpg",
163
+ },
164
+ background: "https://example.com/bg.jpg",
165
+ },
166
+ });
167
+ const domain = "http://localhost:3001";
168
+
169
+ const result = resolveImageUrl(slide, domain);
170
+
171
+ expect(result).toBe("https://example.com/image.jpg");
172
+ expect(result).not.toBe("https://example.com/bg.jpg");
173
+ });
174
+
175
+ it("should return default cover on URL construction error", async () => {
176
+ const { resolveImageUrl } = await import("./useSlides");
177
+ const consoleErrorSpy = vi
178
+ .spyOn(console, "error")
179
+ .mockImplementation(() => {});
180
+
181
+ const slide = createMockSlide({
182
+ hasOgImage: false,
183
+ frontmatter: {
184
+ title: "Test",
185
+ seoMeta: {
186
+ ogImage: "not a valid url at all!!!",
187
+ },
188
+ },
189
+ });
190
+ const domain = "invalid domain";
191
+
192
+ const result = resolveImageUrl(slide, domain);
193
+
194
+ expect(result).toBe("https://cover.sli.dev");
195
+ expect(consoleErrorSpy).toHaveBeenCalled();
196
+
197
+ consoleErrorSpy.mockRestore();
198
+ });
199
+ });
200
+
201
+ describe("resolveImageUrl (Production Mode)", () => {
202
+ beforeAll(async () => {
203
+ vi.resetModules();
204
+ vi.doMock("../constants/env", () => ({
205
+ IS_DEVELOPMENT: false,
206
+ }));
207
+ });
208
+
209
+ afterEach(() => {
210
+ vi.restoreAllMocks();
211
+ });
212
+
213
+ it("should return og-image.png URL with baseUrl in production", async () => {
214
+ const { resolveImageUrl } = await import("./useSlides");
215
+ const slide = createMockSlide({
216
+ hasOgImage: true,
217
+ baseUrl: "/slidev-workspace-starter",
218
+ });
219
+ const domain = "https://my-slides.com";
220
+
221
+ const result = resolveImageUrl(slide, domain);
222
+
223
+ expect(result).toBe(
224
+ "https://my-slides.com/slidev-workspace-starter/og-image.png",
225
+ );
226
+ });
227
+
228
+ it("should resolve seoMeta.ogImage with baseUrl and path in production", async () => {
229
+ const { resolveImageUrl } = await import("./useSlides");
230
+ const slide = createMockSlide({
231
+ hasOgImage: false,
232
+ baseUrl: "/slidev-workspace-starter",
233
+ path: "/slides-presentation-1/",
234
+ frontmatter: {
235
+ title: "Test",
236
+ seoMeta: {
237
+ ogImage: "custom-image.jpg",
238
+ },
239
+ },
240
+ });
241
+ const domain = "https://my-slides.com";
242
+
243
+ const result = resolveImageUrl(slide, domain);
244
+
245
+ expect(result).toBe(
246
+ "https://my-slides.com/slidev-workspace-starter/slides-presentation-1/custom-image.jpg",
247
+ );
248
+ });
249
+
250
+ it("should resolve background with baseUrl and path in production", async () => {
251
+ const { resolveImageUrl } = await import("./useSlides");
252
+ const slide = createMockSlide({
253
+ hasOgImage: false,
254
+ baseUrl: "/slidev-workspace-starter",
255
+ path: "/slides-presentation-1/",
256
+ frontmatter: {
257
+ title: "Test",
258
+ background: "background.jpg",
259
+ },
260
+ });
261
+ const domain = "https://my-slides.com";
262
+
263
+ const result = resolveImageUrl(slide, domain);
264
+
265
+ expect(result).toBe(
266
+ "https://my-slides.com/slidev-workspace-starter/slides-presentation-1/background.jpg",
267
+ );
268
+ });
269
+
270
+ it("should keep absolute URL seoMeta.ogImage unchanged in production", async () => {
271
+ const { resolveImageUrl } = await import("./useSlides");
272
+ const slide = createMockSlide({
273
+ hasOgImage: false,
274
+ baseUrl: "/slidev-workspace-starter",
275
+ frontmatter: {
276
+ title: "Test",
277
+ seoMeta: {
278
+ ogImage: "https://example.com/image.jpg",
279
+ },
280
+ },
281
+ });
282
+ const domain = "https://my-slides.com";
283
+
284
+ const result = resolveImageUrl(slide, domain);
285
+
286
+ expect(result).toBe("https://example.com/image.jpg");
287
+ });
288
+
289
+ it("should keep absolute URL background unchanged in production", async () => {
290
+ const { resolveImageUrl } = await import("./useSlides");
291
+ const slide = createMockSlide({
292
+ hasOgImage: false,
293
+ baseUrl: "/slidev-workspace-starter",
294
+ frontmatter: {
295
+ title: "Test",
296
+ background: "https://example.com/bg.jpg",
297
+ },
298
+ });
299
+ const domain = "https://my-slides.com";
300
+
301
+ const result = resolveImageUrl(slide, domain);
302
+
303
+ expect(result).toBe("https://example.com/bg.jpg");
304
+ });
305
+
306
+ it("should return default cover when no image sources in production", async () => {
307
+ const { resolveImageUrl } = await import("./useSlides");
308
+ const slide = createMockSlide({
309
+ hasOgImage: false,
310
+ frontmatter: {
311
+ title: "Test",
312
+ },
313
+ });
314
+ const domain = "https://my-slides.com";
315
+
316
+ const result = resolveImageUrl(slide, domain);
317
+
318
+ expect(result).toBe("https://cover.sli.dev");
319
+ });
320
+ });
10
321
 
11
322
  describe("useSlides (Development Mode)", () => {
12
323
  beforeAll(async () => {
@@ -122,7 +433,7 @@ describe("useSlides (Development Mode)", () => {
122
433
 
123
434
  const secondSlide = slides.value[1];
124
435
 
125
- expect(secondSlide.image).toBe("https://example.com/bg.jpg");
436
+ expect(secondSlide.image).toBe("https://example.com/og-image.jpg");
126
437
  });
127
438
 
128
439
  it("should resolve relative background paths in development mode", async () => {
@@ -130,9 +441,7 @@ describe("useSlides (Development Mode)", () => {
130
441
 
131
442
  const firstSlide = slides.value[0];
132
443
 
133
- expect(firstSlide.image).toBe(
134
- "http://localhost:3001/slides-presentation-1/bg1.jpg",
135
- );
444
+ expect(firstSlide.image).toBe("http://localhost:3001/og-image.png");
136
445
  });
137
446
 
138
447
  it("should use default cover when no background is provided", async () => {
@@ -143,6 +452,35 @@ describe("useSlides (Development Mode)", () => {
143
452
  expect(thirdSlide.image).toBe("https://cover.sli.dev");
144
453
  });
145
454
  });
455
+
456
+ describe("og-image.png priority", () => {
457
+ it("should prioritize og-image.png when hasOgImage is true", async () => {
458
+ const { slides } = await setupUseSlides();
459
+
460
+ const firstSlide = slides.value[0];
461
+
462
+ // First slide has hasOgImage: true, so should use og-image.png
463
+ expect(firstSlide.image).toContain("og-image.png");
464
+ });
465
+
466
+ it("should use seoMeta.ogImage when hasOgImage is false", async () => {
467
+ const { slides } = await setupUseSlides();
468
+
469
+ const secondSlide = slides.value[1];
470
+
471
+ // Second slide has hasOgImage: false, so should use seoMeta.ogImage
472
+ expect(secondSlide.image).toBe("https://example.com/og-image.jpg");
473
+ });
474
+
475
+ it("should use background when hasOgImage is false and no seoMeta.ogImage", async () => {
476
+ const { slides } = await setupUseSlides();
477
+
478
+ const thirdSlide = slides.value[2];
479
+
480
+ // Third slide has hasOgImage: false and no seoMeta, so should use background or default
481
+ expect(thirdSlide.image).toBe("https://cover.sli.dev");
482
+ });
483
+ });
146
484
  });
147
485
 
148
486
  describe("useSlides (Production Mode)", () => {
@@ -201,7 +539,7 @@ describe("useSlides (Production Mode)", () => {
201
539
  const firstSlide = result.slides.value[0];
202
540
 
203
541
  expect(firstSlide.image).toBe(
204
- "https://my-slides.com/slidev-workspace-starter/slides-presentation-1/bg1.jpg",
542
+ "https://my-slides.com/slidev-workspace-starter/og-image.png",
205
543
  );
206
544
  });
207
545
 
@@ -209,7 +547,7 @@ describe("useSlides (Production Mode)", () => {
209
547
  const result = await setupUseSlidesProduction();
210
548
  const secondSlide = result.slides.value[1];
211
549
 
212
- expect(secondSlide.image).toBe("https://example.com/bg.jpg");
550
+ expect(secondSlide.image).toBe("https://example.com/og-image.jpg");
213
551
  });
214
552
 
215
553
  it("should use default cover when no background in production", async () => {
@@ -219,4 +557,25 @@ describe("useSlides (Production Mode)", () => {
219
557
  expect(thirdSlide.image).toBe("https://cover.sli.dev");
220
558
  });
221
559
  });
560
+
561
+ describe("og-image.png priority in production", () => {
562
+ it("should prioritize og-image.png when hasOgImage is true in production", async () => {
563
+ const result = await setupUseSlidesProduction();
564
+ const firstSlide = result.slides.value[0];
565
+
566
+ // First slide has hasOgImage: true, so should use og-image.png
567
+ expect(firstSlide.image).toContain("og-image.png");
568
+ expect(firstSlide.image).toBe(
569
+ "https://my-slides.com/slidev-workspace-starter/og-image.png",
570
+ );
571
+ });
572
+
573
+ it("should use seoMeta.ogImage when hasOgImage is false in production", async () => {
574
+ const result = await setupUseSlidesProduction();
575
+ const secondSlide = result.slides.value[1];
576
+
577
+ // Second slide has hasOgImage: false, so should use seoMeta.ogImage
578
+ expect(secondSlide.image).toBe("https://example.com/og-image.jpg");
579
+ });
580
+ });
222
581
  });
@@ -18,51 +18,70 @@ function isUrl(str: string | undefined): boolean {
18
18
  }
19
19
 
20
20
  /**
21
- * Resolve background image path.
22
- * If the background is not a URL, construct the full path using slide.path as the base.
21
+ * Resolve image URL with fallback priority:
22
+ * 1. og-image.png (if it exists in the slides root directory)
23
+ * 2. seoMeta.ogImage (explicit og-image config)
24
+ * 3. background (background image)
25
+ * 4. default cover image (https://cover.sli.dev)
23
26
  *
24
- * Example (development mode):
25
- * {
26
- * background: "/bg1.jpg",
27
- * slidePath: "/slides-presentation-1",
28
- * baseUrl: "/slidev-workspace-starter/",
29
- * domain: "http://localhost:3001" // domain of the current server
30
- * }
31
- * returns: "http://localhost:3001/slides-presentation-1/bg1.jpg"
27
+ * Example (development mode with og-image.png):
28
+ * returns: "http://localhost:3001/og-image.png"
32
29
  *
33
- * Example (production mode):
34
- * {
35
- * background: "/bg1.jpg",
36
- * slidePath: "/slides-presentation-1",
37
- * baseUrl: "/slidev-workspace-starter/",
38
- * domain: "https://my-slides.com" // domain of the current server
39
- * }
40
- * returns: "https://my-slides.com/slidev-workspace-starter/slides-presentation-1/bg1.jpg"
30
+ * Example (production mode with og-image.png):
31
+ * returns: "https://my-slides.com/slidev-workspace-starter/og-image.png"
41
32
  */
42
- function resolveBackgroundPath(params: {
43
- background: string | undefined;
44
- slidePath: string;
45
- baseUrl: string;
46
- domain: string;
47
- }): string {
48
- const { background, slidePath, domain, baseUrl } = params;
49
-
50
- if (!background) {
51
- return "";
33
+ export function resolveImageUrl(slide: SlideInfo, domain: string): string {
34
+ const { hasOgImage, path: slidePath, baseUrl, frontmatter } = slide;
35
+ const seoOgImage = frontmatter.seoMeta?.ogImage;
36
+ const background = frontmatter.background;
37
+
38
+ // Priority 1: og-image.png (if exists)
39
+ if (hasOgImage) {
40
+ try {
41
+ const path = IS_DEVELOPMENT
42
+ ? "/og-image.png"
43
+ : pathJoin(baseUrl, "og-image.png");
44
+ return new URL(path, domain).href;
45
+ } catch (error) {
46
+ console.error("Failed to resolve og-image.png path:", error);
47
+ return "https://cover.sli.dev";
48
+ }
52
49
  }
53
50
 
54
- if (isUrl(background)) {
55
- return background;
51
+ // Priority 2: seoMeta.ogImage
52
+ if (seoOgImage) {
53
+ if (isUrl(seoOgImage)) {
54
+ return seoOgImage;
55
+ }
56
+
57
+ try {
58
+ return IS_DEVELOPMENT
59
+ ? new URL(seoOgImage, domain).href
60
+ : new URL(pathJoin(baseUrl, slidePath, seoOgImage), domain).href;
61
+ } catch (error) {
62
+ console.error("Failed to resolve seoMeta.ogImage path:", error);
63
+ return "https://cover.sli.dev";
64
+ }
56
65
  }
57
66
 
58
- try {
59
- return IS_DEVELOPMENT
60
- ? new URL(pathJoin(slidePath, background), domain).href
61
- : new URL(pathJoin(baseUrl, slidePath, background), domain).href;
62
- } catch (error) {
63
- console.error("Failed to resolve background path:", error);
64
- return background;
67
+ // Priority 3: background
68
+ if (background) {
69
+ if (isUrl(background)) {
70
+ return background;
71
+ }
72
+
73
+ try {
74
+ return IS_DEVELOPMENT
75
+ ? new URL(background, domain).href
76
+ : new URL(pathJoin(baseUrl, slidePath, background), domain).href;
77
+ } catch (error) {
78
+ console.error("Failed to resolve background path:", error);
79
+ return "https://cover.sli.dev";
80
+ }
65
81
  }
82
+
83
+ // Priority 4: default cover
84
+ return "https://cover.sli.dev";
66
85
  }
67
86
 
68
87
  export function useSlides() {
@@ -93,18 +112,9 @@ export function useSlides() {
93
112
  const port = 3001 + index;
94
113
  // Create dev server URL
95
114
  const devServerUrl = `http://localhost:${port}`;
115
+ const domain = IS_DEVELOPMENT ? devServerUrl : window.location.origin;
96
116
 
97
- // Resolve background image path
98
- const background =
99
- slide.frontmatter.seoMeta?.ogImage || slide.frontmatter.background;
100
- const imageUrl = background
101
- ? resolveBackgroundPath({
102
- background: slide.frontmatter.background,
103
- slidePath: slide.path,
104
- baseUrl: slide.baseUrl,
105
- domain: IS_DEVELOPMENT ? devServerUrl : window.location.origin,
106
- })
107
- : "https://cover.sli.dev";
117
+ const imageUrl = resolveImageUrl(slide, domain);
108
118
 
109
119
  return {
110
120
  title: slide.frontmatter.title || slide.path,