{"collection":"posts","slug":"1699332127006-revisiting-obsidian-as-a-cms","cid":"bafkreieo4tipxhq3v4bjtzl4ptldoulcnvhltuafv3ydppkr7iqvfmhe6q","title":"Revisiting Obsidian as a CMS","excerpt":"A home-built publishing system using Obsidian for writing, GitHub for storage, and Next.js for rendering.","body":"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.\n\nThe whole thing is a few JavaScript functions on top of GitHub's GraphQL API.\n\n`fetchFromGitHubGraphQL` is the wrapper:\n\n```tsx\nasync function fetchFromGitHubGraphQL(query: string, variables: any) {\n  const token = process.env.NEXT_PUBLIC_GITHUB;\n  const response = await fetch(\"https://api.github.com/graphql\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${token}`,\n    },\n    body: JSON.stringify({ query, variables }),\n  });\n\n  if (!response.ok) {\n    console.error(\"HTTP Error:\", response.status);\n    return response;\n  }\n\n  return response.json();\n}\n```\n\nObsidian syncs on save, so the repo always has the latest markdown.\n\n`parseMarkdownContent` runs each file through `gray-matter` and pulls out the frontmatter and body:\n\n```tsx\nfunction parseMarkdownContent(content: string) {\n  const { data, content: body } = matter(content);\n  return {\n    slug: data.id,\n    name: data.name,\n    created: data.created ? new Date(data.created).getTime() : null,\n    updated: data.updated ? new Date(data.updated).getTime() : null,\n    body: body,\n    public: data.public,\n    tags: data.tags,\n    address: data.address,\n  };\n}\n```\n\n`getObsidianEntries` lists everything in the `Content` folder:\n\n```tsx\nexport async function getObsidianEntries() {\n  const {\n    data: {\n      repository: {\n        object: { entries },\n      },\n    },\n  } = await fetchFromGitHubGraphQL(\n    `\n      query fetchEntries($owner: String!, $name: String!) {\n        repository(owner: $owner, name: $name) {\n          object(expression: \"HEAD:Content/\") {\n            ... on Tree {\n              entries {\n                name\n                object {\n                  ... on Blob {\n                    text\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    `,\n    {\n      owner: `GITHUB_USERNAME`,\n      name: `REPO_NAME`,\n      first: 100,\n    }\n  );\n\n  if (entries.errors) {\n    console.error(\"GraphQL Error:\", entries.errors);\n    return [];\n  }\n\n  if (!entries) {\n    console.error(\"No data returned from the GraphQL query.\");\n    return [];\n  }\n\n  return Promise.all(\n    entries.map((entry: { object: { text: any } }) =\u003e {\n      const content = entry.object.text;\n      return parseMarkdownContent(content);\n    })\n  );\n}\n```\n\nErrors get logged and the function returns an empty array. Otherwise each entry gets parsed and returned.\n\n`getObsidianEntry` pulls a single file by slug:\n\n```tsx\nexport async function getObsidianEntry(slug: string) {\n  const { data } = await fetchFromGitHubGraphQL(\n    `\n      query fetchSingleEntry($owner: String!, $name: String!, $entryName: String!) {\n        repository(owner: $owner, name: $name) {\n          object(expression: $entryName) {\n            ... on Blob {\n              text\n            }\n          }\n        }\n      }\n    `,\n    {\n      owner: `GITHUB_USERNAME`,\n      name: `REPO_NAME`,\n      entryName: `HEAD:Content/${slug}.md`,\n    }\n  );\n\n  const text = data.repository.object.text;\n  return parseMarkdownContent(text);\n}\n```\n\n## File names\n\nEvery file is a millisecond timestamp, Zettelkasten-style:\n\n`1671418753342.md`\n\nUnique, chronologically sorted, and the name itself is the slug.\n\n## The whole pipeline\n\nWrite in Obsidian. Obsidian syncs to GitHub. Next.js queries GitHub's GraphQL API and renders. That's it.","tags":["development","web-development","obsidian","github","nextjs","cms","graphql"],"published":true,"createdAt":"2023-11-06T08:57:00Z","updatedAt":"2026-07-16T14:56:28Z"}
