-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Frontmatter injection for MD and MDX (#4176)
* feat: inject vfile data as exports * feat: add vfile to renderMarkdown output * feat: add safe astroExports parser to utils * refactor: expose vite-plugin-utils on astro package * feat: handle astroExports in mdx * deps: vfile * chore: lockfile * test: astroExports in mdx * refactor: merge plugin exports into forntmatter * refactor: astroExports -> astro.frontmatter * refactor: md astroExports -> astro.frontmatter * feat: astro.frontmatter vite-plugin-markdown * chore: remove unused import * fix: inline safelyGetAstroData in MDX integration * chore: check that frontmatter export is valid export name * chore: error log naming * test: mdx remark frontmatter injection * fix: inconsistent shiki mod resolution * fix: add new frontmatter and heading props * test: remark vdata * fix: spread astro.data.frontmatter * test deps: mdast-util-to-string, reading-time * fix: astro-md test package name * test: md frontmatter injection * fix: layouts * deps: remove vite-plugin-utils export * fix: package lock * chore: remove dup import * chore: changeset * chore: add comment on safelyGetAstroData source * deps: move mdast-util-to-string + reading-time to test fixture * chore: move remark plugins to test fixture * fix: override plugin frontmatter with user frontmatter * test: md injected frontmatter overrides * test: frontmatter injection overrides mdx
- Loading branch information
1 parent
4678a3f
commit 2675b86
Showing
32 changed files
with
491 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
'astro': minor | ||
'@astrojs/mdx': minor | ||
'@astrojs/markdown-remark': patch | ||
--- | ||
|
||
Support frontmatter injection for MD and MDX using remark and rehype plugins |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
packages/astro/test/astro-markdown-frontmatter-injection.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { expect } from 'chai'; | ||
import { loadFixture } from './test-utils.js'; | ||
|
||
const FIXTURE_ROOT = './fixtures/astro-markdown-frontmatter-injection/'; | ||
|
||
describe('Astro Markdown - frontmatter injection', () => { | ||
let fixture; | ||
|
||
before(async () => { | ||
fixture = await loadFixture({ | ||
root: FIXTURE_ROOT, | ||
}); | ||
await fixture.build(); | ||
}); | ||
|
||
it('remark supports custom vfile data - get title', async () => { | ||
const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json')); | ||
const titles = frontmatterByPage.map((frontmatter = {}) => frontmatter.title); | ||
expect(titles).to.contain('Page 1'); | ||
expect(titles).to.contain('Page 2'); | ||
}); | ||
|
||
it('rehype supports custom vfile data - reading time', async () => { | ||
const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json')); | ||
const readingTimes = frontmatterByPage.map((frontmatter = {}) => frontmatter.injectedReadingTime); | ||
expect(readingTimes.length).to.be.greaterThan(0); | ||
for (let readingTime of readingTimes) { | ||
expect(readingTime).to.not.be.null; | ||
expect(readingTime.text).match(/^\d+ min read/); | ||
} | ||
}); | ||
|
||
it('overrides injected frontmatter with user frontmatter', async () => { | ||
const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json')); | ||
const readingTimes = frontmatterByPage.map((frontmatter = {}) => frontmatter.injectedReadingTime?.text); | ||
const titles = frontmatterByPage.map((frontmatter = {}) => frontmatter.title); | ||
expect(titles).to.contain('Overridden title'); | ||
expect(readingTimes).to.contain('1000 min read'); | ||
}); | ||
}); |
11 changes: 11 additions & 0 deletions
11
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/astro.config.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { defineConfig } from 'astro/config'; | ||
import { rehypeReadingTime, remarkTitle } from './src/markdown-plugins.mjs' | ||
|
||
// https://astro.build/config | ||
export default defineConfig({ | ||
site: 'https://astro.build/', | ||
markdown: { | ||
remarkPlugins: [remarkTitle], | ||
rehypePlugins: [rehypeReadingTime], | ||
} | ||
}); |
11 changes: 11 additions & 0 deletions
11
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"name": "@test/astro-markdown-frontmatter-injection", | ||
"version": "0.0.0", | ||
"private": true, | ||
"dependencies": { | ||
"astro": "workspace:*", | ||
"mdast-util-to-string": "^3.1.0", | ||
"reading-time": "^1.5.0", | ||
"unist-util-visit": "^4.1.0" | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/markdown-plugins.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import getReadingTime from 'reading-time'; | ||
import { toString } from 'mdast-util-to-string'; | ||
import { visit } from 'unist-util-visit'; | ||
|
||
export function rehypeReadingTime() { | ||
return function (tree, { data }) { | ||
const readingTime = getReadingTime(toString(tree)); | ||
data.astro.frontmatter.injectedReadingTime = readingTime; | ||
}; | ||
} | ||
|
||
export function remarkTitle() { | ||
return function (tree, { data }) { | ||
visit(tree, ['heading'], (node) => { | ||
if (node.depth === 1) { | ||
data.astro.frontmatter.title = toString(node.children); | ||
} | ||
}); | ||
}; | ||
} |
6 changes: 6 additions & 0 deletions
6
packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/glob.json.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export async function get() { | ||
const docs = await import.meta.glob('./*.md', { eager: true }); | ||
return { | ||
body: JSON.stringify(Object.values(docs).map(doc => doc.frontmatter)), | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
...es/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-1.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Page 1 | ||
|
||
Look at that! |
19 changes: 19 additions & 0 deletions
19
...es/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-2.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Page 2 | ||
|
||
## Table of contents | ||
|
||
## Section 1 | ||
|
||
Some text! | ||
|
||
### Subsection 1 | ||
|
||
Some subsection test! | ||
|
||
### Subsection 2 | ||
|
||
Oh cool, more text! | ||
|
||
## Section 2 | ||
|
||
More content |
7 changes: 7 additions & 0 deletions
7
.../test/fixtures/astro-markdown-frontmatter-injection/src/pages/with-overrides.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
title: 'Overridden title' | ||
injectedReadingTime: | ||
text: '1000 min read' | ||
--- | ||
|
||
# Working! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { name as isValidIdentifierName } from 'estree-util-is-identifier-name'; | ||
import type { VFile } from 'vfile'; | ||
import type { MdxjsEsm } from 'mdast-util-mdx'; | ||
import type { MarkdownAstroData } from 'astro'; | ||
import type { Data } from 'vfile'; | ||
import { jsToTreeNode } from './utils.js'; | ||
|
||
export function remarkInitializeAstroData() { | ||
return function (tree: any, vfile: VFile) { | ||
if (!vfile.data.astro) { | ||
vfile.data.astro = { frontmatter: {} }; | ||
} | ||
}; | ||
} | ||
|
||
export function rehypeApplyFrontmatterExport( | ||
pageFrontmatter: Record<string, any>, | ||
exportName = 'frontmatter' | ||
) { | ||
return function (tree: any, vfile: VFile) { | ||
if (!isValidIdentifierName(exportName)) { | ||
throw new Error( | ||
`[MDX] ${JSON.stringify( | ||
exportName | ||
)} is not a valid frontmatter export name! Make sure "frontmatterOptions.name" could be used as a JS export (i.e. "export const frontmatterName = ...")` | ||
); | ||
} | ||
const { frontmatter: injectedFrontmatter } = safelyGetAstroData(vfile.data); | ||
const frontmatter = { ...injectedFrontmatter, ...pageFrontmatter }; | ||
let exportNodes: MdxjsEsm[] = []; | ||
if (!exportName) { | ||
exportNodes = Object.entries(frontmatter).map(([k, v]) => { | ||
if (!isValidIdentifierName(k)) { | ||
throw new Error( | ||
`[MDX] A remark or rehype plugin tried to inject ${JSON.stringify( | ||
k | ||
)} as a top-level export, which is not a valid export name.` | ||
); | ||
} | ||
return jsToTreeNode(`export const ${k} = ${JSON.stringify(v)};`); | ||
}); | ||
} else { | ||
exportNodes = [jsToTreeNode(`export const ${exportName} = ${JSON.stringify(frontmatter)};`)]; | ||
} | ||
tree.children = exportNodes.concat(tree.children); | ||
}; | ||
} | ||
|
||
/** | ||
* Copied from markdown utils | ||
* @see "vite-plugin-utils" | ||
*/ | ||
function isValidAstroData(obj: unknown): obj is MarkdownAstroData { | ||
if (typeof obj === 'object' && obj !== null && obj.hasOwnProperty('frontmatter')) { | ||
const { frontmatter } = obj as any; | ||
try { | ||
// ensure frontmatter is JSON-serializable | ||
JSON.stringify(frontmatter); | ||
} catch { | ||
return false; | ||
} | ||
return typeof frontmatter === 'object' && frontmatter !== null; | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Copied from markdown utils | ||
* @see "vite-plugin-utils" | ||
*/ | ||
export function safelyGetAstroData(vfileData: Data): MarkdownAstroData { | ||
const { astro } = vfileData; | ||
|
||
if (!astro) return { frontmatter: {} }; | ||
if (!isValidAstroData(astro)) { | ||
throw Error( | ||
`[MDX] A remark or rehype plugin tried to add invalid frontmatter. Ensure "astro.frontmatter" is a JSON object!` | ||
); | ||
} | ||
|
||
return astro; | ||
} |
Oops, something went wrong.