-
-
Notifications
You must be signed in to change notification settings - Fork 258
refactor: split out blog route from shared MdxRoute file #1213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b2d50cd
fe5a3f6
2c379f7
beb303d
c377bf6
1c63f90
8592c96
16f3109
8ecfeb8
5e1026f
fdcf5c7
40addbb
060518f
1635863
8df913d
621bd73
4d449a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| type loaderData = { | ||
| compiledMdx: CompiledMdx.t, | ||
| blogPost: BlogApi.post, | ||
| title: string, | ||
| } | ||
|
|
||
| let loader: ReactRouter.Loader.t<loaderData> = async ({request}) => { | ||
| let {pathname} = WebAPI.URL.make(~url=request.url) | ||
| let filePath = MdxFile.resolveFilePath( | ||
| (pathname :> string), | ||
| ~dir="markdown-pages/blog", | ||
| ~alias="blog", | ||
| ) | ||
|
|
||
| let raw = await Node.Fs.readFile(filePath, "utf-8") | ||
| let {frontmatter}: MarkdownParser.result = MarkdownParser.parseSync(raw) | ||
|
|
||
| let frontmatter = switch BlogFrontmatter.decode(frontmatter) { | ||
| | Ok(fm) => fm | ||
| | Error(msg) => JsError.throwWithMessage(msg) | ||
| } | ||
|
|
||
| let compiledMdx = await MdxFile.compileMdx(raw, ~filePath, ~remarkPlugins=Mdx.plugins) | ||
|
|
||
| let archived = filePath->String.includes("/archived/") | ||
|
|
||
| let slug = | ||
| filePath | ||
| ->Node.Path.basename | ||
| ->String.replace(".mdx", "") | ||
| ->String.replaceRegExp(/^\d\d\d\d-\d\d-\d\d-/, "") | ||
|
|
||
| let path = archived ? "archived/" ++ slug : slug | ||
|
|
||
| let blogPost: BlogApi.post = { | ||
| path, | ||
| archived, | ||
| frontmatter, | ||
| } | ||
|
|
||
| { | ||
| compiledMdx, | ||
| blogPost, | ||
| title: `${frontmatter.title} | ReScript Blog`, | ||
| } | ||
| } | ||
|
|
||
| let default = () => { | ||
| let {compiledMdx, blogPost: {frontmatter, archived, path}} = ReactRouter.useLoaderData() | ||
|
|
||
| <BlogArticle frontmatter isArchived=archived path> | ||
| <MdxContent compiledMdx /> | ||
| </BlogArticle> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| type loaderData = { | ||
| compiledMdx: CompiledMdx.t, | ||
| blogPost: BlogApi.post, | ||
| title: string, | ||
| } | ||
|
|
||
| let loader: ReactRouter.Loader.t<loaderData> | ||
|
|
||
| let default: unit => React.element |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,68 @@ | ||||||||||||||||||||||||
| type fileData = { | ||||||||||||||||||||||||
| content: string, | ||||||||||||||||||||||||
| frontmatter: JSON.t, | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| type compileInput = {value: string, path: string} | ||||||||||||||||||||||||
| type compileOptions = { | ||||||||||||||||||||||||
| outputFormat: string, | ||||||||||||||||||||||||
| remarkPlugins: array<Mdx.remarkPlugin>, | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| @module("@mdx-js/mdx") | ||||||||||||||||||||||||
| external compile: (compileInput, compileOptions) => promise<CompiledMdx.compileResult> = "compile" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @module("remark-frontmatter") external remarkFrontmatter: Mdx.remarkPlugin = "default" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| let compileMdx = async (content, ~filePath, ~remarkPlugins=[]) => { | ||||||||||||||||||||||||
| let compiled = await compile( | ||||||||||||||||||||||||
| {value: content, path: filePath}, | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| outputFormat: "function-body", | ||||||||||||||||||||||||
| remarkPlugins: [remarkFrontmatter, ...remarkPlugins], | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| compiled->CompiledMdx.fromCompileResult | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| let resolveFilePath = (pathname, ~dir, ~alias) => { | ||||||||||||||||||||||||
| let path = if pathname->String.startsWith("/") { | ||||||||||||||||||||||||
| pathname->String.slice(~start=1, ~end=String.length(pathname)) | ||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||
| pathname | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| let relativePath = path->String.replace(alias, dir) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| let relativePath = path->String.replace(alias, dir) | |
| let relativePath = | |
| if path->String.startsWith(alias ++ "/") { | |
| let rest = path->String.slice(~start=String.length(alias) + 1, ~end=String.length(path)) | |
| Node.Path.join2(dir, rest) | |
| } else if path->String.startsWith(alias) { | |
| let rest = path->String.slice(~start=String.length(alias), ~end=String.length(path)) | |
| Node.Path.join2(dir, rest) | |
| } else { | |
| path | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| type fileData = { | ||
| content: string, | ||
| frontmatter: JSON.t, | ||
| } | ||
|
|
||
| /** Maps a URL pathname to an .mdx file path on disk. | ||
| * e.g. `/blog/release-12-0-0` with ~dir="markdown-pages/blog" ~alias="blog" | ||
| * → `markdown-pages/blog/release-12-0-0.mdx` | ||
| */ | ||
| let resolveFilePath: (string, ~dir: string, ~alias: string) => string | ||
|
|
||
| /** Read a file from disk and parse its frontmatter using MarkdownParser. */ | ||
| let loadFile: string => promise<fileData> | ||
|
|
||
| /** Scan a directory recursively for .mdx files and return URL paths. | ||
| * e.g. scanPaths(~dir="markdown-pages/blog", ~alias="blog") | ||
| * → ["blog/release-12-0-0", "blog/archived/some-post", ...] | ||
| */ | ||
| let scanPaths: (~dir: string, ~alias: string) => array<string> | ||
|
|
||
| /** Compile raw MDX content into a function-body string using @mdx-js/mdx. */ | ||
| let compileMdx: ( | ||
| string, | ||
| ~filePath: string, | ||
| ~remarkPlugins: array<Mdx.remarkPlugin>=?, | ||
| ) => promise<CompiledMdx.t> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| type t = string | ||
|
|
||
| type compileResult | ||
|
|
||
| @send external fromCompileResult: compileResult => t = "toString" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| type t | ||
|
|
||
| type compileResult | ||
|
|
||
| @send external fromCompileResult: compileResult => t = "toString" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| // --------------------------------------------------------------------------- | ||
| // JSX runtime values needed by runSync | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| // We re-import the jsx-runtime exports as opaque values so we can pass them | ||
| // through to runSync without running into ReScript's monomorphisation of | ||
| // the polymorphic `React.jsx` / `React.jsxs` signatures. | ||
| /** | ||
| * MdxContent — renders compiled MDX content as a React component. | ||
| * | ||
| * Uses `runSync` from `@mdx-js/mdx` to evaluate compiled MDX (produced by | ||
| * `MdxFile.compileMdx`) and renders the result with a shared component map. | ||
| */ | ||
| type jsxRuntimeValue | ||
|
|
||
| @module("react/jsx-runtime") external fragment: jsxRuntimeValue = "Fragment" | ||
| @module("react/jsx-runtime") external jsx: jsxRuntimeValue = "jsx" | ||
| @module("react/jsx-runtime") external jsxs: jsxRuntimeValue = "jsxs" | ||
|
|
||
| @val @scope(("import", "meta")) external importMetaUrl: string = "url" | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // @mdx-js/mdx runSync binding | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| type runOptions = { | ||
| @as("Fragment") fragment: jsxRuntimeValue, | ||
| jsx: jsxRuntimeValue, | ||
| jsxs: jsxRuntimeValue, | ||
| baseUrl: string, | ||
| } | ||
|
|
||
| type mdxModule | ||
|
|
||
| @module("@mdx-js/mdx") | ||
| external runSync: (CompiledMdx.t, runOptions) => mdxModule = "runSync" | ||
|
|
||
| @get external getDefault: mdxModule => React.component<{..}> = "default" | ||
|
|
||
| let runOptions = { | ||
| fragment, | ||
| jsx, | ||
| jsxs, | ||
| baseUrl: importMetaUrl, | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Shared MDX component map | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| let components = { | ||
| // Standard HTML element overrides | ||
| "a": Markdown.A.make, | ||
| "blockquote": Markdown.Blockquote.make, | ||
| "code": Markdown.Code.make, | ||
| "h1": Markdown.H1.make, | ||
| "h2": Markdown.H2.make, | ||
| "h3": Markdown.H3.make, | ||
| "h4": Markdown.H4.make, | ||
| "h5": Markdown.H5.make, | ||
| "hr": Markdown.Hr.make, | ||
| "li": Markdown.Li.make, | ||
| "ol": Markdown.Ol.make, | ||
| "p": Markdown.P.make, | ||
| "pre": Markdown.Pre.make, | ||
| "strong": Markdown.Strong.make, | ||
| "table": Markdown.Table.make, | ||
| "th": Markdown.Th.make, | ||
| "thead": Markdown.Thead.make, | ||
| "td": Markdown.Td.make, | ||
| "ul": Markdown.Ul.make, | ||
| // Custom MDX components | ||
| "Cite": Markdown.Cite.make, | ||
| "CodeTab": Markdown.CodeTab.make, | ||
| "Image": Markdown.Image.make, | ||
| "Info": Markdown.Info.make, | ||
| "Intro": Markdown.Intro.make, | ||
| "UrlBox": Markdown.UrlBox.make, | ||
| "Video": Markdown.Video.make, | ||
| "Warn": Markdown.Warn.make, | ||
| "CommunityContent": CommunityContent.make, | ||
| "WarningTable": WarningTable.make, | ||
| "Docson": DocsonLazy.make, | ||
| "Suspense": React.Suspense.make, | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // React component | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| @react.component | ||
| let make = (~compiledMdx: CompiledMdx.t) => { | ||
| let element = React.useMemo(() => { | ||
| let mdxModule = runSync(compiledMdx, runOptions) | ||
| let content = getDefault(mdxModule) | ||
| React.jsx(content, {"components": components}) | ||
| }, [compiledMdx]) | ||
|
|
||
| element | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| @react.component | ||
| let make: (~compiledMdx: CompiledMdx.t) => React.element |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The filter
String.startsWith(_, "blog")will exclude any generated MDX route whose path happens to start withblog(e.g.blogger/...), not justblog/andblog. To avoid surprising exclusions, consider matching"blog"exactly or the"blog/"prefix instead.