Directory structure recommendation for multiple slide decks?
karlhorky opened this issue · comments
Hi @cpojer, first of all, thanks for ReMDX, really cool project!
Looking at the demo repos, it seems like the slides.re.mdx
is referenced in index.html
in the root:
<script type="module">
import '@nkzw/remdx/style.css';
import './style.css';
import { render } from '@nkzw/remdx';
render(document.getElementById('app'), import('./slides.re.mdx'));
</script>
Is it ok to have multiple slide decks per repo / folder (while still maintaining one package.json
with the vite
, @nkzw/vite-plugin-remdx
dependencies and the associated scripts)? eg. by passing in another CLI arg to npm run dev
which would be passed along to the index.html
file...
If there's nothing technically holding this back, it would be great to get a recommendation on directory structure for multi-slide-deck folders or repos (either just in this issue as a reply or also in the readme as permanent documentation)
Hey Karl,
thanks so much for trying out ReMDX! ReMDX should work and scale just fine with any number of slide decks in any type of repo configuration that you have. For example, my primary Athena Crisis presentation works by integrating ReMDX directly into the game's monorepo, which allows me to add any parts of the game into a slide deck. ReMDX at its core is just a Vite plugin that transforms MDX (with "slide page breaks") into a data structure with React components. Once you import a remdx file in JS, you can do anything you like: merge multiple slide deck files together, further transform the deck, or render multiple decks on the same page.
If you are working with a monorepo setup, add ReMDX to the frontend project next to where you are already using Vite. If you just want to have multiple slide decks in one repo, what you described should work out of the box: just create multiple index.html
files – one for each slide deck – or use one index file and toggle based on the URL or build a UI to select the slide deck that should be shown:
const currentDeck = new URLSearchParams(location.search).get('deck');
const deck = currentDeck === 'apple' ? import('./apple.re.mdx') : import('./banana.re.mdx');
render(document.getElementById('app'), deck);
With this you can have one index.html
file and toggle the deck via ?deck=apple
and ?deck=banana
. This should give you enough of an idea of how flexible ReMDX is. If you like, you can also build a React UI to select slide decks and manage loading and showing them via React state.
In terms of recommendations for a repo structure, my goal is generally to allow for flexibility. Do whatever works for you, and if it doesn't work or breaks down, let's figure out a path forward. The only thing I'd like to do over time is add a more opinionated set of basic components in a separate package. Currently ReMDX is barebones because I don't want to be too prescriptive, and it's also why I open sourced some of my slide decks so you can get an idea of the core components you'll likely need to put together a presentation.
I'm going to close this issue since there are no changes necessary for ReMDX, but feel free to keep the conversation going if you have questions.
Great thanks! I'll look at these options and see what works.
I'm guessing for our slide decks (lecture slide decks for upleveled.io), we'll want to both run dev server and also build specific slide decks from the command like, so I'll look at how I can deal with the CLI argument of the slide deck name from there.
I'll circle back around and report my findings / approach when I'm further :)
In that case instead of using the URL search for state you could use an env variable that you set through your Vite config based on the params that your dev server or build process is started with.
So, some progress - not quite there yet though.
Here are the things I've tried, and the dead ends I've run into with each option:
-
Vite
define
option + dynamic import - run withpnpm dev <slide deck name>
, where"dev": "vite dev --"
inpackage.json
Upsides: Works in development, also with hot reloading
Downsides: Build fails,...Cp(()=>import("./slides.re.mdx"),[]))
appears indist/assets/index-7dc2a7e8.js
built file, with noslides.re.mdx
in thedist/
folder (maybe because of vitejs/vite#14102)<!-- index.html --> <script type="module"> import '@nkzw/remdx/style.css'; import { render } from '@nkzw/remdx'; render(document.getElementById('app'), import(`./${__DECK__}.re.mdx`)); </script>
// vite.config.ts import remdx from '@nkzw/vite-plugin-remdx'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; export default defineConfig(() => { return { define: { __DECK__: JSON.stringify(process.argv[4]) }, plugins: [remdx(), react()], }; });
-
Custom Vite Plugin for dynamic slide content using Vite Virtual Modules
Upsides: Works with dev mode and build mode
Downsides: hot reloading is broken (I assume because of myfs.readFile
in the Vite config)// vite.config.ts import remdx from '@nkzw/vite-plugin-remdx'; import react from '@vitejs/plugin-react'; import { readFile } from 'fs/promises'; import { defineConfig } from 'vite'; function virtualSlideDeckPlugin() { const virtualModuleId = 'virtual:slides.re.mdx'; const resolvedVirtualModuleId = '\0' + virtualModuleId; return { name: 'dynamic-html-plugin', enforce: 'pre', resolveId(id: string) { if (id === virtualModuleId) { return resolvedVirtualModuleId; } }, load(id: string) { if (id === resolvedVirtualModuleId) { return readFile(`./${process.argv[4]}.re.mdx`, 'utf-8'); } }, }; } export default defineConfig(({ mode }) => { return { plugins: [dynamicHtmlPlugin(), virtualSlideDeckPlugin(), remdx(), react()], }; });
-
@rollup/plugin-virtual
to provide dynamic html for entry point - deleteindex.html
and past HTML content into Vite config
Upsides: None, doesn't work yet
Downsides: Could not get this working - Vite server returned 404// vite.config.ts import remdx from '@nkzw/vite-plugin-remdx'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; import virtual from '@rollup/plugin-virtual'; export default defineConfig(({ mode }) => { return { build: { rollupOptions: { input: 'html', plugins: [ virtual({ html: `<html content here, including process.argv[4]>`}), ], }, }, plugins: [remdx(), react()], }; });
-
Also tried creating a Vite plugin with the Vite
transformIndexHtml
option to dynamically transform the import module id in the HTML, but this ran too late, leading to module resolution problems
Ok, got something working 🚀
Digging a bit deeper on option 4 with the Vite transformIndexHtml
option, it seems like there's an option order: 'pre'
to run this before the module resolution:
package.json
{
"name": "slides",
"version": "0.0.1",
"type": "module",
"scripts": {
- "build": "vite build",
- "dev": "vite dev"
+ "build": "vite build --",
+ "dev": "vite dev --"
},
"dependencies": {
"@nkzw/remdx": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@nkzw/vite-plugin-remdx": "*",
"@rollup/plugin-virtual": "^3.0.2",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^4.0.0",
"prettier": "3.2.5",
"vite": "^5.0.0",
"vite-plugin-dynamic-import": "^1.5.0"
},
"pnpm": {
"overrides": {
"shiki": "^0.11.0"
}
}
}
vite.config.ts
import { access } from 'node:fs/promises';
import mdx from '@mdx-js/rollup';
import remdx from '@nkzw/vite-plugin-remdx';
import react from '@vitejs/plugin-react';
import remarkFrontmatter from 'remark-frontmatter';
import { defineConfig } from 'vite';
const slideDeckName = process.argv[4];
const slideDeckDirPath = `./slideDecks/${slideDeckName}`;
const slideDeckPath = `${slideDeckDirPath}/slides.re.mdx`;
if (!slideDeckName) {
throw new Error(`Slide deck name is required:
pnpm deck <slide-deck-name>
`);
}
try {
await access(slideDeckDirPath);
await access(slideDeckPath);
} catch (error) {
throw new Error(`Slide deck "${slideDeckName}" does not exist`);
}
function addDynamicSlideDeckPathToIndexHtml() {
return {
name: 'add-dynamic-slide-deck-path-to-html',
transformIndexHtml: {
order: 'pre' as const,
handler(html: string) {
return html.replace(
'./slideDecks/__SLIDE_DECK_NAME__/slides.re.mdx',
slideDeckPath,
);
},
},
};
}
function addAssetsPathPrefixToIndexHtml() {
return {
name: 'add-assets-path-prefix-to-html',
transformIndexHtml: {
order: 'post' as const,
handler(html: string) {
return html.replaceAll(
'"/assets/',
`"/slide-decks/${slideDeckName}/assets/`,
);
},
},
};
}
export default defineConfig(() => {
return {
base: `/slide-decks/${slideDeckName}/`,
build: {
target: 'esnext',
outDir: `${slideDeckDirPath}/dist`,
},
plugins: [
addDynamicSlideDeckPathToIndexHtml(),
addAssetsPathPrefixToIndexHtml(),
remdx(),
mdx({ exclude: ['**/*.re.mdx'], remarkPlugins: [remarkFrontmatter] }),
react(),
],
};
});
index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>ReMDX</title>
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1, viewport-fit=cover"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
</head>
<body>
<div id="app"></div>
<script type="module">
import '@nkzw/remdx/style.css';
import { render } from '@nkzw/remdx';
render(
document.getElementById('app'),
import('./slideDecks/__SLIDE_DECK_NAME__/slides.re.mdx'),
);
</script>
</body>
</html>
This enables the CLI to be used to run a deck at ./slideDecks/deck1/slides.re.mdx
with this command:
pnpm dev deck1
Building is pretty much the same:
pnpm build deck1
That's awesome, love seeing that progress 🎉