Your Confluence pages, as markdown in git

September 13, 2026

Confluence is where the rest of the company reads documentation. Markdown in git is where documentation can be diffed, reviewed in a pull request, and read by your tooling — your editor, your scripts, your AI.

Picking one means losing the other. So I wrote a small tool that keeps both, in both directions, and published it: @alagrede/confluence-md-sync.

Confluence  ──  pull  ──▶  docs/specs/*.md  ──▶  serve  (localhost preview)
                                  │
                push ◀────────────┘   (after you edit a file)

Node ≥ 18 and zero dependencies — the code imports only node:* builtins and the global fetch. Nothing to audit, nothing to keep up to date.

Setting it up

npm install --save-dev @alagrede/confluence-md-sync
npx confluence-md-sync init          # writes confluence-md-sync.config.mjs

Then two things to fill in. What to mirror, in confluence-md-sync.config.mjs:

export default {
    sources: [
        { rootId: '1845363038', outDir: 'docs/specs', label: 'Specifications' },
    ],
};

rootId is read straight out of a Confluence URL — …/spaces/HANDBOOK/folder/1845363038 or …/pages/1845363038/Home, both work. The root itself isn't written as a page: its children become the mirror. So point it at the parent of what you want, not at a leaf.

Credentials, as three variables and an API token:

mkdir -p ~/.config/confluence-md-sync
cat >> ~/.config/confluence-md-sync/env <<'EOF'
CONFLUENCE_BASE_URL=https://your-org.atlassian.net/wiki
CONFLUENCE_EMAIL=you@example.com
CONFLUENCE_API_TOKEN=your-token
EOF

That location keeps the token out of the repository entirely (a gitignored ./.env works too). The token is personal: it carries your own rights, so nothing can be written to a page you couldn't already edit.

Then the loop:

npx confluence-md-sync pull --dry-run    # safe: GETs only, writes nothing
npx confluence-md-sync pull              # write the mirror
npx confluence-md-sync serve --open      # read it in the browser
git add docs/ && git commit -m "Mirror Confluence specs"

That last line is the point of the whole thing: the mirror belongs in git, so from then on every Confluence edit arrives as a reviewable git diff.

A pull, and a read

Room management (HANDBOOK) → docs/specs

   +  docs/specs/home.md
   ~  docs/specs/booking/index.md
   =  docs/specs/booking/rules.md

2 created, 1 updated, 1 unchanged, 7 attachment(s) downloaded.

Pages are converted, images downloaded, and a file written only when it actually changed — run it twice and everything reports unchanged. Nothing is ever deleted: files that no longer match a page are listed at the end for you to review and git rm.

serve then renders the mirror on 127.0.0.1:4801 — sidebar, tables, screenshots, a link back to each Confluence page with its version number, light and dark. It needs no Confluence access at all, since it only reads local files. Which makes it the thing to hand to a teammate who just wants to read.

Publishing a correction back

A review says a page states something false. You fix the .md, you publish:

npx confluence-md-sync push --only the-page            # dry run by default
npx confluence-md-sync push --only the-page --apply    # publish

push replaces the page body — it is not a merge. So it refuses in three cases, all overridable with --force: without --apply it only reports; a page that moved in Confluence since your last pull is refused rather than flattening someone else's work; and a page whose prose is identical to Confluence's is skipped, so a repo-wide push can't reformat pages nobody touched.

What it's actually for

Once your specs are markdown files in a repository, ordinary things become possible that Confluence makes hard:

  • Review documentation like code — a spec change shows up as a diff in a pull request, next to the code that implements it.
  • Grep it, script it, feed it to an AI — plain files, so your agent can read the whole handbook without a Confluence integration.
  • Read it offline, in the editor you already use.
  • Leave the company in Confluence — nobody has to migrate or lose their wiki.

And the mirror is just a folder of markdown with its images beside it — so you can open docs/specs in Znote as a linked folder and read your company's documentation as notes: same tree, images inline, full-text search, and an AI block right next to a spec when you need to interrogate it.

The honest limits

Confluence's storage format is richer than markdown, and the tool says so rather than pretending. Macros, layouts and column widths are lost on the way out — enough to read and review, not to rebuild the page identically, which is exactly why push guards pages you never touched. Info / note / warning panels are one-way: they pull as blockquotes, and pushing one back produces a paragraph starting with a literal >, so a page whose panels matter is better corrected in Confluence. And it targets Confluence Cloud REST v1 — not tested against Server / Data Center.

Anything the converter doesn't know comes out as escaped text instead of being silently dropped — visible in the push dry run, and fixable, rather than vanishing.

Also a library

The two converters are pure functions over strings, usable without the CLI — for a CI check that a page round-trips, or a bot that posts markdown to Confluence:

import { storageToMarkdown, markdownToStorage } from '@alagrede/confluence-md-sync';

They carry the risk in this project, so they carry the tests: 79 of them, over entities, nested lists, tables, macros, image tokenisation, path refusals, and a markdown → storage → markdown round trip.

One caveat on the name: the unscoped confluence-md-sync on npm is an unrelated project by another author. This one is @alagrede/confluence-md-sync — the command it installs is still confluence-md-sync. MIT, sources on GitHub.

Get Znote →


Znote is a local-first Markdown editor with executable code blocks and AI grounded in your notes. Opens Obsidian vaults natively.