home menu

Revisiting Obsidian as a CMS

A home-built publishing system using Obsidian for writing, GitHub for storage, and Next.

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.

collection
posts
rkey
1699332127006-revisiting-obsidian-as-a-cms
record cid
bafkreifbnqsr2hqdnyiahcsgerqxlbn5pavcudnlvzq4lzvuxffjgba66u
record
https://content.farfield.systems/api/entries/1699332127006-revisiting-obsidian-as-a-cms
created
updated