---
title: "Obsidian Vault Integration"
description: "Obsidian Vault Integration empowers Fumadocs to ingest, parse, and transform raw Obsidian vaults directly into standards-compliant MDX content, assets, and structured metadata. By bridging propriet..."
last_updated: "2026-10-12T07:53:59.831064+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/content-sources/obsidian-vault-integration"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/obsidian/src/mdx/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/mdx/index.ts)
- [packages/obsidian/src/convert.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/convert.ts)
- [packages/obsidian/src/remark/remark-convert.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-convert.ts)
- [packages/obsidian/src/remark/remark-wikilinks.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-wikilinks.ts)
- [packages/obsidian/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/index.ts)
- [packages/obsidian/src/build-storage.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/build-storage.ts)
- [packages/mdx/src/vite/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/vite/index.ts)
- [packages/python/src/convert.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/src/convert.ts)
- [packages/obsidian/src/remark/remark-obsidian-comment.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-obsidian-comment.ts)
- [packages/mdx/src/loaders/mdx/remark-include.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/loaders/mdx/remark-include.ts)
- [packages/preview/src/pages/...slugs.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/content-collections/src/configuration.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content-collections/src/configuration.ts)
- [packages/local-md/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts)
- [packages/core/src/mdx-plugins/remark-llms.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-llms.ts)
- [packages/mdx/src/loaders/mdx/remark-postprocess.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/loaders/mdx/remark-postprocess.ts)
- [packages/asyncapi/src/utils/pages/to-text.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/to-text.ts)
- [packages/core/src/mdx-plugins/remark-structure.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-structure.ts)
- [packages/obsidian/src/remark/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/index.ts)
- [packages/obsidian/src/remark/remark-block-id.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-block-id.ts)
- [packages/core/src/mdx-plugins/rehype-toc.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-toc.ts)
- [packages/sanity/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/index.ts)
- [packages/preview/src/lib/source/storage.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/storage.ts)
- [packages/core/src/mdx-plugins/remark-mdx-files.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-mdx-files.ts)
- [packages/openapi/src/utils/pages/to-text.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-text.ts)
- [packages/twoslash/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts)
- [packages/basehub/src/toc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/basehub/src/toc.tsx)
- [packages/core/src/mdx-plugins/remark-steps.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-steps.ts)
- [packages/preview/src/components/markdown.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/markdown.tsx)
- [packages/core/src/source/llms.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/llms.ts)
- [packages/content/src/runtime.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content/src/runtime.ts)
</details>

## Overview

Obsidian Vault Integration empowers Fumadocs to ingest, parse, and transform raw Obsidian vaults directly into standards-compliant MDX content, assets, and structured metadata. By bridging proprietary Obsidian markdown features with modern documentation tooling, it resolves critical incompatibilities surrounding internal referencing and block formatting. The integration embodies design decisions focused on unified processor pipelines, virtual vault storage, and robust AST transformations that make interconnected knowledge bases fully compatible with [Source Loader](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/source-loader) and [Local Markdown Runtime](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/content-sources/local-markdown-runtime).

Sources: [packages/obsidian/src/convert.ts:57-118](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/convert.ts#L57-L118), [packages/obsidian/src/mdx/index.ts:16-44](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/mdx/index.ts#L16-L44), [packages/obsidian/src/build-storage.ts:73-138](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/build-storage.ts#L73-L138)

## Public vault processing interface

### Public API functions and configuration options

The public interface for the Obsidian integration centers around the `fromVault` function and `writeVaultFiles`, supported by structured configuration types. The `Options` interface extends `ReadFilesOptions` by incorporating optional `convert` properties via `ConvertOptions` and output parameters via `WriteFilesOptions`. Executing `fromVault` invokes `readVaultFiles` with the provided configuration, passes the resulting files to `convertVaultFiles`, and finally writes out the serialized artifacts using `writeVaultFiles`.

```typescript
export interface Options extends ReadFilesOptions {
  convert?: ConvertOptions;
  out?: WriteFilesOptions;
}

export async function fromVault(options: Options) {
  const files = await readVaultFiles(options);
  const converted = await convertVaultFiles(files, options.convert);

  await writeVaultFiles(converted, options.out);
}
```
Sources: [packages/obsidian/src/index.ts:10-20](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/index.ts#L10-L20)

| Option Property | Type | Default Value | Description |
| :--- | :--- | :--- | :--- |
| `publicDir` | `string` | `'public'` | Target directory for asset outputs mapped from media files. |
| `contentDir` | `string` | `'content/docs/vault'` | Target directory for parsed content and data files. |
| `remarkPlugins` | `PluggableList` | `[]` | Extra remark plugins added to the parsing processor pipeline. |
| `transformFrontmatter` | `function` | `undefined` | Callback hook to modify generated frontmatter data per file. |

Sources: [packages/obsidian/src/index.ts:28-28](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/index.ts#L28-L28), [packages/obsidian/src/convert.ts:34-49](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/convert.ts#L34-L49)

> [!NOTE]
> The `convertVaultFiles` processor integrates `remarkParse`, `remarkGfm`, `remarkMath`, user-supplied `remarkPlugins`, and internal storage plugins before initializing stringification via `remarkMdx` and `remarkStringify`.
> Sources: [packages/obsidian/src/convert.ts:65-71](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/convert.ts#L65-L71)

### Output serialization and directory mapping

The file writing mechanism handles discrete output categories produced during vault conversion. The `writeVaultFiles` function accepts an array of `OutputFile` objects alongside `WriteFilesOptions`, mapping each file type to a designated base directory.

```typescript
export async function writeVaultFiles(files: OutputFile[], options: WriteFilesOptions = {}) {
  const { publicDir = 'public', contentDir = 'content/docs/vault' } = options;

  const map = {
    asset: publicDir,
    content: contentDir,
    data: contentDir,
    custom: '',
  } as const;

  await Promise.all(
    files.map(async (file) => {
      const mappedPath = path.join(map[file.type], file.path);

      await fs.mkdir(path.dirname(mappedPath), { recursive: true });
      await fs.writeFile(mappedPath, file.content);
    }),
  );
}
```
Sources: [packages/obsidian/src/index.ts:27-44](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/index.ts#L27-L44)

> [!WARNING]
> When `file.type` is set to `'custom'`, `map['custom']` resolves to an empty string, causing the target file path to be written directly relative to the current working directory unless absolute path resolution is managed upstream.
> Sources: [packages/obsidian/src/index.ts:34-39](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/index.ts#L34-L39)

## Vault storage and path normalization

### File discovery and path normalization

The storage engine processes raw vault inputs through path normalization and classification. The `buildStorage` function accepts an array of `VaultFile` objects and optional configuration, iterating through each file to normalize paths, resolve output destinations via rename presets or custom functions, and categorize files into distinct formats (`content`, `data`, or `media`).

```typescript
export function buildStorage(
  rawFiles: VaultFile[],
  options: VaultStorageOptions = {},
): VaultStorage {
  const {
    url = (file) => {
      const segs = normalize(file)
        .split('/')
        .filter((v) => v.length > 0);
      return `/${segs.join('/')}`;
    },
    outputPath: outputPathOption = 'simple',
    enforceMdx = true,
  } = options;
  const getOutputPath =
    typeof outputPathOption === 'function'
      ? outputPathOption
      : createRenameOutput(outputPathOption);

  const storage = new Map<string, ParsedFile>();

  for (const rawFile of rawFiles) {
    const normalizedPath = normalize(rawFile.path);
    let outPath = getOutputPath(normalizedPath, rawFile);
    let parsed: ParsedFile;

    const ext = path.extname(normalizedPath);

    if (['.json', '.yaml', '.yml', '.toml'].includes(ext)) {
      parsed = {
        format: 'data',
        path: normalizedPath,
        _raw: rawFile._raw,
        outPath,
        content: rawFile.content,
      };
    } else if (['.md', '.mdx'].includes(ext)) {
      const { data, content } = frontmatter(String(rawFile.content));
      if (enforceMdx) {
        outPath = outPath.slice(0, -path.extname(outPath).length) + '.mdx';
      }

      parsed = {
        format: 'content',
        _raw: rawFile._raw,
        path: normalizedPath,
        outPath,
        frontmatter: frontmatterSchema.parse(data),
        content,
      };
    } else {
      parsed = {
        format: 'media',
        path: normalizedPath,
        _raw: rawFile._raw,
        outPath,
        content: rawFile.content,
        url: url(outPath, rawFile),
      };
    }

    storage.set(normalizedPath, parsed);
  }

  return { files: storage };
}
```
Sources: [packages/obsidian/src/build-storage.ts:73-138](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/build-storage.ts#L73-L138)

Path normalization is handled by the `normalize` helper, which enforces directory traversal security by throwing an error if a path points outside the vault. It normalizes directory separators using `slash` and strips leading `./` relative prefixes.

```typescript
function normalize(filePath: string): string {
  filePath = slash(filePath);
  if (filePath.startsWith('../')) throw new Error(`${filePath} points outside of vault folder`);

  return filePath.startsWith('./') ? filePath.slice(2) : filePath;
}
```
Sources: [packages/obsidian/src/build-storage.ts:160-165](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/build-storage.ts#L160-L165)

> [!CAUTION]
> Any file path beginning with `../` triggers an immediate error during path normalization, preventing unauthorized traversal outside the designated vault folder boundary.
> Sources: [packages/obsidian/src/build-storage.ts:162-162](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/build-storage.ts#L162-L162)

### Call-chain execution walkthrough

The initialization sequence for processing vault files through `remarkObsidian` executes the following call chain:

1. `remarkObsidian` — Receives `files` and initializes `buildStorage` with custom options.
2. `buildStorage` — Iterates over raw files and invokes path handling.
3. `normalize` — Sanitizes path separators and checks for directory traversal violations.

```mermaid
sequenceDiagram
    participant remarkObsidian as remarkObsidian (mdx/index.ts)
    participant buildStorage as buildStorage (build-storage.ts)
    participant normalize as normalize (build-storage.ts)

    remarkObsidian->>buildStorage: buildStorage(files, options)
    loop For each rawFile
        buildStorage->>normalize: normalize(rawFile.path)
        normalize-->>buildStorage: normalizedPath
    end
```
Sources: [packages/obsidian/src/mdx/index.ts:16-22](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/mdx/index.ts#L16-L22), [packages/obsidian/src/build-storage.ts:73-95](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/build-storage.ts#L73-L95), [packages/obsidian/src/build-storage.ts:160-165](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/build-storage.ts#L160-L165)

### Storage configuration options and parsed file formats

| Option Parameter | Type | Default Value | Description |
| :--- | :--- | :--- | :--- |
| `outputPath` | `RenameOutputFn \| RenameOutputPreset` | `'simple'` | Strategy or function used to transform output file paths (`'simple'` or `'ignore'`). |
| `url` | `function` | `(file) => /{normalized}` | Generates URLs for media files based on output paths. |
| `enforceMdx` | `boolean` | `true` | Forces all Markdown documents to use the `.mdx` extension. |

Sources: [packages/obsidian/src/build-storage.ts:11-30](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/build-storage.ts#L11-L30)

> [!NOTE]
> When `enforceMdx` is active, content files ending in `.md` have their output extension rewritten to `.mdx` after frontmatter extraction.
> Sources: [packages/obsidian/src/build-storage.ts:111-113](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/build-storage.ts#L111-L113)

## Wikilink resolution and ref linking

### AST traversal and text replacement

The `remarkWikilinks` transformer uses `unist-util-visit` to traverse the Markdown Abstract Syntax Tree (AST), targeting `paragraph` nodes to locate and replace Obsidian wikilink syntax. When processing a paragraph, it recursively visits child elements to inspect text nodes while maintaining parent stack references.

```typescript
visit(tree, 'paragraph', (node, index, parent) => {
  if (typeof index !== 'number' || !parent) return;

  const replaceParagraph: RootContent[] = [node];
  let parents: Parent[] = [];

  function traverse(cur: RootContent) {
    if (cur.type === 'text') {
      const output = resolveParagraphText(cur.value, sourceFile, resolver);
      const idx = parents[0].children.indexOf(cur);
      let insertIdx = idx;

      parents[0].children.splice(insertIdx, 1);
      for (const item of output) {
        if (item.inParagraph) {
          parents[0].children.splice(insertIdx, 0, ...item.children);
          insertIdx += item.children.length;
        } else {
          replaceParagraph.push(...item.children);
          // clone parents
          parents = structuredClone(parents);
          // removed inserted elements
          parents[0].children.splice(idx, insertIdx - idx);
          insertIdx = idx;
          replaceParagraph.push(parents.at(-1) as Paragraph);
        }
      }

      return;
    }

    if ('children' in cur && Array.isArray(cur.children)) {
      parents.unshift(cur);
      for (const child of cur.children) {
        traverse(child);
      }
      parents.shift();
    }
  }

  traverse(node);
  parent.children.splice(index, 1, ...replaceParagraph);
});
```
Sources: [packages/obsidian/src/remark/remark-wikilinks.ts:28-70](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-wikilinks.ts#L28-L70)

The text content of paragraphs is parsed using regular expressions designed to capture standard wikilinks and embedded assets. The `RegexWikilink` pattern matches both standard and embedded bracketed references across text strings.

```typescript
const RegexWikilink = /!?\[\[(?<content>([^\]]|\\])+)]]/g;
const RegexContent =
  /^(?<name>(?:\\#|\\\||[^#|])*)(?:#(?<heading>(?:\\\||[^|])+))?(?:\|(?<alias>.+))?$/;
```
Sources: [packages/obsidian/src/remark/remark-wikilinks.ts:15-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-wikilinks.ts#L15-L17)

> [!WARNING]
> Matching unescaped pipes or hash characters inside wikilink syntax incorrectly splits target paths, headings, and aliases. The `RegexContent` parser strictly separates name, heading, and alias parts using explicit group boundaries.
> Sources: [packages/obsidian/src/remark/remark-wikilinks.ts:16-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-wikilinks.ts#L16-L17)

### Wikilink parsing and resolution rules

The `resolveWikilink` function processes extracted wikilink components based on whether the reference is an embedded asset (prefixed with `!`) or a standard hyperlink. It handles heading-only references, media files, and link aliases.

```typescript
function resolveWikilink(
  isEmbed: boolean,
  content: string,
  sourceFile: ParsedFile,
  resolver: VaultResolver,
): RootContent | undefined {
  const match = RegexContent.exec(content);
  if (!match?.groups) return;

  const { name, heading, alias } = match.groups as {
    name: string;
    heading?: string;
    alias?: string;
  };

  const isHeadingOnly = name.length === 0 && heading;

  if (isEmbed) {
    const ref = isHeadingOnly ? sourceFile : resolver.resolveAny(name, sourceFile.path);

    if (!ref || ref.format === 'data') {
      console.warn(`failed to resolve ${name} wikilink`);
      return;
    }

    if (ref.format === 'content') {
      console.warn(
        'some features of embed content blocks are not supported yet, use at your own risk.',
      );

      return {
        type: 'mdxJsxFlowElement',
        name: 'include',
        attributes: [],
        children: [
          {
            type: 'paragraph',
            children: [
              {
                type: 'text',
                value: getFileHref(ref, sourceFile, heading),
              },
            ],
          },
        ],
      } satisfies MdxJsxFlowElement;
    }

    if (alias) console.warn('we do not support specifying image size like `![[image.png|300]].');

    return {
      type: 'image',
      url: ref.url ?? getFileHref(ref, sourceFile, heading),
      alt: name,
    };
  }

  let url: string;

  if (isHeadingOnly) {
    url = `#${getHeadingHash(heading)}`;
  } else {
    const ref = resolver.resolveAny(name, sourceFile.path);

    if (!ref) {
      console.warn(`failed to resolve ${name} wikilink`);
      return;
    }

    url = getFileHref(ref, sourceFile, heading);
  }

  return {
    type: 'link',
    url,
    data: {
      isWikiLink: true,
    },
    children: [
      {
```
Sources: [packages/obsidian/src/remark/remark-wikilinks.ts:139-224](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-wikilinks.ts#L139-L224)

| Link Property / Parameter | Regular Expression Group | Behavior & Fallback |
| :--- | :--- | :--- |
| `name` | `name` | Resolves target file via `resolver.resolveAny`. Defaults to `sourceFile` if heading-only. |
| `heading` | `heading` | Appends hash anchor via `getHeadingHash` or `getFileHref`. |
| `alias` | `alias` | Sets display text for links (`alias ?? content`). Warns if image size syntax is specified. |

Sources: [packages/obsidian/src/remark/remark-wikilinks.ts:145-152](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-wikilinks.ts#L145-L152), [packages/obsidian/src/remark/remark-wikilinks.ts:187-221](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-wikilinks.ts#L187-L221)

> [!NOTE]
> When resolving an embed content block (`ref.format === 'content'`), the transformer generates an MDX JSX flow element with the name `'include'`, wrapping the file reference in a paragraph.
> Sources: [packages/obsidian/src/remark/remark-wikilinks.ts:164-184](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-wikilinks.ts#L164-L184)

## Callout and block reference translation

### Obsidian callout transformation

The `remarkConvert` transformer checks blockquote nodes to translate Obsidian-style callout blocks into structured components. It extracts the callout type and optional collapsible indicator using the `RegexCalloutHead` pattern.

```typescript
const RegexCalloutHead = /^\[!(?<type>\w+)](?<collapsible>\+)?/;
```
Sources: [packages/obsidian/src/remark/remark-convert.ts:10-10](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-convert.ts#L10-L10)

The `resolveCallout` function inspects the first child paragraph of a blockquote for callout syntax. If matched, it strips the prefix, separates the title from the body content across newlines, and constructs a callout node via `createCallout`.

```typescript
function resolveCallout(node: Blockquote) {
  const head = node.children[0];
  if (!head || head.type !== 'paragraph') return;
  const textNode = head.children[0];
  if (!textNode || textNode.type !== 'text') return;

  const match = RegexCalloutHead.exec(textNode.value);
  if (!match) return;

  textNode.value = textNode.value.slice(match[0].length).trimStart();

  const [title, rest] = separate(/\r?\n/, head.children) ?? [head.children];
  const body = node.children.slice(1);
  if (rest) {
    body.unshift({
      type: 'paragraph',
      children: rest as PhrasingContent[],
    });
  }

  return createCallout(
    match[1],
    [
      {
        type: 'paragraph',
        children: title as PhrasingContent[],
      },
    ],
    body,
  );
}
```
Sources: [packages/obsidian/src/remark/remark-convert.ts:16-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-convert.ts#L11-L46)

> [!WARNING]
> Blockquotes lacking a paragraph as their initial child or missing the `[!type]` header format are ignored by `resolveCallout` and remain standard blockquotes.
> Sources: [packages/obsidian/src/remark/remark-convert.ts:17-23](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-convert.ts#L17-L23)

### Block ID parsing and section wrapping

The `remarkBlockId` plugin traverses paragraph nodes to extract Obsidian block references matching the trailing caret identifier pattern.

```typescript
const Regex = /(?<!\\)\^(?<block_id>\w+)$/m;
```
Sources: [packages/obsidian/src/remark/remark-block-id.ts:7-7](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-block-id.ts#L7-L7)

It visits child text nodes in reverse order, searching for block identifiers. When a valid ID is isolated, the plugin trims the trailing identifier string from the text node value and wraps the paragraph inside an MDX JSX flow element representing a `section` tag with an `id` attribute.

```typescript
visit(tree, 'paragraph', (node) => {
  let id: string | undefined;

  visit(
    node,
    ['link', 'text', 'mdxJsxFlowElement'],
    (textNode) => {
      if (textNode.type !== 'text') return 'skip';

      const value = textNode.value;
      const match = Regex.exec(value);
      // if last text node isn't a block id, skip
      if (!match) return false;

      id = match[1];
      textNode.value =
        value.slice(0, match.index).trimEnd() + value.slice(match.index + match[0].length);
      return false;
    },
    true,
  );

  if (id) {
    replace(node, {
      type: 'mdxJsxFlowElement',
      name: 'section',
      attributes: [
        {
          type: 'mdxJsxAttribute',
          name: 'id',
          value: `^${id}`,
        },
      ],
      children: [
        {
          ...node,
        },
      ],
    } satisfies MdxJsxFlowElement);
  }

  return 'skip';
});
```
Sources: [packages/obsidian/src/remark/remark-block-id.ts:11-53](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-block-id.ts#L11-L53)

> [!TIP]
> Escaped carets (`\^`) are excluded by negative lookbehind in the regex, ensuring literal carets inside paragraph text do not inadvertently trigger block ID extraction.
> Sources: [packages/obsidian/src/remark/remark-block-id.ts:7-7](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-block-id.ts#L7-L7)

## Comment stripping and remark pipeline

### Comment stripping mechanism

The `remarkObsidianComment` function implements a unified transformer that purges Obsidian comment syntax from the Abstract Syntax Tree. It uses a negative lookbehind regular expression delimiter `/(?<!\\)%%/` to identify the opening and closing boundaries of comment blocks while preserving escaped percent sequences.
Sources: [packages/obsidian/src/remark/remark-obsidian-comment.ts:5-7](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-obsidian-comment.ts#L5-L7)

The internal `removeComment` helper function recursively processes arrays of root content nodes by calling `separate` with the delimiter. If both a starting and ending delimiter pair are successfully found, the content preceding the opening comment and any remaining nodes following the closing comment are spliced together, discarding all nested nodes residing within the comment boundaries.
Sources: [packages/obsidian/src/remark/remark-obsidian-comment.ts:8-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-obsidian-comment.ts#L8-L17)

> [!CAUTION]
> If an opening comment delimiter lacks a corresponding closing delimiter in the node stream, `removeComment` halts processing and returns the original node array unmodified, preventing partial document truncation.
> Sources: [packages/obsidian/src/remark/remark-obsidian-comment.ts:13-14](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/remark-obsidian-comment.ts#L13-L14)

### Remark transformation plugin suite

The `getRemarkPlugins` function composes the complete array of unified plugins used to transform Obsidian markdown vaults, accepting storage and resolver arguments to build the full processing pipeline.
Sources: [packages/obsidian/src/remark/index.ts:9-9](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/index.ts#L9-L9)

| Plugin Function | Required Options | Processing Role |
| :--- | :--- | :--- |
| `remarkWikilinks` | `{ resolver, storage }` | Resolves internal wikilinks and embedded content references. |
| `remarkConvert` | None | Translates Obsidian callout syntax within blockquotes. |
| `remarkObsidianComment` | None | Strips `%%` comment blocks from document nodes. |
| `remarkBlockId` | None | Extracts trailing caret block identifiers and wraps sections. |

Sources: [packages/obsidian/src/remark/index.ts:9-15](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/remark/index.ts#L9-L15)

## Related

- [Source Loader](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/source-loader)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/llms.txt) for all pages in this wiki.
