react-router-markdown 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # react-router-markdown
2
2
 
3
- Vite plugin to parse Markdown files as React Router pages. It supports defining React Router's meta
4
- function returned data, as frontmatter.
3
+ Vite plugin to import Markdown files as React components with frontmatter support. It follows React
4
+ Router's route module conventions, including the default component, `meta`, and `handle` exports,
5
+ but does not depend on React Router. Use it with React Router, any other React framework that
6
+ supports Vite, or a plain React + Vite app.
5
7
 
6
8
  ## Setup
7
9
 
@@ -50,14 +52,14 @@ export default defineConfig({
50
52
 
51
53
  ### Imports and TypeScript support
52
54
 
53
- In order to TypeScript import `.md` files with type hints, add the following to your `vite.d.ts`
55
+ In order for TypeScript to import `.md` files with type hints, add the following to your `vite.d.ts`
54
56
  file:
55
57
 
56
58
  ```tsx
57
59
  /// <reference types="react-router-markdown/references" />
58
60
  ```
59
61
 
60
- Optionally, you can also add the following to your `tsconfig.json` file:
62
+ Alternatively, you can add the following to your `tsconfig.json` file:
61
63
 
62
64
  ```json
63
65
  {
@@ -68,3 +70,70 @@ Optionally, you can also add the following to your `tsconfig.json` file:
68
70
  ```
69
71
 
70
72
  Now you can use any `.md` file as a React Router page.
73
+
74
+ ## Example
75
+
76
+ `app/routes/about.md`:
77
+
78
+ ```md
79
+ ---
80
+ title: 'About Us'
81
+ meta:
82
+ - title: 'About Us - ACME'
83
+ - name: description
84
+ content: 'Meta description here'
85
+ - name: robots
86
+ content: 'index, follow'
87
+ ---
88
+
89
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam elementum venenatis facilisis. Nam sit
90
+ amet finibus lectus.
91
+
92
+ Nunc lobortis neque magna, id mollis arcu pulvinar ultrices. Aenean mollis dolor nulla. Aliquam
93
+ aliquet tempus ultrices. Integer ac est finibus, efficitur orci faucibus, luctus odio. Vivamus vitae
94
+ venenatis quam, imperdiet rutrum massa.
95
+ ```
96
+
97
+ `app/router.ts`:
98
+
99
+ ```tsx
100
+ import { type RouteConfig, route } from '@react-router/dev/routes'
101
+
102
+ export default [route('about', './routes/about.md')] satisfies RouteConfig
103
+ ```
104
+
105
+ `app/components/markdown-wrapper.tsx`:
106
+
107
+ ```tsx
108
+ import { cn } from '@/lib/utils'
109
+
110
+ export default function MarkdownWrapper({
111
+ children,
112
+ frontmatter,
113
+ }: {
114
+ children: React.ReactNode
115
+ frontmatter: {
116
+ title?: string
117
+ subtitle?: string
118
+ }
119
+ }) {
120
+ const hasHeader = frontmatter.title || frontmatter.subtitle
121
+ return (
122
+ <article className={cn('prose prose-slate dark:prose-invert max-w-none')}>
123
+ {hasHeader && (
124
+ <header className="mb-8 space-y-4">
125
+ {frontmatter.title && (
126
+ <h1 className={cn('text-4xl font-bold tracking-tight lg:text-5xl')}>
127
+ {frontmatter.title}
128
+ </h1>
129
+ )}
130
+ {frontmatter.subtitle && (
131
+ <p className="text-muted-foreground text-xl leading-7">{frontmatter.subtitle}</p>
132
+ )}
133
+ </header>
134
+ )}
135
+ <div className="space-y-6">{children}</div>
136
+ </article>
137
+ )
138
+ }
139
+ ```
package/dist/index.d.mts CHANGED
@@ -8,12 +8,6 @@ type MarkdownPageProps = {
8
8
  frontmatter: Record<string, unknown>;
9
9
  }> | null;
10
10
  };
11
- declare function MarkdownPage({
12
- parseHtml,
13
- rawContent,
14
- frontmatter,
15
- Wrapper
16
- }: MarkdownPageProps): React.ReactElement;
11
+ export declare function MarkdownPage({ parseHtml, rawContent, frontmatter, Wrapper }: MarkdownPageProps): React.ReactElement;
17
12
  //#endregion
18
- export { MarkdownPage };
19
13
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/markdow-page.ts"],"sourcesContent":["import { raw } from 'hast-util-raw'\nimport ReactMarkdown from 'react-markdown'\nimport { jsx } from 'react/jsx-runtime'\n\ntype HastNodes = Parameters<typeof raw>[0]\ntype HastOptions = NonNullable<Parameters<typeof raw>[1]>\ntype MarkdownPageProps = {\n parseHtml: boolean\n rawContent: string\n frontmatter: Record<string, unknown>\n Wrapper: React.ComponentType<{\n children: React.ReactNode\n frontmatter: Record<string, unknown>\n }> | null\n}\n\nfunction rehypeRaw(options: HastOptions) {\n return (tree: HastNodes, file: HastOptions['file']) => raw(tree, { ...options, file })\n}\n\nexport function MarkdownPage({\n parseHtml,\n rawContent,\n frontmatter,\n Wrapper,\n}: MarkdownPageProps): React.ReactElement {\n const rehypePlugins = parseHtml ? [rehypeRaw] : []\n const mdElement = jsx(ReactMarkdown, { children: rawContent, rehypePlugins: rehypePlugins })\n\n if (!Wrapper) {\n return mdElement\n }\n\n return jsx(Wrapper, {\n children: mdElement,\n frontmatter: frontmatter,\n })\n}\n"],"mappings":";;;;AAgBA,SAAS,UAAU,SAAsB;AACvC,SAAQ,MAAiB,SAA8B,IAAI,MAAM;EAAE,GAAG;EAAS;EAAM,CAAC;;AAGxF,SAAgB,aAAa,EAC3B,WACA,YACA,aACA,WACwC;CAExC,MAAM,YAAY,IAAI,eAAe;EAAE,UAAU;EAAY,eADvC,YAAY,CAAC,UAAU,GAAG,EAAE;EACyC,CAAC;AAE5F,KAAI,CAAC,QACH,QAAO;AAGT,QAAO,IAAI,SAAS;EAClB,UAAU;EACG;EACd,CAAC"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/markdow-page.ts"],"sourcesContent":["import { raw } from 'hast-util-raw'\nimport ReactMarkdown from 'react-markdown'\nimport { jsx } from 'react/jsx-runtime'\n\ntype HastNodes = Parameters<typeof raw>[0]\ntype HastOptions = NonNullable<Parameters<typeof raw>[1]>\ntype MarkdownPageProps = {\n parseHtml: boolean\n rawContent: string\n frontmatter: Record<string, unknown>\n Wrapper: React.ComponentType<{\n children: React.ReactNode\n frontmatter: Record<string, unknown>\n }> | null\n}\n\nfunction rehypeRaw(options: HastOptions) {\n return (tree: HastNodes, file: HastOptions['file']) => raw(tree, { ...options, file })\n}\n\nexport function MarkdownPage({\n parseHtml,\n rawContent,\n frontmatter,\n Wrapper,\n}: MarkdownPageProps): React.ReactElement {\n const rehypePlugins = parseHtml ? [rehypeRaw] : []\n const mdElement = jsx(ReactMarkdown, { children: rawContent, rehypePlugins: rehypePlugins })\n\n if (!Wrapper) {\n return mdElement\n }\n\n return jsx(Wrapper, {\n children: mdElement,\n frontmatter: frontmatter,\n })\n}\n"],"mappings":";;;;AAgBA,SAAS,UAAU,SAAsB;CACvC,QAAQ,MAAiB,SAA8B,IAAI,MAAM;EAAE,GAAG;EAAS;CAAK,CAAC;AACvF;AAEA,SAAgB,aAAa,EAC3B,WACA,YACA,aACA,WACwC;CAExC,MAAM,YAAY,IAAI,eAAe;EAAE,UAAU;EAAY,eADvC,YAAY,CAAC,SAAS,IAAI,CAAC;CACyC,CAAC;CAE3F,IAAI,CAAC,SACH,OAAO;CAGT,OAAO,IAAI,SAAS;EAClB,UAAU;EACG;CACf,CAAC;AACH"}
package/dist/vite.d.mts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Plugin } from "vite";
2
-
3
2
  //#region src/lib/vite-plugin.d.ts
4
3
  type ReactRouterMarkdownOptions<D = Record<string, any>> = {
5
4
  /**
package/dist/vite.mjs CHANGED
@@ -8,12 +8,14 @@ function reactRouterMarkdown(options = {}) {
8
8
  enforce: "pre",
9
9
  transform(code, id) {
10
10
  if (!(id.split("?", 1)[0] ?? id).endsWith(".md")) return null;
11
- const { data, content } = matter(beforeTransform(code));
11
+ const transformedCode = beforeTransform(code);
12
+ const { data, content } = matter(transformedCode);
12
13
  const [transformedContent, transformedFrontmatter] = afterTransform(content, data || {});
13
14
  const handleData = handleDataFn(transformedContent, transformedFrontmatter);
14
15
  return {
15
16
  code: [
16
- `import { MarkdownPage } from 'react-router-markdown';`,
17
+ "import { MarkdownPage } from 'react-router-markdown';",
18
+ "import { jsx } from 'react/jsx-runtime';",
17
19
  wrapperModule ? `import Wrapper from ${JSON.stringify(wrapperModule)};` : "const Wrapper = null;",
18
20
  "",
19
21
  `export const frontmatter = ${JSON.stringify(transformedFrontmatter || {})};`,
@@ -23,7 +25,7 @@ function reactRouterMarkdown(options = {}) {
23
25
  `export const handle = ${JSON.stringify(handleData)};`,
24
26
  "",
25
27
  `function Page() {
26
- return _jsx(MarkdownPage, {
28
+ return jsx(MarkdownPage, {
27
29
  Wrapper,
28
30
  parseHtml: ${parseHtml ? "true" : "false"},
29
31
  rawContent: rawContent,
package/dist/vite.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"vite.mjs","names":[],"sources":["../src/lib/vite-plugin.ts","../src/vite.ts"],"sourcesContent":["import matter from 'gray-matter'\nimport { type Plugin } from 'vite'\n\nexport type ReactRouterMarkdownOptions<D = Record<string, any>> = {\n /**\n * Module specifier to import a wrapper component from.\n * Must default-export a React component.\n *\n * Example: '#/components/markdown/wrapper'\n * @default undefined\n */\n wrapperModule?: string\n /**\n * If true, any HTML tags in the markdown content will be parsed as HTML instead of escaped.\n * @default false\n */\n parseHtml?: boolean\n /**\n * A function to return the handle data to be used for each page.\n * It accepts the post-processed content and frontmatter as arguments.\n * @default undefined\n */\n handleData?: (content: string, frontmatter: Record<string, any>) => Partial<D>\n\n /**\n * A function to transform the raw MD file content before it is parsed.\n * @default undefined\n */\n beforeTransform?: (content: string) => string\n\n /**\n * A function to transform the parsed MD content and/or frontmatter after they are parsed.\n * It should return back the transformed content and frontmatter.\n * @default undefined\n */\n afterTransform?: (\n content: string,\n frontmatter: Record<string, any>,\n ) => [string, Record<string, any>]\n}\n\nexport function reactRouterMarkdown<D = Record<string, any>>(\n options: ReactRouterMarkdownOptions<D> = {},\n): Plugin {\n const {\n wrapperModule,\n parseHtml = false,\n handleData: handleDataFn = () => ({}) as Partial<D>,\n beforeTransform = (content) => content,\n afterTransform = (content, frontmatter) => [content, frontmatter],\n }: ReactRouterMarkdownOptions<D> = options\n\n return {\n name: 'react-router-markdown',\n enforce: 'pre',\n\n transform(code, id) {\n const filePath = id.split('?', 1)[0] ?? id\n if (!filePath.endsWith('.md')) return null\n\n const transformedCode = beforeTransform(code)\n const { data, content } = matter(transformedCode)\n const [transformedContent, transformedFrontmatter] = afterTransform(content, data || {})\n const handleData = handleDataFn(transformedContent, transformedFrontmatter)\n\n return {\n code: [\n `import { MarkdownPage } from 'react-router-markdown';`,\n wrapperModule\n ? `import Wrapper from ${JSON.stringify(wrapperModule)};`\n : 'const Wrapper = null;',\n '',\n `export const frontmatter = ${JSON.stringify(transformedFrontmatter || {})};`,\n `export const rawContent = ${JSON.stringify(transformedContent || '')};`,\n '',\n 'export const meta = () => frontmatter.meta || [];',\n `export const handle = ${JSON.stringify(handleData)};`,\n '',\n `function Page() {\n return _jsx(MarkdownPage, {\n Wrapper,\n parseHtml: ${parseHtml ? 'true' : 'false'},\n rawContent: rawContent,\n frontmatter: frontmatter,\n })\n }\n export default Page;\n `,\n ].join('\\n'),\n map: null,\n }\n },\n }\n}\n","export { type ReactRouterMarkdownOptions } from './lib/vite-plugin'\n\nimport { reactRouterMarkdown } from './lib/vite-plugin'\n\nexport default reactRouterMarkdown\n"],"mappings":";;;AAyCA,SAAgB,oBACd,UAAyC,EAAE,EACnC;CACR,MAAM,EACJ,eACA,YAAY,OACZ,YAAY,sBAAsB,EAAE,GACpC,mBAAmB,YAAY,SAC/B,kBAAkB,SAAS,gBAAgB,CAAC,SAAS,YAAY,KAChC;AAEnC,QAAO;EACL,MAAM;EACN,SAAS;EAET,UAAU,MAAM,IAAI;AAElB,OAAI,EADa,GAAG,MAAM,KAAK,EAAE,CAAC,MAAM,IAC1B,SAAS,MAAM,CAAE,QAAO;GAGtC,MAAM,EAAE,MAAM,YAAY,OADF,gBAAgB,KAAK,CACI;GACjD,MAAM,CAAC,oBAAoB,0BAA0B,eAAe,SAAS,QAAQ,EAAE,CAAC;GACxF,MAAM,aAAa,aAAa,oBAAoB,uBAAuB;AAE3E,UAAO;IACL,MAAM;KACJ;KACA,gBACI,uBAAuB,KAAK,UAAU,cAAc,CAAC,KACrD;KACJ;KACA,8BAA8B,KAAK,UAAU,0BAA0B,EAAE,CAAC,CAAC;KAC3E,6BAA6B,KAAK,UAAU,sBAAsB,GAAG,CAAC;KACtE;KACA;KACA,yBAAyB,KAAK,UAAU,WAAW,CAAC;KACpD;KACA;;;2BAGiB,YAAY,SAAS,QAAQ;;;;;;;KAO/C,CAAC,KAAK,KAAK;IACZ,KAAK;IACN;;EAEJ;;;;ACxFH,IAAA,eAAe"}
1
+ {"version":3,"file":"vite.mjs","names":[],"sources":["../src/lib/vite-plugin.ts","../src/vite.ts"],"sourcesContent":["import matter from 'gray-matter'\nimport { type Plugin } from 'vite'\n\nexport type ReactRouterMarkdownOptions<D = Record<string, any>> = {\n /**\n * Module specifier to import a wrapper component from.\n * Must default-export a React component.\n *\n * Example: '#/components/markdown/wrapper'\n * @default undefined\n */\n wrapperModule?: string\n /**\n * If true, any HTML tags in the markdown content will be parsed as HTML instead of escaped.\n * @default false\n */\n parseHtml?: boolean\n /**\n * A function to return the handle data to be used for each page.\n * It accepts the post-processed content and frontmatter as arguments.\n * @default undefined\n */\n handleData?: (content: string, frontmatter: Record<string, any>) => Partial<D>\n\n /**\n * A function to transform the raw MD file content before it is parsed.\n * @default undefined\n */\n beforeTransform?: (content: string) => string\n\n /**\n * A function to transform the parsed MD content and/or frontmatter after they are parsed.\n * It should return back the transformed content and frontmatter.\n * @default undefined\n */\n afterTransform?: (\n content: string,\n frontmatter: Record<string, any>,\n ) => [string, Record<string, any>]\n}\n\nexport function reactRouterMarkdown<D = Record<string, any>>(\n options: ReactRouterMarkdownOptions<D> = {},\n): Plugin {\n const {\n wrapperModule,\n parseHtml = false,\n handleData: handleDataFn = () => ({}) as Partial<D>,\n beforeTransform = (content) => content,\n afterTransform = (content, frontmatter) => [content, frontmatter],\n }: ReactRouterMarkdownOptions<D> = options\n\n return {\n name: 'react-router-markdown',\n enforce: 'pre',\n\n transform(code, id) {\n const filePath = id.split('?', 1)[0] ?? id\n if (!filePath.endsWith('.md')) return null\n\n const transformedCode = beforeTransform(code)\n const { data, content } = matter(transformedCode)\n const [transformedContent, transformedFrontmatter] = afterTransform(content, data || {})\n const handleData = handleDataFn(transformedContent, transformedFrontmatter)\n\n return {\n code: [\n \"import { MarkdownPage } from 'react-router-markdown';\",\n \"import { jsx } from 'react/jsx-runtime';\",\n wrapperModule\n ? `import Wrapper from ${JSON.stringify(wrapperModule)};`\n : 'const Wrapper = null;',\n '',\n `export const frontmatter = ${JSON.stringify(transformedFrontmatter || {})};`,\n `export const rawContent = ${JSON.stringify(transformedContent || '')};`,\n '',\n 'export const meta = () => frontmatter.meta || [];',\n `export const handle = ${JSON.stringify(handleData)};`,\n '',\n `function Page() {\n return jsx(MarkdownPage, {\n Wrapper,\n parseHtml: ${parseHtml ? 'true' : 'false'},\n rawContent: rawContent,\n frontmatter: frontmatter,\n })\n }\n export default Page;\n `,\n ].join('\\n'),\n map: null,\n }\n },\n }\n}\n","export { type ReactRouterMarkdownOptions } from './lib/vite-plugin'\n\nimport { reactRouterMarkdown } from './lib/vite-plugin'\n\nexport default reactRouterMarkdown\n"],"mappings":";;;AAyCA,SAAgB,oBACd,UAAyC,CAAC,GAClC;CACR,MAAM,EACJ,eACA,YAAY,OACZ,YAAY,sBAAsB,CAAC,IACnC,mBAAmB,YAAY,SAC/B,kBAAkB,SAAS,gBAAgB,CAAC,SAAS,WAAW,MAC/B;CAEnC,OAAO;EACL,MAAM;EACN,SAAS;EAET,UAAU,MAAM,IAAI;GAElB,IAAI,EADa,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,MAAM,GAAA,CAC1B,SAAS,KAAK,GAAG,OAAO;GAEtC,MAAM,kBAAkB,gBAAgB,IAAI;GAC5C,MAAM,EAAE,MAAM,YAAY,OAAO,eAAe;GAChD,MAAM,CAAC,oBAAoB,0BAA0B,eAAe,SAAS,QAAQ,CAAC,CAAC;GACvF,MAAM,aAAa,aAAa,oBAAoB,sBAAsB;GAE1E,OAAO;IACL,MAAM;KACJ;KACA;KACA,gBACI,uBAAuB,KAAK,UAAU,aAAa,EAAE,KACrD;KACJ;KACA,8BAA8B,KAAK,UAAU,0BAA0B,CAAC,CAAC,EAAE;KAC3E,6BAA6B,KAAK,UAAU,sBAAsB,EAAE,EAAE;KACtE;KACA;KACA,yBAAyB,KAAK,UAAU,UAAU,EAAE;KACpD;KACA;;;2BAGiB,YAAY,SAAS,QAAQ;;;;;;;IAOhD,CAAC,CAAC,KAAK,IAAI;IACX,KAAK;GACP;EACF;CACF;AACF;;;AC1FA,IAAA,eAAe"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-router-markdown",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Vite plugin to parse and use markdown files as React Router pages, including frontmatter support.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -44,6 +44,7 @@
44
44
  "format": "oxfmt --write .",
45
45
  "format:check": "oxfmt --check .",
46
46
  "lint": "pnpm typecheck && pnpm publint",
47
+ "prepublishOnly": "pnpm build",
47
48
  "publint": "publint",
48
49
  "test": "vitest --run",
49
50
  "test:coverage": "vitest --run --coverage",
@@ -56,20 +57,20 @@
56
57
  "react-markdown": "^10.1.0"
57
58
  },
58
59
  "devDependencies": {
59
- "@types/node": "^25.5.0",
60
- "@types/react": "^19.2.14",
61
- "@vitest/coverage-v8": "^4.1.2",
62
- "oxfmt": "^0.42.0",
63
- "publint": "^0.3.18",
64
- "react": "^19.2.4",
65
- "tsdown": "^0.21.7",
66
- "typescript": "^6.0.2",
67
- "vite": "^8.0.3",
68
- "vitest": "^4.1.2"
60
+ "@types/node": "^26.5.1",
61
+ "@types/react": "^19.3.0",
62
+ "@vitest/coverage-v8": "^5.0.0",
63
+ "oxfmt": "^0.67.0",
64
+ "publint": "^0.3.24",
65
+ "react": "^19.3.0",
66
+ "tsdown": "^0.23.0",
67
+ "typescript": "^7.0.2",
68
+ "vite": "^8.3.0",
69
+ "vitest": "^5.0.0"
69
70
  },
70
71
  "peerDependencies": {
71
72
  "react": "^19.2.4",
72
- "vite": "^7.0.0 || ^8.0.0"
73
+ "vite": "^8.0.0"
73
74
  },
74
- "packageManager": "pnpm@10.33.0"
75
+ "packageManager": "pnpm@12.4.1"
75
76
  }