homemenu

Revisiting Obsidian as a CMS

On this page

I gave the Notion API a real shot and came back. The homebrew Obsidian pipeline does what I need: write in Obsidian, sync to GitHub, render with Next.js.

The whole thing is a few JavaScript functions on top of GitHub's GraphQL API.

fetchFromGitHubGraphQL is the wrapper:

async function fetchFromGitHubGraphQL(query: string, variables: any) {
  const token = process.env.NEXT_PUBLIC_GITHUB;
  const response = await fetch("https://api.github.com/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ query, variables }),
  });

  if (!response.ok) {
    console.error("HTTP Error:", response.status);
    return response;
  }

  return response.json();
}

Obsidian syncs on save, so the repo always has the latest markdown.

parseMarkdownContent runs each file through gray-matter and pulls out the frontmatter and body:

function parseMarkdownContent(content: string) {
  const { data, content: body } = matter(content);
  return {
    slug: data.id,
    name: data.name,
    created: data.created ? new Date(data.created).getTime() : null,
    updated: data.updated ? new Date(data.updated).getTime() : null,
    body: body,
    public: data.public,
    tags: data.tags,
    address: data.address,
  };
}

getObsidianEntries lists everything in the Content folder:

export async function getObsidianEntries() {
  const {
    data: {
      repository: {
        object: { entries },
      },
    },
  } = await fetchFromGitHubGraphQL(
    `
      query fetchEntries($owner: String!, $name: String!) {
        repository(owner: $owner, name: $name) {
          object(expression: "HEAD:Content/") {
            ... on Tree {
              entries {
                name
                object {
                  ... on Blob {
                    text
                  }
                }
              }
            }
          }
        }
      }
    `,
    {
      owner: `GITHUB_USERNAME`,
      name: `REPO_NAME`,
      first: 100,
    }
  );

  if (entries.errors) {
    console.error("GraphQL Error:", entries.errors);
    return [];
  }

  if (!entries) {
    console.error("No data returned from the GraphQL query.");
    return [];
  }

  return Promise.all(
    entries.map((entry: { object: { text: any } }) => {
      const content = entry.object.text;
      return parseMarkdownContent(content);
    })
  );
}

Errors get logged and the function returns an empty array. Otherwise each entry gets parsed and returned.

getObsidianEntry pulls a single file by slug:

export async function getObsidianEntry(slug: string) {
  const { data } = await fetchFromGitHubGraphQL(
    `
      query fetchSingleEntry($owner: String!, $name: String!, $entryName: String!) {
        repository(owner: $owner, name: $name) {
          object(expression: $entryName) {
            ... on Blob {
              text
            }
          }
        }
      }
    `,
    {
      owner: `GITHUB_USERNAME`,
      name: `REPO_NAME`,
      entryName: `HEAD:Content/${slug}.md`,
    }
  );

  const text = data.repository.object.text;
  return parseMarkdownContent(text);
}

File names

Every file is a millisecond timestamp, Zettelkasten-style:

1671418753342.md

Unique, chronologically sorted, and the name itself is the slug.

The whole pipeline

Write in Obsidian. Obsidian syncs to GitHub. Next.js queries GitHub's GraphQL API and renders. That's it.