Core & UI & MDX: Support MDX in codeblock tab value

This commit is contained in:
Fuma Nama
2025-05-17 16:18:46 +08:00
parent e955a98da3
commit 4ae7b4ad3b
19 changed files with 309 additions and 275 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@fumadocs/mdx-remote': patch
'fumadocs-core': patch
'fumadocs-mdx': patch
---
Support MDX in codeblock tab value
@@ -41,25 +41,15 @@ export default withContentCollections(config);
To integrate with Fumadocs, add the following to your `content-collections.ts`.
```json doc-gen:file
{
"file": "../../examples/content-collections/content-collections.ts",
"codeblock": {
"meta": "title=\"content-collections.ts\""
}
}
```
<include cwd meta='title="content-collections.ts"'>
../../examples/content-collections/content-collections.ts
</include>
And pass it to Source API.
```json doc-gen:file
{
"file": "../../examples/content-collections/lib/source.ts",
"codeblock": {
"meta": "title=\"lib/source.ts\""
}
}
```
<include cwd meta='title="lib/source.ts"'>
../../examples/content-collections/lib/source.ts
</include>
Done! You can access the pages and generated page tree from Source API.
@@ -9,15 +9,9 @@ Fumadocs core provides necessary middleware and options for i18n support.
You can define a config to share between utilities.
```json doc-gen:file
{
"file": "../../examples/i18n/lib/i18n.ts",
"codeblock": {
"lang": "ts",
"meta": "title=\"lib/i18n.ts\""
}
}
```
<include cwd meta='title="lib/i18n.ts"'>
../../examples/i18n/lib/i18n.ts
</include>
### Hide Locale Prefix
@@ -51,14 +45,8 @@ This may cause undesired cache problems, and need to pay extra attention on SEO
Redirects users to appropriate locale, it can be customised from `i18n.ts`.
```json doc-gen:file
{
"file": "../../examples/i18n/middleware.ts",
"codeblock": {
"lang": "ts",
"meta": "title=\"middleware.ts\""
}
}
```
<include cwd meta='title="middleware.ts"'>
../../examples/i18n/middleware.ts
</include>
> When `hideLocale` is enabled, it uses `NextResponse.rewrite` to hide locale prefixes.
@@ -45,14 +45,9 @@ credentials in environment variables.
Export the search indexes from Next.js using a route handler, this way we can access the search indexes after production build:
```json doc-gen:file
{
"file": "../../examples/next-mdx/app/static.json/algolia.ts",
"codeblock": {
"meta": "title=\"app/static.json/route.ts\""
}
}
```
<include cwd meta='title="app/static.json/route.ts"'>
../../examples/next-mdx/app/static.json/algolia.ts
</include>
Make a script to sync search indexes:
@@ -27,26 +27,15 @@ REST API integration requires your docs to upload the indexes.
3. Then, using the private API key and index ID from dashboard, create a script to sync search indexes.
```json doc-gen:file
{
"file": "../../examples/next-mdx/scripts/sync-orama-cloud.mjs",
"codeblock": {
"lang": "js",
"meta": "title=\"sync-index.mjs\""
}
}
```
<include cwd meta='title="sync-index.mjs"' lang="js">
../../examples/next-mdx/scripts/sync-orama-cloud.mjs
</include>
4. Create a route handler in your Next.js app to export search indexes.
```json doc-gen:file
{
"file": "../../examples/next-mdx/app/static.json/orama-cloud.ts",
"codeblock": {
"meta": "title=\"app/static.json/route.ts\""
}
}
```
<include cwd meta='title="app/static.json/route.ts"'>
../../examples/next-mdx/app/static.json/orama-cloud.ts
</include>
5. Run the script after `next build`.
@@ -17,15 +17,9 @@ class-variance-authority lucide-react
Copy the component:
```json doc-gen:file
{
"file": "./components/rate.tsx",
"codeblock": {
"lang": "tsx",
"meta": "title=\"components/rate.tsx\""
}
}
```
<include cwd meta='title="components/rate.tsx"'>
./components/rate.tsx
</include>
The `@/lib/cn` import specifier may be different for your project, change it to import your `cn()` function if needed. (e.g. like `@/lib/utils`)
@@ -64,15 +58,9 @@ To report your feedback to GitHub Discussion, make a custom `onRateAction`.
You can copy this example as a starting point:
```json doc-gen:file
{
"file": "./lib/github.ts",
"codeblock": {
"lang": "ts",
"meta": "title=\"lib/github.ts\""
}
}
```
<include cwd meta="lib/github.ts">
./lib/github.ts
</include>
- Create your own GitHub App and obtain its app ID and private key.
- Fill required environment variables.
@@ -16,25 +16,39 @@ For docs pages, Fumadocs has a built-in metadata image generator.
You will need a route handler to get started.
```json doc-gen:file
{
"file": "../../examples/next-mdx/app/docs-og/[...slug]/route.tsx",
"codeblock": {
"meta": "title=\"app/docs-og/[...slug]/route.tsx\""
}
}
```
<include cwd meta='title="app/docs-og/[...slug]/route.tsx"'>
../../examples/next-mdx/app/docs-og/[...slug]/route.tsx
</include>
> We need to append `image.png` to the end of slugs so that we can access it via `/docs-og/my-page/image.png`.
In your docs page, add the image to metadata.
```json doc-gen:file
{
"file": "../../examples/next-mdx/app/docs/[[...slug]]/page.with-og-image.tsx",
"codeblock": {
"meta": "title=\"app/docs/[[...slug]]/page.tsx\""
}
```tsx title="app/docs/[[...slug]]/page.tsx"
import { notFound } from 'next/navigation';
import { source } from '@/lib/source';
export async function generateMetadata({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const { slug = [] } = await params;
const page = source.getPage(slug);
if (!page) notFound();
const image = ['/docs-og', ...slug, 'image.png'].join('/');
return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: image,
},
twitter: {
card: 'summary_large_image',
images: image,
},
};
}
```
@@ -10,7 +10,7 @@ preview: tabs
Add MDX components.
```tsx
```tsx title="mdx-components.tsx"
import defaultMdxComponents from 'fumadocs-ui/mdx';
import * as TabsComponents from 'fumadocs-ui/components/tabs';
import type { MDXComponents } from 'mdx/types';
@@ -38,15 +38,9 @@ export const source = loader({
Create a middleware that redirects users to appropriate locale.
```json doc-gen:file
{
"file": "../../examples/i18n/middleware.ts",
"codeblock": {
"lang": "ts",
"meta": "title=\"middleware.ts\""
}
}
```
<include cwd meta='title="middleware.ts"'>
../../examples/i18n/middleware.ts
</include>
<Callout title="Custom Middleware">
@@ -113,25 +113,15 @@ Add the following Tailwind CSS styles to `global.css`.
Create a `app/layout.config.tsx` file to put the shared options for our layouts.
```json doc-gen:file
{
"file": "../../examples/next-mdx/app/layout.config.tsx",
"codeblock": {
"meta": "title=\"app/layout.config.tsx\""
}
}
```
<include cwd meta='title="app/layout.config.tsx"'>
../../examples/next-mdx/app/layout.config.tsx
</include>
Create a folder `/app/docs` for our docs, and give it a proper layout.
```json doc-gen:file
{
"file": "../../examples/next-mdx/app/docs/layout.tsx",
"codeblock": {
"meta": "title=\"app/docs/layout.tsx\""
}
}
```
<include cwd meta='title="app/docs/layout.tsx"'>
../../examples/next-mdx/app/docs/layout.tsx
</include>
> `pageTree` refers to Page Tree, it should be provided by your content source.
+65 -5
View File
@@ -283,11 +283,23 @@ return new ResizeObserver(() => {}) // [!code focus]
#### Tab Groups
You can use code blocks with the `<Tab />` component.
Make sure to add MDX components first:
```tsx title="mdx-components.tsx"
import defaultMdxComponents from 'fumadocs-ui/mdx';
import * as TabsComponents from 'fumadocs-ui/components/tabs';
import type { MDXComponents } from 'mdx/types';
export function getMDXComponents(components?: MDXComponents): MDXComponents {
return {
...defaultMdxComponents,
...TabsComponents, // [!code ++]
...components,
};
}
```
````mdx
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
```ts tab="Tab 1"
console.log('A');
```
@@ -297,8 +309,6 @@ console.log('B');
```
````
> Note that you can add MDX components instead of importing them in MDX files.
```ts tab="Tab 1"
console.log('A');
```
@@ -341,3 +351,53 @@ npm i next -D
```
To enable, see [Remark Install](/docs/headless/mdx/install).
### Tab Groups with MDX
You can use MDX inside tab values too, enable it the remark plugin:
```ts tab="Fumadocs MDX" title="source.config.ts"
import { defineConfig } from 'fumadocs-mdx/config';
export default defineConfig({
mdxOptions: {
remarkCodeTabOptions: {
parseMdx: true, // [!code ++]
},
},
});
```
```ts tab="MDX Compiler"
import { compile } from '@mdx-js/mdx';
import { remarkCodeTab } from 'fumadocs-core/mdx-plugins';
await compile('...', {
remarkPlugins: [
[
remarkCodeTab,
{
parseMdx: true, // [!code ++]
},
],
],
});
```
````mdx
```ts tab="<Building /> Tab 1"
console.log('A');
```
```ts tab="<Rocket /> Tab 2"
console.log('B');
```
````
```ts tab="<Building /> Tab 1"
console.log('A');
```
```ts tab="<Rocket /> Tab 2"
console.log('B');
```
+1 -2
View File
@@ -1,6 +1,6 @@
import { remark } from 'remark';
import remarkGfm from 'remark-gfm';
import { fileGenerator, remarkDocGen, remarkInstall } from 'fumadocs-docgen';
import { remarkInstall } from 'fumadocs-docgen';
import remarkMdx from 'remark-mdx';
import { remarkAutoTypeTable } from 'fumadocs-typescript';
import { remarkInclude } from 'fumadocs-mdx/config';
@@ -11,7 +11,6 @@ const processor = remark()
.use(remarkInclude)
.use(remarkGfm)
.use(remarkAutoTypeTable)
.use(remarkDocGen, { generators: [fileGenerator()] })
.use(remarkInstall);
export async function getLLMText(page: Page) {
+1
View File
@@ -22,6 +22,7 @@
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-tooltip": "^1.2.6",
"@shikijs/rehype": "^3.4.2",
"@shikijs/transformers": "^3.4.2",
"ai": "^4.3.15",
"class-variance-authority": "^0.7.1",
"fumadocs-core": "workspace:*",
+6 -19
View File
@@ -8,7 +8,7 @@ import {
import { transformerTwoslash } from 'fumadocs-twoslash';
import { createFileSystemTypesCache } from 'fumadocs-twoslash/cache-fs';
import remarkMath from 'remark-math';
import { fileGenerator, remarkDocGen, remarkInstall } from 'fumadocs-docgen';
import { remarkInstall } from 'fumadocs-docgen';
import { remarkTypeScriptToJavaScript } from 'fumadocs-docgen/remark-ts2js';
import rehypeKatex from 'rehype-katex';
import { z } from 'zod';
@@ -17,6 +17,7 @@ import {
remarkSteps,
} from 'fumadocs-core/mdx-plugins';
import { remarkAutoTypeTable } from 'fumadocs-typescript';
import { transformerRemoveNotationEscape } from '@shikijs/transformers';
export const docs = defineDocs({
docs: {
@@ -63,31 +64,17 @@ export default defineConfig({
transformerTwoslash({
typesCache: createFileSystemTypesCache(),
}),
{
name: 'transformers:remove-notation-escape',
code(hast) {
for (const line of hast.children) {
if (line.type !== 'element') continue;
const lastSpan = line.children.findLast(
(v) => v.type === 'element',
);
const head = lastSpan?.children[0];
if (head?.type !== 'text') continue;
head.value = head.value.replace(/\[\\!code/g, '[!code');
}
},
},
transformerRemoveNotationEscape(),
],
},
remarkCodeTabOptions: {
parseMdx: true,
},
remarkPlugins: [
remarkSteps,
remarkMath,
remarkAutoTypeTable,
[remarkInstall, { persist: { id: 'package-manager' } }],
[remarkDocGen, { generators: [fileGenerator()] }],
remarkTypeScriptToJavaScript,
],
rehypePlugins: (v) => [rehypeKatex, ...v],
@@ -1,57 +0,0 @@
import { source } from '@/lib/source';
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from 'fumadocs-ui/page';
import { notFound } from 'next/navigation';
import { getMDXComponents } from '@/mdx-components';
import type { Metadata } from 'next';
export default async function Page(props: {
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
const MDX = page.data.body;
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDX components={getMDXComponents()} />
</DocsBody>
</DocsPage>
);
}
export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const { slug = [] } = await params;
const page = source.getPage(slug);
if (!page) notFound();
const image = ['/docs-og', ...slug, 'image.png'].join('/');
return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: image,
},
twitter: {
card: 'summary_large_image',
images: image,
},
} satisfies Metadata;
}
+131 -49
View File
@@ -1,11 +1,64 @@
import type { Transformer } from 'unified';
import type { Code, Root, RootContent } from 'mdast';
import type { Processor, Transformer } from 'unified';
import type { Code, Root } from 'mdast';
import { visit } from 'unist-util-visit';
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
const TabRegex = /tab="(.+?)"/;
function toTab(nodes: Code[]) {
const names = nodes.map((node, i) => {
export interface RemarkCodeTabOptions {
/**
* Parse MDX in tab values
*
* @defaultValue false
*/
parseMdx?: boolean;
}
export function remarkCodeTab(
this: Processor,
options: RemarkCodeTabOptions = {},
): Transformer<Root, Root> {
return (tree) => {
visit(tree, (node) => {
if (!('children' in node)) return;
if (node.type === 'mdxJsxFlowElement' && node.name === 'Tabs') return;
let start = -1;
let i = 0;
while (i < node.children.length) {
const child = node.children[i];
const isSwitcher =
child.type === 'code' && child.meta && child.meta.match(TabRegex);
if (isSwitcher && start === -1) {
start = i;
}
// if switcher code blocks terminated, convert them to tabs
const isLast = i === node.children.length - 1;
if (start !== -1 && (isLast || !isSwitcher)) {
const end = isSwitcher ? i + 1 : i;
const targets = node.children.slice(start, end) as Code[];
node.children.splice(
start,
end - start,
options.parseMdx ? toTabMdx(this, targets) : toTab(targets),
);
if (isLast) break;
i = start + 1;
start = -1;
} else {
i++;
}
}
});
};
}
function processTabValue(nodes: Code[]) {
return nodes.map((node, i) => {
let title = `Tab ${i + 1}`;
node.meta = node.meta?.replace(TabRegex, (_, value) => {
@@ -15,17 +68,10 @@ function toTab(nodes: Code[]) {
return title;
});
}
const itemsArr = {
type: 'ExpressionStatement',
expression: {
type: 'ArrayExpression',
elements: names.map((name) => ({
type: 'Literal',
value: name,
})),
},
};
function toTab(nodes: Code[]): MdxJsxFlowElement {
const names = processTabValue(nodes);
return {
type: 'mdxJsxFlowElement',
@@ -36,12 +82,24 @@ function toTab(nodes: Code[]) {
name: 'items',
value: {
type: 'mdxJsxAttributeValueExpression',
value: names.join(', '),
data: {
estree: {
type: 'Program',
sourceType: 'module',
comments: [],
body: [itemsArr],
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'ArrayExpression',
elements: names.map((name) => ({
type: 'Literal',
value: name,
})),
},
},
],
},
},
},
@@ -64,42 +122,66 @@ function toTab(nodes: Code[]) {
};
}
export function remarkCodeTab(): Transformer<Root, Root> {
return (tree) => {
visit(tree, (node) => {
if (!('children' in node)) return;
if (node.type === 'mdxJsxFlowElement' && node.name === 'Tabs') return;
let start = -1;
let i = 0;
function toTabMdx(processor: Processor, nodes: Code[]): MdxJsxFlowElement {
const names = processTabValue(nodes);
function inline(node: Root) {
if (node.type === 'root') {
node.children = node.children.flatMap((child) => {
if (child.type === 'paragraph') return child.children;
while (i < node.children.length) {
const child = node.children[i];
const isSwitcher =
child.type === 'code' && child.meta && child.meta.match(TabRegex);
return child;
});
}
if (isSwitcher && start === -1) {
start = i;
}
return node;
}
// if switcher code blocks terminated, convert them to tabs
const isLast = i === node.children.length - 1;
if (start !== -1 && (isLast || !isSwitcher)) {
const end = isSwitcher ? i + 1 : i;
const targets = node.children.slice(start, end);
node.children.splice(
start,
end - start,
toTab(targets as Code[]) as RootContent,
);
if (isLast) break;
i = start;
start = -1;
}
i++;
}
});
return {
type: 'mdxJsxFlowElement',
name: 'Tabs',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'defaultValue',
value: names[0],
},
],
children: [
{
type: 'mdxJsxFlowElement',
name: 'TabsList',
attributes: [],
children: names.map((name) => ({
type: 'mdxJsxFlowElement',
name: 'TabsTrigger',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'value',
value: name,
},
],
children: [
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed
inline(processor.parse(name) as Root) as any,
],
})),
},
...nodes.map(
(node, i) =>
({
type: 'mdxJsxFlowElement',
name: 'TabsContent',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'value',
value: names[i],
},
],
children: [node],
}) satisfies MdxJsxFlowElement,
),
],
};
}
+22 -20
View File
@@ -1,9 +1,3 @@
import type {
RehypeCodeOptions,
RehypeTocOptions,
RemarkHeadingOptions,
RemarkImageOptions,
} from 'fumadocs-core/mdx-plugins';
import * as Plugins from 'fumadocs-core/mdx-plugins';
import { type CompileOptions, createProcessor } from '@mdx-js/mdx';
import type { MDXComponents } from 'mdx/types';
@@ -20,10 +14,10 @@ export type MDXOptions = Omit<
remarkPlugins?: ResolvePlugins;
rehypePlugins?: ResolvePlugins;
remarkHeadingOptions?: RemarkHeadingOptions | false;
rehypeCodeOptions?: RehypeCodeOptions | false;
rehypeTocOptions?: RehypeTocOptions | false;
remarkCodeTabOptions?: false;
remarkHeadingOptions?: Plugins.RemarkHeadingOptions | false;
rehypeCodeOptions?: Plugins.RehypeCodeOptions | false;
rehypeTocOptions?: Plugins.RehypeTocOptions | false;
remarkCodeTabOptions?: Plugins.RemarkCodeTabOptions | false;
/**
* The directory to find image sizes
@@ -33,7 +27,7 @@ export type MDXOptions = Omit<
*/
imageDir?: string;
remarkImageOptions?: RemarkImageOptions | false;
remarkImageOptions?: Plugins.RemarkImageOptions | false;
};
export interface CompileMDXOptions {
@@ -64,13 +58,19 @@ export interface CompileMDXResult<TFrontmatter = Record<string, unknown>> {
}
export function createCompiler(mdxOptions?: MDXOptions) {
let format = mdxOptions?.format;
if (!format || format === 'detect') format = 'mdx';
let instance: ReturnType<typeof createProcessor> | undefined;
const processor = createProcessor({
...getCompileOptions(mdxOptions),
format,
});
function getProcessor() {
if (instance) return instance;
let format = mdxOptions?.format;
if (!format || format === 'detect') format = 'mdx';
return (instance = createProcessor({
...getCompileOptions(mdxOptions),
format,
}));
}
return {
async render(
@@ -90,7 +90,7 @@ export function createCompiler(mdxOptions?: MDXOptions) {
* Compile VFile
*/
async compileFile(from: Compatible): Promise<VFile> {
return processor.process(from);
return getProcessor().process(from);
},
async compile<Frontmatter extends object = Record<string, unknown>>(
options: Omit<CompileMDXOptions, 'mdxOptions'>,
@@ -173,10 +173,12 @@ function getCompileOptions({
useImport: false,
publicDir: imageDir,
...remarkImageOptions,
} satisfies RemarkImageOptions,
} satisfies Plugins.RemarkImageOptions,
]
: null,
remarkCodeTab && remarkCodeTabOptions !== false ? remarkCodeTab : null,
remarkCodeTab && remarkCodeTabOptions !== false
? [remarkCodeTab, remarkCodeTabOptions]
: null,
...v,
],
options.remarkPlugins,
+5 -3
View File
@@ -20,7 +20,7 @@ export type DefaultMDXOptions = Omit<
remarkStructureOptions?: plugins.StructureOptions | false;
remarkHeadingOptions?: plugins.RemarkHeadingOptions;
remarkImageOptions?: plugins.RemarkImageOptions | false;
remarkCodeTabOptions?: false;
remarkCodeTabOptions?: plugins.RemarkCodeTabOptions | false;
rehypeCodeOptions?: plugins.RehypeCodeOptions | false;
};
@@ -68,8 +68,10 @@ export function getDefaultMDXOptions({
remarkImageOptions !== false && [plugins.remarkImage, remarkImageOptions],
// Fumadocs 14 compatibility
'remarkCodeTab' in plugins &&
remarkCodeTabOptions !== false &&
plugins.remarkCodeTab,
remarkCodeTabOptions !== false && [
plugins.remarkCodeTab,
remarkCodeTabOptions,
],
...v,
remarkStructureOptions !== false && [
plugins.remarkStructure,
+6 -3
View File
@@ -86,6 +86,9 @@ importers:
'@shikijs/rehype':
specifier: ^3.4.2
version: 3.4.2
'@shikijs/transformers':
specifier: ^3.4.2
version: 3.4.2
ai:
specifier: ^4.3.15
version: 4.3.15([email protected])([email protected])
@@ -276,7 +279,7 @@ importers:
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected])([email protected]([email protected]))([email protected])
'@content-collections/next':
specifier: ^0.2.6
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
'@types/mdx':
specifier: ^2.0.13
version: 2.0.13
@@ -871,7 +874,7 @@ importers:
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected])([email protected]([email protected]))([email protected])
'@content-collections/next':
specifier: ^0.2.6
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
'@react-router/dev':
specifier: ^7.6.0
version: 7.6.0(@react-router/[email protected]([email protected]([email protected]([email protected]))([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))([email protected])
@@ -10919,7 +10922,7 @@ snapshots:
- acorn
- supports-color
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))':
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))':
dependencies:
'@content-collections/core': 0.8.2([email protected])
'@content-collections/integrations': 0.2.1(@content-collections/[email protected]([email protected]))