Skip to content

Commit 3a34a18

Browse files
naitokosukeautofix-ci[bot]antfu
authored
feat: auto gen og image (#2226)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Anthony Fu <github@antfu.me>
1 parent d0c56fe commit 3a34a18

File tree

6 files changed

+156
-3
lines changed

6 files changed

+156
-3
lines changed

.github/workflows/smoke.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ jobs:
107107
working-directory: ../temp/slidev-project
108108
if: ${{ matrix.pm != 'yarn' }}
109109

110+
- name: Install Playwright browsers
111+
run: pnpx playwright install chromium
112+
working-directory: ../temp/slidev-project
113+
110114
- name: Install project (yarn)
111115
run: yarn add /tmp/slidev-pkgs/cli.tgz playwright-chromium
112116
working-directory: ../temp/slidev-project

demo/starter/slides.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ transition: slide-left
2121
# enable MDC Syntax: https://sli.dev/features/mdc
2222
mdc: true
2323
# open graph
24-
# seoMeta:
25-
# ogImage: https://cover.sli.dev
24+
seoMeta:
25+
# By default, Slidev will use ./og-image.png if it exists,
26+
# or generate one from the first slide if not found.
27+
ogImage: auto
28+
# ogImage: https://cover.sli.dev
2629
---
2730

2831
# Welcome to Slidev

docs/features/og-image.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
relates:
3+
- features/seo-meta
4+
tags: ['SEO', head]
5+
description: |
6+
Set the Open Graph image for your slides.
7+
---
8+
9+
# Open Graph Image
10+
11+
Slidev allows you to set the Open Graph image via the `seoMeta.ogImage` option in the headmatter:
12+
13+
```md
14+
---
15+
seoMeta:
16+
ogImage: https://url.to.your.image.png
17+
---
18+
19+
# Your slides here
20+
```
21+
22+
Learn more about [SEO Meta Tags](./seo-meta).
23+
24+
## Local Image
25+
26+
If you have `./og-image.png` in your project root, Slidev will grab it as the Open Graph image automatically without any configuration.
27+
28+
## Auto-generate
29+
30+
Since v52.1.0, Slidev supports auto-generating the Open Graph image from the first slide.
31+
32+
You can set `seoMeta.ogImage` to `auto` to enable this feature.
33+
34+
```md
35+
---
36+
seoMeta:
37+
ogImage: auto
38+
---
39+
```
40+
41+
It will use [playwright](https://playwright.dev/) to capture the first slide and save it as `./og-image.png` (same as `slidev export`). You may also commit the generated image to your repository to avoid the auto-generation. Or if you generate it on CI, you might also want to setup the playwright environment.

docs/features/seo-meta.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
depends:
3+
- custom/index#headmatter
4+
relates:
5+
- features/og-image
6+
tags: [SEO, head]
7+
description: |
8+
Configure SEO meta tags for better social media sharing and search engine optimization.
9+
---
10+
11+
# SEO Meta Tags
12+
13+
Slidev allows you to configure SEO meta tags in the headmatter to improve social media sharing and search engine optimization. You can set up Open Graph and Twitter Card meta tags to control how your slides appear when shared on social platforms.
14+
15+
## Configuration
16+
17+
Add the `seoMeta` configuration to your slides deck frontmatter:
18+
19+
```yaml
20+
---
21+
# SEO meta tags
22+
seoMeta:
23+
ogTitle: Slidev Starter Template
24+
ogDescription: Presentation slides for developers
25+
ogImage: https://cover.sli.dev
26+
ogUrl: https://example.com
27+
twitterCard: summary_large_image
28+
twitterTitle: Slidev Starter Template
29+
twitterDescription: Presentation slides for developers
30+
twitterImage: https://cover.sli.dev
31+
twitterSite: username
32+
twitterUrl: https://example.com
33+
---
34+
```
35+
36+
This feature is powered by [unhead](https://unhead.unjs.io/)'s `useHead` hook, please refer to the [documentation](https://unhead.unjs.io/docs/head/api/composables/use-seo-meta) for more details.

packages/slidev/node/commands/build.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,67 @@ export async function build(
5454

5555
const outDir = resolve(options.userRoot, config.build.outDir)
5656

57+
// copy or generate ogImage if it's a relative path, skip if not
58+
if (options.data.config.seoMeta?.ogImage === 'auto' || options.data.config.seoMeta?.ogImage?.startsWith('.')) {
59+
const filename = options.data.config.seoMeta?.ogImage === 'auto' ? 'og-image.png' : options.data.config.seoMeta.ogImage
60+
const projectOgImagePath = resolve(options.userRoot, filename)
61+
const outputOgImagePath = resolve(outDir, filename)
62+
63+
const projectOgImageExists = await fs.access(projectOgImagePath).then(() => true).catch(() => false)
64+
if (projectOgImageExists) {
65+
await fs.copyFile(projectOgImagePath, outputOgImagePath)
66+
}
67+
else if (options.data.config.seoMeta?.ogImage === 'auto') {
68+
const port = 12445
69+
const app = connect()
70+
const server = http.createServer(app)
71+
app.use(
72+
config.base,
73+
sirv(outDir, {
74+
etag: true,
75+
single: true,
76+
dev: true,
77+
}),
78+
)
79+
server.listen(port)
80+
81+
const { exportSlides } = await import('./export')
82+
const tempDir = resolve(outDir, 'temp')
83+
await fs.mkdir(tempDir, { recursive: true })
84+
85+
await exportSlides({
86+
port,
87+
base: config.base,
88+
slides: options.data.slides,
89+
total: options.data.slides.length,
90+
format: 'png',
91+
output: tempDir,
92+
range: '1',
93+
width: options.data.config.canvasWidth,
94+
height: Math.round(options.data.config.canvasWidth / options.data.config.aspectRatio),
95+
routerMode: options.data.config.routerMode,
96+
waitUntil: 'networkidle',
97+
timeout: 30000,
98+
perSlide: true,
99+
omitBackground: false,
100+
})
101+
102+
const tempFiles = await fs.readdir(tempDir)
103+
const pngFile = tempFiles.find(file => file.endsWith('.png'))
104+
if (pngFile) {
105+
const generatedPath = resolve(tempDir, pngFile)
106+
await fs.copyFile(generatedPath, projectOgImagePath)
107+
await fs.copyFile(generatedPath, outputOgImagePath)
108+
}
109+
110+
await fs.rm(tempDir, { recursive: true, force: true })
111+
server.close()
112+
}
113+
else {
114+
throw new Error(`[Slidev] ogImage: ${filename} not found`)
115+
}
116+
}
117+
57118
// copy index.html to 404.html for GitHub Pages
58119
await fs.copyFile(resolve(outDir, 'index.html'), resolve(outDir, '404.html'))
59120
// _redirects for SPA

packages/slidev/node/setups/indexHtml.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ export default async function setupIndexHtml({ mode, entry, clientRoot, userRoot
5656
const { info, author, keywords } = data.headmatter
5757
const seoMeta = (data.headmatter.seoMeta ?? {}) as SeoMeta
5858

59+
const ogImage = seoMeta.ogImage === 'auto'
60+
? './og-image.png'
61+
: seoMeta.ogImage
62+
? seoMeta.ogImage
63+
: existsSync(join(userRoot, 'og-image.png'))
64+
? './og-image.png'
65+
: undefined
66+
5967
const title = getSlideTitle(data)
6068
const description = info ? toAttrValue(info) : null
6169
const unhead = createHead({
@@ -76,7 +84,7 @@ export default async function setupIndexHtml({ mode, entry, clientRoot, userRoot
7684
{ name: 'keywords', content: keywords ? toAttrValue(Array.isArray(keywords) ? keywords.join(', ') : keywords) : null },
7785
{ property: 'og:title', content: seoMeta.ogTitle || title },
7886
{ property: 'og:description', content: seoMeta.ogDescription || description },
79-
{ property: 'og:image', content: seoMeta.ogImage },
87+
{ property: 'og:image', content: ogImage },
8088
{ property: 'og:url', content: seoMeta.ogUrl },
8189
{ property: 'twitter:card', content: seoMeta.twitterCard },
8290
{ property: 'twitter:site', content: seoMeta.twitterSite },

0 commit comments

Comments
 (0)