Integrating Obsidian with Astro the right way

July 18, 2025 in astro and obsidian.

How to do it properly?

Just use the built-in method.

import type { Loader } from 'astro/loaders';
import matter from 'gray-matter';
import path from "path";

// Types
export interface StringDictionary {
    [key: string]: string;
}

type Doc = {
    _id: string;
    children?: string[];
    data?: string[];
};

type Row = {
    id: string;
    key: string;
    value: any;
    doc: Doc;
};

// Helpers
function getAuthHeader(username: string, password: string) {
    const credentials = btoa(`${username}:${password}`);
    return {
        'Content-Type': 'application/json',
        Authorization: `Basic ${credentials}`,
    };
}

async function fetchDocs(hostname: string, db: string, headers: any, childIds: string[]) {
    const res = await fetch(`https://${hostname}/${db}/_all_docs?include_docs=true`, {
        method: 'POST',
        headers,
        body: JSON.stringify({ keys: childIds }),
    });

    const { rows }: { rows: Row[] } = await res.json();
    return Object.fromEntries(rows.map(row => [row.id, row.doc]));
}

export async function loadFromCouchDB(
    hostname: string,
    db: string,
    username: string,
    password: string
) {
    const headers = getAuthHeader(username, password);

    // Fetch top-level docs
    const mainRes = await fetch(`https://${hostname}/${db}/_find`, {
        method: 'POST',
        headers,
        body: JSON.stringify({
            selector: {
                _id: { $gte: 'website/' },
                deleted: { $exists: false },
            },
        }),
    });

    const { docs }: { docs: Doc[] } = await mainRes.json();
    const childIds = docs.flatMap(doc => doc.children || []);
    const childMap = await fetchDocs(hostname, db, headers, childIds);

    return docs.map(doc => {
        const childContent = (doc.children || [])
            .map(id => childMap[id]?.data)
            .filter(Boolean)
            .flat()
            .join('');

        return {
            ...doc,
            body: childContent,
        };
    });
}

// Astro loader
export function dbLoader(settings: StringDictionary): Loader {
    return {
        name: 'couchdb_fetcher',
        async load({ renderMarkdown, store, generateDigest, parseData }) {
            store.clear();
            const entries = await loadFromCouchDB(
                settings.hostname,
                settings.name,
                settings.username,
                settings.password
            );

            for (const entry of entries) {
                const digest = generateDigest(entry);
                const { data, content } = matter(entry.body);
                const rendered = await renderMarkdown(content);
                const id = path.parse(entry._id).name;

                const entry_data = await parseData(
                    {
                        id: id,
                        data: data,
                    }
                );

                store.set({
                    id,
                    data: entry_data,
                    body: content,
                    digest,
                    rendered,
                });
            }
        },
    };
}

Then loading it can be done like this:

// Import the glob loader
import { glob, file } from 'astro/loaders'
import { dbLoader } from './utils/custom_loader.ts'

import { z, defineCollection } from 'astro:content'

const posts = defineCollection({
    loader: dbLoader({
        hostname: process.env.COUCHDB_HOST!,
        name: process.env.DB_NAME!,
        username: process.env.COUCHDB_USER!,
        password: process.env.COUCHDB_PASSWORD!,
    }),
    schema: z.object({
        title: z.string(),
        description: z.string(),
        tags: z.array(z.string()),
        date: z.date(),
    }),
});


export const collections = { posts }

Finally, it can be used as:

---
import { getCollection, render } from 'astro:content'
const posts = await getCollection('posts')
const {Content} = render(posts[0])
---
<Content/>