Skip to content

Commit

Permalink
Merge branch 'main' into fix/prerender-dynamic
Browse files Browse the repository at this point in the history
  • Loading branch information
natemoo-re committed Jan 3, 2023
2 parents 8d82b50 + 163a9a9 commit 9865539
Show file tree
Hide file tree
Showing 29 changed files with 196 additions and 167 deletions.
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.
2 changes: 1 addition & 1 deletion examples/with-content/src/content/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
4 changes: 0 additions & 4 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1465,10 +1465,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)};
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.',
},
/**
* @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 {
InvalidAstroDataError,
safelyGetAstroData,
} 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 {
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.

15 changes: 6 additions & 9 deletions packages/integrations/mdx/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 =
Expand Down Expand Up @@ -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 ?? []),
Expand Down
Loading

0 comments on commit 9865539

Please sign in to comment.