Is your feature request related to a problem? Please describe.
I need to be able to distribute a rich presentation (i.e. build with components and assets) as a single, self-contained file that users without any tooling can just open in their browser and enjoy the full experience.
Describe the solution you'd like
Offer something like SLIDEV_SINGLEFILE=1 slidev build mypres.md or slidev build-single mypres.md or similar, that outputs a single, self-contained html file with all assets embedded (likely leveraging vite-plugin-singlefile).
Describe alternatives you've considered
I could do this before with revealjs and pandoc.
Some experimentation results
I did some experimentation (note that I'm a total ts/vite noob) and with some artificial help arrived at a vite-config that seems to be working, but it was brutal and could probably be realized much better/more robust.
In my specific case, a complicating factor is that I wanted to have my complete slidev environment (sl1dev-toolkit) as a reusable and reproducible nix devShell flake, including my themes and components. It also needs to work offline. A solution that worked well is to have the node_modules dir realized by the toolkit flake, and in the content (presentation) repo it's simply a link farm to the nix store. With the added benefit that templates/themes are consistent and not duplicated.
Here the vite.config.ts
import { defineConfig } from 'vite'
import { loadEnv } from 'vite'
import fs from 'node:fs'
import path from 'node:path'
import { createRequire } from 'node:module'
import { viteSingleFile } from 'vite-plugin-singlefile'
const require = createRequire(import.meta.url)
const slidevTypes = require.resolve('@slidev/types/package.json')
.replace('/package.json', '')
const env = loadEnv('', process.cwd(), '')
const assets = env['SLIDEV_SL1DEV_ASSETS'] ?? ''
const singleFile = process.env.SLIDEV_SINGLEFILE === '1'
let outDir = 'dist'
export default defineConfig({
resolve: {
alias: {
'@sl1dev-assets': assets,
'@slidev/types': slidevTypes, // resolve from local node_modules
'markdown-it': require.resolve('markdown-it'),
'plotly.js-dist': require.resolve('plotly.js-dist'),
},
},
plugins: [
{
name: 'sl1dev-assets',
configureServer(server) {
server.middlewares.use('/sl1dev-assets', (req, res, next) => {
const filePath = path.join(assets, req.url ?? '')
if (fs.existsSync(filePath)) {
res.setHeader('Content-Type', getContentType(filePath))
fs.createReadStream(filePath).pipe(res)
} else {
next()
}
})
},
},
...(singleFile ? [{
name: 'sl1dev-force-singlefile',
enforce: 'post' as const,
config() {
return {
base: './',
define: { __SLIDEV_HASH_ROUTE__: true },
}
},
configResolved(config) {
outDir = config.build.outDir
config.build.rollupOptions.output.inlineDynamicImports = true
delete config.build.rollupOptions.output.manualChunks
},
closeBundle() {
setTimeout(() => {
for (const f of ['404.html', '_redirects']) {
const p = path.resolve(outDir, f)
if (fs.existsSync(p)) fs.unlinkSync(p)
}
}, 100)
},
}, viteSingleFile({ removeViteModuleLoader: true })] : []),
],
server: {
fs: { allow: [assets, '.'] },
watch: {
ignored: (path) => path.includes('/nix/store/'),
},
},
build: {
rollupOptions: {
plugins: [
{
// Two problems solved here:
//
// 1. /sl1dev-assets/* URL paths — served by the dev server
// middleware at runtime but invisible to Rollup during build.
// Map them to the real filesystem path in the assets directory.
//
// 2. Nix store importers — components installed into the Nix store
// have no node_modules ancestor, so Rollup's default resolver
// fails on their bare-specifier imports. Resolve those through
// the toolkit's require, which does have node_modules in scope.
name: 'resolve-sl1dev',
resolveId(id: string, importer: string | undefined) {
if (id.startsWith('/sl1dev-assets/') && assets) {
return path.join(assets, id.slice('/sl1dev-assets/'.length))
}
if (
importer?.includes('/nix/store/') &&
!id.startsWith('.') &&
!path.isAbsolute(id)
) {
try {
return require.resolve(id)
} catch {
return null
}
}
},
},
],
},
...(singleFile ? {
assetsInlineLimit: 100_000_000,
} : {}),
},
define: {
'import.meta.env.VITE_SL1DEV_ASSETS': JSON.stringify(assets),
},
optimizeDeps: {
include: ['plotly.js-dist'],
},
})
function getContentType(filePath: string) {
const ext = path.extname(filePath)
return { '.png': 'image/png', '.svg': 'image/svg+xml', '.jpg': 'image/jpeg' }[ext] ?? 'application/octet-stream'
}
The env vars are set by the flake so that the store paths to assets can be passed
Is your feature request related to a problem? Please describe.
I need to be able to distribute a rich presentation (i.e. build with components and assets) as a single, self-contained file that users without any tooling can just open in their browser and enjoy the full experience.
Describe the solution you'd like
Offer something like
SLIDEV_SINGLEFILE=1 slidev build mypres.mdorslidev build-single mypres.mdor similar, that outputs a single, self-contained html file with all assets embedded (likely leveragingvite-plugin-singlefile).Describe alternatives you've considered
I could do this before with revealjs and pandoc.
Some experimentation results
I did some experimentation (note that I'm a total ts/vite noob) and with some artificial help arrived at a vite-config that seems to be working, but it was brutal and could probably be realized much better/more robust.
In my specific case, a complicating factor is that I wanted to have my complete slidev environment (
sl1dev-toolkit) as a reusable and reproduciblenix devShell flake, including my themes and components. It also needs to work offline. A solution that worked well is to have thenode_modulesdir realized by the toolkit flake, and in the content (presentation) repo it's simply a link farm to the nix store. With the added benefit that templates/themes are consistent and not duplicated.Here the
vite.config.tsThe env vars are set by the flake so that the store paths to assets can be passed