I migrated to Notion to reduce overhead. Using Obsidian as a CMS was a fun experiment, but it required too much custom work to be practical.
This concept picked up a bit of traction when I mentioned it on Farcaster. There are a few moving pieces that come together to provide a CMS
of sorts that can be edited from anywhere.
It all revolves around an Obsidian template the presets some structured YAML frontmatter. A Zettelkasten inspired tag is used for the file names (millisecond time), along with the created
timestamp and the initial post title.
---
title: "1671418753342"
created: "1671418753342"
longform: false
published: false
---
Using the Zettelkasten tag as the title allows most content to exist as a "journal" entry. If something needs a more descriptive name, that can be set instead.
Obsidian is configured to automatically push / pull content from a private Github repo ~1 minute after editing has stopped. This allows content to be continuously backed up and versioned, while a published
flag in the YAML determines whether or not the entry should be displayed publically.
When content is pushed to the repo a Github Workflow runs that scrapes the assets
folder and uploads content to a Cloudflare R2 bucket. It's similar to Amazon S3, but has a very generous free tier which is perfect for my current use case.
name: Cloudflare
on:
push:
branches:
- main
workflow_dispatch: null
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: R2 Directory Upload
uses: willfurstenau/r2-dir-upload@main
with:
accountid: '${{ secrets.CF_ACCOUNT_ID }}'
accesskeyid: '${{ secrets.CF_ACCESS_KEY }}'
secretaccesskey: '${{ secrets.CF_SECRET_KEY }}'
bucket: iam-bucket
source: '${{ github.workspace }}/Assets'
destination: /
Once content is in the Github repo, and any assets have been pushed to Cloudflare, we can turn our attention to NextJS!
We'll be using 2 basic queries to get our content. Up first: getObsidianEntries
.
export default async function getObsidianEntries() {
const token = process.env.NEXT_PUBLIC_GITHUB;
const {
data: {
repository: {
object: { entries },
},
},
} = await fetch(`https://api.github.com/graphql`, {
method: `POST`,
headers: {
"Content-Type": `application/json`,
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query: `
query fetchEntries($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
object(expression: "HEAD:Content/") {
... on Tree {
entries {
name
object {
... on Blob {
text
}
}
}
}
}
}
}
`,
variables: {
owner: `GITHUB_USERNAME`,
name: `REPO_NAME`,
first: 100,
},
}),
next: {
revalidate: 1 * 30,
},
}).then((res) => res.json());
return entries;
}
You'll need an access token set up with Github in order to access the GraphQL endpoint, and your private repositories. You can read more about that here: Authenticating with GraphQL
You then need to drill down in the repository object, filtered for the contain on your main branch (note the expression: "HEAD:Content/"
filter). You can then pass in some variables to define your username, and the specific repo you're looking to query. We're making our GraphQL call using fetch()
, and leveraging Next13's revalidation flag to make sure content stays fresh.
Once you have your entries, you can manipulate the content however you'd like. I am parsing the YAML frontmatter using grey-matter, but rendering the content using react-remark. This lets me manipulate the YAML with a very terse syntax, while leveraging the broad Unified system for Remark.
My second query, which is used to build the individual post pages, is called getObsidianEntry
:
export default async function getObsidianEntry(slug: any) {
const paths = await getObsidianEntries();
const _paths = await Promise.all(paths);
const entry = _paths.find((entry: any) => entry.slug === slug);
return entry;
}
I'm not overly happy with this piece. My preference would be to requery the GraphQL endpoint and only return 1 specific entry, but I have not been able to get a filter to work for that. In the meantime, I am simply filtering out the appropriate entry using the page slug as a key (the Zettelkasten tag).
So there you have it. Obsidian as a CMS. If you're interested in setting up something similar, I hope these notes can help serve as bread-crumbs to guide you along the way.