Skip to content
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

Frontmatter injection for MD and MDX #4176

Merged
merged 35 commits into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e848e0a
feat: inject vfile data as exports
bholmesdev Aug 1, 2022
5c95301
feat: add vfile to renderMarkdown output
bholmesdev Aug 2, 2022
522a9b6
feat: add safe astroExports parser to utils
bholmesdev Aug 2, 2022
90fda92
refactor: expose vite-plugin-utils on astro package
bholmesdev Aug 2, 2022
4f5d3ea
feat: handle astroExports in mdx
bholmesdev Aug 2, 2022
cb5cc36
deps: vfile
bholmesdev Aug 2, 2022
6f9b6f1
chore: lockfile
bholmesdev Aug 2, 2022
4c310ca
test: astroExports in mdx
bholmesdev Aug 2, 2022
155a805
refactor: merge plugin exports into forntmatter
bholmesdev Aug 2, 2022
b387f28
refactor: astroExports -> astro.frontmatter
bholmesdev Aug 2, 2022
e10d58d
refactor: md astroExports -> astro.frontmatter
bholmesdev Aug 4, 2022
9f556e6
feat: astro.frontmatter vite-plugin-markdown
bholmesdev Aug 4, 2022
113a9b7
chore: remove unused import
bholmesdev Aug 4, 2022
286c9b3
fix: inline safelyGetAstroData in MDX integration
bholmesdev Aug 4, 2022
5800887
chore: check that frontmatter export is valid export name
bholmesdev Aug 4, 2022
7f6a1f1
chore: error log naming
bholmesdev Aug 4, 2022
12afe63
test: mdx remark frontmatter injection
bholmesdev Aug 5, 2022
9a7a10d
fix: inconsistent shiki mod resolution
bholmesdev Aug 5, 2022
ac237cf
fix: add new frontmatter and heading props
bholmesdev Aug 5, 2022
3c7a530
test: remark vdata
bholmesdev Aug 5, 2022
5dde99f
fix: spread astro.data.frontmatter
bholmesdev Aug 5, 2022
11a23a6
test deps: mdast-util-to-string, reading-time
bholmesdev Aug 5, 2022
cd18073
fix: astro-md test package name
bholmesdev Aug 5, 2022
8a823b8
test: md frontmatter injection
bholmesdev Aug 5, 2022
d2080bd
fix: layouts
bholmesdev Aug 5, 2022
26ce328
deps: remove vite-plugin-utils export
bholmesdev Aug 5, 2022
53c1871
fix: package lock
bholmesdev Aug 5, 2022
b8cb430
chore: remove dup import
bholmesdev Aug 5, 2022
a15ea73
chore: changeset
bholmesdev Aug 5, 2022
4135eaf
chore: add comment on safelyGetAstroData source
bholmesdev Aug 5, 2022
949444a
deps: move mdast-util-to-string + reading-time to test fixture
bholmesdev Aug 5, 2022
58610de
chore: move remark plugins to test fixture
bholmesdev Aug 5, 2022
0119240
fix: override plugin frontmatter with user frontmatter
bholmesdev Aug 5, 2022
92161f7
test: md injected frontmatter overrides
bholmesdev Aug 5, 2022
cf72d48
test: frontmatter injection overrides mdx
bholmesdev Aug 5, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/cool-crabs-trade.md
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
2 changes: 2 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1110,3 +1110,5 @@ export interface SSRResult {
response: ResponseInit;
_metadata: SSRMetadata;
}

export type MarkdownAstroData = { frontmatter: object };
12 changes: 10 additions & 2 deletions packages/astro/src/vite-plugin-markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { collectErrorMetadata } from '../core/errors.js';
import type { LogOptions } from '../core/logger/core.js';
import { warn } from '../core/logger/core.js';
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
import { getFileInfo } from '../vite-plugin-utils/index.js';
import { getFileInfo, safelyGetAstroData } from '../vite-plugin-utils/index.js';

interface AstroPluginOptions {
config: AstroConfig;
Expand Down Expand Up @@ -44,7 +44,14 @@ export default function markdown({ config, logging }: AstroPluginOptions): Plugi

const html = renderResult.code;
const { headings } = renderResult.metadata;
const frontmatter = { ...raw.data, url: fileUrl, file: fileId } as any;
const { frontmatter: injectedFrontmatter } = safelyGetAstroData(renderResult.vfile.data);
const frontmatter = {
...injectedFrontmatter,
...raw.data,
url: fileUrl,
file: fileId,
} as any;

const { layout } = frontmatter;

if (frontmatter.setup) {
Expand Down Expand Up @@ -94,6 +101,7 @@ export default function markdown({ config, logging }: AstroPluginOptions): Plugi
}
export default Content;
`);

return {
code,
meta: {
Expand Down
30 changes: 29 additions & 1 deletion packages/astro/src/vite-plugin-utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AstroConfig } from '../@types/astro';
import { Data } from 'vfile';
import type { AstroConfig, MarkdownAstroData } from '../@types/astro';
import { appendForwardSlash } from '../core/path.js';

export function getFileInfo(id: string, config: AstroConfig) {
Expand All @@ -15,3 +16,30 @@ export function getFileInfo(id: string, config: AstroConfig) {
}
return { fileId, fileUrl };
}

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;
}

export function safelyGetAstroData(vfileData: Data): MarkdownAstroData {
const { astro } = vfileData;

if (!astro) return { frontmatter: {} };
if (!isValidAstroData(astro)) {
throw Error(
`[Markdown] A remark or rehype plugin tried to add invalid frontmatter. Ensure "astro.frontmatter" is a JSON object!`
);
}

return astro;
}
40 changes: 40 additions & 0 deletions packages/astro/test/astro-markdown-frontmatter-injection.test.js
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');
});
});
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],
}
});
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"
}
}
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);
}
});
};
}
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)),
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Page 1

Look at that!
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: 'Overridden title'
injectedReadingTime:
text: '1000 min read'
---

# Working!
2 changes: 1 addition & 1 deletion packages/astro/test/fixtures/astro-markdown/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@test/astro-markdown-md-mode",
"name": "@test/astro-markdown",
"version": "0.0.0",
"private": true,
"dependencies": {
Expand Down
3 changes: 2 additions & 1 deletion packages/integrations/mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"remark-shiki-twoslash": "^3.1.0",
"remark-smartypants": "^2.0.0",
"shiki": "^0.10.1",
"unist-util-visit": "^4.1.0"
"unist-util-visit": "^4.1.0",
"vfile": "^5.3.2"
},
"devDependencies": {
"@types/chai": "^4.3.1",
Expand Down
82 changes: 82 additions & 0 deletions packages/integrations/mdx/src/astro-data-utils.ts
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;
}
Loading