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
This commit is contained in:
parent
16c7d0bfd4
commit
e2019be6ff
29 changed files with 234 additions and 204 deletions
47
.changeset/beige-pumpkins-pump.md
Normal file
47
.changeset/beige-pumpkins-pump.md
Normal file
|
@ -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.
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -1464,10 +1464,6 @@ export interface SSRResult {
|
|||
_metadata: SSRMetadata;
|
||||
}
|
||||
|
||||
export type MarkdownAstroData = {
|
||||
frontmatter: MD['frontmatter'];
|
||||
};
|
||||
|
||||
/* Preview server stuff */
|
||||
export interface PreviewServer {
|
||||
host?: string;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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)};
|
||||
`;
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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)};
|
||||
|
|
|
@ -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:
|
||||
*
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}`
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
---
|
||||
description: 'Page 1 description'
|
||||
---
|
||||
|
||||
# Page 1
|
||||
|
||||
Look at that!
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
---
|
||||
description: 'Page 2 description'
|
||||
---
|
||||
|
||||
# Page 2
|
||||
|
||||
## Table of contents
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
title: 'Overridden title'
|
||||
injectedReadingTime:
|
||||
text: '1000 min read'
|
||||
---
|
||||
|
||||
# Working!
|
|
@ -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 ?? []),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -250,41 +245,6 @@ function ignoreStringPlugins(plugins: any[]) {
|
|||
return validPlugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
|
|
|
@ -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],
|
||||
})],
|
||||
});
|
||||
|
|
|
@ -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}`
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
layout: '../layouts/Base.astro'
|
||||
description: Page 1 description
|
||||
---
|
||||
|
||||
# Page 1
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
layout: '../layouts/Base.astro'
|
||||
description: Page 2 description
|
||||
---
|
||||
|
||||
# Page 2
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
title: 'Overridden title'
|
||||
injectedReadingTime:
|
||||
text: '1000 min read'
|
||||
---
|
||||
|
||||
# Working!
|
|
@ -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 () => {
|
||||
|
|
|
@ -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",
|
||||
|
|
41
packages/markdown/remark/src/frontmatter-injection.ts
Normal file
41
packages/markdown/remark/src/frontmatter-injection.ts
Normal file
|
@ -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 };
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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)) {
|
||||
|
|
5
packages/markdown/remark/src/internal.ts
Normal file
5
packages/markdown/remark/src/internal.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export {
|
||||
InvalidAstroDataError,
|
||||
safelyGetAstroData,
|
||||
toRemarkInitializeAstroData,
|
||||
} from './frontmatter-injection.js';
|
|
@ -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: {} };
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue