summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Matt Kane <m@mk.gg> 2025-06-04 11:36:42 +0100
committerGravatar GitHub <noreply@github.com> 2025-06-04 11:36:42 +0100
commit1766d222e7bb4adb6d15090e2d6331a0d8978303 (patch)
tree2bd31b07d0f64307984893f89e50f633b29fc7c5
parentb7258f1243189218604346f5e0301dbdd363a57f (diff)
downloadastro-1766d222e7bb4adb6d15090e2d6331a0d8978303.tar.gz
astro-1766d222e7bb4adb6d15090e2d6331a0d8978303.tar.zst
astro-1766d222e7bb4adb6d15090e2d6331a0d8978303.zip
feat: adds support for rendering markdown in loaders (#13850)
* wip: loader markdown support * Better name * Add tests and docs * Lint * Remove options * Lint * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --------- Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com>
-rw-r--r--.changeset/dark-bees-stand.md47
-rw-r--r--packages/astro/src/content/content-layer.ts14
-rw-r--r--packages/astro/src/content/loaders/types.ts4
-rw-r--r--packages/astro/src/content/utils.ts1
-rw-r--r--packages/astro/test/content-layer.test.js5
-rw-r--r--packages/astro/test/fixtures/content-layer/src/content.config.ts11
-rw-r--r--packages/astro/test/fixtures/content-layer/src/pages/index.astro6
7 files changed, 85 insertions, 3 deletions
diff --git a/.changeset/dark-bees-stand.md b/.changeset/dark-bees-stand.md
new file mode 100644
index 000000000..a3b788605
--- /dev/null
+++ b/.changeset/dark-bees-stand.md
@@ -0,0 +1,47 @@
+---
+'astro': minor
+---
+
+Provides a Markdown renderer to content loaders
+
+When creating a content loader, you will now have access to a `renderMarkdown` function that allows you to render Markdown content directly within your loaders. It uses the same settings and plugins as the renderer used for Markdown files in Astro, and follows any Markdown settings you have configured in your Astro project.
+
+This allows you to render Markdown content from various sources, such as a CMS or other data sources, directly in your loaders without needing to preprocess the Markdown content separately.
+
+```ts
+import type { Loader } from 'astro/loaders';
+import { loadFromCMS } from './cms';
+
+export function myLoader(settings): Loader {
+ return {
+ name: 'my-loader',
+ async load({ renderMarkdown, store }) {
+ const entries = await loadFromCMS();
+
+ store.clear();
+
+ for (const entry of entries) {
+ // Assume each entry has a 'content' field with markdown content
+ store.set(entry.id, {
+ id: entry.id,
+ data: entry,
+ rendered: await renderMarkdown(entry.content),
+ });
+ }
+ },
+ };
+}
+```
+
+The return value of `renderMarkdown` is an object with two properties: `html` and `metadata`. These match the `rendered` property of content entries in content collections, so you can use them to render the content in your components or pages.
+
+```astro
+---
+import { getEntry, render } from 'astro:content';
+const entry = await getEntry('my-collection', Astro.params.id);
+const { Content } = await render(entry);
+---
+<Content />
+```
+
+For more information, see the [Content Loader API docs](https://docs.astro.build/en/reference/content-loader-reference/#rendermarkdown).
diff --git a/packages/astro/src/content/content-layer.ts b/packages/astro/src/content/content-layer.ts
index 1992e7706..d1b487552 100644
--- a/packages/astro/src/content/content-layer.ts
+++ b/packages/astro/src/content/content-layer.ts
@@ -1,4 +1,5 @@
import { promises as fs, existsSync } from 'node:fs';
+import { type MarkdownProcessor, createMarkdownProcessor } from '@astrojs/markdown-remark';
import PQueue from 'p-queue';
import type { FSWatcher } from 'vite';
import xxhash from 'xxhash-wasm';
@@ -14,6 +15,7 @@ import {
DATA_STORE_FILE,
MODULES_IMPORTS_FILE,
} from './consts.js';
+import type { RenderedContent } from './data-store.js';
import type { LoaderContext } from './loaders/types.js';
import type { MutableDataStore } from './mutable-data-store.js';
import {
@@ -46,7 +48,7 @@ class ContentLayer {
#watcher?: WrappedWatcher;
#lastConfigDigest?: string;
#unsubscribe?: () => void;
-
+ #markdownProcessor?: MarkdownProcessor;
#generateDigest?: (data: Record<string, unknown> | string) => string;
#queue: PQueue;
@@ -127,6 +129,7 @@ class ContentLayer {
logger: this.#logger.forkIntegrationLogger(loaderName),
config: this.#settings.config,
parseData,
+ renderMarkdown: this.#processMarkdown.bind(this),
generateDigest: await this.#getGenerateDigest(),
watcher: this.#watcher,
refreshContextData,
@@ -137,6 +140,15 @@ class ContentLayer {
};
}
+ async #processMarkdown(content: string): Promise<RenderedContent> {
+ this.#markdownProcessor ??= await createMarkdownProcessor(this.#settings.config.markdown);
+ const { code, metadata } = await this.#markdownProcessor.render(content);
+ return {
+ html: code,
+ metadata,
+ };
+ }
+
/**
* Enqueues a sync job that runs the `load()` method of each collection's loader, which will load the data and save it in the data store.
* The loader itself is responsible for deciding whether this will clear and reload the full collection, or
diff --git a/packages/astro/src/content/loaders/types.ts b/packages/astro/src/content/loaders/types.ts
index 4c2d8a359..d41017ead 100644
--- a/packages/astro/src/content/loaders/types.ts
+++ b/packages/astro/src/content/loaders/types.ts
@@ -3,6 +3,7 @@ import type { ZodSchema } from 'zod';
import type { AstroIntegrationLogger } from '../../core/logger/core.js';
import type { AstroConfig } from '../../types/public/config.js';
import type { ContentEntryType } from '../../types/public/content.js';
+import type { RenderedContent } from '../data-store.js';
import type { DataStore, MetaStore } from '../mutable-data-store.js';
export type { DataStore, MetaStore };
@@ -29,6 +30,9 @@ export interface LoaderContext {
/** Validates and parses the data according to the collection schema */
parseData<TData extends Record<string, unknown>>(props: ParseDataOptions<TData>): Promise<TData>;
+ /** Renders markdown content to HTML and metadata */
+ renderMarkdown(content: string): Promise<RenderedContent>;
+
/** Generates a non-cryptographic content digest. This can be used to check if the data has changed */
generateDigest(data: Record<string, unknown> | string): string;
diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts
index a319dfd29..912ac58ed 100644
--- a/packages/astro/src/content/utils.ts
+++ b/packages/astro/src/content/utils.ts
@@ -88,6 +88,7 @@ const collectionConfigParser = z.union([
config: z.any(),
entryTypes: z.any(),
parseData: z.any(),
+ renderMarkdown: z.any(),
generateDigest: z.function(z.tuple([z.any()], z.string())),
watcher: z.any().optional(),
refreshContextData: z.record(z.unknown()).optional(),
diff --git a/packages/astro/test/content-layer.test.js b/packages/astro/test/content-layer.test.js
index 268f03dc7..caf59459f 100644
--- a/packages/astro/test/content-layer.test.js
+++ b/packages/astro/test/content-layer.test.js
@@ -90,6 +90,11 @@ describe('Content Layer', () => {
]);
});
+ it('can render markdown in loaders', async () => {
+ const html = await fixture.readFile('/index.html');
+ assert.ok(cheerio.load(html)('section h1').text().includes('heading 1'));
+ });
+
it('handles negative matches in glob() loader', async () => {
assert.ok(json.hasOwnProperty('probes'));
assert.ok(Array.isArray(json.probes));
diff --git a/packages/astro/test/fixtures/content-layer/src/content.config.ts b/packages/astro/test/fixtures/content-layer/src/content.config.ts
index 88ed0231e..0a9c1654e 100644
--- a/packages/astro/test/fixtures/content-layer/src/content.config.ts
+++ b/packages/astro/test/fixtures/content-layer/src/content.config.ts
@@ -173,10 +173,18 @@ const images = defineCollection({
}),
});
+const markdownContent = `
+# heading 1
+hello
+## heading 2
+![image](./image.png)
+![image 2](https://example.com/image.png)
+`
+
const increment = defineCollection({
loader: {
name: 'increment-loader',
- load: async ({ store, refreshContextData, parseData }) => {
+ load: async ({ store, refreshContextData, parseData, renderMarkdown }) => {
const entry = store.get<{ lastValue: number }>('value');
const lastValue = entry?.data.lastValue ?? 0;
const raw = {
@@ -192,6 +200,7 @@ const increment = defineCollection({
store.set({
id: raw.id,
data: parsed,
+ rendered: await renderMarkdown(markdownContent)
});
},
// Example of a loader that returns an async schema function
diff --git a/packages/astro/test/fixtures/content-layer/src/pages/index.astro b/packages/astro/test/fixtures/content-layer/src/pages/index.astro
index dbd18118a..fca8766a1 100644
--- a/packages/astro/test/fixtures/content-layer/src/pages/index.astro
+++ b/packages/astro/test/fixtures/content-layer/src/pages/index.astro
@@ -1,10 +1,11 @@
---
-import { getCollection, getEntry } from 'astro:content';
+import { getCollection, getEntry, render } from 'astro:content';
const blog = await getCollection('blog');
const first = await getEntry('blog', 1);
const dogs = await getCollection('dogs');
const increment = await getEntry('increment', 'value');
+const { Content } = await render(increment);
---
<html>
<head>
@@ -19,6 +20,9 @@ const increment = await getEntry('increment', 'value');
<li><a href={`/dogs/${dog.id}`}>{ dog.data?.breed }</a></li>
))}
</ul>
+ <section>
+ <Content />
+ </section>
<h1>Blog Posts</h1>
<h2>{first.data.title}</h2>