1
1
mirror of https://github.com/jxnblk/mdx-deck.git synced 2024-11-29 13:58:02 +03:00

Merge pull request #107 from jxnblk/head-component

Add Head component and support for OG metadata
This commit is contained in:
Brent Jackson 2018-08-14 21:11:46 -04:00 committed by GitHub
commit 4c7c89d463
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 711 additions and 124 deletions

View File

@ -4,6 +4,7 @@ node_js:
before_deploy:
- npm install
- npm run build
- npm run screenshot
deploy:
provider: pages
skip_cleanup: true

View File

@ -262,16 +262,8 @@ Add a `build` script to your `package.json` to export a presentation as HTML wit
}
```
### PDF Export
See more exporting options in the [Exporting Documentation](docs/exporting.md)
Presentations can be exported as PDF using the CLI.
This works well as a backup option for any unforeseen technical difficulties.
```json
"script": {
"pdf": "mdx-deck pdf deck.mdx"
}
```
## CLI Options
@ -279,7 +271,9 @@ This works well as a backup option for any unforeseen technical difficulties.
-p --port Dev server port
--no-open Prevent from opening in default browser
-d --out-dir Output directory for exporting
--title Title for the HTML document
--out-file Filename for screenshot or PDF export
--width Width in pixels
--height Height in pixels
```
## Docs
@ -288,6 +282,7 @@ This works well as a backup option for any unforeseen technical difficulties.
- [Built-in Themes](docs/themes.md)
- [Layouts](docs/layouts.md)
- [Components](docs/components.md)
- [Exporting](docs/exporting.md)
- [Advanced Usage](docs/advanced.md)
- [React API](docs/react.md)

103
cli.js
View File

@ -3,7 +3,6 @@ const path = require('path')
const meow = require('meow')
const open = require('react-dev-utils/openBrowser')
const chalk = require('chalk')
const ok = require('ok-cli')
const remark = {
emoji: require('remark-emoji'),
unwrapImages: require('remark-unwrap-images')
@ -11,53 +10,7 @@ const remark = {
const pkg = require('./package.json')
const config = require('pkg-conf').sync('mdx-deck')
const log = (...args) => {
console.log(
chalk.magenta('[mdx-deck]'),
...args
)
}
log.error = (...args) => {
console.log(
chalk.red('[err]'),
...args
)
}
const getConfig = conf => {
conf.module.rules = [
...conf.module.rules
.filter(rule => !rule.test.test('.mdx')),
{
test: /\.mdx?$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
presets: [
'babel-preset-env',
'babel-preset-stage-0',
'babel-preset-react',
].map(require.resolve)
}
},
{
loader: require.resolve('./lib/loader.js'),
options: {
mdPlugins: [
remark.emoji,
remark.unwrapImages
]
}
}
]
}
]
return conf
}
const log = require('./lib/log')
const cli = meow(`
${chalk.gray('Usage')}
@ -68,9 +21,9 @@ const cli = meow(`
$ ${chalk.magenta('mdx-deck pdf deck.mdx')}
${chalk.gray('Options')}
$ ${chalk.magenta('mdx-deck screenshot deck.mdx')}
--title Title for the HTML document
${chalk.gray('Options')}
${chalk.gray('Dev server options')}
@ -81,11 +34,11 @@ const cli = meow(`
-d --out-dir Output directory for exporting
${chalk.gray('PDF options')}
${chalk.gray('Export options')}
--out-file Filename for PDF export
--out-file Filename for screenshot or PDF export
--width Width in pixels
--heigh Height in pixels
--height Height in pixels
`, {
description: chalk.magenta('[mdx-deck] ') + chalk.gray(pkg.description),
@ -103,9 +56,6 @@ const cli = meow(`
type: 'string',
alias: 'd'
},
title: {
type: 'string'
},
outFile: {
type: 'string',
}
@ -118,26 +68,26 @@ const doc = file || cmd
if (!doc) cli.showHelp(0)
const opts = Object.assign({
entry: path.join(__dirname, './dist/entry.js'),
dirname: path.dirname(path.resolve(doc)),
globals: {
DOC_FILENAME: JSON.stringify(path.resolve(doc))
FILENAME: JSON.stringify(path.resolve(doc))
},
config: getConfig,
title: 'mdx-deck',
port: 8080,
outDir: 'dist',
outFile: 'presentation.pdf'
}, config, cli.flags)
opts.outDir = path.resolve(opts.outDir)
let dev
switch (cmd) {
case 'build':
log('building')
ok.build(opts)
const build = require('./lib/build')
build(opts)
.then(res => {
log('done')
process.exit(0)
})
.catch(err => {
log.error(err)
@ -147,7 +97,8 @@ switch (cmd) {
case 'pdf':
log('exporting to PDF')
const pdf = require('./lib/pdf')
ok(opts)
dev = require('./lib/dev')
dev(opts)
.then(({ server }) => {
log('rendering PDF')
pdf(opts)
@ -166,10 +117,34 @@ switch (cmd) {
process.exit(1)
})
break
case 'screenshot':
log('exporting to PNG')
const screenshot = require('./lib/screenshot')
dev = require('./lib/dev')
dev(opts)
.then(({ server }) => {
log('rendering screenshot')
screenshot(opts)
.then(filename => {
server.close()
log('done', filename)
process.exit(0)
})
.catch(err => {
log.error(err)
process.exit(1)
})
})
.catch(err => {
log.error(err)
process.exit(1)
})
break
case 'dev':
default:
log('starting dev server')
ok(opts)
dev = require('./lib/dev')
dev(opts)
.then(res => {
const url = 'http://localhost:' + res.port
if (opts.open) open(url)

View File

@ -3,6 +3,24 @@
mdx-deck includes a few built-in components to help with creating presentations.
## Head
Use the `<Head />` component to set content in the document head.
```mdx
// example for twitter cards
import { Head } from 'mdx-deck'
<Head>
<title>My Presentation</title>
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:site' content='@jxnblk' />
<meta name='twitter:title' content='My Presentation' />
<meta name='twitter:description' content='A really great presentation' />
<meta name='twitter:image' content='https://example.com/card.png' />
</Head>
```
## Image
Use the `<Image />` component to render a fullscreen image (using the CSS `background-image` property).

49
docs/exporting.md Normal file
View File

@ -0,0 +1,49 @@
# Exporting
## Static Bundle
To export your deck as a static HTML page with JS bundle,
add a `build` script to your `package.json` file.
```json
"scripts": {
"build": "mdx-deck build deck.mdx"
}
```
## PDF Export
Presentations can be exported as PDF using the CLI.
This works well as a backup option for any unforeseen technical difficulties.
```json
"script": {
"pdf": "mdx-deck pdf deck.mdx"
}
```
## Screenshots
A PNG image of the first slide can be exported with the `screenshot` command.
This is useful for creating open graph images for Twitter, Facebook, or Slack.
```json
"script": {
"screenshot": "mdx-deck screenshot deck.mdx"
}
```
### OG Image
To use the image as an open graph image, use the [Head](components.md#Head) component to add a meta tag.
Note that the meta tag should point to a full URL, including schema and domain name.
```mdx
import { Head } from 'mdx-deck'
<Head>
<meta name='og:image' content='https://example.com/card.png' />
</Head>
```

View File

@ -1,8 +1,17 @@
export { future as theme } from '../themes'
import { Image, Notes, Appear } from '../dist'
import { Head, Image, Notes, Appear } from '../dist'
import { Invert, Split, SplitRight, FullScreenCode } from '../layouts'
import Counter from './Counter'
<Head>
<title>mdx-deck</title>
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:site' content='@jxnblk' />
<meta name='twitter:title' content='mdx-deck' />
<meta name='twitter:description' content='MDX-based presentation decks' />
<meta name='twitter:image' content='https://jxnblk.com/mdx-deck/card.png' />
</Head>
# mdx-deck
MDX-based presention decks

33
lib/build.js Normal file
View File

@ -0,0 +1,33 @@
const webpack = require('webpack')
const createConfig = require('./config')
const renderHTML = require('./html')
const log = require('./log')
const build = async (opts = {}) => {
log('rendering static html')
const { body, head } = await renderHTML(opts)
opts.head = head
opts.body = body
log('bundling js')
const config = createConfig(opts)
config.mode = 'production'
config.output = {
path: opts.outDir
}
const compiler = webpack(config)
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) {
reject(err)
return
}
resolve(stats)
})
})
}
module.exports = build

139
lib/config.js Normal file
View File

@ -0,0 +1,139 @@
const path = require('path')
const webpack = require('webpack')
const HTMLPlugin = require('mini-html-webpack-plugin')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
const chalk = require('chalk')
const remark = {
emoji: require('remark-emoji'),
unwrapImages: require('remark-unwrap-images')
}
const babel = {
presets: [
'babel-preset-env',
'babel-preset-stage-0',
'babel-preset-react',
].map(require.resolve)
}
const rules = [
{
test: /\.js$/,
exclude: /node_modules/,
loader: require.resolve('babel-loader'),
options: babel
},
{
test: /\.js$/,
exclude: path.resolve(__dirname, '../node_modules'),
include: [
path.resolve(__dirname, '..'),
],
loader: require.resolve('babel-loader'),
options: babel
},
{
test: /\.mdx?$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
options: babel
},
{
loader: require.resolve('./loader.js'),
options: {
mdPlugins: [
remark.emoji,
remark.unwrapImages
]
}
}
]
}
]
const template = ({
head = '<title>mdx-deck</title>',
body = '',
js,
publicPath
}) => `<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<style>*{box-sizing:border-box}body{font-family:system-ui,sans-serif;margin:0}</style>
<meta name='generator' content='mdx-deck'>
${head}
</head>
<body>
<div id=root>${body}</div>
${HTMLPlugin.generateJSReferences(js, publicPath)}
</body>
</html>
`
const baseConfig = {
stats: 'errors-only',
mode: 'development',
module: {
rules
},
resolve: {
modules: [
path.relative(process.cwd(), path.join(__dirname, '../node_modules')),
'node_modules'
]
},
plugins: [
new ProgressBarPlugin({
width: '24',
complete: '█',
incomplete: chalk.gray('░'),
format: [
chalk.magenta('[mdx-deck] :bar'),
chalk.magenta(':percent'),
chalk.gray(':elapseds :msg'),
].join(' '),
summary: false,
customSummary: () => {},
})
]
}
const createConfig = (opts = {}) => {
const config = Object.assign({}, baseConfig)
config.context = opts.dirname
config.resolve.modules.push(
opts.dirname,
path.join(opts.dirname, 'node_modules')
)
config.entry = [
path.join(__dirname, '../dist/entry.js')
]
const defs = Object.assign({}, opts.globals, {
OPTIONS: JSON.stringify(opts),
HOT_PORT: JSON.stringify(opts.hotPort)
})
config.plugins.push(
new webpack.DefinePlugin(defs),
new HTMLPlugin({ template, context: opts })
)
if (config.resolve.alias) {
const hotAlias = config.resolve.alias['webpack-hot-client/client']
if (!fs.existsSync(hotAlias)) {
const hotPath = path.dirname(require.resolve('webpack-hot-client/client'))
config.resolve.alias['webpack-hot-client/client'] = hotPath
}
}
return config
}
module.exports = createConfig

45
lib/dev.js Normal file
View File

@ -0,0 +1,45 @@
const path = require('path')
const Koa = require('koa')
const getPort = require('get-port')
const koaWebpack = require('koa-webpack')
const koaStatic = require('koa-static')
const createConfig = require('./config')
const devMiddleware = {
publicPath: '/',
clientLogLevel: 'error',
stats: 'errors-only',
logLevel: 'error',
}
const start = async (opts = {}) => {
const app = new Koa()
opts.hotPort = await getPort()
const hotClient = {
port: opts.hotPort,
logLevel: 'error'
}
opts.dirname = opts.dirname || path.dirname(opts.entry)
const config = createConfig(opts)
config.entry.push(
path.join(__dirname, './overlay.js'),
)
const middleware = await koaWebpack({
config,
devMiddleware,
hotClient
})
const port = opts.port || await getPort()
app.use(middleware)
app.use(koaStatic(opts.dirname))
const server = app.listen(port)
return new Promise((resolve) => {
middleware.devMiddleware.waitUntilValid(() => {
resolve({ server, app, middleware, port })
})
})
}
module.exports = start

59
lib/html.js Normal file
View File

@ -0,0 +1,59 @@
const fs = require('fs')
const path = require('path')
const React = require('react')
const {
renderToString,
renderToStaticMarkup
} = require('react-dom/server')
const webpack = require('webpack')
const rimraf = require('rimraf')
const createConfig = require('./config')
const getApp = async opts => {
opts.tempdir = path.join(opts.outDir, 'TEMP')
if (!fs.existsSync(opts.outDir)) fs.mkdirSync(opts.outDir)
if (!fs.existsSync(opts.tempdir)) fs.mkdirSync(opts.tempdir)
const config = createConfig(opts)
config.output = {
path: opts.tempdir,
filename: '[name].js',
libraryTarget: 'umd'
}
config.entry = {
App: path.join(__dirname, '../dist/entry.js')
}
config.target = 'node'
const compiler = webpack(config)
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) {
reject(err)
return
}
const App = require(
path.resolve(opts.tempdir, './App.js')
).default
rimraf(opts.tempdir, err => {
if (err) console.error(err)
})
resolve(App)
})
})
}
const renderHTML = async opts => {
const App = await getApp(opts)
const headTags = []
const body = renderToString(
React.createElement(App, { headTags })
)
const head = renderToStaticMarkup(headTags)
return { body, head }
}
module.exports = renderHTML

16
lib/log.js Normal file
View File

@ -0,0 +1,16 @@
const chalk = require('chalk')
const log = (...args) => {
console.log(
chalk.magenta('[mdx-deck]'),
...args
)
}
log.error = (...args) => {
console.log(
chalk.red('[err]'),
...args
)
}
module.exports = log

81
lib/overlay.js Normal file
View File

@ -0,0 +1,81 @@
const ansiHTML = require('ansi-html')
const Entities = require('html-entities').AllHtmlEntities
const entities = new Entities()
const colors = {
reset: ['transparent', 'transparent'],
black: '000000',
red: 'FF0000',
green: '00FF00',
yellow: 'FFFF00',
blue: '0000FF',
magenta: 'FF00FF',
cyan: '00FFFF',
lightgrey: 'EEEEEE',
darkgrey: '666666'
};
ansiHTML.setColors(colors)
let overlay
const style = (el, styles) => {
for (const key in styles) {
el.style[key] = styles[key]
}
return el
}
const show = ({
title = '',
text = ''
}) => {
overlay = document.body.appendChild(
document.createElement('pre')
)
style(overlay, {
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0,
boxSizing: 'border-box',
fontFamily: 'Menlo, monospace',
fontSize: '12px',
overflow: 'auto',
lineHeight: 1.5,
padding: '8px',
margin: 0,
color: 'magenta',
backgroundColor: 'black'
})
const code = ansiHTML(entities.encode(text))
overlay.innerHTML = `<span>${title}</span>
<br />
<br />${code}
`
}
const destroy = () => {
if (!overlay) return
document.body.removeChild(overlay)
}
const ws = new WebSocket('ws://localhost:' + HOT_PORT)
ws.addEventListener('message', msg => {
const data = JSON.parse(msg.data)
switch (data.type) {
case 'errors':
const [ text ] = data.data.errors
console.error(data.data.errors)
show({ title: 'failed to compile', text })
break
case 'ok':
destroy()
break
}
})
ws.addEventListener('close', () => {
show({ title: 'disconnected' })
})

View File

@ -6,7 +6,7 @@ module.exports = async (opts = {}) => {
const page = await browser.newPage()
const {
outDir,
outFile,
outFile = 'presentation.pdf',
port,
width = 1280,
height = 960,

38
lib/screenshot.js Normal file
View File

@ -0,0 +1,38 @@
const fs = require('fs')
const path = require('path')
const puppeteer = require('puppeteer')
module.exports = async (opts) => {
const {
width = 1280,
height = 720,
outFile = 'card.png'
} = opts
const file = path.join(opts.outDir, outFile)
if (!fs.existsSync(opts.outDir)) fs.mkdirSync(opts.outDir)
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.setViewport({ width, height })
await page.goto('http://localhost:' + opts.port, {
waitUntil: 'networkidle2'
})
await page.screenshot({
path: file,
type: 'png',
clip: {
x: 0,
y: 0,
width,
height,
}
})
await browser.close()
return outFile
}

View File

@ -1,7 +1,7 @@
{
"name": "mdx-deck",
"version": "1.5.15",
"description": "MDX-based slide deck presentations",
"description": "MDX-based presentation decks",
"main": "dist/index.js",
"bin": {
"mdx-deck": "./cli.js"
@ -13,6 +13,7 @@
"start": "./cli.js docs/index.mdx -p 8989",
"build": "./cli.js build docs/index.mdx -d site",
"pdf": "./cli.js pdf docs/index.mdx -d site",
"screenshot": "./cli.js screenshot docs/index.mdx -d site",
"help": "./cli.js",
"test": "jest"
},
@ -23,29 +24,43 @@
"@compositor/webfont": "^1.0.39",
"@mdx-js/mdx": "^0.15.0-1",
"@mdx-js/tag": "^0.14.1",
"ansi-html": "0.0.7",
"babel-loader": "^7.1.5",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"chalk": "^2.4.1",
"clipboardy": "^1.2.3",
"get-port": "^4.0.0",
"gray-matter": "^4.0.1",
"hhmmss": "^1.0.0",
"html-entities": "^1.2.1",
"koa": "^2.5.2",
"koa-static": "^5.0.0",
"koa-webpack": "^5.1.0",
"loader-utils": "^1.1.0",
"lodash.debounce": "^4.0.8",
"meow": "^5.0.0",
"mini-html-webpack-plugin": "^0.2.3",
"normalize-newline": "^3.0.0",
"ok-cli": "^3.1.1",
"pkg-conf": "^2.1.0",
"progress-bar-webpack-plugin": "^1.11.0",
"prop-types": "^15.6.2",
"puppeteer": "^1.6.1",
"querystring": "^0.2.0",
"react": "^16.4.1",
"react": "^16.4.2",
"react-dev-utils": "^5.0.1",
"react-dom": "^16.4.2",
"react-swipeable": "^4.3.0",
"react-syntax-highlighter": "^8.0.1",
"remark-emoji": "^2.0.1",
"remark-unwrap-images": "0.0.2-0",
"rimraf": "^2.6.2",
"stringify-object": "^3.2.2",
"styled-components": ">=3.0.0",
"styled-system": "^3.0.2",
"superbox": "^2.1.0",
"webpack": "^4.16.5",
"webpack-hot-client": "^4.1.1"
},
"peerDependencies": {},

96
src/Head.js Normal file
View File

@ -0,0 +1,96 @@
import React from 'react'
import { createPortal } from 'react-dom'
const noop = () => {
console.warn('Missing HeadProvider')
}
export const Context = React.createContext({
tags: [],
push: noop
})
export class HeadProvider extends React.Component {
static defaultProps = {
tags: []
}
push = (elements) => {
this.props.tags.push(...elements)
}
render () {
const context = {
...this.props,
push: this.push
}
return (
<Context.Provider value={context}>
{this.props.children}
</Context.Provider>
)
}
}
export class Head extends React.Component {
state = {
didMount: false
}
rehydrate = () => {
const children = React.Children.toArray(this.props.children)
const nodes = [
...document.head.querySelectorAll('[data-head]')
]
nodes.forEach(node => {
node.remove()
})
children.forEach(child => {
if (child.type === 'title') {
const title = document.head.querySelector('title')
if (title) title.remove()
}
if (child.type === 'meta') {
const { name } = child.props
let meta
if (name) meta = document.head.querySelector(`meta[name="${name}"]`)
if (meta) meta.remove()
}
})
this.setState({
didMount: true
})
}
componentDidMount () {
this.rehydrate()
}
render () {
const children = React.Children.toArray(this.props.children)
.map(child => React.cloneElement(child, {
'data-head': true
}))
const { didMount } = this.state
if (!didMount) {
return (
<Context.Consumer
children={({ push }) => {
push(children)
return false
}}
/>
)
}
return createPortal(
children,
document.head
)
}
}

View File

@ -1,8 +1,9 @@
import React from 'react'
import { render } from 'react-dom'
import PropTypes from 'prop-types'
import SlideDeck from './index'
const mod = require(DOC_FILENAME)
const mod = require(FILENAME)
const slides = mod.default
const { theme, components, Provider } = mod
@ -10,6 +11,7 @@ export default class App extends React.Component {
render () {
return (
<SlideDeck
{...this.props}
slides={slides}
theme={theme}
components={components}
@ -18,3 +20,12 @@ export default class App extends React.Component {
)
}
}
if (typeof document !== 'undefined') {
render(
<App />,
document.getElementById('root')
)
}
if (module.hot) module.hot.accept()

View File

@ -6,6 +6,7 @@ import debounce from 'lodash.debounce'
import querystring from 'querystring'
import Swipeable from 'react-swipeable'
import { Provider as ContextProvider } from './context'
import { HeadProvider } from './Head'
import DefaultProvider from './Provider'
import Carousel from './Carousel'
import Slide from './Slide'
@ -19,6 +20,7 @@ import GoogleFonts from './GoogleFonts'
import defaultTheme from './themes'
import defaultComponents from './components'
export { Head } from './Head'
export { default as Image } from './Image'
export { default as Notes } from './Notes'
export { default as Appear } from './Appear'
@ -81,7 +83,8 @@ export class SlideDeck extends React.Component {
Provider: PropTypes.func,
width: PropTypes.string,
height: PropTypes.string,
ignoreKeyEvents: PropTypes.bool
ignoreKeyEvents: PropTypes.bool,
headTags: PropTypes.array.isRequired,
}
static defaultProps = {
@ -91,7 +94,8 @@ export class SlideDeck extends React.Component {
Provider: DefaultProvider,
width: '100vw',
height: '100vh',
ignoreKeyEvents: false
ignoreKeyEvents: false,
headTags: [],
}
state = {
@ -222,7 +226,8 @@ export class SlideDeck extends React.Component {
components: propsComponents,
Provider: PropsProvider,
width,
height
height,
headTags
} = this.props
const { index, length, mode, step} = this.state
@ -247,48 +252,50 @@ export class SlideDeck extends React.Component {
return (
<ContextProvider value={context}>
<ThemeProvider theme={theme}>
<MDXProvider
components={{
...defaultComponents,
...components
}}>
<Provider {...this.state} update={this.update}>
{mode === modes.grid ? (
<Grid
slides={slides}
update={this.update}
/>
) : (
<Swipeable
onSwipedLeft={() => this.update(inc)}
onSwipedRight={() => this.update(dec)}
trackMouse>
<Wrapper
{...this.state}
<HeadProvider tags={headTags}>
<ThemeProvider theme={theme}>
<MDXProvider
components={{
...defaultComponents,
...components
}}>
<Provider {...this.state} update={this.update}>
{mode === modes.grid ? (
<Grid
slides={slides}
width={width}
height={height}
update={this.update}>
<GoogleFonts />
<Carousel index={index}>
{slides.map((Component, i) => (
<Slide
key={i}
id={'slide-' + i}
index={i}
className='Slide'
>
<Component />
</Slide>
))}
</Carousel>
</Wrapper>
</Swipeable>
)}
</Provider>
</MDXProvider>
</ThemeProvider>
update={this.update}
/>
) : (
<Swipeable
onSwipedLeft={() => this.update(inc)}
onSwipedRight={() => this.update(dec)}
trackMouse>
<Wrapper
{...this.state}
slides={slides}
width={width}
height={height}
update={this.update}>
<GoogleFonts />
<Carousel index={index}>
{slides.map((Component, i) => (
<Slide
key={i}
id={'slide-' + i}
index={i}
className='Slide'
>
<Component />
</Slide>
))}
</Carousel>
</Wrapper>
</Swipeable>
)}
</Provider>
</MDXProvider>
</ThemeProvider>
</HeadProvider>
</ContextProvider>
)
}