Revisiting Obsidian as a CMS
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.
Discussion in the ATmosphere