1
- import type { ResolvedSlidevOptions } from '@slidev/types'
1
+ import type { ResolvedSlidevOptions , SeoMeta } from '@slidev/types'
2
+ import type { ResolvableLink } from 'unhead/types'
2
3
import { existsSync , readFileSync } from 'node:fs'
3
4
import { join } from 'node:path'
4
5
import { slash } from '@antfu/utils'
5
6
import { white , yellow } from 'ansis'
6
7
import { escapeHtml } from 'markdown-it/lib/common/utils.mjs'
8
+ import { createHead , transformHtmlTemplate } from 'unhead/server'
7
9
import { version } from '../../package.json'
8
10
import { getSlideTitle } from '../commands/shared'
9
11
import { toAtFS } from '../resolver'
@@ -13,22 +15,11 @@ function toAttrValue(unsafe: unknown) {
13
15
return JSON . stringify ( escapeHtml ( String ( unsafe ) ) )
14
16
}
15
17
16
- export default function setupIndexHtml ( { mode, entry, clientRoot, userRoot, roots, data, base } : Omit < ResolvedSlidevOptions , 'utils' > ) : string {
18
+ export default async function setupIndexHtml ( { mode, entry, clientRoot, userRoot, roots, data, base } : Omit < ResolvedSlidevOptions , 'utils' > ) : Promise < string > {
17
19
let main = readFileSync ( join ( clientRoot , 'index.html' ) , 'utf-8' )
18
20
let head = ''
19
21
let body = ''
20
22
21
- const { info, author, keywords } = data . headmatter
22
- head += [
23
- `<meta name="slidev:version" content="${ version } ">` ,
24
- mode === 'dev' && `<meta charset="slidev:entry" content="${ slash ( entry ) } ">` ,
25
- `<link rel="icon" href="${ data . config . favicon } ">` ,
26
- `<title>${ getSlideTitle ( data ) } </title>` ,
27
- info && `<meta name="description" content=${ toAttrValue ( info ) } >` ,
28
- author && `<meta name="author" content=${ toAttrValue ( author ) } >` ,
29
- keywords && `<meta name="keywords" content=${ toAttrValue ( Array . isArray ( keywords ) ? keywords . join ( ', ' ) : keywords ) } >` ,
30
- ] . filter ( Boolean ) . join ( '\n' )
31
-
32
23
for ( const root of roots ) {
33
24
const path = join ( root , 'index.html' )
34
25
if ( ! existsSync ( path ) )
@@ -46,25 +37,63 @@ export default function setupIndexHtml({ mode, entry, clientRoot, userRoot, root
46
37
body += `\n${ ( index . match ( / < b o d y > ( [ \s \S ] * ?) < \/ b o d y > / i) ?. [ 1 ] || '' ) . trim ( ) } `
47
38
}
48
39
49
- if ( data . features . tweet )
40
+ if ( data . features . tweet ) {
50
41
body += '\n<script async src="https://platform.twitter.com/widgets.js"></script>'
42
+ }
51
43
44
+ const webFontsLink : ResolvableLink [ ] = [ ]
52
45
if ( data . config . fonts . webfonts . length ) {
53
46
const { provider } = data . config . fonts
54
- if ( provider === 'google' )
55
- head += `\n<link rel="stylesheet" href="${ generateGoogleFontsUrl ( data . config . fonts ) } " type="text/css">`
56
- else if ( provider === 'coollabs' )
57
- head += `\n<link rel="stylesheet" href="${ generateCoollabsFontsUrl ( data . config . fonts ) } " type="text/css">`
47
+ if ( provider === 'google' ) {
48
+ webFontsLink . push ( { rel : 'stylesheet' , href : generateGoogleFontsUrl ( data . config . fonts ) , type : 'text/css' } )
49
+ }
50
+ else if ( provider === 'coollabs' ) {
51
+ webFontsLink . push ( { rel : 'stylesheet' , href : generateCoollabsFontsUrl ( data . config . fonts ) , type : 'text/css' } )
52
+ }
58
53
}
59
54
60
- if ( data . headmatter . lang )
61
- main = main . replace ( '<html lang="en">' , `<html lang="${ data . headmatter . lang } ">` )
55
+ const { info, author, keywords } = data . headmatter
56
+ const seoMeta = ( data . headmatter . seoMeta ?? { } ) as SeoMeta
57
+
58
+ const title = getSlideTitle ( data )
59
+ const description = info ? toAttrValue ( info ) : null
60
+ const unhead = createHead ( {
61
+ init : [
62
+ {
63
+ htmlAttrs : { lang : ( data . headmatter . lang as string | undefined ) ?? 'en' } ,
64
+ title,
65
+ link : [
66
+ { rel : 'icon' , href : data . config . favicon } ,
67
+ ...webFontsLink ,
68
+ ] ,
69
+ meta : [
70
+ { property : 'slidev:version' , content : version } ,
71
+ { charset : 'slidev:entry' , content : mode === 'dev' && slash ( entry ) } ,
72
+ { name : 'description' , content : description } ,
73
+ { name : 'author' , content : author ? toAttrValue ( author ) : null } ,
74
+ { name : 'keywords' , content : keywords ? toAttrValue ( Array . isArray ( keywords ) ? keywords . join ( ', ' ) : keywords ) : null } ,
75
+ { property : 'og:title' , content : seoMeta . ogTitle || title } ,
76
+ { property : 'og:description' , content : seoMeta . ogDescription || description } ,
77
+ { property : 'og:image' , content : seoMeta . ogImage } ,
78
+ { property : 'og:url' , content : seoMeta . ogUrl } ,
79
+ { property : 'twitter:card' , content : seoMeta . twitterCard } ,
80
+ { property : 'twitter:site' , content : seoMeta . twitterSite } ,
81
+ { property : 'twitter:title' , content : seoMeta . twitterTitle } ,
82
+ { property : 'twitter:description' , content : seoMeta . twitterDescription } ,
83
+ { property : 'twitter:image' , content : seoMeta . twitterImage } ,
84
+ { property : 'twitter:url' , content : seoMeta . twitterUrl } ,
85
+ ] ,
86
+ } ,
87
+ ] ,
88
+ } )
62
89
63
90
const baseInDev = mode === 'dev' && base ? base . slice ( 0 , - 1 ) : ''
64
91
main = main
65
92
. replace ( '__ENTRY__' , baseInDev + toAtFS ( join ( clientRoot , 'main.ts' ) ) )
66
93
. replace ( '<!-- head -->' , head )
67
94
. replace ( '<!-- body -->' , body )
68
95
69
- return main
96
+ const html = await transformHtmlTemplate ( unhead , main )
97
+
98
+ return html
70
99
}
0 commit comments