diff --git a/app/routes.res b/app/routes.res index 656db708c..48df1a454 100644 --- a/app/routes.res +++ b/app/routes.res @@ -28,7 +28,15 @@ let stdlibRoutes = let beltRoutes = beltPaths->Array.map(path => route(path, "./routes/ApiRoute.jsx", ~options={id: path})) -let mdxRoutes = mdxRoutes("./routes/MdxRoute.jsx") +let blogArticleRoutes = + MdxFile.scanPaths(~dir="markdown-pages/blog", ~alias="blog")->Array.map(path => + route(path, "./routes/BlogArticleRoute.jsx", ~options={id: path}) + ) + +let mdxRoutes = + mdxRoutes("./routes/MdxRoute.jsx")->Array.filter(r => + !(r.path->Option.map(String.includes(_, "blog"))->Option.getOr(false)) + ) let default = [ index("./routes/LandingPageRoute.jsx"), @@ -44,6 +52,7 @@ let default = [ route("docs/manual/api/dom", "./routes/ApiRoute.jsx", ~options={id: "api-dom"}), ...stdlibRoutes, ...beltRoutes, + ...blogArticleRoutes, ...mdxRoutes, route("*", "./routes/NotFoundRoute.jsx"), ] diff --git a/app/routes/BlogArticleRoute.res b/app/routes/BlogArticleRoute.res new file mode 100644 index 000000000..2462b51ef --- /dev/null +++ b/app/routes/BlogArticleRoute.res @@ -0,0 +1,52 @@ +type loaderData = { + content: string, + blogPost: BlogApi.post, + title: string, +} + +let loader: ReactRouter.Loader.t = async ({request}) => { + let {pathname} = WebAPI.URL.make(~url=request.url) + let filePath = MdxFile.resolveFilePath( + (pathname :> string), + ~dir="markdown-pages/blog", + ~alias="blog", + ) + let {content, frontmatter} = await MdxFile.loadFile(filePath) + + let frontmatter = switch BlogFrontmatter.decode(frontmatter) { + | Ok(fm) => fm + | Error(msg) => JsExn.throw(msg) + } + + 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, + } + + { + content, + blogPost, + title: `${frontmatter.title} | ReScript Blog`, + } +} + +let default = () => { + let {content, blogPost: {frontmatter, archived, path}} = ReactRouter.useLoaderData() + + + + content + + +} diff --git a/app/routes/BlogArticleRoute.resi b/app/routes/BlogArticleRoute.resi new file mode 100644 index 000000000..ea5d3f13e --- /dev/null +++ b/app/routes/BlogArticleRoute.resi @@ -0,0 +1,9 @@ +type loaderData = { + content: string, + blogPost: BlogApi.post, + title: string, +} + +let loader: ReactRouter.Loader.t + +let default: unit => React.element diff --git a/app/routes/MdxRoute.res b/app/routes/MdxRoute.res index 55f1bf4fb..f61e9db29 100644 --- a/app/routes/MdxRoute.res +++ b/app/routes/MdxRoute.res @@ -4,7 +4,6 @@ type loaderData = { ...Mdx.t, categories: array, entries: array, - blogPost?: BlogApi.post, mdxSources?: array, activeSyntaxItem?: SyntaxLookup.item, breadcrumbs?: list, @@ -134,18 +133,7 @@ let loader: ReactRouter.Loader.t = async ({request}) => { let mdx = await loadMdx(request, ~options={remarkPlugins: Mdx.plugins}) - if pathname->String.includes("blog") { - let res: loaderData = { - __raw: mdx.__raw, - attributes: mdx.attributes, - entries: [], - categories: [], - blogPost: mdx.attributes->BlogLoader.transform, - title: `${mdx.attributes.title} | ReScript Blog`, - filePath: None, - } - res - } else if pathname->String.includes("syntax-lookup") { + if pathname->String.includes("syntax-lookup") { let mdxSources = (await allMdx(~filterByPaths=["markdown-pages/syntax-lookup"])) ->Array.filter(page => @@ -418,12 +406,6 @@ let default = () => {
{component()}
- } else if (pathname :> string)->String.includes("blog") { - switch loaderData.blogPost { - | Some({frontmatter, archived, path}) => - {component()} - | None => React.null // TODO: Post RR7 show an error? - } } else { switch loaderData.mdxSources { | Some(mdxSources) => diff --git a/app/routes/MdxRoute.resi b/app/routes/MdxRoute.resi index b6a26c12c..8e4a827fb 100644 --- a/app/routes/MdxRoute.resi +++ b/app/routes/MdxRoute.resi @@ -2,7 +2,6 @@ type loaderData = { ...Mdx.t, categories: array, entries: array, - blogPost?: BlogApi.post, mdxSources?: array, activeSyntaxItem?: SyntaxLookup.item, breadcrumbs?: list, diff --git a/package.json b/package.json index b232590b1..5a027921b 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "react-router": "^7.12.0", "react-router-dom": "^7.9.4", "react-router-mdx": "patch:react-router-mdx@npm%3A1.0.8#~/.yarn/patches/react-router-mdx-npm-1.0.8-d4402c3003.patch", + "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "rehype-stringify": "^10.0.1", "remark": "^15.0.1", diff --git a/src/MdxFile.res b/src/MdxFile.res new file mode 100644 index 000000000..aca6dc073 --- /dev/null +++ b/src/MdxFile.res @@ -0,0 +1,43 @@ +type fileData = { + content: string, + frontmatter: JSON.t, +} + +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) + relativePath ++ ".mdx" +} + +let loadFile = async filePath => { + let raw = await Node.Fs.readFile(filePath, "utf-8") + let {frontmatter, content}: MarkdownParser.result = MarkdownParser.parseSync(raw) + {content, frontmatter} +} + +// Recursively scan a directory for .mdx files +let rec scanDir = (baseDir, currentDir) => { + let entries = Node.Fs.readdirSync(currentDir) + entries->Array.flatMap(entry => { + let fullPath = Node.Path.join2(currentDir, entry) + if Node.Fs.statSync(fullPath)["isDirectory"]() { + scanDir(baseDir, fullPath) + } else if Node.Path.extname(entry) === ".mdx" { + // Get the relative path from baseDir + let relativePath = fullPath->String.replace(baseDir ++ "/", "")->String.replace(".mdx", "") + [relativePath] + } else { + [] + } + }) +} + +let scanPaths = (~dir, ~alias) => { + scanDir(dir, dir)->Array.map(relativePath => { + alias ++ "/" ++ relativePath + }) +} diff --git a/src/MdxFile.resi b/src/MdxFile.resi new file mode 100644 index 000000000..ff5405471 --- /dev/null +++ b/src/MdxFile.resi @@ -0,0 +1,19 @@ +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 + +/** 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 diff --git a/src/bindings/Rehype.res b/src/bindings/Rehype.res index 522219f68..4ed0f2a9f 100644 --- a/src/bindings/Rehype.res +++ b/src/bindings/Rehype.res @@ -13,3 +13,4 @@ type rec rehypePlugin = | WithOptions(array) @module("rehype-slug") external slug: plugin = "default" +@module("rehype-raw") external raw: plugin = "default" diff --git a/yarn.lock b/yarn.lock index 02ae82c71..8bcfee44f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5877,6 +5877,22 @@ __metadata: languageName: node linkType: hard +"hast-util-from-parse5@npm:^8.0.0": + version: 8.0.3 + resolution: "hast-util-from-parse5@npm:8.0.3" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/unist": "npm:^3.0.0" + devlop: "npm:^1.0.0" + hastscript: "npm:^9.0.0" + property-information: "npm:^7.0.0" + vfile: "npm:^6.0.0" + vfile-location: "npm:^5.0.0" + web-namespaces: "npm:^2.0.0" + checksum: 10c0/40ace6c0ad43c26f721c7499fe408e639cde917b2350c9299635e6326559855896dae3c3ebf7440df54766b96c4276a7823e8f376a2b6a28b37b591f03412545 + languageName: node + linkType: hard + "hast-util-heading-rank@npm:^3.0.0": version: 3.0.0 resolution: "hast-util-heading-rank@npm:3.0.0" @@ -5886,6 +5902,36 @@ __metadata: languageName: node linkType: hard +"hast-util-parse-selector@npm:^4.0.0": + version: 4.0.0 + resolution: "hast-util-parse-selector@npm:4.0.0" + dependencies: + "@types/hast": "npm:^3.0.0" + checksum: 10c0/5e98168cb44470dc274aabf1a28317e4feb09b1eaf7a48bbaa8c1de1b43a89cd195cb1284e535698e658e3ec26ad91bc5e52c9563c36feb75abbc68aaf68fb9f + languageName: node + linkType: hard + +"hast-util-raw@npm:^9.0.0": + version: 9.1.0 + resolution: "hast-util-raw@npm:9.1.0" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/unist": "npm:^3.0.0" + "@ungap/structured-clone": "npm:^1.0.0" + hast-util-from-parse5: "npm:^8.0.0" + hast-util-to-parse5: "npm:^8.0.0" + html-void-elements: "npm:^3.0.0" + mdast-util-to-hast: "npm:^13.0.0" + parse5: "npm:^7.0.0" + unist-util-position: "npm:^5.0.0" + unist-util-visit: "npm:^5.0.0" + vfile: "npm:^6.0.0" + web-namespaces: "npm:^2.0.0" + zwitch: "npm:^2.0.0" + checksum: 10c0/d0d909d2aedecef6a06f0005cfae410d6475e6e182d768bde30c3af9fcbbe4f9beb0522bdc21d0679cb3c243c0df40385797ed255148d68b3d3f12e82d12aacc + languageName: node + linkType: hard + "hast-util-to-estree@npm:^3.0.0": version: 3.1.3 resolution: "hast-util-to-estree@npm:3.1.3" @@ -5952,6 +5998,21 @@ __metadata: languageName: node linkType: hard +"hast-util-to-parse5@npm:^8.0.0": + version: 8.0.1 + resolution: "hast-util-to-parse5@npm:8.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + comma-separated-tokens: "npm:^2.0.0" + devlop: "npm:^1.0.0" + property-information: "npm:^7.0.0" + space-separated-tokens: "npm:^2.0.0" + web-namespaces: "npm:^2.0.0" + zwitch: "npm:^2.0.0" + checksum: 10c0/8e8a1817c7ff8906ac66e7201b1b8d19d9e1b705e695a6e71620270d498d982ec1ecc0e227bd517f723e91e7fdfb90ef75f9ae64d14b3b65239a7d5e1194d7dd + languageName: node + linkType: hard + "hast-util-to-string@npm:^3.0.0": version: 3.0.1 resolution: "hast-util-to-string@npm:3.0.1" @@ -5970,6 +6031,19 @@ __metadata: languageName: node linkType: hard +"hastscript@npm:^9.0.0": + version: 9.0.1 + resolution: "hastscript@npm:9.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + comma-separated-tokens: "npm:^2.0.0" + hast-util-parse-selector: "npm:^4.0.0" + property-information: "npm:^7.0.0" + space-separated-tokens: "npm:^2.0.0" + checksum: 10c0/18dc8064e5c3a7a2ae862978e626b97a254e1c8a67ee9d0c9f06d373bba155ed805fc5b5ce21b990fb7bc174624889e5e1ce1cade264f1b1d58b48f994bc85ce + languageName: node + linkType: hard + "help-me@npm:^5.0.0": version: 5.0.0 resolution: "help-me@npm:5.0.0" @@ -8597,7 +8671,7 @@ __metadata: languageName: node linkType: hard -"parse5@npm:^7.2.1": +"parse5@npm:^7.0.0, parse5@npm:^7.2.1": version: 7.3.0 resolution: "parse5@npm:7.3.0" dependencies: @@ -9301,6 +9375,17 @@ __metadata: languageName: node linkType: hard +"rehype-raw@npm:^7.0.0": + version: 7.0.0 + resolution: "rehype-raw@npm:7.0.0" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-raw: "npm:^9.0.0" + vfile: "npm:^6.0.0" + checksum: 10c0/1435b4b6640a5bc3abe3b2133885c4dbff5ef2190ef9cfe09d6a63f74dd7d7ffd0cede70603278560ccf1acbfb9da9faae4b68065a28bc5aa88ad18e40f32d52 + languageName: node + linkType: hard + "rehype-recma@npm:^1.0.0": version: 1.0.0 resolution: "rehype-recma@npm:1.0.0" @@ -9525,6 +9610,7 @@ __metadata: react-router: "npm:^7.12.0" react-router-dom: "npm:^7.9.4" react-router-mdx: "patch:react-router-mdx@npm%3A1.0.8#~/.yarn/patches/react-router-mdx-npm-1.0.8-d4402c3003.patch" + rehype-raw: "npm:^7.0.0" rehype-slug: "npm:^6.0.0" rehype-stringify: "npm:^10.0.1" remark: "npm:^15.0.1" @@ -11132,6 +11218,16 @@ __metadata: languageName: node linkType: hard +"vfile-location@npm:^5.0.0": + version: 5.0.3 + resolution: "vfile-location@npm:5.0.3" + dependencies: + "@types/unist": "npm:^3.0.0" + vfile: "npm:^6.0.0" + checksum: 10c0/1711f67802a5bc175ea69750d59863343ed43d1b1bb25c0a9063e4c70595e673e53e2ed5cdbb6dcdc370059b31605144d95e8c061b9361bcc2b036b8f63a4966 + languageName: node + linkType: hard + "vfile-matter@npm:^5.0.0": version: 5.0.1 resolution: "vfile-matter@npm:5.0.1" @@ -11456,6 +11552,13 @@ __metadata: languageName: node linkType: hard +"web-namespaces@npm:^2.0.0": + version: 2.0.1 + resolution: "web-namespaces@npm:2.0.1" + checksum: 10c0/df245f466ad83bd5cd80bfffc1674c7f64b7b84d1de0e4d2c0934fb0782e0a599164e7197a4bce310ee3342fd61817b8047ff04f076a1ce12dd470584142a4bd + languageName: node + linkType: hard + "webidl-conversions@npm:^7.0.0": version: 7.0.0 resolution: "webidl-conversions@npm:7.0.0"