{"kind":"doc","doc":{"title":"Revisiting Obsidian as a CMS","section":{"slug":"posts","name":"Posts"},"rkey":"1699332127006-revisiting-obsidian-as-a-cms","cid":"bafkreieo4tipxhq3v4bjtzl4ptldoulcnvhltuafv3ydppkr7iqvfmhe6q","renderKey":"bafkreieo4tipxhq3v4bjtzl4ptldoulcnvhltuafv3ydppkr7iqvfmhe6q@2023-11-06T08:57:00Z@2026-07-16T14:56:28Z@Posts|","href":"/posts/1699332127006-revisiting-obsidian-as-a-cms","publishedAt":"2023-11-06T08:57:00Z","updatedAt":"2026-07-16T14:56:28Z","tags":["development","web-development","obsidian","github","nextjs","cms","graphql"],"description":"A home-built publishing system using Obsidian for writing, GitHub for storage, and Next.js for rendering.","readingMinutes":2,"markdown":"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 } }) => {\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.","media":{},"series":{},"related":[{"title":"Revisiting Obsidian as a CMS, again","href":"/posts/1710177704945-revisiting-obsidian-as-a-cms-again","section":"posts","description":"Obsidian, GitHub's GraphQL API, and Astro form a free, flexible publishing system for Markdown content.","publishedAt":"2024-03-11T10:21:00Z"},{"title":"Obsidian as a CMS","href":"/posts/1670659200001-obsidian-as-a-cms","section":"posts","description":"Obsidian as a CMS using structured frontmatter, GitHub sync, Cloudflare R2 for assets, and Next.js for rendering.","publishedAt":"2022-12-10T08:55:00Z"},{"title":"Farfield","href":"/posts/1779066375000-farfield","section":"posts","description":"Five small Go services, single binaries, content-addressed records, running on my homelab. The new backend for the site.","publishedAt":"2026-05-18T01:06:15Z"},{"title":"Obsidian Plugins","href":"/experiments/1789490903000-obsidian-plugins","section":"experiments","description":"An overview of three Obsidian plugins the author built: an image uploader that sends embedded images to Pinata and rewrites references as ipfs:// URIs, plus AI-powered generators for note excerpts and tags.","publishedAt":"2026-09-20T05:10:12Z"},{"title":"Post-UI","href":"/posts/1784126858578-post-ui","section":"posts","description":"Do we still need to ship a frontend?","publishedAt":"2026-07-15T17:27:00Z"},{"title":"Single-origin deployment with bhvr","href":"/posts/1747182886660-single-origin-deployment-with-bhvr","section":"posts","description":"Fried my Pi using a 10V solar panel without a buck converter.","publishedAt":"2025-05-13T17:34:00Z"}],"prev":{"title":"Sourdough","href":"/posts/1587970800000-sourdough"},"next":{"title":"Case Study: Rebuilding Opul on Framer","href":"/posts/1709280000000-case-study-revance-opul"}}}