Markdoc - remove $entry
variable (#7244)
* refactor: remove entry prop from `getRenderModule()` * refactor: remove `$entry` from markdoc * test: update entry-prop -> variables test * refactor: unify `getEntryConfigByExt` * chore: clean up shared content / data get logic * docs: update `$entry` recommendation * chore: rename entry-prop -> variables * chore: changeset * chore: missed a spot
This commit is contained in:
parent
c7897f20a9
commit
bef3a75dbc
16 changed files with 223 additions and 263 deletions
17
.changeset/metal-bugs-drive.md
Normal file
17
.changeset/metal-bugs-drive.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
'@astrojs/markdoc': minor
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Remove the auto-generated `$entry` variable for Markdoc entries. To access frontmatter as a variable, you can pass `entry.data` as a prop where you render your content:
|
||||
|
||||
```astro
|
||||
---
|
||||
import { getEntry } from 'astro:content';
|
||||
|
||||
const entry = await getEntry('docs', 'why-markdoc');
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
|
||||
<Content frontmatter={entry.data} />
|
||||
```
|
|
@ -1270,8 +1270,9 @@ export interface ContentEntryType {
|
|||
getRenderModule?(
|
||||
this: rollup.PluginContext,
|
||||
params: {
|
||||
contents: string;
|
||||
fileUrl: URL;
|
||||
viteId: string;
|
||||
entry: ContentEntryModule;
|
||||
}
|
||||
): rollup.LoadResult | Promise<rollup.LoadResult>;
|
||||
contentModuleTypes?: string;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { info, warn, type LogOptions } from '../core/logger/core.js';
|
|||
import { isRelativePath } from '../core/path.js';
|
||||
import { CONTENT_TYPES_FILE, VIRTUAL_MODULE_ID } from './consts.js';
|
||||
import {
|
||||
getContentEntryConfigByExtMap,
|
||||
getEntryConfigByExtMap,
|
||||
getContentEntryIdAndSlug,
|
||||
getContentPaths,
|
||||
getDataEntryExts,
|
||||
|
@ -74,7 +74,7 @@ export async function createContentTypesGenerator({
|
|||
}: CreateContentGeneratorParams) {
|
||||
const collectionEntryMap: CollectionEntryMap = {};
|
||||
const contentPaths = getContentPaths(settings.config, fs);
|
||||
const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings);
|
||||
const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes);
|
||||
const contentEntryExts = [...contentEntryConfigByExt.keys()];
|
||||
const dataEntryExts = getDataEntryExts(settings);
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
|||
AstroConfig,
|
||||
AstroSettings,
|
||||
ContentEntryType,
|
||||
DataEntryType,
|
||||
ImageInputFormat,
|
||||
} from '../@types/astro.js';
|
||||
import { VALID_INPUT_FORMATS } from '../assets/consts.js';
|
||||
|
@ -172,9 +173,11 @@ export function getDataEntryExts(settings: Pick<AstroSettings, 'dataEntryTypes'>
|
|||
return settings.dataEntryTypes.map((t) => t.extensions).flat();
|
||||
}
|
||||
|
||||
export function getContentEntryConfigByExtMap(settings: Pick<AstroSettings, 'contentEntryTypes'>) {
|
||||
const map: Map<string, ContentEntryType> = new Map();
|
||||
for (const entryType of settings.contentEntryTypes) {
|
||||
export function getEntryConfigByExtMap<TEntryType extends ContentEntryType | DataEntryType>(
|
||||
entryTypes: TEntryType[]
|
||||
): Map<string, TEntryType> {
|
||||
const map: Map<string, TEntryType> = new Map();
|
||||
for (const entryType of entryTypes) {
|
||||
for (const ext of entryType.extensions) {
|
||||
map.set(ext, entryType);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { PluginContext } from 'rollup';
|
|||
import { pathToFileURL } from 'url';
|
||||
import type { Plugin } from 'vite';
|
||||
import type {
|
||||
AstroConfig,
|
||||
AstroSettings,
|
||||
ContentEntryModule,
|
||||
ContentEntryType,
|
||||
|
@ -16,7 +17,7 @@ import { AstroError } from '../core/errors/errors.js';
|
|||
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
|
||||
import { CONTENT_FLAG, DATA_FLAG } from './consts.js';
|
||||
import {
|
||||
getContentEntryConfigByExtMap,
|
||||
getEntryConfigByExtMap,
|
||||
getContentEntryExts,
|
||||
getContentEntryIdAndSlug,
|
||||
getContentPaths,
|
||||
|
@ -30,7 +31,6 @@ import {
|
|||
parseEntrySlug,
|
||||
reloadContentConfigObserver,
|
||||
type ContentConfig,
|
||||
type ContentPaths,
|
||||
} from './utils.js';
|
||||
|
||||
function getContentRendererByViteId(
|
||||
|
@ -70,14 +70,9 @@ export function astroContentImportPlugin({
|
|||
const contentEntryExts = getContentEntryExts(settings);
|
||||
const dataEntryExts = getDataEntryExts(settings);
|
||||
|
||||
const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings);
|
||||
|
||||
const dataEntryExtToParser: Map<string, DataEntryType['getEntryInfo']> = new Map();
|
||||
for (const entryType of settings.dataEntryTypes) {
|
||||
for (const ext of entryType.extensions) {
|
||||
dataEntryExtToParser.set(ext, entryType.getEntryInfo);
|
||||
}
|
||||
}
|
||||
const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes);
|
||||
const dataEntryConfigByExt = getEntryConfigByExtMap(settings.dataEntryTypes);
|
||||
const { contentDir } = contentPaths;
|
||||
|
||||
const plugins: Plugin[] = [
|
||||
{
|
||||
|
@ -89,9 +84,9 @@ export function astroContentImportPlugin({
|
|||
// This cache only exists for the `render()` function specific to content.
|
||||
const { id, data, collection, _internal } = await getDataEntryModule({
|
||||
fileId,
|
||||
dataEntryExtToParser,
|
||||
contentPaths,
|
||||
settings,
|
||||
entryConfigByExt: dataEntryConfigByExt,
|
||||
contentDir,
|
||||
config: settings.config,
|
||||
fs,
|
||||
pluginContext: this,
|
||||
});
|
||||
|
@ -109,8 +104,12 @@ export const _internal = {
|
|||
return code;
|
||||
} else if (hasContentFlag(viteId, CONTENT_FLAG)) {
|
||||
const fileId = viteId.split('?')[0];
|
||||
const { id, slug, collection, body, data, _internal } = await setContentEntryModuleCache({
|
||||
const { id, slug, collection, body, data, _internal } = await getContentEntryModule({
|
||||
fileId,
|
||||
entryConfigByExt: contentEntryConfigByExt,
|
||||
contentDir,
|
||||
config: settings.config,
|
||||
fs,
|
||||
pluginContext: this,
|
||||
});
|
||||
|
||||
|
@ -170,152 +169,153 @@ export const _internal = {
|
|||
if (settings.contentEntryTypes.some((t) => t.getRenderModule)) {
|
||||
plugins.push({
|
||||
name: 'astro:content-render-imports',
|
||||
async transform(_, viteId) {
|
||||
async transform(contents, viteId) {
|
||||
const contentRenderer = getContentRendererByViteId(viteId, settings);
|
||||
if (!contentRenderer) return;
|
||||
|
||||
const { fileId } = getFileInfo(viteId, settings.config);
|
||||
const entry = await getContentEntryModuleFromCache(fileId);
|
||||
if (!entry) {
|
||||
// Cached entry must exist (or be in-flight) when importing the module via content collections.
|
||||
// This is ensured by the `astro:content-imports` plugin.
|
||||
throw new AstroError({
|
||||
...AstroErrorData.UnknownContentCollectionError,
|
||||
message: `Unable to render ${JSON.stringify(
|
||||
fileId
|
||||
)}. Did you import this module directly without using a content collection query?`,
|
||||
});
|
||||
}
|
||||
|
||||
return contentRenderer.bind(this)({ entry, viteId });
|
||||
const fileId = viteId.split('?')[0];
|
||||
return contentRenderer.bind(this)({ viteId, contents, fileUrl: pathToFileURL(fileId) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* There are two content collection plugins that depend on the same entry data:
|
||||
* - `astro:content-imports` - creates module containing the `getCollection()` result.
|
||||
* - `astro:content-render-imports` - creates module containing the `collectionEntry.render()` result.
|
||||
*
|
||||
* We could run the same transforms to generate the slug and parsed data in each plugin,
|
||||
* though this would run the user's collection schema _twice_ for each entry.
|
||||
*
|
||||
* Instead, we've implemented a cache for all content entry data. To avoid race conditions,
|
||||
* this may store either the module itself or a queue of promises awaiting this module.
|
||||
* See the implementations of `getContentEntryModuleFromCache` and `setContentEntryModuleCache`.
|
||||
*/
|
||||
const contentEntryModuleByIdCache = new Map<
|
||||
string,
|
||||
ContentEntryModule | AwaitingCacheResultQueue
|
||||
>();
|
||||
type AwaitingCacheResultQueue = {
|
||||
awaitingQueue: ((val: ContentEntryModule) => void)[];
|
||||
};
|
||||
function isAwaitingQueue(
|
||||
cacheEntry: ReturnType<typeof contentEntryModuleByIdCache.get>
|
||||
): cacheEntry is AwaitingCacheResultQueue {
|
||||
return typeof cacheEntry === 'object' && cacheEntry != null && 'awaitingQueue' in cacheEntry;
|
||||
}
|
||||
|
||||
function getContentEntryModuleFromCache(id: string): Promise<ContentEntryModule | undefined> {
|
||||
const cacheEntry = contentEntryModuleByIdCache.get(id);
|
||||
// It's possible to request an entry while `setContentEntryModuleCache` is still
|
||||
// setting that entry. In this case, queue a promise for `setContentEntryModuleCache`
|
||||
// to resolve once it is complete.
|
||||
if (isAwaitingQueue(cacheEntry)) {
|
||||
return new Promise<ContentEntryModule>((resolve, reject) => {
|
||||
cacheEntry.awaitingQueue.push(resolve);
|
||||
});
|
||||
} else if (cacheEntry) {
|
||||
return Promise.resolve(cacheEntry);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async function setContentEntryModuleCache({
|
||||
fileId,
|
||||
pluginContext,
|
||||
}: {
|
||||
fileId: string;
|
||||
pluginContext: PluginContext;
|
||||
}): Promise<ContentEntryModule> {
|
||||
// Create a queue so, if `getContentEntryModuleFromCache` is called
|
||||
// while this function is running, we can resolve all requests
|
||||
// in the `awaitingQueue` with the result.
|
||||
contentEntryModuleByIdCache.set(fileId, { awaitingQueue: [] });
|
||||
|
||||
const contentConfig = await getContentConfigFromGlobal();
|
||||
const rawContents = await fs.promises.readFile(fileId, 'utf-8');
|
||||
const fileExt = extname(fileId);
|
||||
if (!contentEntryConfigByExt.has(fileExt)) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.UnknownContentCollectionError,
|
||||
message: `No parser found for content entry ${JSON.stringify(
|
||||
fileId
|
||||
)}. Did you apply an integration for this file type?`,
|
||||
});
|
||||
}
|
||||
const contentEntryConfig = contentEntryConfigByExt.get(fileExt)!;
|
||||
const {
|
||||
rawData,
|
||||
body,
|
||||
slug: frontmatterSlug,
|
||||
data: unvalidatedData,
|
||||
} = await contentEntryConfig.getEntryInfo({
|
||||
fileUrl: pathToFileURL(fileId),
|
||||
contents: rawContents,
|
||||
});
|
||||
const entry = pathToFileURL(fileId);
|
||||
const { contentDir } = contentPaths;
|
||||
const collection = getEntryCollectionName({ entry, contentDir });
|
||||
if (collection === undefined)
|
||||
throw new AstroError(AstroErrorData.UnknownContentCollectionError);
|
||||
|
||||
const { id, slug: generatedSlug } = getContentEntryIdAndSlug({ entry, contentDir, collection });
|
||||
|
||||
const _internal = { filePath: fileId, rawData: rawData };
|
||||
// TODO: move slug calculation to the start of the build
|
||||
// to generate a performant lookup map for `getEntryBySlug`
|
||||
const slug = parseEntrySlug({
|
||||
id,
|
||||
collection,
|
||||
generatedSlug,
|
||||
frontmatterSlug,
|
||||
});
|
||||
|
||||
const collectionConfig = contentConfig?.collections[collection];
|
||||
let data = collectionConfig
|
||||
? await getEntryData(
|
||||
{ id, collection, _internal, unvalidatedData },
|
||||
collectionConfig,
|
||||
pluginContext,
|
||||
settings.config
|
||||
)
|
||||
: unvalidatedData;
|
||||
|
||||
const contentEntryModule: ContentEntryModule = {
|
||||
id,
|
||||
slug,
|
||||
collection,
|
||||
data,
|
||||
body,
|
||||
_internal,
|
||||
};
|
||||
|
||||
const cacheEntry = contentEntryModuleByIdCache.get(fileId);
|
||||
// Pass the entry to all promises awaiting this result
|
||||
if (isAwaitingQueue(cacheEntry)) {
|
||||
for (const resolve of cacheEntry.awaitingQueue) {
|
||||
resolve(contentEntryModule);
|
||||
}
|
||||
}
|
||||
contentEntryModuleByIdCache.set(fileId, contentEntryModule);
|
||||
return contentEntryModule;
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
type GetEntryModuleParams<TEntryType extends ContentEntryType | DataEntryType> = {
|
||||
fs: typeof fsMod;
|
||||
fileId: string;
|
||||
contentDir: URL;
|
||||
pluginContext: PluginContext;
|
||||
entryConfigByExt: Map<string, TEntryType>;
|
||||
config: AstroConfig;
|
||||
};
|
||||
|
||||
async function getContentEntryModule(
|
||||
params: GetEntryModuleParams<ContentEntryType>
|
||||
): Promise<ContentEntryModule> {
|
||||
const { fileId, contentDir, pluginContext, config } = params;
|
||||
const { collectionConfig, entryConfig, entry, rawContents, collection } =
|
||||
await getEntryModuleBaseInfo(params);
|
||||
|
||||
const {
|
||||
rawData,
|
||||
data: unvalidatedData,
|
||||
body,
|
||||
slug: frontmatterSlug,
|
||||
} = await entryConfig.getEntryInfo({
|
||||
fileUrl: pathToFileURL(fileId),
|
||||
contents: rawContents,
|
||||
});
|
||||
const _internal = { filePath: fileId, rawData };
|
||||
const { id, slug: generatedSlug } = getContentEntryIdAndSlug({ entry, contentDir, collection });
|
||||
|
||||
const slug = parseEntrySlug({
|
||||
id,
|
||||
collection,
|
||||
generatedSlug,
|
||||
frontmatterSlug,
|
||||
});
|
||||
|
||||
const data = collectionConfig
|
||||
? await getEntryData(
|
||||
{ id, collection, _internal, unvalidatedData },
|
||||
collectionConfig,
|
||||
pluginContext,
|
||||
config
|
||||
)
|
||||
: unvalidatedData;
|
||||
|
||||
const contentEntryModule: ContentEntryModule = {
|
||||
id,
|
||||
slug,
|
||||
collection,
|
||||
data,
|
||||
body,
|
||||
_internal,
|
||||
};
|
||||
|
||||
return contentEntryModule;
|
||||
}
|
||||
|
||||
async function getDataEntryModule(
|
||||
params: GetEntryModuleParams<DataEntryType>
|
||||
): Promise<DataEntryModule> {
|
||||
const { fileId, contentDir, pluginContext, config } = params;
|
||||
const { collectionConfig, entryConfig, entry, rawContents, collection } =
|
||||
await getEntryModuleBaseInfo(params);
|
||||
|
||||
const { rawData = '', data: unvalidatedData } = await entryConfig.getEntryInfo({
|
||||
fileUrl: pathToFileURL(fileId),
|
||||
contents: rawContents,
|
||||
});
|
||||
const _internal = { filePath: fileId, rawData };
|
||||
const id = getDataEntryId({ entry, contentDir, collection });
|
||||
|
||||
const data = collectionConfig
|
||||
? await getEntryData(
|
||||
{ id, collection, _internal, unvalidatedData },
|
||||
collectionConfig,
|
||||
pluginContext,
|
||||
config
|
||||
)
|
||||
: unvalidatedData;
|
||||
|
||||
const dataEntryModule: DataEntryModule = {
|
||||
id,
|
||||
collection,
|
||||
data,
|
||||
_internal,
|
||||
};
|
||||
|
||||
return dataEntryModule;
|
||||
}
|
||||
|
||||
// Shared logic for `getContentEntryModule` and `getDataEntryModule`
|
||||
// Extracting to a helper was easier that conditionals and generics :)
|
||||
async function getEntryModuleBaseInfo<TEntryType extends ContentEntryType | DataEntryType>({
|
||||
fileId,
|
||||
entryConfigByExt,
|
||||
contentDir,
|
||||
fs,
|
||||
}: GetEntryModuleParams<TEntryType>) {
|
||||
const contentConfig = await getContentConfigFromGlobal();
|
||||
let rawContents;
|
||||
try {
|
||||
rawContents = await fs.promises.readFile(fileId, 'utf-8');
|
||||
} catch (e) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.UnknownContentCollectionError,
|
||||
message: `Unexpected error reading entry ${JSON.stringify(fileId)}.`,
|
||||
stack: e instanceof Error ? e.stack : undefined,
|
||||
});
|
||||
}
|
||||
const fileExt = extname(fileId);
|
||||
const entryConfig = entryConfigByExt.get(fileExt);
|
||||
|
||||
if (!entryConfig) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.UnknownContentCollectionError,
|
||||
message: `No parser found for data entry ${JSON.stringify(
|
||||
fileId
|
||||
)}. Did you apply an integration for this file type?`,
|
||||
});
|
||||
}
|
||||
const entry = pathToFileURL(fileId);
|
||||
const collection = getEntryCollectionName({ entry, contentDir });
|
||||
if (collection === undefined) throw new AstroError(AstroErrorData.UnknownContentCollectionError);
|
||||
|
||||
const collectionConfig = contentConfig?.collections[collection];
|
||||
|
||||
return {
|
||||
collectionConfig,
|
||||
entry,
|
||||
entryConfig,
|
||||
collection,
|
||||
rawContents,
|
||||
};
|
||||
}
|
||||
|
||||
async function getContentConfigFromGlobal() {
|
||||
const observable = globalContentConfigObserver.get();
|
||||
|
||||
|
@ -352,68 +352,3 @@ async function getContentConfigFromGlobal() {
|
|||
|
||||
return contentConfig;
|
||||
}
|
||||
|
||||
type GetDataEntryModuleParams = {
|
||||
fs: typeof fsMod;
|
||||
fileId: string;
|
||||
contentPaths: Pick<ContentPaths, 'contentDir'>;
|
||||
pluginContext: PluginContext;
|
||||
dataEntryExtToParser: Map<string, DataEntryType['getEntryInfo']>;
|
||||
settings: Pick<AstroSettings, 'config'>;
|
||||
};
|
||||
|
||||
async function getDataEntryModule({
|
||||
fileId,
|
||||
dataEntryExtToParser,
|
||||
contentPaths,
|
||||
fs,
|
||||
pluginContext,
|
||||
settings,
|
||||
}: GetDataEntryModuleParams): Promise<DataEntryModule> {
|
||||
const contentConfig = await getContentConfigFromGlobal();
|
||||
let rawContents;
|
||||
try {
|
||||
rawContents = await fs.promises.readFile(fileId, 'utf-8');
|
||||
} catch (e) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.UnknownContentCollectionError,
|
||||
message: `Unexpected error reading entry ${JSON.stringify(fileId)}.`,
|
||||
stack: e instanceof Error ? e.stack : undefined,
|
||||
});
|
||||
}
|
||||
const fileExt = extname(fileId);
|
||||
const dataEntryParser = dataEntryExtToParser.get(fileExt);
|
||||
|
||||
if (!dataEntryParser) {
|
||||
throw new AstroError({
|
||||
...AstroErrorData.UnknownContentCollectionError,
|
||||
message: `No parser found for data entry ${JSON.stringify(
|
||||
fileId
|
||||
)}. Did you apply an integration for this file type?`,
|
||||
});
|
||||
}
|
||||
const { data: unvalidatedData, rawData = '' } = await dataEntryParser({
|
||||
fileUrl: pathToFileURL(fileId),
|
||||
contents: rawContents,
|
||||
});
|
||||
const entry = pathToFileURL(fileId);
|
||||
const { contentDir } = contentPaths;
|
||||
const collection = getEntryCollectionName({ entry, contentDir });
|
||||
if (collection === undefined) throw new AstroError(AstroErrorData.UnknownContentCollectionError);
|
||||
|
||||
const id = getDataEntryId({ entry, contentDir, collection });
|
||||
|
||||
const _internal = { filePath: fileId, rawData };
|
||||
|
||||
const collectionConfig = contentConfig?.collections[collection];
|
||||
const data = collectionConfig
|
||||
? await getEntryData(
|
||||
{ id, collection, _internal, unvalidatedData },
|
||||
collectionConfig,
|
||||
pluginContext,
|
||||
settings.config
|
||||
)
|
||||
: unvalidatedData;
|
||||
|
||||
return { id, collection, data, _internal };
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@ import { extname } from 'node:path';
|
|||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import pLimit from 'p-limit';
|
||||
import type { Plugin } from 'vite';
|
||||
import type { AstroSettings } from '../@types/astro.js';
|
||||
import type { AstroSettings, ContentEntryType } from '../@types/astro.js';
|
||||
import { AstroError, AstroErrorData } from '../core/errors/index.js';
|
||||
import { rootRelativePath } from '../core/util.js';
|
||||
import { VIRTUAL_MODULE_ID } from './consts.js';
|
||||
import {
|
||||
getContentEntryConfigByExtMap,
|
||||
getEntryConfigByExtMap,
|
||||
getContentEntryIdAndSlug,
|
||||
getContentPaths,
|
||||
getDataEntryExts,
|
||||
|
@ -32,7 +32,7 @@ export function astroContentVirtualModPlugin({
|
|||
const contentPaths = getContentPaths(settings.config);
|
||||
const relContentDir = rootRelativePath(settings.config.root, contentPaths.contentDir);
|
||||
|
||||
const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings);
|
||||
const contentEntryConfigByExt = getEntryConfigByExtMap(settings.contentEntryTypes);
|
||||
const contentEntryExts = [...contentEntryConfigByExt.keys()];
|
||||
const dataEntryExts = getDataEntryExts(settings);
|
||||
|
||||
|
@ -92,7 +92,7 @@ export async function getStringifiedLookupMap({
|
|||
root,
|
||||
fs,
|
||||
}: {
|
||||
contentEntryConfigByExt: ReturnType<typeof getContentEntryConfigByExtMap>;
|
||||
contentEntryConfigByExt: Map<string, ContentEntryType>;
|
||||
dataEntryExts: string[];
|
||||
contentPaths: Pick<ContentPaths, 'contentDir' | 'config'>;
|
||||
root: URL;
|
||||
|
|
|
@ -290,20 +290,6 @@ export default defineMarkdocConfig({
|
|||
})
|
||||
```
|
||||
|
||||
### Access frontmatter and content collection information from your templates
|
||||
|
||||
You can access content collection information from your Markdoc templates using the `$entry` variable. This includes the entry `slug`, `collection` name, and frontmatter `data` parsed by your content collection schema (if any). This example renders the `title` frontmatter property as a heading:
|
||||
|
||||
```md
|
||||
---
|
||||
title: Welcome to Markdoc 👋
|
||||
---
|
||||
|
||||
# {% $entry.data.title %}
|
||||
```
|
||||
|
||||
The `$entry` object matches [the `CollectionEntry` type](https://docs.astro.build/en/reference/api-reference/#collection-entry-type), excluding the `.render()` property.
|
||||
|
||||
### Markdoc config
|
||||
|
||||
The `markdoc.config.mjs|ts` file accepts [all Markdoc configuration options](https://markdoc.dev/docs/config), including [tags](https://markdoc.dev/docs/tags) and [functions](https://markdoc.dev/docs/functions).
|
||||
|
@ -379,6 +365,23 @@ export default defineMarkdocConfig({
|
|||
})
|
||||
```
|
||||
|
||||
### Access frontmatter from your Markdoc content
|
||||
|
||||
To access frontmatter, you can pass the entry `data` property [as a variable](#pass-markdoc-variables) where you render your content:
|
||||
|
||||
```astro
|
||||
---
|
||||
import { getEntry } from 'astro:content';
|
||||
|
||||
const entry = await getEntry('docs', 'why-markdoc');
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
|
||||
<Content frontmatter={entry.data} />
|
||||
```
|
||||
|
||||
This can now be accessed as `$frontmatter` in your Markdoc.
|
||||
|
||||
## Examples
|
||||
|
||||
* The [Astro Markdoc starter template](https://github.com/withastro/astro/tree/latest/examples/with-markdoc) shows how to use Markdoc files in your Astro project.
|
||||
|
|
|
@ -11,6 +11,7 @@ import { bold, red, yellow } from 'kleur/colors';
|
|||
import type * as rollup from 'rollup';
|
||||
import { loadMarkdocConfig, type MarkdocConfigResult } from './load-config.js';
|
||||
import { setupConfig } from './runtime.js';
|
||||
import path from 'node:path';
|
||||
|
||||
type SetupHookParams = HookParameters<'astro:config:setup'> & {
|
||||
// `contentEntryType` is not a public API
|
||||
|
@ -61,10 +62,13 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
|
|||
addContentEntryType({
|
||||
extensions: ['.mdoc'],
|
||||
getEntryInfo,
|
||||
async getRenderModule({ entry, viteId }) {
|
||||
async getRenderModule({ contents, fileUrl, viteId }) {
|
||||
const entry = getEntryInfo({ contents, fileUrl });
|
||||
const ast = Markdoc.parse(entry.body);
|
||||
const pluginContext = this;
|
||||
const markdocConfig = await setupConfig(userMarkdocConfig, entry);
|
||||
const markdocConfig = await setupConfig(userMarkdocConfig);
|
||||
|
||||
const filePath = fileURLToPath(fileUrl);
|
||||
|
||||
const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => {
|
||||
return (
|
||||
|
@ -77,10 +81,11 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
|
|||
});
|
||||
if (validationErrors.length) {
|
||||
// Heuristic: take number of newlines for `rawData` and add 2 for the `---` fences
|
||||
const frontmatterBlockOffset = entry._internal.rawData.split('\n').length + 2;
|
||||
const frontmatterBlockOffset = entry.rawData.split('\n').length + 2;
|
||||
const rootRelativePath = path.relative(fileURLToPath(astroConfig.root), filePath);
|
||||
throw new MarkdocError({
|
||||
message: [
|
||||
`**${String(entry.collection)} → ${String(entry.id)}** contains invalid content:`,
|
||||
`**${String(rootRelativePath)}** contains invalid content:`,
|
||||
...validationErrors.map((e) => `- ${e.error.message}`),
|
||||
].join('\n'),
|
||||
location: {
|
||||
|
@ -96,7 +101,7 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
|
|||
await emitOptimizedImages(ast.children, {
|
||||
astroConfig,
|
||||
pluginContext,
|
||||
filePath: entry._internal.filePath,
|
||||
filePath,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -13,13 +13,9 @@ export { default as Markdoc } from '@markdoc/markdoc';
|
|||
* TODO: virtual module to merge configs per-build instead of per-file?
|
||||
*/
|
||||
export async function setupConfig(
|
||||
userConfig: AstroMarkdocConfig,
|
||||
entry: ContentEntryModule
|
||||
userConfig: AstroMarkdocConfig
|
||||
): Promise<Omit<AstroMarkdocConfig, 'extends'>> {
|
||||
let defaultConfig: AstroMarkdocConfig = {
|
||||
...setupHeadingConfig(),
|
||||
variables: { entry },
|
||||
};
|
||||
let defaultConfig: AstroMarkdocConfig = setupHeadingConfig();
|
||||
|
||||
if (userConfig.extends) {
|
||||
for (let extension of userConfig.extends) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@test/markdoc-entry-prop",
|
||||
"name": "@test/markdoc-variables",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
|
@ -14,6 +14,6 @@ const { Content } = await entry.render();
|
|||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<Content />
|
||||
<Content {entry} />
|
||||
</body>
|
||||
</html>
|
|
@ -3,9 +3,9 @@ import { expect } from 'chai';
|
|||
import { loadFixture } from '../../../astro/test/test-utils.js';
|
||||
import markdoc from '../dist/index.js';
|
||||
|
||||
const root = new URL('./fixtures/entry-prop/', import.meta.url);
|
||||
const root = new URL('./fixtures/variables/', import.meta.url);
|
||||
|
||||
describe('Markdoc - Entry prop', () => {
|
||||
describe('Markdoc - Variables', () => {
|
||||
let baseFixture;
|
||||
|
||||
before(async () => {
|
|
@ -4044,15 +4044,6 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/markdoc/test/fixtures/entry-prop:
|
||||
dependencies:
|
||||
'@astrojs/markdoc':
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/markdoc/test/fixtures/headings:
|
||||
dependencies:
|
||||
'@astrojs/markdoc':
|
||||
|
@ -4120,6 +4111,15 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/markdoc/test/fixtures/variables:
|
||||
dependencies:
|
||||
'@astrojs/markdoc':
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/mdx:
|
||||
dependencies:
|
||||
'@astrojs/markdown-remark':
|
||||
|
|
Loading…
Reference in a new issue