Novelty Liu29 downloadsIncrementally publish notes and their images to a GitHub repository (co-located assets, git-tree atomic commit).
Translations: 中文
An Obsidian plugin that publishes vault notes together with their images to a GitHub repository. Incremental, atomic, and safe to re-run.
Flag a note with gh-publish: true and run Publish to Git Repo — the note (and any images it embeds) lands in your repo as clean Markdown + image files. It is pure file sync: it does not render or host a website. Pair it with any static-site generator (Jekyll, Hugo, Eleventy, Astro, …) or simply use the repo as a Markdown archive.
index.md + images/); a plain-text note stays a single .md file.Design inspired by
oleeskild/obsidian-digital-garden, but lighter: it only syncs files from your vault to a GitHub repo.

Create a (public or private) GitHub repository to hold your published notes. You don't need to initialize it with a README.
Create a token with Contents: Read and write permission for that repository.
repo scope.main.js, manifest.json, and styles.css into <vault>/.obsidian/plugins/publish-to-git-repo/, then enable the plugin in Settings → Community plugins.Settings → Publish to Git Repo:
| Field | Example |
|---|---|
| GitHub token | your token from step 2 |
| Repository | owner/repo |
| Branch | main (default) |
| Base path | notes (optional) |
| Default storage path | posts (required) |
Add this to a note's frontmatter:
---
gh-publish: true
---
Then run the Publish to Git Repo command (Command palette: Cmd/Ctrl+P). You'll see a preview of what will change before anything is uploaded.
That's it — your note and its images are now in your repo. Run the command again any time; only what changed gets uploaded.
| Command | What it does |
|---|---|
| Publish to Git Repo | Scans all gh-publish: true notes, shows a preview, and uploads the new/changed ones in one commit. |
| Mark/unmark for publishing | Toggles gh-publish on the active note. Marking also seeds an empty gh-path for you to fill in; unmarking removes both (a gh-path value you typed is kept). Only available when a Markdown note is open. |
You can assign hotkeys to both via Settings → Hotkeys.
| Setting | Required | Notes |
|---|---|---|
| GitHub token | ✅ | Needs Contents: Read and write (classic token: check repo) |
| Repository | ✅ | owner/repo |
| Branch | — | Defaults to main |
| Base path | — | Root prefix inside the repo, e.g. notes |
| Default storage path | ✅ | Relative to the base path, e.g. posts |
| Published base URL | — | When set, the plugin writes gh-published-url back into each note; left unset otherwise |
⚠️ The token is stored in plaintext in
<vault>/.obsidian/plugins/publish-to-git-repo/data.json. Don't share that vault or itsdata.jsonin public environments.
---
gh-publish: true # allow this note to be published
gh-path: essays/2024 # optional, relative to base path; overrides the default storage path for this note
gh-published: true # managed by the plugin — written back after publishing
gh-published-url: https://... # managed by the plugin — publish link (only when Published base URL is set)
---
gh-publish — the master switch. Only notes where this is exactly true are published. Prefer the Mark/unmark for publishing command to set it: it writes boolean true (not the string "true", which the plugin ignores) and also adds an empty gh-path placeholder.gh-path — optional per-note sub-folder. If empty or whitespace-only, the note uses the default storage path instead — an empty value does not publish to the repo root. The Mark/unmark for publishing command adds it as an empty placeholder when marking, so you can fill it in; unmarking removes that empty placeholder (a value you set is preserved).gh-published / gh-published-url — written back automatically by the plugin after each successful publish to record status. You don't need to set or edit them. Changing or removing them does not trigger or prevent a republish — republishing is driven by content comparison, not these flags.Whether a note becomes a folder depends on whether it embeds images or other assets:
| Case | Path in repo |
|---|---|
| Note without images/assets | {base}/{default path or gh-path}/{note name}.md |
| Note with images/assets | {base}/{default path or gh-path}/{note name}/index.md |
| Embedded images & assets | {...}/{note name}/images/{asset name} |
{base} is the base path if set. The note name is sanitized (path-illegal characters removed; CJK characters and spaces preserved).
For gh-published-url, the filename marker is the note name with the .md extension and any leading YYYY-MM-DD- date prefix removed — kept as-is, no slugification.
Example. With base path notes and default storage path posts, the note 2024-03-01-hello.md containing one image ![[pic.png]]:
notes/posts/hello/index.md
notes/posts/hello/images/pic.png
The same note with no images becomes a single file:
notes/posts/hello.md
Removing the last image from a note switches its layout from a folder (
hello/index.md) to a flat file (hello.md) on the next publish. The old folder is not deleted automatically — see Known limitations.
| In your vault | In the repo |
|---|---|
![[pic.png]] |
 |
[[wikilinks]] |
Kept as-is (not processed) |
![[note]] note embeds |
Not processed |
Image embeds are resolved through Obsidian's metadata cache (not text matching), so a literal ![[...]] inside a code block is never touched. Embeds the plugin can't resolve are reported as failures — never silently dropped.
This section explains the publish flow and the guarantees behind it. (For the code architecture and internals, see README-developers.md.)
gh-publish: true.gh-published (and gh-published-url, if configured) is written back into each note's frontmatter.gh-* frontmatter is stripped and embeds rewritten consistently, so a note's bytes don't drift between runs and its content hash stays stable.images/ folder (one copy per note).gh-publish or deleting a note does not remove the corresponding file from the repo.[[wikilinks]] are kept as-is, and ![[note]] note embeds are not expanded.data.json in your vault (the settings page warns about this).Build steps, architecture, and internals live in README-developers.md (English only).