Revisiting Obsidian as a CMS
2 min read
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.