Skip to content

feat: auto gen og #2226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ jobs:
working-directory: ../temp/slidev-project
if: ${{ matrix.pm != 'yarn' }}

- name: Install Playwright browsers
run: pnpx playwright install chromium
working-directory: ../temp/slidev-project

- name: Install project (yarn)
run: yarn add /tmp/slidev-pkgs/cli.tgz playwright-chromium
working-directory: ../temp/slidev-project
Expand Down
7 changes: 5 additions & 2 deletions demo/starter/slides.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ transition: slide-left
# enable MDC Syntax: https://sli.dev/features/mdc
mdc: true
# open graph
# seoMeta:
# ogImage: https://cover.sli.dev
seoMeta:
# By default, Slidev will use ./og-image.png if it exists,
# or generate one from the first slide if not found.
ogImage: auto
# ogImage: https://cover.sli.dev
---

# Welcome to Slidev
Expand Down
41 changes: 41 additions & 0 deletions docs/features/og-image.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
relates:
- features/seo-meta
tags: ['SEO', head]
description: |
Set the Open Graph image for your slides.
---

# Open Graph Image

Slidev allows you to set the Open Graph image via the `seoMeta.ogImage` option in the headmatter:

```md
---
seoMeta:
ogImage: https://url.to.your.image.png
---

# Your slides here
```

Learn more about [SEO Meta Tags](./seo-meta).

## Local Image

If you have `./og-image.png` in your project root, Slidev will grab it as the Open Graph image automatically without any configuration.

## Auto-generate

Since v52.1.0, Slidev supports auto-generating the Open Graph image from the first slide.

You can set `seoMeta.ogImage` to `auto` to enable this feature.

```md
---
seoMeta:
ogImage: auto
---
```

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.
36 changes: 36 additions & 0 deletions docs/features/seo-meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
depends:
- custom/index#headmatter
relates:
- features/og-image
tags: [SEO, head]
description: |
Configure SEO meta tags for better social media sharing and search engine optimization.
---

# SEO Meta Tags

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.

## Configuration

Add the `seoMeta` configuration to your slides deck frontmatter:

```yaml
---
# SEO meta tags
seoMeta:
ogTitle: Slidev Starter Template
ogDescription: Presentation slides for developers
ogImage: https://cover.sli.dev
ogUrl: https://example.com
twitterCard: summary_large_image
twitterTitle: Slidev Starter Template
twitterDescription: Presentation slides for developers
twitterImage: https://cover.sli.dev
twitterSite: username
twitterUrl: https://example.com
---
```

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.
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@slidev/docs",
"type": "module",
"version": "52.0.0",
"version": "52.0.1",
"license": "MIT",
"funding": "https://github.com/sponsors/antfu",
"homepage": "https://sli.dev",
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"type": "module",
"version": "52.0.0",
"version": "52.0.1",
"private": true,
"packageManager": "pnpm@10.12.4",
"packageManager": "pnpm@10.13.1",
"engines": {
"node": ">=18.0.0"
},
Expand Down Expand Up @@ -50,7 +50,6 @@
"@types/yargs": "catalog:types",
"@vueuse/core": "catalog:frontend",
"bumpp": "catalog:dev",
"cross-env": "catalog:dev",
"cypress": "catalog:dev",
"eslint": "catalog:dev",
"eslint-plugin-format": "catalog:dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/client/composables/useNav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ const useNavState = createSharedComposable((): SlidevContextNavState => {
const hasPrimarySlide = computed(() => !!currentRoute.params.no)
const currentSlideNo = computed(() => hasPrimarySlide.value ? getSlide(currentRoute.params.no as string)?.no ?? 1 : 1)
const currentSlideRoute = computed(() => slides.value[currentSlideNo.value - 1])
const printRange = ref(parseRangeString(slides.value.length, currentRoute.query.range as string | undefined))
const printRange = ref(parseRangeString(slides.value.length, currentRoute?.query?.range as string | undefined))

const queryClicksRaw = useRouteQuery<string>('clicks', '0')

Expand Down
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@slidev/client",
"type": "module",
"version": "52.0.0",
"version": "52.0.1",
"description": "Presentation slides for developers",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/create-app/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "create-slidev",
"type": "module",
"version": "52.0.0",
"version": "52.0.1",
"description": "Create starter template for Slidev",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions packages/create-app/template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"export": "slidev export"
},
"dependencies": {
"@slidev/cli": "^52.0.0",
"@slidev/cli": "^52.0.1",
"@slidev/theme-default": "latest",
"@slidev/theme-seriph": "latest",
"vue": "^3.5.17"
"vue": "^3.5.18"
}
}
2 changes: 1 addition & 1 deletion packages/create-theme/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "create-slidev-theme",
"type": "module",
"version": "52.0.0",
"version": "52.0.1",
"description": "Create starter theme template for Slidev",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions packages/create-theme/template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
"screenshot": "slidev export example.md --format png"
},
"dependencies": {
"@slidev/types": "^52.0.0"
"@slidev/types": "^52.0.1"
},
"devDependencies": {
"@slidev/cli": "^52.0.0"
"@slidev/cli": "^52.0.1"
},
"//": "Learn more: https://sli.dev/guide/write-theme.html",
"slidev": {
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@slidev/parser",
"version": "52.0.0",
"version": "52.0.1",
"description": "Markdown parser for Slidev",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
Expand Down
61 changes: 61 additions & 0 deletions packages/slidev/node/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,67 @@ export async function build(

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

// copy or generate ogImage if it's a relative path, skip if not
if (options.data.config.seoMeta?.ogImage === 'auto' || options.data.config.seoMeta?.ogImage?.startsWith('.')) {
const filename = options.data.config.seoMeta?.ogImage === 'auto' ? 'og-image.png' : options.data.config.seoMeta.ogImage
const projectOgImagePath = resolve(options.userRoot, filename)
const outputOgImagePath = resolve(outDir, filename)

const projectOgImageExists = await fs.access(projectOgImagePath).then(() => true).catch(() => false)
if (projectOgImageExists) {
await fs.copyFile(projectOgImagePath, outputOgImagePath)
}
else if (options.data.config.seoMeta?.ogImage === 'auto') {
const port = 12445
const app = connect()
const server = http.createServer(app)
app.use(
config.base,
sirv(outDir, {
etag: true,
single: true,
dev: true,
}),
)
server.listen(port)

const { exportSlides } = await import('./export')
const tempDir = resolve(outDir, 'temp')
await fs.mkdir(tempDir, { recursive: true })

await exportSlides({
port,
base: config.base,
slides: options.data.slides,
total: options.data.slides.length,
format: 'png',
output: tempDir,
range: '1',
width: options.data.config.canvasWidth,
height: Math.round(options.data.config.canvasWidth / options.data.config.aspectRatio),
routerMode: options.data.config.routerMode,
waitUntil: 'networkidle',
timeout: 30000,
perSlide: true,
omitBackground: false,
})

const tempFiles = await fs.readdir(tempDir)
const pngFile = tempFiles.find(file => file.endsWith('.png'))
if (pngFile) {
const generatedPath = resolve(tempDir, pngFile)
await fs.copyFile(generatedPath, projectOgImagePath)
await fs.copyFile(generatedPath, outputOgImagePath)
}

await fs.rm(tempDir, { recursive: true, force: true })
server.close()
}
else {
throw new Error(`[Slidev] ogImage: ${filename} not found`)
}
}

// copy index.html to 404.html for GitHub Pages
await fs.copyFile(resolve(outDir, 'index.html'), resolve(outDir, '404.html'))
// _redirects for SPA
Expand Down
11 changes: 10 additions & 1 deletion packages/slidev/node/setups/indexHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ export default async function setupIndexHtml({ mode, entry, clientRoot, userRoot
const { info, author, keywords } = data.headmatter
const seoMeta = (data.headmatter.seoMeta ?? {}) as SeoMeta

const ogImage = seoMeta.ogImage === 'auto'
? './og-image.png'
: seoMeta.ogImage
? seoMeta.ogImage
: existsSync(join(userRoot, 'og-image.png'))
? './og-image.png'
: undefined

const title = getSlideTitle(data)
const description = info ? toAttrValue(info) : null
const unhead = createHead({
Expand All @@ -68,14 +76,15 @@ export default async function setupIndexHtml({ mode, entry, clientRoot, userRoot
...webFontsLink,
].filter(x => x),
meta: [
{ 'http-equiv': 'Content-Type', 'content': 'text/html; charset=UTF-8' },
{ property: 'slidev:version', content: version },
{ charset: 'slidev:entry', content: mode === 'dev' && slash(entry) },
{ name: 'description', content: description },
{ name: 'author', content: author ? toAttrValue(author) : null },
{ name: 'keywords', content: keywords ? toAttrValue(Array.isArray(keywords) ? keywords.join(', ') : keywords) : null },
{ property: 'og:title', content: seoMeta.ogTitle || title },
{ property: 'og:description', content: seoMeta.ogDescription || description },
{ property: 'og:image', content: seoMeta.ogImage },
{ property: 'og:image', content: ogImage },
{ property: 'og:url', content: seoMeta.ogUrl },
{ property: 'twitter:card', content: seoMeta.twitterCard },
{ property: 'twitter:site', content: seoMeta.twitterSite },
Expand Down
2 changes: 1 addition & 1 deletion packages/slidev/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@slidev/cli",
"type": "module",
"version": "52.0.0",
"version": "52.0.1",
"description": "Presentation slides for developers",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@slidev/types",
"version": "52.0.0",
"version": "52.0.1",
"description": "Shared types declarations for Slidev",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"displayName": "Slidev",
"type": "module",
"preview": true,
"version": "52.0.0",
"version": "52.0.1",
"private": true,
"description": "Slidev support for VS Code",
"license": "MIT",
Expand Down
Loading
Loading