vault2confluence
v0.4.2
Published
One-way sync of a local Obsidian/markdown vault to a Confluence Cloud page tree
Downloads
252
Maintainers
Readme
vault2confluence
One-way sync of a local markdown vault (Obsidian or otherwise) onto a page tree in Confluence Cloud. Local files are the single source of truth: every run mirrors the source folder's structure onto Confluence exactly, overwriting existing content and trashing any Confluence page under the target root that no longer has a matching local file/folder.
Installation
From npm (Recommended)
npm install -g vault2confluenceOr from source
git clone https://github.com/huayueh/vault2confluence.git
cd vault2confluence
npm install
npm run build
npm linkSetup & Authentication
Auth is via environment variables (not CLI flags — configure once, reuse across runs).
Either export them in your shell profile, or create ~/.vault2confluence in your home
directory — the CLI loads it automatically on every run, regardless of what directory you
run the command from. Never commit credentials to version control.
# ~/.vault2confluence
[email protected]
CONFLUENCE_API_TOKEN=<from https://id.atlassian.com/manage-profile/security/api-tokens>Must be a classic (unscoped) API token — the client talks to the site-specific
/wiki/rest/api/... URL directly with Basic Auth, which scoped tokens (routed through
api.atlassian.com/ex/confluence/<cloudid>/... instead) don't support here. When creating
the token, use the plain "Create API token" button, not "...with scopes".
Usage
vault2confluence sync \
--source /path/to/vault/folder \
--target "https://yourorg.atlassian.net/wiki/spaces/ENG/pages/123456789/Page+Title" \
--prefix "myproject" \
[--dry-run] \
[--yes]--source: Required. Local folder (walked recursively) or a single.mdfile.- When
--sourceis a single file, it creates or updates that single page under--targetwithout trashing sibling pages.
- When
--target: Required. A Confluence page URL. This page must already exist — the tool never creates or renames the root, only the tree underneath it. The page ID is parsed straight out of the URL; short links (/wiki/x/...) aren't supported, paste the full address-bar URL.--prefix: Required. Short project prefix code (e.g.li,lp,eng) to namespace all Confluence page titles, preventing space-wide title collisions.- Numbered titles:
01-overview$\to$01-li-overview,1.1-ui-signup$\to$1.1-li-ui-signup. - Non-numbered titles:
account-management$\to$li-account-management,glossary$\to$li-glossary.
- Numbered titles:
--dry-run: prints every planned create/update/delete without calling any write API. Always run this first against a new root.--yes: skips the interactive confirmation prompt before trashing orphaned pages (for non-interactive use in directory mode). Without it, you'll be prompted once with the full list of pages that are about to be trashed.
Run with --dry-run during development: npm run dev -- sync --source ... --target ... --dry-run.
Design (why it works this way)
- Hierarchy: source folders mirror onto container pages 1:1; markdown files become leaf
pages. A folder's body is its own
index.md/_index.mdif present, otherwise an auto-generated list of its direct children (regenerated every run) — it's a navigational stand-in, not meant to hold real content of its own. - Identity: pages are matched by title, scoped to space + immediate parent — there's no stored page ID anywhere (not even in frontmatter), specifically so pre-existing Confluence pages that predate this tool are found and updated in place rather than duplicated. Trade-off: renaming a local file creates a new page and orphans the old one.
- Frontmatter is invisible in the page body. It's parsed and stripped, never rendered.
It has exactly two operational uses:
tags→ Confluence labels (domain/x→domain:x), andlinks→ folded into the same link-resolution index used for inline[[wikilinks]]. Everything else in frontmatter (summary,generated.*,warnings,sources, etc.) is parsed only so it doesn't break anything, then discarded. - Always overwrite: no drift detection, no "was this hand-edited in Confluence" check. Local is truth.
- Deletion: the entire page subtree under
--targetis considered exclusively owned by this sync. Anything there without a matching local file/folder gets trashed (Confluence's default DELETE — recoverable by a space admin for the retention window), gated by a confirmation prompt unless--yes. - Wikilinks (
[[note]],[[note|alias]],![[embed]]) are masked to placeholder tokens in the raw text before the markdown parser ever sees them — not patched up afterward in the AST. This matters because|inside a table cell is otherwise indistinguishable from a GFM table separator to the parser (confirmed against a real file in this vault:03-iam-and-auth/../02-architecture/index.mdhas a wikilink-with-alias inside a table cell that would otherwise get split into two cells).- Unresolved or ambiguous targets fall back to plain text + a warning, never a crash.
![[note-name]](transclusion) has no Confluence equivalent — rendered as a link to that note instead, with a warning.- Malformed nested-bracket artifacts (found in real vault content, likely from a prior automated tool) degrade to readable-ish plain text rather than throwing.
- Images: both
and![[embed.png]]resolve against the note's own directory (then the vault root as a fallback), upload as Confluence attachments, and embed via<ac:image>. Images not found on disk are skipped with a warning, not fatal. - Callouts (
> [!warning] ...) map onto Confluence's four panel macros (info/tip/note/warning) — Obsidian has ~15 callout types, Confluence has four, so this is a many-to-one collapse (seeCALLOUT_TO_PANELinsrc/markdown/macros.ts). - Dead
file://links (absolute local paths into source code, common in this vault) are dropped — kept as visible text only, with a warning — since they're meaningless off the author's machine.
Known gap: Mermaid
Confluence Cloud has a native "Mermaid diagram" macro, but its exact storage-format markup (macro key + schema) hasn't been captured yet. Mermaid blocks currently render as a plain code block (safe, valid, readable — just not an actual diagram), with a warning on every sync. To wire up real diagram rendering:
- Create a page manually in the Confluence editor, insert a Mermaid diagram block.
- Fetch that page's body via the API in storage representation
(
GET /rest/api/content/{id}?expand=body.storage). - Copy the literal
<ac:structured-macro ...>markup you get back intomermaidBlock()insrc/markdown/macros.ts, replacing the current code-block fallback.
Project layout
src/
cli.ts entrypoint (commander)
confluence/
auth.ts env-var auth loading
targetUrl.ts --target URL -> {baseUrl, pageId}
client.ts REST API wrapper (content, attachments, labels, trash)
markdown/
frontmatter.ts split + parse frontmatter; tags -> labels
linkIndex.ts title -> target index, built once per run
wikilinks.ts wikilink masking/resolution (pre-parse + AST-level)
macros.ts storage-format fragment builders (panels, code, mermaid stub, images)
toStorageFormat.ts the markdown AST -> Confluence storage-format serializer
sync/
walk.ts walks --source into the folder/file tree
render.ts tree node -> {storage, labels, attachments}
syncer.ts orchestrates create/update/upload/label/delete against the API
util/
logger.ts, mime.ts, prompt.tsStatus
Validated against live Confluence Cloud instances with production vault content (80+ pages
synced across deeply nested hierarchies). Confirmed: frontmatter stripping, tag→label conversion,
wikilink resolution (including the table/alias collision bug), dead-link handling, mermaid
fallback, folder-index generation, and image attachment upload. Always run --dry-run first
and review the planned actions, especially on the very first run against any given root.
