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 +4 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +39 -33
- package/dist/plugin-slides.js +4 -1
- package/package.json +1 -1
- package/src/preview/components/SlideCard.vue +44 -2
- package/src/preview/components/ui/skeleton/Skeleton.vue +17 -0
- package/src/preview/components/ui/skeleton/index.ts +1 -0
- package/src/preview/composables/__mocks__/slidev-content.ts +3 -0
- package/src/preview/composables/useSlides.test.ts +367 -6
- package/src/preview/composables/useSlides.ts +58 -48
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
|
|
49
|
-
*
|
|
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
|
|
70
|
-
const {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
|
|
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
|
|
77
|
-
return
|
|
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
|
|
101
|
-
const imageUrl =
|
|
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,
|
package/dist/plugin-slides.js
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
22
|
-
*
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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,
|