summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/beige-pumpkins-pump.md47
-rw-r--r--examples/with-content/src/content/types.generated.d.ts79
-rw-r--r--packages/astro/src/@types/astro.ts4
-rw-r--r--packages/astro/src/content/internal.ts5
-rw-r--r--packages/astro/src/content/template/types.generated.d.ts2
-rw-r--r--packages/astro/src/content/vite-plugin-content-assets.ts2
-rw-r--r--packages/astro/src/core/errors/errors-data.ts14
-rw-r--r--packages/astro/src/vite-plugin-markdown/index.ts28
-rw-r--r--packages/astro/src/vite-plugin-utils/index.ts30
-rw-r--r--packages/astro/test/astro-markdown-frontmatter-injection.test.js11
-rw-r--r--packages/astro/test/fixtures/astro-markdown-frontmatter-injection/astro.config.mjs4
-rw-r--r--packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/markdown-plugins.mjs6
-rw-r--r--packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-1.md4
-rw-r--r--packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-2.md4
-rw-r--r--packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/with-overrides.md7
-rw-r--r--packages/integrations/mdx/src/index.ts15
-rw-r--r--packages/integrations/mdx/src/plugins.ts76
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/astro.config.mjs4
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs7
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx1
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx1
-rw-r--r--packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/with-overrides.mdx7
-rw-r--r--packages/integrations/mdx/test/mdx-frontmatter-injection.test.js11
-rw-r--r--packages/markdown/remark/package.json3
-rw-r--r--packages/markdown/remark/src/frontmatter-injection.ts41
-rw-r--r--packages/markdown/remark/src/index.ts5
-rw-r--r--packages/markdown/remark/src/internal.ts5
-rw-r--r--packages/markdown/remark/src/remark-initialize-astro-data.ts9
-rw-r--r--packages/markdown/remark/src/types.ts6
29 files changed, 234 insertions, 204 deletions
diff --git a/.changeset/beige-pumpkins-pump.md b/.changeset/beige-pumpkins-pump.md
new file mode 100644
index 000000000..54b33a619
--- /dev/null
+++ b/.changeset/beige-pumpkins-pump.md
@@ -0,0 +1,47 @@
+---
+'astro': major
+'@astrojs/markdown-remark': major
+'@astrojs/mdx': minor
+---
+
+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.
+
+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:
+
+```ts
+export function remarkInjectSocialImagePlugin() {
+ return function (tree, file) {
+ const { frontmatter } = file.data.astro;
+ frontmatter.socialImageSrc = new URL(
+ frontmatter.imageSrc,
+ 'https://my-blog.com/',
+ ).pathname;
+ }
+}
+```
+
+#### Content Collections - new `remarkPluginFrontmatter` property
+
+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.
+
+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.
+
+
+#### Migration instructions
+
+Plugin authors should now **check for user frontmatter when applying defaults.**
+
+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:
+
+```diff
+export function remarkInjectTitlePlugin() {
+ return function (tree, file) {
+ const { frontmatter } = file.data.astro;
++ if (!frontmatter.title) {
+ frontmatter.title = 'Default title';
++ }
+ }
+}
+```
+
+This differs from previous behavior, where a Markdown file's frontmatter would _always_ override frontmatter injected via remark or reype.
diff --git a/examples/with-content/src/content/types.generated.d.ts b/examples/with-content/src/content/types.generated.d.ts
index 7f0f0df84..ef8bd420e 100644
--- a/examples/with-content/src/content/types.generated.d.ts
+++ b/examples/with-content/src/content/types.generated.d.ts
@@ -37,49 +37,50 @@ declare module 'astro:content' {
render(): Promise<{
Content: import('astro').MarkdownInstance<{}>['Content'];
headings: import('astro').MarkdownHeading[];
- injectedFrontmatter: Record<string, any>;
+ remarkPluginFrontmatter: Record<string, any>;
}>;
};
const entryMap: {
- blog: {
- 'first-post.md': {
- id: 'first-post.md';
- slug: 'first-post';
- body: string;
- collection: 'blog';
- data: InferEntrySchema<'blog'>;
- };
- 'markdown-style-guide.md': {
- id: 'markdown-style-guide.md';
- slug: 'markdown-style-guide';
- body: string;
- collection: 'blog';
- data: InferEntrySchema<'blog'>;
- };
- 'second-post.md': {
- id: 'second-post.md';
- slug: 'second-post';
- body: string;
- collection: 'blog';
- data: InferEntrySchema<'blog'>;
- };
- 'third-post.md': {
- id: 'third-post.md';
- slug: 'third-post';
- body: string;
- collection: 'blog';
- data: InferEntrySchema<'blog'>;
- };
- 'using-mdx.mdx': {
- id: 'using-mdx.mdx';
- slug: 'using-mdx';
- body: string;
- collection: 'blog';
- data: InferEntrySchema<'blog'>;
- };
- };
+ "blog": {
+"first-post.md": {
+ id: "first-post.md",
+ slug: "first-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"markdown-style-guide.md": {
+ id: "markdown-style-guide.md",
+ slug: "markdown-style-guide",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"second-post.md": {
+ id: "second-post.md",
+ slug: "second-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"third-post.md": {
+ id: "third-post.md",
+ slug: "third-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"using-mdx.mdx": {
+ id: "using-mdx.mdx",
+ slug: "using-mdx",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+},
+
};
- type ContentConfig = typeof import('./config');
+ type ContentConfig = typeof import("./config");
}
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 1206c3431..41a7cd416 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -1464,10 +1464,6 @@ export interface SSRResult {
_metadata: SSRMetadata;
}
-export type MarkdownAstroData = {
- frontmatter: MD['frontmatter'];
-};
-
/* Preview server stuff */
export interface PreviewServer {
host?: string;
diff --git a/packages/astro/src/content/internal.ts b/packages/astro/src/content/internal.ts
index f49dd8d22..d3af7f8f9 100644
--- a/packages/astro/src/content/internal.ts
+++ b/packages/astro/src/content/internal.ts
@@ -137,12 +137,9 @@ async function render({
propagation: 'self',
});
- if (!mod._internal && id.endsWith('.mdx')) {
- throw new Error(`[Content] Failed to render MDX entry. Try installing @astrojs/mdx@latest`);
- }
return {
Content,
headings: mod.getHeadings(),
- injectedFrontmatter: mod._internal.injectedFrontmatter,
+ remarkPluginFrontmatter: mod.frontmatter,
};
}
diff --git a/packages/astro/src/content/template/types.generated.d.ts b/packages/astro/src/content/template/types.generated.d.ts
index fc1d489f0..0edf3dcd2 100644
--- a/packages/astro/src/content/template/types.generated.d.ts
+++ b/packages/astro/src/content/template/types.generated.d.ts
@@ -37,7 +37,7 @@ declare module 'astro:content' {
render(): Promise<{
Content: import('astro').MarkdownInstance<{}>['Content'];
headings: import('astro').MarkdownHeading[];
- injectedFrontmatter: Record<string, any>;
+ remarkPluginFrontmatter: Record<string, any>;
}>;
};
diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts
index a76dcead8..d886765bc 100644
--- a/packages/astro/src/content/vite-plugin-content-assets.ts
+++ b/packages/astro/src/content/vite-plugin-content-assets.ts
@@ -34,7 +34,7 @@ export function astroDelayedAssetPlugin({ mode }: { mode: string }): Plugin {
if (isDelayedAsset(id)) {
const basePath = id.split('?')[0];
const code = `
- export { Content, getHeadings, _internal } from ${JSON.stringify(basePath)};
+ export { Content, getHeadings } from ${JSON.stringify(basePath)};
export const collectedLinks = ${JSON.stringify(LINKS_PLACEHOLDER)};
export const collectedStyles = ${JSON.stringify(STYLES_PLACEHOLDER)};
`;
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts
index 57cf9f4d6..805cb0abe 100644
--- a/packages/astro/src/core/errors/errors-data.ts
+++ b/packages/astro/src/core/errors/errors-data.ts
@@ -520,6 +520,20 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
},
hint: 'See https://docs.astro.build/en/guides/content-collections/ for more information on content schemas.',
},
+ /**
+ * @docs
+ * @see
+ * - [Frontmatter injection](https://docs.astro.build/en/guides/markdown-content/#example-injecting-frontmatter)
+ * @description
+ * 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.
+ */
+ InvalidFrontmatterInjectionError: {
+ title: 'Invalid frontmatter injection.',
+ code: 6003,
+ message:
+ '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`.',
+ hint: 'See the frontmatter injection docs https://docs.astro.build/en/guides/markdown-content/#example-injecting-frontmatter for more information.',
+ },
// Config Errors - 7xxx
UnknownConfigError: {
title: 'Unknown configuration error.',
diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts
index b879be70b..53b482d8e 100644
--- a/packages/astro/src/vite-plugin-markdown/index.ts
+++ b/packages/astro/src/vite-plugin-markdown/index.ts
@@ -1,4 +1,8 @@
import { renderMarkdown } from '@astrojs/markdown-remark';
+import {
+ safelyGetAstroData,
+ InvalidAstroDataError,
+} from '@astrojs/markdown-remark/dist/internal.js';
import fs from 'fs';
import matter from 'gray-matter';
import { fileURLToPath } from 'node:url';
@@ -6,16 +10,12 @@ import type { Plugin } from 'vite';
import { normalizePath } from 'vite';
import type { AstroSettings } from '../@types/astro';
import { getContentPaths } from '../content/index.js';
-import { AstroErrorData, MarkdownError } from '../core/errors/index.js';
+import { AstroError, AstroErrorData, MarkdownError } from '../core/errors/index.js';
import type { LogOptions } from '../core/logger/core.js';
import { warn } from '../core/logger/core.js';
import { isMarkdownFile } from '../core/util.js';
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
-import {
- escapeViteEnvReferences,
- getFileInfo,
- safelyGetAstroData,
-} from '../vite-plugin-utils/index.js';
+import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
interface AstroPluginOptions {
settings: AstroSettings;
@@ -74,16 +74,17 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
isAstroFlavoredMd: false,
isExperimentalContentCollections: settings.config.experimental.contentCollections,
contentDir: getContentPaths(settings.config).contentDir,
- } as any);
+ frontmatter: raw.data,
+ });
const html = renderResult.code;
const { headings } = renderResult.metadata;
- const { frontmatter: injectedFrontmatter } = safelyGetAstroData(renderResult.vfile.data);
- const frontmatter = {
- ...injectedFrontmatter,
- ...raw.data,
- } as any;
+ const astroData = safelyGetAstroData(renderResult.vfile.data);
+ if (astroData instanceof InvalidAstroDataError) {
+ throw new AstroError(AstroErrorData.InvalidFrontmatterInjectionError);
+ }
+ const { frontmatter } = astroData;
const { layout } = frontmatter;
if (frontmatter.setup) {
@@ -100,9 +101,6 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
const html = ${JSON.stringify(html)};
- export const _internal = {
- injectedFrontmatter: ${JSON.stringify(injectedFrontmatter)},
- }
export const frontmatter = ${JSON.stringify(frontmatter)};
export const file = ${JSON.stringify(fileId)};
export const url = ${JSON.stringify(fileUrl)};
diff --git a/packages/astro/src/vite-plugin-utils/index.ts b/packages/astro/src/vite-plugin-utils/index.ts
index 51479380e..17882e602 100644
--- a/packages/astro/src/vite-plugin-utils/index.ts
+++ b/packages/astro/src/vite-plugin-utils/index.ts
@@ -1,6 +1,5 @@
import ancestor from 'common-ancestor-path';
-import type { Data } from 'vfile';
-import type { AstroConfig, MarkdownAstroData } from '../@types/astro';
+import type { AstroConfig } from '../@types/astro';
import {
appendExtension,
appendForwardSlash,
@@ -36,33 +35,6 @@ 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;
-}
-
/**
* Normalizes different file names like:
*
diff --git a/packages/astro/test/astro-markdown-frontmatter-injection.test.js b/packages/astro/test/astro-markdown-frontmatter-injection.test.js
index 436366355..6616ed513 100644
--- a/packages/astro/test/astro-markdown-frontmatter-injection.test.js
+++ b/packages/astro/test/astro-markdown-frontmatter-injection.test.js
@@ -32,13 +32,10 @@ describe('Astro Markdown - frontmatter injection', () => {
}
});
- it('overrides injected frontmatter with user frontmatter', async () => {
+ it('allow user frontmatter mutation', 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');
+ const descriptions = frontmatterByPage.map((frontmatter = {}) => frontmatter.description);
+ expect(descriptions).to.contain('Processed by remarkDescription plugin: Page 1 description');
+ expect(descriptions).to.contain('Processed by remarkDescription plugin: Page 2 description');
});
});
diff --git a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/astro.config.mjs b/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/astro.config.mjs
index 6ff9e1eed..18cde7c50 100644
--- a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/astro.config.mjs
+++ b/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/astro.config.mjs
@@ -1,11 +1,11 @@
import { defineConfig } from 'astro/config';
-import { rehypeReadingTime, remarkTitle } from './src/markdown-plugins.mjs'
+import { rehypeReadingTime, remarkTitle, remarkDescription } from './src/markdown-plugins.mjs'
// https://astro.build/config
export default defineConfig({
site: 'https://astro.build/',
markdown: {
- remarkPlugins: [remarkTitle],
+ remarkPlugins: [remarkTitle, remarkDescription],
rehypePlugins: [rehypeReadingTime],
}
});
diff --git a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/markdown-plugins.mjs b/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/markdown-plugins.mjs
index c0d5f7b2e..d5e1a948a 100644
--- a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/markdown-plugins.mjs
+++ b/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/markdown-plugins.mjs
@@ -18,3 +18,9 @@ export function remarkTitle() {
});
};
}
+
+export function remarkDescription() {
+ return function (tree, { data }) {
+ data.astro.frontmatter.description = `Processed by remarkDescription plugin: ${data.astro.frontmatter.description}`
+ };
+}
diff --git a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-1.md b/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-1.md
index 2fcd655ec..3705f6ab1 100644
--- a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-1.md
+++ b/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-1.md
@@ -1,3 +1,7 @@
+---
+description: 'Page 1 description'
+---
+
# Page 1
Look at that!
diff --git a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-2.md b/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-2.md
index 4a6b9addd..dc37bedc7 100644
--- a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-2.md
+++ b/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/page-2.md
@@ -1,3 +1,7 @@
+---
+description: 'Page 2 description'
+---
+
# Page 2
## Table of contents
diff --git a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/with-overrides.md b/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/with-overrides.md
deleted file mode 100644
index 4e11c1c37..000000000
--- a/packages/astro/test/fixtures/astro-markdown-frontmatter-injection/src/pages/with-overrides.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-title: 'Overridden title'
-injectedReadingTime:
- text: '1000 min read'
----
-
-# Working!
diff --git a/packages/integrations/mdx/src/index.ts b/packages/integrations/mdx/src/index.ts
index d788a171a..e9d81ca1f 100644
--- a/packages/integrations/mdx/src/index.ts
+++ b/packages/integrations/mdx/src/index.ts
@@ -1,3 +1,4 @@
+import { toRemarkInitializeAstroData } from '@astrojs/markdown-remark/dist/internal.js';
import { compile as mdxCompile } from '@mdx-js/mdx';
import { PluggableList } from '@mdx-js/mdx/lib/core.js';
import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
@@ -7,12 +8,7 @@ import fs from 'node:fs/promises';
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
import { VFile } from 'vfile';
import type { Plugin as VitePlugin } from 'vite';
-import {
- getRehypePlugins,
- getRemarkPlugins,
- recmaInjectImportMetaEnvPlugin,
- rehypeApplyFrontmatterExport,
-} from './plugins.js';
+import { getRehypePlugins, getRemarkPlugins, recmaInjectImportMetaEnvPlugin } from './plugins.js';
import { getFileInfo, parseFrontmatter } from './utils.js';
const RAW_CONTENT_ERROR =
@@ -86,9 +82,10 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
const { data: frontmatter, content: pageContent } = parseFrontmatter(code, id);
const compiled = await mdxCompile(new VFile({ value: pageContent, path: id }), {
...mdxPluginOpts,
- rehypePlugins: [
- ...(mdxPluginOpts.rehypePlugins ?? []),
- () => rehypeApplyFrontmatterExport(frontmatter),
+ remarkPlugins: [
+ // Ensure `data.astro` is available to all remark plugins
+ toRemarkInitializeAstroData({ userFrontmatter: frontmatter }),
+ ...(mdxPluginOpts.remarkPlugins ?? []),
],
recmaPlugins: [
...(mdxPluginOpts.recmaPlugins ?? []),
diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/integrations/mdx/src/plugins.ts
index c823c6e7c..4701b1679 100644
--- a/packages/integrations/mdx/src/plugins.ts
+++ b/packages/integrations/mdx/src/plugins.ts
@@ -2,7 +2,11 @@ import { rehypeHeadingIds } from '@astrojs/markdown-remark';
import { nodeTypes } from '@mdx-js/mdx';
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
import type { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
-import type { AstroConfig, MarkdownAstroData } from 'astro';
+import type { AstroConfig } from 'astro';
+import {
+ safelyGetAstroData,
+ InvalidAstroDataError,
+} from '@astrojs/markdown-remark/dist/internal.js';
import type { Literal, MemberExpression } from 'estree';
import { visit as estreeVisit } from 'estree-util-visit';
import { bold, yellow } from 'kleur/colors';
@@ -47,26 +51,18 @@ export function recmaInjectImportMetaEnvPlugin({
};
}
-export function remarkInitializeAstroData() {
+export function rehypeApplyFrontmatterExport() {
return function (tree: any, vfile: VFile) {
- if (!vfile.data.astro) {
- vfile.data.astro = { frontmatter: {} };
- }
- };
-}
-
-export function rehypeApplyFrontmatterExport(pageFrontmatter: Record<string, any>) {
- return function (tree: any, vfile: VFile) {
- const { frontmatter: injectedFrontmatter } = safelyGetAstroData(vfile.data);
- const frontmatter = { ...injectedFrontmatter, ...pageFrontmatter };
+ const astroData = safelyGetAstroData(vfile.data);
+ if (astroData instanceof InvalidAstroDataError)
+ throw new Error(
+ // Copied from Astro core `errors-data`
+ // TODO: find way to import error data from core
+ '[MDX] 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`.'
+ );
+ const { frontmatter } = astroData;
const exportNodes = [
- jsToTreeNode(
- `export const frontmatter = ${JSON.stringify(
- frontmatter
- )};\nexport const _internal = { injectedFrontmatter: ${JSON.stringify(
- injectedFrontmatter
- )} };`
- ),
+ jsToTreeNode(`export const frontmatter = ${JSON.stringify(frontmatter)};`),
];
if (frontmatter.layout) {
// NOTE(bholmesdev) 08-22-2022
@@ -151,10 +147,7 @@ export async function getRemarkPlugins(
mdxOptions: MdxOptions,
config: AstroConfig
): Promise<MdxRollupPluginOptions['remarkPlugins']> {
- let remarkPlugins: PluggableList = [
- // Set "vfile.data.astro" for plugins to inject frontmatter
- remarkInitializeAstroData,
- ];
+ let remarkPlugins: PluggableList = [];
switch (mdxOptions.extendPlugins) {
case false:
break;
@@ -217,6 +210,8 @@ export function getRehypePlugins(
// We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins.
rehypeHeadingIds,
rehypeInjectHeadingsExport,
+ // computed from `astro.data.frontmatter` in VFile data
+ rehypeApplyFrontmatterExport,
];
return rehypePlugins;
}
@@ -251,41 +246,6 @@ function ignoreStringPlugins(plugins: any[]) {
}
/**
- * 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"
- */
-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;
-}
-
-/**
* Check if estree entry is "import.meta.env.VARIABLE"
* If it is, return the variable name (i.e. "VARIABLE")
*/
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/astro.config.mjs b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/astro.config.mjs
index fc15686c2..5335fac87 100644
--- a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/astro.config.mjs
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/astro.config.mjs
@@ -1,12 +1,12 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
-import { rehypeReadingTime, remarkTitle } from './src/markdown-plugins.mjs';
+import { rehypeReadingTime, remarkDescription, remarkTitle } from './src/markdown-plugins.mjs';
// https://astro.build/config
export default defineConfig({
site: 'https://astro.build/',
integrations: [mdx({
- remarkPlugins: [remarkTitle],
+ remarkPlugins: [remarkTitle, remarkDescription],
rehypePlugins: [rehypeReadingTime],
})],
});
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs
index c0d5f7b2e..35b415787 100644
--- a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/markdown-plugins.mjs
@@ -18,3 +18,10 @@ export function remarkTitle() {
});
};
}
+
+export function remarkDescription() {
+ return function (tree, vfile) {
+ const { frontmatter } = vfile.data.astro;
+ frontmatter.description = `Processed by remarkDescription plugin: ${frontmatter.description}`
+ };
+}
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx
index 1092099f0..0d96d95b9 100644
--- a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-1.mdx
@@ -1,5 +1,6 @@
---
layout: '../layouts/Base.astro'
+description: Page 1 description
---
# Page 1
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx
index c82eb97c2..fe6a8286b 100644
--- a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx
+++ b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/page-2.mdx
@@ -1,5 +1,6 @@
---
layout: '../layouts/Base.astro'
+description: Page 2 description
---
# Page 2
diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/with-overrides.mdx b/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/with-overrides.mdx
deleted file mode 100644
index 4e11c1c37..000000000
--- a/packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection/src/pages/with-overrides.mdx
+++ /dev/null
@@ -1,7 +0,0 @@
----
-title: 'Overridden title'
-injectedReadingTime:
- text: '1000 min read'
----
-
-# Working!
diff --git a/packages/integrations/mdx/test/mdx-frontmatter-injection.test.js b/packages/integrations/mdx/test/mdx-frontmatter-injection.test.js
index 780f7252c..8f598b78e 100644
--- a/packages/integrations/mdx/test/mdx-frontmatter-injection.test.js
+++ b/packages/integrations/mdx/test/mdx-frontmatter-injection.test.js
@@ -33,14 +33,11 @@ describe('MDX frontmatter injection', () => {
}
});
- it('overrides injected frontmatter with user frontmatter', async () => {
+ it('allow user frontmatter mutation', 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');
+ const descriptions = frontmatterByPage.map((frontmatter = {}) => frontmatter.description);
+ expect(descriptions).to.contain('Processed by remarkDescription plugin: Page 1 description');
+ expect(descriptions).to.contain('Processed by remarkDescription plugin: Page 2 description');
});
it('passes injected frontmatter to layouts', async () => {
diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json
index 6549848df..5491dd0fe 100644
--- a/packages/markdown/remark/package.json
+++ b/packages/markdown/remark/package.json
@@ -13,7 +13,8 @@
"homepage": "https://astro.build",
"main": "./dist/index.js",
"exports": {
- ".": "./dist/index.js"
+ ".": "./dist/index.js",
+ "./dist/internal.js": "./dist/internal.js"
},
"scripts": {
"prepublish": "pnpm build",
diff --git a/packages/markdown/remark/src/frontmatter-injection.ts b/packages/markdown/remark/src/frontmatter-injection.ts
new file mode 100644
index 000000000..921d01297
--- /dev/null
+++ b/packages/markdown/remark/src/frontmatter-injection.ts
@@ -0,0 +1,41 @@
+import type { Data, VFile } from 'vfile';
+import type { MarkdownAstroData } from './types.js';
+
+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 class InvalidAstroDataError extends TypeError {}
+
+export function safelyGetAstroData(vfileData: Data): MarkdownAstroData | InvalidAstroDataError {
+ const { astro } = vfileData;
+
+ if (!astro || !isValidAstroData(astro)) {
+ return new InvalidAstroDataError();
+ }
+
+ return astro;
+}
+
+export function toRemarkInitializeAstroData({
+ userFrontmatter,
+}: {
+ userFrontmatter: Record<string, any>;
+}) {
+ return () =>
+ function (tree: any, vfile: VFile) {
+ if (!vfile.data.astro) {
+ vfile.data.astro = { frontmatter: userFrontmatter };
+ }
+ };
+}
diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts
index 66889108e..480cc9e38 100644
--- a/packages/markdown/remark/src/index.ts
+++ b/packages/markdown/remark/src/index.ts
@@ -8,7 +8,7 @@ import rehypeIslands from './rehype-islands.js';
import rehypeJsx from './rehype-jsx.js';
import toRemarkContentRelImageError from './remark-content-rel-image-error.js';
import remarkEscape from './remark-escape.js';
-import { remarkInitializeAstroData } from './remark-initialize-astro-data.js';
+import { toRemarkInitializeAstroData } from './frontmatter-injection.js';
import remarkMarkAndUnravel from './remark-mark-and-unravel.js';
import remarkMdxish from './remark-mdxish.js';
import remarkPrism from './remark-prism.js';
@@ -45,13 +45,14 @@ export async function renderMarkdown(
isAstroFlavoredMd = false,
isExperimentalContentCollections = false,
contentDir,
+ frontmatter: userFrontmatter = {},
} = opts;
const input = new VFile({ value: content, path: fileURL });
const scopedClassName = opts.$?.scopedClassName;
let parser = unified()
.use(markdown)
- .use(remarkInitializeAstroData)
+ .use(toRemarkInitializeAstroData({ userFrontmatter }))
.use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
if (extendDefaultPlugins || (remarkPlugins.length === 0 && rehypePlugins.length === 0)) {
diff --git a/packages/markdown/remark/src/internal.ts b/packages/markdown/remark/src/internal.ts
new file mode 100644
index 000000000..0ab7e34bb
--- /dev/null
+++ b/packages/markdown/remark/src/internal.ts
@@ -0,0 +1,5 @@
+export {
+ InvalidAstroDataError,
+ safelyGetAstroData,
+ toRemarkInitializeAstroData,
+} from './frontmatter-injection.js';
diff --git a/packages/markdown/remark/src/remark-initialize-astro-data.ts b/packages/markdown/remark/src/remark-initialize-astro-data.ts
deleted file mode 100644
index 37af8aeaf..000000000
--- a/packages/markdown/remark/src/remark-initialize-astro-data.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { VFile } from 'vfile';
-
-export function remarkInitializeAstroData() {
- return function (tree: any, vfile: VFile) {
- if (!vfile.data.astro) {
- vfile.data.astro = { frontmatter: {} };
- }
- };
-}
diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts
index 15465d950..f52fd4bf5 100644
--- a/packages/markdown/remark/src/types.ts
+++ b/packages/markdown/remark/src/types.ts
@@ -11,6 +11,10 @@ import type { VFile } from 'vfile';
export type { Node } from 'unist';
+export type MarkdownAstroData = {
+ frontmatter: Record<string, any>;
+};
+
export type RemarkPlugin<PluginParameters extends any[] = any[]> = unified.Plugin<
PluginParameters,
mdast.Root
@@ -58,6 +62,8 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
isExperimentalContentCollections?: boolean;
/** Used to prevent relative image imports from `src/content/` */
contentDir: URL;
+ /** Used for frontmatter injection plugins */
+ frontmatter?: Record<string, any>;
}
export interface MarkdownHeading {