nkzw-tech / remdx

Beautiful Minimalist React & MDX Presentations

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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:

https://github.com/nkzw-tech/dev-velocity-talk/blob/c1f3cb336e29d3b16d652f815af5e83e0ec57521/index.html#L20

<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:

  1. Vite define option + dynamic import - run with pnpm dev <slide deck name>, where "dev": "vite dev --" in package.json
    Upsides: Works in development, also with hot reloading
    Downsides: Build fails, ...Cp(()=>import("./slides.re.mdx"),[])) appears in dist/assets/index-7dc2a7e8.js built file, with no slides.re.mdx in the dist/ 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()],
      };
    });
  2. 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 my fs.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()],
      };
    });
  3. @rollup/plugin-virtual to provide dynamic html for entry point - delete index.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()],
      };
    });
  4. 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 🎉