{
  "$type": "site.standard.document",
  "content": {
    "$type": "site.subgraph.content.markdown",
    "body": "\nI 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.\n"
  },
  "description": "A home-built publishing system using Obsidian for writing, GitHub for storage, and Next.",
  "publishedAt": "2023-11-07T04:42:07.006Z",
  "site": "at://did:plc:p5xem22ammiafn5kxonaksfa/site.standard.publication/3mlp3ywhyv2kx",
  "tags": [
    "development",
    "web-development",
    "obsidian",
    "github",
    "nextjs",
    "cms",
    "graphql"
  ],
  "textContent": "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\nfetchFromGitHubGraphQL is the wrapper:\n\nasync 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 }), });\n\nif (!response.ok) { console.error(\"HTTP Error:\", response.status); return response; }\n\nreturn response.json(); }\n\nObsidian syncs on save, so the repo always has the latest markdown.\n\nparseMarkdownContent runs each file through gray-matter and pulls out the frontmatter and body:\n\nfunction 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, }; }\n\ngetObsidianEntries lists everything in the Content folder:\n\nexport 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, } );\n\nif (entries.errors) { console.error(\"GraphQL Error:\", entries.errors); return []; }\n\nif (!entries) { console.error(\"No data returned from the GraphQL query.\"); return []; }\n\nreturn Promise.all( entries.map((entry: { object: { text: any } }) => { const content = entry.object.text; return parseMarkdownContent(content); }) ); }\n\nErrors get logged and the function returns an empty array. Otherwise each entry gets parsed and returned.\n\ngetObsidianEntry pulls a single file by slug:\n\nexport 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`, } );\n\nconst text = data.repository.object.text; return parseMarkdownContent(text); }\n\nFile names\n\nEvery file is a millisecond timestamp, Zettelkasten-style:\n\n1671418753342.md\n\nUnique, chronologically sorted, and the name itself is the slug.\n\nThe whole pipeline\n\nWrite in Obsidian. Obsidian syncs to GitHub. Next.js queries GitHub's GraphQL API and renders. That's it.",
  "title": "Revisiting Obsidian as a CMS"
}