Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
name: Test + Typecheck
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: pnpm

- name: Install pnpm
run: corepack enable && corepack prepare pnpm@latest --activate

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm run typecheck

- name: Run tests (including SSR import smoke test)
run: pnpm test -- --run
122 changes: 121 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,115 @@ Notes:
- `vue-i18n` is optional: the library provides a synchronous fallback translator. If your app uses `vue-i18n`, the library will automatically wire into it at runtime when available.
- If you're installing this library inside a monorepo or using pnpm workspaces, install peers at the workspace root so they are available to consuming packages.

## Server-Side Rendering (SSR)

This library is designed to be safe to import in SSR builds, but several features depend on browser-only APIs (DOM, Clipboard, Web Workers, Monaco, Mermaid, KaTeX). To avoid runtime errors during server-side rendering, the library lazy-loads heavyweight peers and guards browser globals where possible. However, some advanced features (Monaco editor, Mermaid progressive rendering, Web Workers) are inherently client-side and must be used inside client-only blocks in SSR environments.

Recommended consumption patterns:

- For Nuxt 3 (or other SSR frameworks): render the component client-side only. Example using Nuxt's <client-only> wrapper:

```vue
<template>
<client-only>
<MarkdownRender :content="markdown" />
</client-only>
</template>
```

For a fuller Nuxt 3 recipe and extra notes, see the docs: `docs/nuxt-ssr.md`.

- For Vite + plain SSR (or when you control hydration): conditionally render on the client with a small wrapper component:

```vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import MarkdownRender from 'vue-renderer-markdown'

const mounted = ref(false)
onMounted(() => {
mounted.value = true
})
</script>

<template>
<div v-if="mounted">
<MarkdownRender :content="markdown" />
</div>
<div v-else>
<!-- SSR fallback: simple preformatted text to avoid heavy client peers -->
<pre>{{ markdown }}</pre>
</div>
</template>
```

Notes and caveats:

- If you depend on Monaco, Mermaid, KaTeX or the optional icon pack at runtime, install those peers in your app and ensure they are available on the client. The library will attempt to lazy-load them only in the browser.
- The library aims to avoid throwing on import during SSR. If you still see ReferenceErrors (e.g. `window is not defined`), please open an issue and include the stack trace — I'll prioritize a fix.
- For server-rendered markup that needs diagrams or highlighted code server-side, consider generating static HTML on the server (e.g., pre-render Mermaid/KaTeX output) and pass it into the renderer as raw HTML or a safe, server-side AST.

If you'd like, I can add an explicit "SSR" recipe and a Nuxt module example to the repo — say the word and I'll add it.

### SSR recipe (Nuxt 3 and Vite SSR)

Below are concrete recipes to run this renderer safely in SSR environments. These cover common setups and show how to avoid importing client-only peers during server rendering.

- Nuxt 3 (recommended for full-app SSR)

1. Install peers you need on the client (for example `mermaid`, `vue-use-monaco`) as normal dependencies in your Nuxt app.
2. Use Nuxt's `<client-only>` wrapper for pages or components that rely on client-only features like Monaco or progressive Mermaid:

```vue
<template>
<client-only>
<MarkdownRender :content="markdown" />
</client-only>
</template>
```

3. If you need server-rendered HTML for specific diagrams or math, pre-render those outputs on the server (for example using a small service or build step that runs KaTeX or Mermaid CLI) and pass the resulting HTML into your page as pre-rendered fragments.

- Vite + custom SSR (manual hydrate)

If you run your own Vite SSR pipeline, prefer a client-only wrapper to delay browser-only initialization until hydration:

```vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import MarkdownRender from 'vue-renderer-markdown'

const mounted = ref(false)
onMounted(() => {
mounted.value = true
})
</script>

<template>
<div v-if="mounted">
<MarkdownRender :content="markdown" />
</div>
<div v-else>
<!-- Lightweight SSR fallback to avoid heavy peers on the server -->
<pre>{{ markdown }}</pre>
</div>
</template>
```

Notes:

- The package aims to be import-safe during SSR: heavy peers are lazy-loaded in the browser and many DOM/Worker usages are guarded. However, some features (Monaco editor, Web Workers, progressive Mermaid rendering) are client-only by nature — they must be used inside client-only wrappers or deferred with lifecycle hooks.
- To guard against regressions, this repo includes a small SSR smoke test you can run locally:

```bash
pnpm run check:ssr
```

This uses Vitest to import the library entry (`src/exports`) in a Node environment and will fail if the import throws during SSR.

- CI: a small GitHub Actions workflow (`.github/workflows/ci.yml`) has been added to run typecheck and tests (including the SSR smoke test) on push and PR to `main`.

If you'd like, I can add a short Nuxt module wrapper or a dedicated example page for Nuxt to the `playground/` directory — say the word and I'll scaffold it.
## Quick Start

### Choose Your Code Block Rendering Style
Expand Down Expand Up @@ -636,11 +745,22 @@ If your project uses Webpack instead of Vite, you can use the official `monaco-e
Install:

```bash
# pnpm (dev)
pnpm add -D monaco-editor monaco-editor-webpack-plugin
# or
```

```bash
# npm (dev)
npm install --save-dev monaco-editor monaco-editor-webpack-plugin
```

```bash
# yarn (dev)
yarn add -D monaco-editor monaco-editor-webpack-plugin
```

Note: `pnpm add -D` and `yarn add -D` are equivalent to `npm install --save-dev` and install the packages as development dependencies.

Example `webpack.config.js`:

```js
Expand Down
3 changes: 3 additions & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ declare module 'vue' {
LinkNode: typeof import('./src/components/LinkNode/LinkNode.vue')['default']
ListItemNode: typeof import('./src/components/ListItemNode/ListItemNode.vue')['default']
ListNode: typeof import('./src/components/ListNode/ListNode.vue')['default']
MarkdownCodeBlockNode: typeof import('./src/components/MarkdownCodeBlockNode/MarkdownCodeBlockNode.vue')['default']
MathBlockNode: typeof import('./src/components/MathBlockNode/MathBlockNode.vue')['default']
MathInlineNode: typeof import('./src/components/MathInlineNode/MathInlineNode.vue')['default']
MermaidBlockNode: typeof import('./src/components/MermaidBlockNode/MermaidBlockNode.vue')['default']
NodeRenderer: typeof import('./src/components/NodeRenderer/NodeRenderer.vue')['default']
ParagraphNode: typeof import('./src/components/ParagraphNode/ParagraphNode.vue')['default']
PreCodeNode: typeof import('./src/components/PreCodeNode/PreCodeNode.vue')['default']
ReferenceNode: typeof import('./src/components/ReferenceNode/ReferenceNode.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
StrikethroughNode: typeof import('./src/components/StrikethroughNode/StrikethroughNode.vue')['default']
Expand Down
56 changes: 56 additions & 0 deletions docs/nuxt-ssr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Nuxt 3 SSR usage (example)

This short recipe shows a minimal, safe way to use `vue-renderer-markdown` in Nuxt 3 so that client-only features (Monaco, Mermaid, Web Workers) are only initialized in the browser.

## Install peers (client-side)

Install the peers you need in your Nuxt app. For example, to enable Mermaid and Monaco editor previews (optional):

```bash
# pnpm (recommended)
pnpm add mermaid vue-use-monaco

# npm
npm install mermaid vue-use-monaco
```

Do NOT import these peers from server-only code paths during SSR.

## Example page (client-only wrapper)

Create a page or component that defers mounting the renderer to the client. Nuxt provides a `<client-only>` built-in wrapper which is perfect:

```vue
<script setup lang="ts">
import { ref } from 'vue'
import MarkdownRender from 'vue-renderer-markdown'

const markdown = ref(`# Hello from Nuxt 3\n\nThis content is rendered only on the client.`)
</script>

<template>
<client-only>
<MarkdownRender :content="markdown" />
</client-only>
</template>
```

This ensures the `MarkdownRender` component (and any optional peers it lazy-loads) are only imported/initialized in the browser.

## Server-rendered diagrams or math

If you need server-rendered HTML for diagrams or math (so crawlers or first paint include them), pre-render those outputs during your build step or via a small server-side task. Example approaches:

- Use a build script that runs `mermaid-cli` / `katex` to convert certain diagrams/formulas to HTML/SVG during content generation and embed the resulting HTML into the page.
- Use a server endpoint that returns pre-rendered diagram SVG/HTML and fetch it on the client as needed.

Then pass pre-rendered fragments into the renderer as trusted HTML or a server-side AST so the client avoids heavy initialization for those artifacts.

## Notes

- Prefer `client-only` when using Monaco Editor, Web Workers, or progressive Mermaid.
- The library is designed to be import-safe during SSR: heavy peers are lazy-loaded in the browser and many DOM/Worker usages are guarded. If you see an import-time ReferenceError, please provide the stack trace.

---

If you'd like, I can also add a ready-to-run Nuxt 3 playground example under `playground/` (small project + page) that shows this integration end-to-end.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"test:ui": "vitest --ui",
"test:update": "vitest -u",
"typecheck": "vue-tsc --noEmit",
"check:ssr": "vitest --run -t \"SSR import safety\"",
"prepublishOnly": "npm run build",
"play": "pnpm run -C playground dev",
"play:build": "pnpm run -C playground build",
Expand Down
Loading
Loading