diff options
-rw-r--r-- | .changeset/dark-bees-stand.md | 47 | ||||
-rw-r--r-- | packages/astro/src/content/content-layer.ts | 14 | ||||
-rw-r--r-- | packages/astro/src/content/loaders/types.ts | 4 | ||||
-rw-r--r-- | packages/astro/src/content/utils.ts | 1 | ||||
-rw-r--r-- | packages/astro/test/content-layer.test.js | 5 | ||||
-rw-r--r-- | packages/astro/test/fixtures/content-layer/src/content.config.ts | 11 | ||||
-rw-r--r-- | packages/astro/test/fixtures/content-layer/src/pages/index.astro | 6 |
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 + + +` + 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> |