slidev-workspace 0.4.2 → 0.5.1

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(pathJoin(slidePath, 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.1",
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,319 @@ 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(
119
+ "http://localhost:3001/slides-presentation-1/background.jpg",
120
+ );
121
+ });
122
+
123
+ it("should return default cover when no image sources provided", async () => {
124
+ const { resolveImageUrl } = await import("./useSlides");
125
+ const slide = createMockSlide({
126
+ hasOgImage: false,
127
+ frontmatter: {
128
+ title: "Test",
129
+ },
130
+ });
131
+ const domain = "http://localhost:3001";
132
+
133
+ const result = resolveImageUrl(slide, domain);
134
+
135
+ expect(result).toBe("https://cover.sli.dev");
136
+ });
137
+
138
+ it("should prioritize og-image.png over seoMeta.ogImage", async () => {
139
+ const { resolveImageUrl } = await import("./useSlides");
140
+ const slide = createMockSlide({
141
+ hasOgImage: true,
142
+ frontmatter: {
143
+ title: "Test",
144
+ seoMeta: {
145
+ ogImage: "https://example.com/image.jpg",
146
+ },
147
+ },
148
+ });
149
+ const domain = "http://localhost:3001";
150
+
151
+ const result = resolveImageUrl(slide, domain);
152
+
153
+ expect(result).toBe("http://localhost:3001/og-image.png");
154
+ expect(result).not.toBe("https://example.com/image.jpg");
155
+ });
156
+
157
+ it("should prioritize seoMeta.ogImage over background", async () => {
158
+ const { resolveImageUrl } = await import("./useSlides");
159
+ const slide = createMockSlide({
160
+ hasOgImage: false,
161
+ frontmatter: {
162
+ title: "Test",
163
+ seoMeta: {
164
+ ogImage: "https://example.com/image.jpg",
165
+ },
166
+ background: "https://example.com/bg.jpg",
167
+ },
168
+ });
169
+ const domain = "http://localhost:3001";
170
+
171
+ const result = resolveImageUrl(slide, domain);
172
+
173
+ expect(result).toBe("https://example.com/image.jpg");
174
+ expect(result).not.toBe("https://example.com/bg.jpg");
175
+ });
176
+
177
+ it("should return default cover on URL construction error", async () => {
178
+ const { resolveImageUrl } = await import("./useSlides");
179
+ const consoleErrorSpy = vi
180
+ .spyOn(console, "error")
181
+ .mockImplementation(() => {});
182
+
183
+ const slide = createMockSlide({
184
+ hasOgImage: false,
185
+ frontmatter: {
186
+ title: "Test",
187
+ seoMeta: {
188
+ ogImage: "not a valid url at all!!!",
189
+ },
190
+ },
191
+ });
192
+ const domain = "invalid domain";
193
+
194
+ const result = resolveImageUrl(slide, domain);
195
+
196
+ expect(result).toBe("https://cover.sli.dev");
197
+ expect(consoleErrorSpy).toHaveBeenCalled();
198
+
199
+ consoleErrorSpy.mockRestore();
200
+ });
201
+ });
202
+
203
+ describe("resolveImageUrl (Production Mode)", () => {
204
+ beforeAll(async () => {
205
+ vi.resetModules();
206
+ vi.doMock("../constants/env", () => ({
207
+ IS_DEVELOPMENT: false,
208
+ }));
209
+ });
210
+
211
+ afterEach(() => {
212
+ vi.restoreAllMocks();
213
+ });
214
+
215
+ it("should return og-image.png URL with baseUrl in production", async () => {
216
+ const { resolveImageUrl } = await import("./useSlides");
217
+ const slide = createMockSlide({
218
+ hasOgImage: true,
219
+ baseUrl: "/slidev-workspace-starter",
220
+ });
221
+ const domain = "https://my-slides.com";
222
+
223
+ const result = resolveImageUrl(slide, domain);
224
+
225
+ expect(result).toBe(
226
+ "https://my-slides.com/slidev-workspace-starter/og-image.png",
227
+ );
228
+ });
229
+
230
+ it("should resolve seoMeta.ogImage with baseUrl and path in production", async () => {
231
+ const { resolveImageUrl } = await import("./useSlides");
232
+ const slide = createMockSlide({
233
+ hasOgImage: false,
234
+ baseUrl: "/slidev-workspace-starter",
235
+ path: "/slides-presentation-1/",
236
+ frontmatter: {
237
+ title: "Test",
238
+ seoMeta: {
239
+ ogImage: "custom-image.jpg",
240
+ },
241
+ },
242
+ });
243
+ const domain = "https://my-slides.com";
244
+
245
+ const result = resolveImageUrl(slide, domain);
246
+
247
+ expect(result).toBe(
248
+ "https://my-slides.com/slidev-workspace-starter/slides-presentation-1/custom-image.jpg",
249
+ );
250
+ });
251
+
252
+ it("should resolve background with baseUrl and path in production", async () => {
253
+ const { resolveImageUrl } = await import("./useSlides");
254
+ const slide = createMockSlide({
255
+ hasOgImage: false,
256
+ baseUrl: "/slidev-workspace-starter",
257
+ path: "/slides-presentation-1/",
258
+ frontmatter: {
259
+ title: "Test",
260
+ background: "background.jpg",
261
+ },
262
+ });
263
+ const domain = "https://my-slides.com";
264
+
265
+ const result = resolveImageUrl(slide, domain);
266
+
267
+ expect(result).toBe(
268
+ "https://my-slides.com/slidev-workspace-starter/slides-presentation-1/background.jpg",
269
+ );
270
+ });
271
+
272
+ it("should keep absolute URL seoMeta.ogImage unchanged in production", async () => {
273
+ const { resolveImageUrl } = await import("./useSlides");
274
+ const slide = createMockSlide({
275
+ hasOgImage: false,
276
+ baseUrl: "/slidev-workspace-starter",
277
+ frontmatter: {
278
+ title: "Test",
279
+ seoMeta: {
280
+ ogImage: "https://example.com/image.jpg",
281
+ },
282
+ },
283
+ });
284
+ const domain = "https://my-slides.com";
285
+
286
+ const result = resolveImageUrl(slide, domain);
287
+
288
+ expect(result).toBe("https://example.com/image.jpg");
289
+ });
290
+
291
+ it("should keep absolute URL background unchanged in production", async () => {
292
+ const { resolveImageUrl } = await import("./useSlides");
293
+ const slide = createMockSlide({
294
+ hasOgImage: false,
295
+ baseUrl: "/slidev-workspace-starter",
296
+ frontmatter: {
297
+ title: "Test",
298
+ background: "https://example.com/bg.jpg",
299
+ },
300
+ });
301
+ const domain = "https://my-slides.com";
302
+
303
+ const result = resolveImageUrl(slide, domain);
304
+
305
+ expect(result).toBe("https://example.com/bg.jpg");
306
+ });
307
+
308
+ it("should return default cover when no image sources in production", async () => {
309
+ const { resolveImageUrl } = await import("./useSlides");
310
+ const slide = createMockSlide({
311
+ hasOgImage: false,
312
+ frontmatter: {
313
+ title: "Test",
314
+ },
315
+ });
316
+ const domain = "https://my-slides.com";
317
+
318
+ const result = resolveImageUrl(slide, domain);
319
+
320
+ expect(result).toBe("https://cover.sli.dev");
321
+ });
322
+ });
10
323
 
11
324
  describe("useSlides (Development Mode)", () => {
12
325
  beforeAll(async () => {
@@ -122,7 +435,7 @@ describe("useSlides (Development Mode)", () => {
122
435
 
123
436
  const secondSlide = slides.value[1];
124
437
 
125
- expect(secondSlide.image).toBe("https://example.com/bg.jpg");
438
+ expect(secondSlide.image).toBe("https://example.com/og-image.jpg");
126
439
  });
127
440
 
128
441
  it("should resolve relative background paths in development mode", async () => {
@@ -130,9 +443,7 @@ describe("useSlides (Development Mode)", () => {
130
443
 
131
444
  const firstSlide = slides.value[0];
132
445
 
133
- expect(firstSlide.image).toBe(
134
- "http://localhost:3001/slides-presentation-1/bg1.jpg",
135
- );
446
+ expect(firstSlide.image).toBe("http://localhost:3001/og-image.png");
136
447
  });
137
448
 
138
449
  it("should use default cover when no background is provided", async () => {
@@ -143,6 +454,35 @@ describe("useSlides (Development Mode)", () => {
143
454
  expect(thirdSlide.image).toBe("https://cover.sli.dev");
144
455
  });
145
456
  });
457
+
458
+ describe("og-image.png priority", () => {
459
+ it("should prioritize og-image.png when hasOgImage is true", async () => {
460
+ const { slides } = await setupUseSlides();
461
+
462
+ const firstSlide = slides.value[0];
463
+
464
+ // First slide has hasOgImage: true, so should use og-image.png
465
+ expect(firstSlide.image).toContain("og-image.png");
466
+ });
467
+
468
+ it("should use seoMeta.ogImage when hasOgImage is false", async () => {
469
+ const { slides } = await setupUseSlides();
470
+
471
+ const secondSlide = slides.value[1];
472
+
473
+ // Second slide has hasOgImage: false, so should use seoMeta.ogImage
474
+ expect(secondSlide.image).toBe("https://example.com/og-image.jpg");
475
+ });
476
+
477
+ it("should use background when hasOgImage is false and no seoMeta.ogImage", async () => {
478
+ const { slides } = await setupUseSlides();
479
+
480
+ const thirdSlide = slides.value[2];
481
+
482
+ // Third slide has hasOgImage: false and no seoMeta, so should use background or default
483
+ expect(thirdSlide.image).toBe("https://cover.sli.dev");
484
+ });
485
+ });
146
486
  });
147
487
 
148
488
  describe("useSlides (Production Mode)", () => {
@@ -201,7 +541,7 @@ describe("useSlides (Production Mode)", () => {
201
541
  const firstSlide = result.slides.value[0];
202
542
 
203
543
  expect(firstSlide.image).toBe(
204
- "https://my-slides.com/slidev-workspace-starter/slides-presentation-1/bg1.jpg",
544
+ "https://my-slides.com/slidev-workspace-starter/og-image.png",
205
545
  );
206
546
  });
207
547
 
@@ -209,7 +549,7 @@ describe("useSlides (Production Mode)", () => {
209
549
  const result = await setupUseSlidesProduction();
210
550
  const secondSlide = result.slides.value[1];
211
551
 
212
- expect(secondSlide.image).toBe("https://example.com/bg.jpg");
552
+ expect(secondSlide.image).toBe("https://example.com/og-image.jpg");
213
553
  });
214
554
 
215
555
  it("should use default cover when no background in production", async () => {
@@ -219,4 +559,25 @@ describe("useSlides (Production Mode)", () => {
219
559
  expect(thirdSlide.image).toBe("https://cover.sli.dev");
220
560
  });
221
561
  });
562
+
563
+ describe("og-image.png priority in production", () => {
564
+ it("should prioritize og-image.png when hasOgImage is true in production", async () => {
565
+ const result = await setupUseSlidesProduction();
566
+ const firstSlide = result.slides.value[0];
567
+
568
+ // First slide has hasOgImage: true, so should use og-image.png
569
+ expect(firstSlide.image).toContain("og-image.png");
570
+ expect(firstSlide.image).toBe(
571
+ "https://my-slides.com/slidev-workspace-starter/og-image.png",
572
+ );
573
+ });
574
+
575
+ it("should use seoMeta.ogImage when hasOgImage is false in production", async () => {
576
+ const result = await setupUseSlidesProduction();
577
+ const secondSlide = result.slides.value[1];
578
+
579
+ // Second slide has hasOgImage: false, so should use seoMeta.ogImage
580
+ expect(secondSlide.image).toBe("https://example.com/og-image.jpg");
581
+ });
582
+ });
222
583
  });
@@ -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(pathJoin(slidePath, 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,