Skip to content

feat: support open graph protocol #2096

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 10 commits into from
Mar 31, 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
3 changes: 3 additions & 0 deletions demo/starter/slides.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ drawings:
transition: slide-left
# enable MDC Syntax: https://sli.dev/features/mdc
mdc: true
# open graph
# seoMeta:
# ogImage: https://cover.sli.dev
---

# Welcome to Slidev
Expand Down
13 changes: 13 additions & 0 deletions docs/custom/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,19 @@ drawings:
htmlAttrs:
dir: ltr
lang: en

# 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
---
```

Expand Down
2 changes: 1 addition & 1 deletion docs/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default defineConfig({
llmstxt({
ignoreFiles: [
'index.md',
'README.md'
'README.md',
],
}),
Components({
Expand Down
1 change: 1 addition & 0 deletions packages/client/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,5 @@ export const HEADMATTER_FIELDS = [
'mdc',
'contextMenu',
'wakeLock',
'seoMeta',
]
1 change: 1 addition & 0 deletions packages/parser/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function getDefaultConfig(): SlidevConfig {
wakeLock: true,
remote: false,
mdc: false,
seoMeta: {},
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/slidev/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const CONFIG_RESTART_FIELDS: (keyof SlidevConfig)[] = [
'mdc',
'editor',
'theme',
'seoMeta',
]

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/slidev/node/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export async function createDataUtils(resolved: Omit<ResolvedSlidevOptions, 'uti
return {
...await setupShiki(resolved.roots),
katexOptions: await setupKatex(resolved.roots),
indexHtml: setupIndexHtml(resolved),
indexHtml: await setupIndexHtml(resolved),
define: getDefine(resolved),
iconsResolvePath: [resolved.clientRoot, ...resolved.roots].reverse(),
isMonacoTypesIgnored: pkg => monacoTypesIgnorePackagesMatches.some(i => i.test(pkg)),
Expand Down
71 changes: 50 additions & 21 deletions packages/slidev/node/setups/indexHtml.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { ResolvedSlidevOptions } from '@slidev/types'
import type { ResolvedSlidevOptions, SeoMeta } from '@slidev/types'
import type { ResolvableLink } from 'unhead/types'
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { slash } from '@antfu/utils'
import { white, yellow } from 'ansis'
import { escapeHtml } from 'markdown-it/lib/common/utils.mjs'
import { createHead, transformHtmlTemplate } from 'unhead/server'
import { version } from '../../package.json'
import { getSlideTitle } from '../commands/shared'
import { toAtFS } from '../resolver'
Expand All @@ -13,22 +15,11 @@ function toAttrValue(unsafe: unknown) {
return JSON.stringify(escapeHtml(String(unsafe)))
}

export default function setupIndexHtml({ mode, entry, clientRoot, userRoot, roots, data, base }: Omit<ResolvedSlidevOptions, 'utils'>): string {
export default async function setupIndexHtml({ mode, entry, clientRoot, userRoot, roots, data, base }: Omit<ResolvedSlidevOptions, 'utils'>): Promise<string> {
let main = readFileSync(join(clientRoot, 'index.html'), 'utf-8')
let head = ''
let body = ''

const { info, author, keywords } = data.headmatter
head += [
`<meta name="slidev:version" content="${version}">`,
mode === 'dev' && `<meta charset="slidev:entry" content="${slash(entry)}">`,
`<link rel="icon" href="${data.config.favicon}">`,
`<title>${getSlideTitle(data)}</title>`,
info && `<meta name="description" content=${toAttrValue(info)}>`,
author && `<meta name="author" content=${toAttrValue(author)}>`,
keywords && `<meta name="keywords" content=${toAttrValue(Array.isArray(keywords) ? keywords.join(', ') : keywords)}>`,
].filter(Boolean).join('\n')

for (const root of roots) {
const path = join(root, 'index.html')
if (!existsSync(path))
Expand All @@ -46,25 +37,63 @@ export default function setupIndexHtml({ mode, entry, clientRoot, userRoot, root
body += `\n${(index.match(/<body>([\s\S]*?)<\/body>/i)?.[1] || '').trim()}`
}

if (data.features.tweet)
if (data.features.tweet) {
body += '\n<script async src="https://platform.twitter.com/widgets.js"></script>'
}

const webFontsLink: ResolvableLink[] = []
if (data.config.fonts.webfonts.length) {
const { provider } = data.config.fonts
if (provider === 'google')
head += `\n<link rel="stylesheet" href="${generateGoogleFontsUrl(data.config.fonts)}" type="text/css">`
else if (provider === 'coollabs')
head += `\n<link rel="stylesheet" href="${generateCoollabsFontsUrl(data.config.fonts)}" type="text/css">`
if (provider === 'google') {
webFontsLink.push({ rel: 'stylesheet', href: generateGoogleFontsUrl(data.config.fonts), type: 'text/css' })
}
else if (provider === 'coollabs') {
webFontsLink.push({ rel: 'stylesheet', href: generateCoollabsFontsUrl(data.config.fonts), type: 'text/css' })
}
}

if (data.headmatter.lang)
main = main.replace('<html lang="en">', `<html lang="${data.headmatter.lang}">`)
const { info, author, keywords } = data.headmatter
const seoMeta = (data.headmatter.seoMeta ?? {}) as SeoMeta

const title = getSlideTitle(data)
const description = info ? toAttrValue(info) : null
const unhead = createHead({
init: [
{
htmlAttrs: { lang: (data.headmatter.lang as string | undefined) ?? 'en' },
title,
link: [
{ rel: 'icon', href: data.config.favicon },
...webFontsLink,
],
meta: [
{ 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:url', content: seoMeta.ogUrl },
{ property: 'twitter:card', content: seoMeta.twitterCard },
{ property: 'twitter:site', content: seoMeta.twitterSite },
{ property: 'twitter:title', content: seoMeta.twitterTitle },
{ property: 'twitter:description', content: seoMeta.twitterDescription },
{ property: 'twitter:image', content: seoMeta.twitterImage },
{ property: 'twitter:url', content: seoMeta.twitterUrl },
],
},
],
})

const baseInDev = mode === 'dev' && base ? base.slice(0, -1) : ''
main = main
.replace('__ENTRY__', baseInDev + toAtFS(join(clientRoot, 'main.ts')))
.replace('<!-- head -->', head)
.replace('<!-- body -->', body)

return main
const html = await transformHtmlTemplate(unhead, main)

return html
}
1 change: 1 addition & 0 deletions packages/slidev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"sirv": "catalog:",
"source-map-js": "catalog:",
"typescript": "catalog:",
"unhead": "catalog:",
"unocss": "catalog:",
"unplugin-icons": "catalog:",
"unplugin-vue-components": "catalog:",
Expand Down
22 changes: 22 additions & 0 deletions packages/types/src/frontmatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ export interface HeadmatterConfig extends TransitionOptions {
* @default []
*/
monacoRunAdditionalDeps?: string[]
/**
* Seo meta tags settings
*
* @default {}
*/
seoMeta?: SeoMeta
}

export interface Frontmatter extends TransitionOptions {
Expand Down Expand Up @@ -454,3 +460,19 @@ export interface TransitionGroupProps {
leaveActiveClass?: string
leaveToClass?: string
}

/**
* The following type should map to unhead MataFlat type
*/
export interface SeoMeta {
ogTitle?: string
ogDescription?: string
ogImage?: string
ogUrl?: string
twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'
twitterSite?: string
twitterTitle?: string
twitterDescription?: string
twitterImage?: string
twitterUrl?: string
}
49 changes: 49 additions & 0 deletions packages/vscode/schema/headmatter.json
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,12 @@
"markdownDescription": "Additional local modules to load as dependencies of monaco runnable",
"default": []
},
"seoMeta": {
"$ref": "#/definitions/SeoMeta",
"description": "Seo meta tags settings",
"markdownDescription": "Seo meta tags settings",
"default": {}
},
"defaults": {
"$ref": "#/definitions/Frontmatter",
"description": "Default frontmatter options applied to all slides",
Expand Down Expand Up @@ -777,6 +783,49 @@
}
}
},
"SeoMeta": {
"type": "object",
"properties": {
"ogTitle": {
"type": "string"
},
"ogDescription": {
"type": "string"
},
"ogImage": {
"type": "string"
},
"ogUrl": {
"type": "string"
},
"twitterCard": {
"type": "string",
"enum": [
"summary",
"summary_large_image",
"app",
"player"
]
},
"twitterSite": {
"type": "string"
},
"twitterTitle": {
"type": "string"
},
"twitterDescription": {
"type": "string"
},
"twitterImage": {
"type": "string"
},
"twitterUrl": {
"type": "string"
}
},
"description": "The following type should map to unhead MataFlat type",
"markdownDescription": "The following type should map to unhead MataFlat type"
},
"Frontmatter": {
"type": "object",
"properties": {
Expand Down
Loading
Loading