Skip to content

Commit e2019be

Browse files
authored
Change frontmatter injection ordering (#5687)
* feat: make user frontmatter accessible in md * test: new frontmatter injection * refactor: move injection utils to remark pkg * fix: add dist/internal to remark exports * feat: update frontmater injection in mdx * tests: new mdx injection * chore: changeset * chore: simplify frontmatter destructuring * fix: remove old _internal references * refactor: injectedFrontmatter -> remarkPluginFrontmatter * docs: add content collections change * chore: changeset heading levels
1 parent 16c7d0b commit e2019be

File tree

29 files changed

+234
-204
lines changed

29 files changed

+234
-204
lines changed

.changeset/beige-pumpkins-pump.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'astro': major
3+
'@astrojs/markdown-remark': major
4+
'@astrojs/mdx': minor
5+
---
6+
7+
Give remark and rehype plugins access to user frontmatter via frontmatter injection. This means `data.astro.frontmatter` is now the _complete_ Markdown or MDX document's frontmatter, rather than an empty object.
8+
9+
This allows plugin authors to modify existing frontmatter, or compute new properties based on other properties. For example, say you want to compute a full image URL based on an `imageSrc` slug in your document frontmatter:
10+
11+
```ts
12+
export function remarkInjectSocialImagePlugin() {
13+
return function (tree, file) {
14+
const { frontmatter } = file.data.astro;
15+
frontmatter.socialImageSrc = new URL(
16+
frontmatter.imageSrc,
17+
'https://my-blog.com/',
18+
).pathname;
19+
}
20+
}
21+
```
22+
23+
#### Content Collections - new `remarkPluginFrontmatter` property
24+
25+
We have changed _inject_ frontmatter to _modify_ frontmatter in our docs to improve discoverability. This is based on support forum feedback, where "injection" is rarely the term used.
26+
27+
To reflect this, the `injectedFrontmatter` property has been renamed to `remarkPluginFrontmatter`. This should clarify this plugin is still separate from the `data` export Content Collections expose today.
28+
29+
30+
#### Migration instructions
31+
32+
Plugin authors should now **check for user frontmatter when applying defaults.**
33+
34+
For example, say a remark plugin wants to apply a default `title` if none is present. Add a conditional to check if the property is present, and update if none exists:
35+
36+
```diff
37+
export function remarkInjectTitlePlugin() {
38+
return function (tree, file) {
39+
const { frontmatter } = file.data.astro;
40+
+ if (!frontmatter.title) {
41+
frontmatter.title = 'Default title';
42+
+ }
43+
}
44+
}
45+
```
46+
47+
This differs from previous behavior, where a Markdown file's frontmatter would _always_ override frontmatter injected via remark or reype.

examples/with-content/src/content/types.generated.d.ts

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -37,49 +37,50 @@ declare module 'astro:content' {
3737
render(): Promise<{
3838
Content: import('astro').MarkdownInstance<{}>['Content'];
3939
headings: import('astro').MarkdownHeading[];
40-
injectedFrontmatter: Record<string, any>;
40+
remarkPluginFrontmatter: Record<string, any>;
4141
}>;
4242
};
4343

4444
const entryMap: {
45-
blog: {
46-
'first-post.md': {
47-
id: 'first-post.md';
48-
slug: 'first-post';
49-
body: string;
50-
collection: 'blog';
51-
data: InferEntrySchema<'blog'>;
52-
};
53-
'markdown-style-guide.md': {
54-
id: 'markdown-style-guide.md';
55-
slug: 'markdown-style-guide';
56-
body: string;
57-
collection: 'blog';
58-
data: InferEntrySchema<'blog'>;
59-
};
60-
'second-post.md': {
61-
id: 'second-post.md';
62-
slug: 'second-post';
63-
body: string;
64-
collection: 'blog';
65-
data: InferEntrySchema<'blog'>;
66-
};
67-
'third-post.md': {
68-
id: 'third-post.md';
69-
slug: 'third-post';
70-
body: string;
71-
collection: 'blog';
72-
data: InferEntrySchema<'blog'>;
73-
};
74-
'using-mdx.mdx': {
75-
id: 'using-mdx.mdx';
76-
slug: 'using-mdx';
77-
body: string;
78-
collection: 'blog';
79-
data: InferEntrySchema<'blog'>;
80-
};
81-
};
45+
"blog": {
46+
"first-post.md": {
47+
id: "first-post.md",
48+
slug: "first-post",
49+
body: string,
50+
collection: "blog",
51+
data: InferEntrySchema<"blog">
52+
},
53+
"markdown-style-guide.md": {
54+
id: "markdown-style-guide.md",
55+
slug: "markdown-style-guide",
56+
body: string,
57+
collection: "blog",
58+
data: InferEntrySchema<"blog">
59+
},
60+
"second-post.md": {
61+
id: "second-post.md",
62+
slug: "second-post",
63+
body: string,
64+
collection: "blog",
65+
data: InferEntrySchema<"blog">
66+
},
67+
"third-post.md": {
68+
id: "third-post.md",
69+
slug: "third-post",
70+
body: string,
71+
collection: "blog",
72+
data: InferEntrySchema<"blog">
73+
},
74+
"using-mdx.mdx": {
75+
id: "using-mdx.mdx",
76+
slug: "using-mdx",
77+
body: string,
78+
collection: "blog",
79+
data: InferEntrySchema<"blog">
80+
},
81+
},
82+
8283
};
8384

84-
type ContentConfig = typeof import('./config');
85+
type ContentConfig = typeof import("./config");
8586
}

packages/astro/src/@types/astro.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,10 +1464,6 @@ export interface SSRResult {
14641464
_metadata: SSRMetadata;
14651465
}
14661466

1467-
export type MarkdownAstroData = {
1468-
frontmatter: MD['frontmatter'];
1469-
};
1470-
14711467
/* Preview server stuff */
14721468
export interface PreviewServer {
14731469
host?: string;

packages/astro/src/content/internal.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,9 @@ async function render({
137137
propagation: 'self',
138138
});
139139

140-
if (!mod._internal && id.endsWith('.mdx')) {
141-
throw new Error(`[Content] Failed to render MDX entry. Try installing @astrojs/mdx@latest`);
142-
}
143140
return {
144141
Content,
145142
headings: mod.getHeadings(),
146-
injectedFrontmatter: mod._internal.injectedFrontmatter,
143+
remarkPluginFrontmatter: mod.frontmatter,
147144
};
148145
}

packages/astro/src/content/template/types.generated.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ declare module 'astro:content' {
3737
render(): Promise<{
3838
Content: import('astro').MarkdownInstance<{}>['Content'];
3939
headings: import('astro').MarkdownHeading[];
40-
injectedFrontmatter: Record<string, any>;
40+
remarkPluginFrontmatter: Record<string, any>;
4141
}>;
4242
};
4343

packages/astro/src/content/vite-plugin-content-assets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function astroDelayedAssetPlugin({ mode }: { mode: string }): Plugin {
3434
if (isDelayedAsset(id)) {
3535
const basePath = id.split('?')[0];
3636
const code = `
37-
export { Content, getHeadings, _internal } from ${JSON.stringify(basePath)};
37+
export { Content, getHeadings } from ${JSON.stringify(basePath)};
3838
export const collectedLinks = ${JSON.stringify(LINKS_PLACEHOLDER)};
3939
export const collectedStyles = ${JSON.stringify(STYLES_PLACEHOLDER)};
4040
`;

packages/astro/src/core/errors/errors-data.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,20 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
520520
},
521521
hint: 'See https://docs.astro.build/en/guides/content-collections/ for more information on content schemas.',
522522
},
523+
/**
524+
* @docs
525+
* @see
526+
* - [Frontmatter injection](https://docs.astro.build/en/guides/markdown-content/#example-injecting-frontmatter)
527+
* @description
528+
* A remark or rehype plugin attempted to inject invalid frontmatter. This occurs when "astro.frontmatter" is set to `null`, `undefined`, or an invalid JSON object.
529+
*/
530+
InvalidFrontmatterInjectionError: {
531+
title: 'Invalid frontmatter injection.',
532+
code: 6003,
533+
message:
534+
'A remark or rehype plugin attempted to inject invalid frontmatter. Ensure "astro.frontmatter" is set to a valid JSON object that is not `null` or `undefined`.',
535+
hint: 'See the frontmatter injection docs https://docs.astro.build/en/guides/markdown-content/#example-injecting-frontmatter for more information.',
536+
},
523537
// Config Errors - 7xxx
524538
UnknownConfigError: {
525539
title: 'Unknown configuration error.',

packages/astro/src/vite-plugin-markdown/index.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import { renderMarkdown } from '@astrojs/markdown-remark';
2+
import {
3+
safelyGetAstroData,
4+
InvalidAstroDataError,
5+
} from '@astrojs/markdown-remark/dist/internal.js';
26
import fs from 'fs';
37
import matter from 'gray-matter';
48
import { fileURLToPath } from 'node:url';
59
import type { Plugin } from 'vite';
610
import { normalizePath } from 'vite';
711
import type { AstroSettings } from '../@types/astro';
812
import { getContentPaths } from '../content/index.js';
9-
import { AstroErrorData, MarkdownError } from '../core/errors/index.js';
13+
import { AstroError, AstroErrorData, MarkdownError } from '../core/errors/index.js';
1014
import type { LogOptions } from '../core/logger/core.js';
1115
import { warn } from '../core/logger/core.js';
1216
import { isMarkdownFile } from '../core/util.js';
1317
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
14-
import {
15-
escapeViteEnvReferences,
16-
getFileInfo,
17-
safelyGetAstroData,
18-
} from '../vite-plugin-utils/index.js';
18+
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
1919

2020
interface AstroPluginOptions {
2121
settings: AstroSettings;
@@ -74,16 +74,17 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
7474
isAstroFlavoredMd: false,
7575
isExperimentalContentCollections: settings.config.experimental.contentCollections,
7676
contentDir: getContentPaths(settings.config).contentDir,
77-
} as any);
77+
frontmatter: raw.data,
78+
});
7879

7980
const html = renderResult.code;
8081
const { headings } = renderResult.metadata;
81-
const { frontmatter: injectedFrontmatter } = safelyGetAstroData(renderResult.vfile.data);
82-
const frontmatter = {
83-
...injectedFrontmatter,
84-
...raw.data,
85-
} as any;
82+
const astroData = safelyGetAstroData(renderResult.vfile.data);
83+
if (astroData instanceof InvalidAstroDataError) {
84+
throw new AstroError(AstroErrorData.InvalidFrontmatterInjectionError);
85+
}
8686

87+
const { frontmatter } = astroData;
8788
const { layout } = frontmatter;
8889

8990
if (frontmatter.setup) {
@@ -100,9 +101,6 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
100101
101102
const html = ${JSON.stringify(html)};
102103
103-
export const _internal = {
104-
injectedFrontmatter: ${JSON.stringify(injectedFrontmatter)},
105-
}
106104
export const frontmatter = ${JSON.stringify(frontmatter)};
107105
export const file = ${JSON.stringify(fileId)};
108106
export const url = ${JSON.stringify(fileUrl)};

packages/astro/src/vite-plugin-utils/index.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import ancestor from 'common-ancestor-path';
2-
import type { Data } from 'vfile';
3-
import type { AstroConfig, MarkdownAstroData } from '../@types/astro';
2+
import type { AstroConfig } from '../@types/astro';
43
import {
54
appendExtension,
65
appendForwardSlash,
@@ -36,33 +35,6 @@ export function getFileInfo(id: string, config: AstroConfig) {
3635
return { fileId, fileUrl };
3736
}
3837

39-
function isValidAstroData(obj: unknown): obj is MarkdownAstroData {
40-
if (typeof obj === 'object' && obj !== null && obj.hasOwnProperty('frontmatter')) {
41-
const { frontmatter } = obj as any;
42-
try {
43-
// ensure frontmatter is JSON-serializable
44-
JSON.stringify(frontmatter);
45-
} catch {
46-
return false;
47-
}
48-
return typeof frontmatter === 'object' && frontmatter !== null;
49-
}
50-
return false;
51-
}
52-
53-
export function safelyGetAstroData(vfileData: Data): MarkdownAstroData {
54-
const { astro } = vfileData;
55-
56-
if (!astro) return { frontmatter: {} };
57-
if (!isValidAstroData(astro)) {
58-
throw Error(
59-
`[Markdown] A remark or rehype plugin tried to add invalid frontmatter. Ensure "astro.frontmatter" is a JSON object!`
60-
);
61-
}
62-
63-
return astro;
64-
}
65-
6638
/**
6739
* Normalizes different file names like:
6840
*

packages/astro/test/astro-markdown-frontmatter-injection.test.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,10 @@ describe('Astro Markdown - frontmatter injection', () => {
3232
}
3333
});
3434

35-
it('overrides injected frontmatter with user frontmatter', async () => {
35+
it('allow user frontmatter mutation', async () => {
3636
const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json'));
37-
const readingTimes = frontmatterByPage.map(
38-
(frontmatter = {}) => frontmatter.injectedReadingTime?.text
39-
);
40-
const titles = frontmatterByPage.map((frontmatter = {}) => frontmatter.title);
41-
expect(titles).to.contain('Overridden title');
42-
expect(readingTimes).to.contain('1000 min read');
37+
const descriptions = frontmatterByPage.map((frontmatter = {}) => frontmatter.description);
38+
expect(descriptions).to.contain('Processed by remarkDescription plugin: Page 1 description');
39+
expect(descriptions).to.contain('Processed by remarkDescription plugin: Page 2 description');
4340
});
4441
});
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { defineConfig } from 'astro/config';
2-
import { rehypeReadingTime, remarkTitle } from './src/markdown-plugins.mjs'
2+
import { rehypeReadingTime, remarkTitle, remarkDescription } from './src/markdown-plugins.mjs'
33

44
// https://astro.build/config
55
export default defineConfig({
66
site: 'https://astro.build/',
77
markdown: {
8-
remarkPlugins: [remarkTitle],
8+
remarkPlugins: [remarkTitle, remarkDescription],
99
rehypePlugins: [rehypeReadingTime],
1010
}
1111
});

packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/markdown-plugins.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@ export function remarkTitle() {
1818
});
1919
};
2020
}
21+
22+
export function remarkDescription() {
23+
return function (tree, { data }) {
24+
data.astro.frontmatter.description = `Processed by remarkDescription plugin: ${data.astro.frontmatter.description}`
25+
};
26+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
---
2+
description: 'Page 1 description'
3+
---
4+
15
# Page 1
26

37
Look at that!

packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-2.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
---
2+
description: 'Page 2 description'
3+
---
4+
15
# Page 2
26

37
## Table of contents

packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/with-overrides.md

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)