{
"$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"
}