diff --git a/package.json b/package.json index 4669791..3d97427 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,6 @@ "estree-util-value-to-estree": "^3.5.0", "expressive-code": "^0.41.7", "expressive-code-twoslash": "^0.4.0", - "gray-matter": "^4.0.3", "hastscript": "^9.0.1", "magic-string": "^0.30.21", "mdast-util-find-and-replace": "^3.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fdcbb5..3994f61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,9 +95,6 @@ importers: expressive-code-twoslash: specifier: ^0.4.0 version: 0.4.0(@expressive-code/core@0.41.7)(expressive-code@0.41.7)(typescript@5.9.3) - gray-matter: - specifier: ^4.0.3 - version: 4.0.3 hastscript: specifier: ^9.0.1 version: 9.0.1 @@ -1651,9 +1648,6 @@ packages: resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1971,11 +1965,6 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - estree-util-attach-comments@3.0.0: resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} @@ -2020,10 +2009,6 @@ packages: exsolve@1.1.1: resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} - extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -2079,10 +2064,6 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - h3@2.0.1-rc.16: resolution: {integrity: sha512-h+pjvyujdo9way8qj6FUbhaQcHlR8FEq65EhTX9ViT5pK8aLj68uFl4hBkF+hsTJAH+H1END2Yv6hTIsabGfag==} engines: {node: '>=20.11.1'} @@ -2199,10 +2180,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true - is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2251,10 +2228,6 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@3.15.1: - resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==} - hasBin: true - jsdom@29.1.1: resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} @@ -2274,10 +2247,6 @@ packages: engines: {node: '>=6'} hasBin: true - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} @@ -2855,10 +2824,6 @@ packages: scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} - section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2939,9 +2904,6 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - srvx@0.11.22: resolution: {integrity: sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==} engines: {node: '>=20.16.0'} @@ -2967,10 +2929,6 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -4741,10 +4699,6 @@ snapshots: ansis@4.3.1: {} - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - assertion-error@2.0.1: {} astring@1.9.0: {} @@ -5051,8 +5005,6 @@ snapshots: escape-string-regexp@5.0.0: {} - esprima@4.0.1: {} - estree-util-attach-comments@3.0.0: dependencies: '@types/estree': 1.0.9 @@ -5115,10 +5067,6 @@ snapshots: exsolve@1.1.1: {} - extend-shallow@2.0.1: - dependencies: - is-extendable: 0.1.1 - extend@3.0.2: {} fast-content-type-parse@2.0.1: {} @@ -5164,13 +5112,6 @@ snapshots: dependencies: is-glob: 4.0.3 - gray-matter@4.0.3: - dependencies: - js-yaml: 3.15.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - h3@2.0.1-rc.16(crossws@0.4.10(srvx@0.11.22)): dependencies: rou3: 0.8.1 @@ -5389,8 +5330,6 @@ snapshots: is-docker@3.0.0: {} - is-extendable@0.1.1: {} - is-extglob@2.1.1: {} is-glob@4.0.3: @@ -5423,11 +5362,6 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@3.15.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - jsdom@29.1.1: dependencies: '@asamuzakjp/css-color': 5.1.11 @@ -5458,8 +5392,6 @@ snapshots: json5@2.2.3: {} - kind-of@6.0.3: {} - lightningcss-android-arm64@1.33.0: optional: true @@ -6513,11 +6445,6 @@ snapshots: scule@1.3.0: {} - section-matter@1.0.0: - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - semver@6.3.1: {} seroval-plugins@1.5.6(seroval@1.5.6): @@ -6603,8 +6530,6 @@ snapshots: space-separated-tokens@2.0.2: {} - sprintf-js@1.0.3: {} - srvx@0.11.22: {} srvx@0.12.5: {} @@ -6622,8 +6547,6 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 - strip-bom-string@1.0.0: {} - strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 diff --git a/src/config/frontmatter.ts b/src/config/frontmatter.ts new file mode 100644 index 0000000..565e818 --- /dev/null +++ b/src/config/frontmatter.ts @@ -0,0 +1,57 @@ +import { readFileSync } from "node:fs"; +import type { Literal } from "mdast"; +import remarkFrontmatter from "remark-frontmatter"; +import remarkParse from "remark-parse"; +import { parse as parseToml } from "toml"; +import { type PluggableList, unified } from "unified"; +import { parse as parseYaml } from "yaml"; + +export type FrontmatterOptions = { + remarkPlugins?: PluggableList; +}; + +function getFrontmatterPlugins(plugins: PluggableList): PluggableList { + return plugins.flatMap((plugin): PluggableList => { + if (typeof plugin === "object" && !Array.isArray(plugin)) { + return getFrontmatterPlugins(plugin.plugins ?? []); + } + return (Array.isArray(plugin) ? plugin[0] : plugin) === remarkFrontmatter + ? [plugin] + : []; + }); +} + +/** + * Extracts and parses the leading frontmatter block of a markdown source. + * + * Uses the MDX pipeline's YAML default and any configured `remark-frontmatter` + * overrides, including explicitly enabled TOML. + */ +export function parseFrontmatter( + source: string, + options: FrontmatterOptions = {}, +): Record { + const processor = unified() + .use(remarkParse) + .use(remarkFrontmatter) + .use(getFrontmatterPlugins(options.remarkPlugins ?? [])); + const node = processor + .parse(source) + .children.find((child) => ["yaml", "toml"].includes(child.type)) as + | Literal + | undefined; + if (!node) return {}; + + const parse = node.type === "toml" ? parseToml : parseYaml; + const data = parse(node.value as string); + return data && typeof data === "object" && !Array.isArray(data) + ? (data as Record) + : {}; +} + +export function readFrontmatter( + filePath: string, + options?: FrontmatterOptions, +): Record { + return parseFrontmatter(readFileSync(filePath, "utf8"), options); +} diff --git a/src/config/llms-index.ts b/src/config/llms-index.ts index 468ea85..8dd2260 100644 --- a/src/config/llms-index.ts +++ b/src/config/llms-index.ts @@ -103,7 +103,7 @@ export async function getLlmDocuments( ): Promise { if (!config.llms) return []; - const routes = await getRoutesIndex(root); + const routes = await getRoutesIndex(root, config.markdown); return Promise.all( routes.map(async (route): Promise => { diff --git a/src/config/routes-index.ts b/src/config/routes-index.ts index 9fe23c4..d460f18 100644 --- a/src/config/routes-index.ts +++ b/src/config/routes-index.ts @@ -1,8 +1,7 @@ import { readdir, readFile } from "node:fs/promises"; import { extname, join, relative } from "node:path"; -import matter from "gray-matter"; - +import { type FrontmatterOptions, parseFrontmatter } from "./frontmatter.js"; import type { SolidBaseResolvedConfig } from "./index.js"; import { buildSolidBaseRoutePath, @@ -213,7 +212,10 @@ export function isDefaultLocaleRoute( ); } -export async function getRoutesIndex(root: string): Promise { +export async function getRoutesIndex( + root: string, + frontmatterOptions?: FrontmatterOptions, +): Promise { const routesDir = getRoutesDir(root); const markdownFiles = await collectMarkdownFiles(routesDir); @@ -222,7 +224,7 @@ export async function getRoutesIndex(root: string): Promise { if (isNotFoundRoute(routesDir, filePath)) return null; const source = await readFile(filePath, "utf8"); - const { data } = matter(source); + const frontmatter = parseFrontmatter(source, frontmatterOptions); const routePath = toRoutePath(routesDir, filePath); return { @@ -230,7 +232,7 @@ export async function getRoutesIndex(root: string): Promise { routePath, markdownPath: toMarkdownPath(routePath), source, - frontmatter: data as Record, + frontmatter, } satisfies RouteIndexEntry; }), ).then((routes) => routes.filter((route) => route !== null)); diff --git a/src/config/sidebar.ts b/src/config/sidebar.ts index d9cf8e9..169881e 100644 --- a/src/config/sidebar.ts +++ b/src/config/sidebar.ts @@ -1,6 +1,7 @@ import { lstatSync, readdirSync } from "node:fs"; import path from "node:path"; -import matter from "gray-matter"; + +import { type FrontmatterOptions, readFrontmatter } from "./frontmatter.js"; export type SidebarConfig = | Item[] @@ -28,6 +29,8 @@ export type SidebarItemWithMeta = SidebarItem & { }; export interface FilesystemSidebarOptions { + /** Match the site's markdown frontmatter configuration. */ + frontmatter?: FrontmatterOptions; filter?: (item: SidebarItemWithMeta) => boolean; sort?: (a: SidebarItemWithMeta, b: SidebarItemWithMeta) => number; transform?: (item: SidebarItemWithMeta) => SidebarItemWithMeta; @@ -42,6 +45,7 @@ export function createFilesystemSidebar( const collator = new Intl.Collator(undefined, { numeric: true }); const resolvedOptions: Required = { + frontmatter: {}, filter: (item) => { return ( item.matterData?.excludeFromSidebar !== true && @@ -101,7 +105,7 @@ function traverse( if (title.includes("[...")) return; if (lstatSync(filePath).isFile()) { - const matterData = getMatterData(filePath); + const matterData = getMatterData(filePath, options.frontmatter); return { title: matterData.sidebarTitle ?? matterData.title ?? title, @@ -136,8 +140,9 @@ function stripExtension(filePath: string): string { function getMatterData( filePath: string, + options: FrontmatterOptions, ): { sidebarTitle?: string; title?: string } & {} { - return matter.read(filePath).data; + return readFrontmatter(filePath, options); } function formatTitle(filePath: string): string { diff --git a/src/config/sitemap-index.ts b/src/config/sitemap-index.ts index a9b4779..de84f56 100644 --- a/src/config/sitemap-index.ts +++ b/src/config/sitemap-index.ts @@ -115,6 +115,6 @@ export async function getSitemapEntries( const hostname = getSitemapHostname(config); if (!hostname) return []; - const routes = await getRoutesIndex(root); + const routes = await getRoutesIndex(root, config.markdown); return buildSitemapEntries(hostname, config, routes); } diff --git a/tests/config/frontmatter.test.ts b/tests/config/frontmatter.test.ts new file mode 100644 index 0000000..1535576 --- /dev/null +++ b/tests/config/frontmatter.test.ts @@ -0,0 +1,108 @@ +import remarkFrontmatter from "remark-frontmatter"; +import remarkParse from "remark-parse"; +import { type PluggableList, unified } from "unified"; +import { describe, expect, it } from "vitest"; + +import { parseFrontmatter } from "../../src/config/frontmatter.js"; + +describe("parseFrontmatter", () => { + it("parses YAML frontmatter", () => { + expect( + parseFrontmatter( + ["---", "title: Home", "sidebar: false", "---", "# Body"].join("\n"), + ), + ).toEqual({ title: "Home", sidebar: false }); + }); + + it("ignores TOML fences, matching the MDX pipeline's YAML-only configuration", () => { + expect( + parseFrontmatter(["+++", 'title = "Home"', "+++", "# Body"].join("\n")), + ).toEqual({}); + expect(parseFrontmatter("+++\nThis is regular prose.\n+++\n")).toEqual({}); + }); + + it("parses TOML when explicitly enabled through remark plugins", () => { + expect( + parseFrontmatter('+++\ntitle = "Home"\nllms = false\n+++\n', { + remarkPlugins: [[remarkFrontmatter, ["yaml", "toml"]]], + }), + ).toEqual({ title: "Home", llms: false }); + }); + + it.each([false, true])( + "honors nested presets and registration order (preset last: %j)", + (presetLast) => { + const preset = { + plugins: [{ plugins: [[remarkFrontmatter, ["yaml", "toml"]]] }], + }; + const yamlOnly: PluggableList[number] = [remarkFrontmatter, "yaml"]; + const remarkPlugins: PluggableList = presetLast + ? [yamlOnly, preset] + : [preset, yamlOnly]; + const source = '+++\ntitle = "Home"\n+++\n'; + const tree = unified() + .use(remarkParse) + .use(remarkFrontmatter) + .use(remarkPlugins) + .parse(source); + expect(tree.children.some((node) => node.type === "toml")).toBe( + presetLast, + ); + expect(parseFrontmatter(source, { remarkPlugins })).toEqual( + presetLast ? { title: "Home" } : {}, + ); + }, + ); + + it("handles CRLF line endings and a BOM", () => { + expect( + parseFrontmatter("\uFEFF---\r\ntitle: Home\r\n---\r\n# Body"), + ).toEqual({ + title: "Home", + }); + }); + + it("returns an empty object without frontmatter", () => { + expect(parseFrontmatter("# Body\n\n---\ntitle: Nope\n---")).toEqual({}); + expect(parseFrontmatter("")).toEqual({}); + }); + + it("returns an empty object for empty or non-object frontmatter", () => { + expect(parseFrontmatter("---\n---\n# Body")).toEqual({}); + expect(parseFrontmatter("---\n- a\n- b\n---\n")).toEqual({}); + }); + + it.each(["\n", "\r\n"])( + "terminates empty frontmatter at the first closing fence with %j line endings", + (newline) => { + expect( + parseFrontmatter( + ["---", "---", "title: Body metadata", "sidebar: false", "---"].join( + newline, + ), + ), + ).toEqual({}); + expect( + parseFrontmatter(["---", "---", "invalid: [yaml", "---"].join(newline)), + ).toEqual({}); + }, + ); + + it("terminates non-empty frontmatter at the first closing fence", () => { + expect( + parseFrontmatter( + ["---", "title: Home", "---", "invalid: [yaml", "---"].join("\n"), + ), + ).toEqual({ title: "Home" }); + }); + + it("returns an empty object for an unclosed frontmatter block", () => { + expect(parseFrontmatter("---\ntitle: Home\n")).toEqual({}); + }); + + it("does not evaluate JavaScript frontmatter", () => { + expect( + parseFrontmatter("---js\n{ title: (() => 'evaluated')() }\n---\n"), + ).toEqual({}); + }); +}); diff --git a/tests/config/routes-index.test.ts b/tests/config/routes-index.test.ts index acfd926..baa2694 100644 --- a/tests/config/routes-index.test.ts +++ b/tests/config/routes-index.test.ts @@ -1,17 +1,67 @@ -import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { join, relative } from "node:path"; +import remarkFrontmatter from "remark-frontmatter"; import { describe, expect, it } from "vitest"; +import { getLlmDocuments } from "../../src/config/llms-index.ts"; import { getRoutesIndex, isDefaultLocaleRoute, } from "../../src/config/routes-index.ts"; +import { createFilesystemSidebar } from "../../src/config/sidebar.ts"; +import { getSitemapEntries } from "../../src/config/sitemap-index.ts"; import { fixtureSiteRoot } from "../helpers/fixtures.ts"; describe("getRoutesIndex", () => { + it("honors configured TOML metadata across indexing, sidebars, and generated documents", async () => { + const root = await mkdtemp(join(tmpdir(), "solidbase-toml-")); + const routesDir = join(root, "src", "routes"); + const markdown = { + remarkPlugins: [{ plugins: [[remarkFrontmatter, ["yaml", "toml"]]] }], + }; + const config = { + markdown, + llms: true, + sitemap: true, + siteUrl: "https://example.com", + lang: "en", + } as any; + try { + await mkdir(routesDir, { recursive: true }); + await writeFile( + join(routesDir, "index.mdx"), + '+++\ntitle = "Home"\n+++\n\nWelcome to {frontmatter.title}.', + ); + await writeFile( + join(routesDir, "hidden.mdx"), + '+++\ntitle = "Hidden"\nexcludeFromSidebar = true\nsitemap = false\nllms = false\n+++\n\nHidden body.', + ); + const routes = await getRoutesIndex(root, config.markdown); + expect( + routes.find((route) => route.routePath === "/")?.frontmatter, + ).toEqual({ title: "Home" }); + expect( + createFilesystemSidebar(relative(process.cwd(), routesDir), { + frontmatter: config.markdown, + }).map(({ title, link }: any) => ({ title, link })), + ).toEqual([{ title: "Home", link: "/" }]); + expect( + (await getSitemapEntries(root, config)).map((entry) => entry.routePath), + ).toEqual(["/"]); + const documents = await getLlmDocuments(root, config, async () => null); + expect(documents).toHaveLength(1); + expect(documents[0]).toMatchObject({ + title: "Home", + content: "Welcome to Home.", + }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + it("indexes markdown routes with normalized paths and frontmatter", async () => { const routes = await getRoutesIndex(fixtureSiteRoot);