diff --git a/.github/workflows/package-consumers.yml b/.github/workflows/package-consumers.yml
index c9bff3d4a..4a83a3c35 100644
--- a/.github/workflows/package-consumers.yml
+++ b/.github/workflows/package-consumers.yml
@@ -29,3 +29,5 @@ jobs:
- run: npm run build
- run: npm run coverage
- run: npm run test:consumers
+ - run: npm run build --workspace nextjs
+ - run: npm run test:nextjs
diff --git a/core/README.md b/core/README.md
index d59fc5c92..b29112353 100644
--- a/core/README.md
+++ b/core/README.md
@@ -1064,49 +1064,38 @@ export default function App() {
### Support Nextjs
-Use examples in [nextjs](https://nextjs.org/). [`#52`](https://github.com/uiwjs/react-md-editor/issues/52#issuecomment-848969341) [`#224`](https://github.com/uiwjs/react-md-editor/issues/224#issuecomment-901112079)
+For Next.js App Router, see the working [Next.js example](https://github.com/uiwjs/react-md-editor/tree/master/example/nextjs) ([#682](https://github.com/uiwjs/react-md-editor/issues/682))
-[](https://codesandbox.io/embed/nextjs-example-react-md-editor-https-github-com-uiwjs-react-md-editor-issues-516-1z56px?fontsize=14&hidenavigation=1&theme=dark)
-[](https://codesandbox.io/embed/nextjs-example-react-md-editor-qjhn7?fontsize=14&hidenavigation=1&theme=dark) [](https://stackblitz.com/edit/nextjs-react-md-editor?embed=1&file=pages/index.js&hideExplorer=1&hideNavigation=1&theme=dark)
-[](https://github.com/uiwjs/react-md-editor/issues/52#issuecomment-848969341)
-[](https://github.com/uiwjs/react-md-editor/issues/224#issuecomment-901112079)
+Install both packages so their published styles can be imported directly:
```bash
-npm install next-remove-imports
-npm install @uiw/react-md-editor@v3.6.0
+npm install @uiw/react-md-editor @uiw/react-markdown-preview
```
-```js
-// next.config.js
-const removeImports = require('next-remove-imports')();
-module.exports = removeImports({});
+Load global styles from `app/layout.tsx`:
+
+```tsx
+import '@uiw/react-md-editor/markdown-editor.css';
+import '@uiw/react-markdown-preview/markdown.css';
```
-```jsx
-import "@uiw/react-md-editor/markdown-editor.css";
-import "@uiw/react-markdown-preview/markdown.css";
-import dynamic from "next/dynamic";
-import { useState } from "react";
+In a client component, load the editor without server-side rendering:
-import * as commands from "@uiw/react-md-editor/commands"
+```tsx
+'use client';
+import dynamic from 'next/dynamic';
+import { useState } from 'react';
-const MDEditor = dynamic(
- () => import("@uiw/react-md-editor"),
- { ssr: false }
-);
+const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });
-function HomePage() {
- const [value, setValue] = useState("**Hello world!!!**");
- return (
-
-
-
- );
+export default function Page() {
+ const [value, setValue] = useState(['```js', 'const answer = 42;', '```'].join('\n'));
+ return ;
}
-
-export default HomePage;
```
+If highlighting is missing with older Turbopack versions, try `next dev` without `--turbopack`
+
### Support dark-mode/night-mode
By default, the [`dark-mode`](https://github.com/jaywcjlove/dark-mode/) is automatically switched according to the system. If you need to switch manually, just set the `data-color-mode="dark"` parameter for body.
diff --git a/example/nextjs/README.md b/example/nextjs/README.md
index e215bc4cc..49c3e4940 100644
--- a/example/nextjs/README.md
+++ b/example/nextjs/README.md
@@ -1,36 +1,11 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+# Next.js App Router example
-## Getting Started
+Run `npm install` from the repository root, then `npm run dev --workspace nextjs`
-First, run the development server:
+The page shows a JavaScript code block in the live preview, with syntax highlighting from `@uiw/react-markdown-preview`
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
+The global editor and preview styles are imported by `src/app/layout.tsx`, not by the client component
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+The editor in `src/app/page.tsx` uses `next/dynamic` with `ssr: false` because the editor needs browser APIs
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+This example uses Webpack for `next dev`; earlier Turbopack versions could leave syntax highlighting unstyled (see [#682](https://github.com/uiwjs/react-md-editor/issues/682))
diff --git a/example/nextjs/package.json b/example/nextjs/package.json
index df5fd5acb..ed70acccc 100644
--- a/example/nextjs/package.json
+++ b/example/nextjs/package.json
@@ -3,12 +3,14 @@
"version": "4.2.2",
"private": true,
"scripts": {
- "dev": "next dev --turbopack",
+ "dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
+ "@uiw/react-md-editor": "^4.2.2",
+ "@uiw/react-markdown-preview": "^5.2.0",
"next": "15.3.1",
"react": "^18.0.0",
"react-dom": "^18.0.0"
diff --git a/example/nextjs/src/app/layout.tsx b/example/nextjs/src/app/layout.tsx
index d3ef21d11..2853da2c6 100644
--- a/example/nextjs/src/app/layout.tsx
+++ b/example/nextjs/src/app/layout.tsx
@@ -1,6 +1,8 @@
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
+import '@uiw/react-md-editor/markdown-editor.css';
+import '@uiw/react-markdown-preview/markdown.css';
const geistSans = Geist({
variable: '--font-geist-sans',
diff --git a/example/nextjs/src/app/page.tsx b/example/nextjs/src/app/page.tsx
index 8386d7c09..2bd21538b 100644
--- a/example/nextjs/src/app/page.tsx
+++ b/example/nextjs/src/app/page.tsx
@@ -6,8 +6,17 @@ import type { ContextStore } from '@uiw/react-md-editor';
const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });
type OnChange = (value?: string, event?: React.ChangeEvent, state?: ContextStore) => void;
+const initialMarkdown = [
+ '# Syntax highlighting',
+ '',
+ '```js',
+ 'const answer = 42;',
+ 'console.log(answer);',
+ '```',
+].join('\n');
+
export default function Home() {
- const [value, setValue] = React.useState('**Hello world!!!**');
+ const [value, setValue] = React.useState(initialMarkdown);
const onChange = React.useCallback((val) => {
setValue(val || '');
diff --git a/package.json b/package.json
index b1d7095c6..5e7a1c249 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,8 @@
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"remove": "npm run clean && lerna exec \"rm -rf package-lock.json\" --scope react-code-preview-layout --scope website",
"clean": "lerna clean --yes",
- "test:consumers": "node --test test/package-exports.test.mjs && node test/package-consumers.mjs"
+ "test:consumers": "node --test test/package-exports.test.mjs && node test/package-consumers.mjs",
+ "test:nextjs": "node --test test/nextjs-styles.test.mjs"
},
"repository": {
"type": "git",
diff --git a/test/nextjs-highlight.test.tsx b/test/nextjs-highlight.test.tsx
new file mode 100644
index 000000000..c59e45dc0
--- /dev/null
+++ b/test/nextjs-highlight.test.tsx
@@ -0,0 +1,22 @@
+/**
+ * @jest-environment jsdom
+ */
+import React from 'react';
+import { render } from '@testing-library/react';
+import MDEditor from '../core/src';
+import MDEditorCommon from '../core/src/index.common';
+
+const source = ['# Syntax highlighting', '', '```js', 'const answer = 42;', 'console.log(answer);', '```'].join('\n');
+
+describe.each([
+ ['default', MDEditor],
+ ['common', MDEditorCommon],
+] as const)('%s fenced code preview', (_mode, Editor) => {
+ it('keeps JavaScript tokens in the preview', () => {
+ const { container } = render();
+ const code = container.querySelector('.w-md-editor-preview code.language-js');
+ expect(code).not.toBeNull();
+ expect(code?.querySelector('.token.keyword')?.textContent).toBe('const');
+ expect(code?.querySelector('.token.function')?.textContent).toBe('log');
+ });
+});
diff --git a/test/nextjs-styles.test.mjs b/test/nextjs-styles.test.mjs
new file mode 100644
index 000000000..1ca4714ea
--- /dev/null
+++ b/test/nextjs-styles.test.mjs
@@ -0,0 +1,31 @@
+import assert from 'node:assert/strict';
+import { readdir, readFile } from 'node:fs/promises';
+import { dirname, join, resolve } from 'node:path';
+import { test } from 'node:test';
+import { fileURLToPath } from 'node:url';
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const staticDir = join(root, 'example/nextjs/.next/static');
+
+async function findCss(directory) {
+ const entries = await readdir(directory, { withFileTypes: true });
+ const files = await Promise.all(
+ entries.map(async (entry) => {
+ const path = join(directory, entry.name);
+ if (entry.isDirectory()) return findCss(path);
+ return entry.isFile() && entry.name.endsWith('.css') ? [path] : [];
+ }),
+ );
+ return files.flat();
+}
+
+test('Next.js build includes the editor and syntax highlighting styles', async () => {
+ const files = await findCss(staticDir);
+ assert.ok(files.length > 0, 'Next.js did not emit CSS');
+ const css = (await Promise.all(files.map((file) => readFile(file, 'utf8')))).join('\n');
+
+ assert.match(css, /\.w-md-editor\b/, 'editor layout CSS missing');
+ assert.match(css, /\.wmde-markdown\b/, 'markdown preview CSS missing');
+ assert.match(css, /\.token\.keyword\b/, 'Prism highlighting CSS missing');
+ assert.match(css, /--color-prettylights-syntax-keyword\b/, 'syntax colour variables missing');
+});