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

Change frontmatter injection ordering #5687

Merged
merged 12 commits into from
Jan 3, 2023
47 changes: 47 additions & 0 deletions .changeset/beige-pumpkins-pump.md
Original file line number Diff line number Diff line change
@@ -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.
79 changes: 40 additions & 39 deletions examples/with-content/src/content/types.generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
4 changes: 0 additions & 4 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1464,10 +1464,6 @@ export interface SSRResult {
_metadata: SSRMetadata;
}

export type MarkdownAstroData = {
frontmatter: MD['frontmatter'];
};

/* Preview server stuff */
export interface PreviewServer {
host?: string;
Expand Down
5 changes: 1 addition & 4 deletions packages/astro/src/content/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
2 changes: 1 addition & 1 deletion packages/astro/src/content/template/types.generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}>;
};

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/content/vite-plugin-content-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems frontmatter needed to be added here where _internal was removed.

export const collectedLinks = ${JSON.stringify(LINKS_PLACEHOLDER)};
export const collectedStyles = ${JSON.stringify(STYLES_PLACEHOLDER)};
`;
Expand Down
14 changes: 14 additions & 0 deletions packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
},
/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💚 this

* @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.',
Expand Down
28 changes: 13 additions & 15 deletions packages/astro/src/vite-plugin-markdown/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
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';
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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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)};
Expand Down
30 changes: 1 addition & 29 deletions packages/astro/src/vite-plugin-utils/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -36,33 +35,6 @@ export function getFileInfo(id: string, config: AstroConfig) {
return { fileId, fileUrl };
}

function isValidAstroData(obj: unknown): obj is MarkdownAstroData {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move frontmatter injection utils to @astrojs/markdown-remark

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:
*
Expand Down
11 changes: 4 additions & 7 deletions packages/astro/test/astro-markdown-frontmatter-injection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Original file line number Diff line number Diff line change
@@ -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],
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
};
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
description: 'Page 1 description'
---

# Page 1

Look at that!
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
description: 'Page 2 description'
---

# Page 2

## Table of contents
Expand Down

This file was deleted.

Loading