
Docs as Code: How to Export Confluence to Git-Ready Markdown
Docs-as-code treats documentation like source code: written in plain text, versioned in Git, reviewed in pull requests, and deployed through a CI/CD pipeline. The format is almost always Markdown.
Confluence doesn't fit this model natively. Its built-in exports produce formats like PDF, Word and HTML, none of which belong in a Git repository. But for many teams, Confluence is where the documentation actually gets written and reviewed, and replacing it isn't always an option.
The practical solution: use Confluence as the editing surface and Git as the published copy. Export Confluence to Markdown on a cadence, commit the output, and let your pipeline handle the rest.
Markdown Exporter for Confluence is built for this workflow. You can run exports by hand from the page tree, or call its REST API from a scheduled job.
What You Need
- Confluence Cloud (the app runs on Atlassian Forge, so it is Cloud only)
- Markdown Exporter for Confluence installed on your site
- A Git repository with a
docs/folder (or equivalent) - Optionally: a static site generator like MkDocs or Docusaurus
Step 1: Structure Your Confluence Pages for Export
The exporter mirrors your Confluence page hierarchy as folders. If your pages are organised logically in Confluence, the output will be organised logically on disk.
A Confluence structure like this:
Documentation
├── Getting Started
│ ├── Installation
│ └── Quick Start
├── Architecture
│ ├── Overview
│ └── API Reference
└── Operations
└── Runbooks
becomes a set of Markdown files and folders in the ZIP. Each page is a .md file, and a page with children also gets a folder of the same name holding its child pages. With the default {title} filename pattern, it looks roughly like this:
getting-started.md
Getting Started/
├── installation.md
└── quick-start.md
architecture.md
Architecture/
├── overview.md
└── api-reference.md
operations.md
Operations/
└── runbooks.md
Links between exported pages become relative .md paths, so the files still link to each other once they're in your repo. Run one test export and look at the result before wiring up your site generator, so your config matches the real folder and file names.
Step 2: Pick a Stable Filename Pattern
Consistent filenames matter in a Git repo. If a file name changes between exports, Git sees a deletion and a new file, and the history for that page gets harder to follow.
Markdown Exporter lets you set a filename pattern using tokens:
| Token | Value |
|---|---|
{title} |
The page title, slugified (lowercase, hyphens, no special characters) |
{date} |
The page's last-edited date, YYYY-MM-DD |
{id} |
The Confluence page ID |
For docs-as-code, use {title} (the default) or {id}-{title}. Avoid {date} here: because it's the last-edited date, the filename would change every time someone edits the page. The pattern must include {title} or {id} so every file in a multi-page export is unique, and each page in the tree shows a live preview of its filename as you type.
Step 3: Set Up YAML Front Matter and Panel Style
Most static site generators read metadata from YAML front matter. Turn on Add YAML front matter and choose which built-in fields to include:
---
title: "API Reference"
author: "Platform Team"
date: 2026-04-23
space: DOCS
confluence_id: "789012"
---
author is the last editor and date is the date of the last edit. You can also add custom key-value fields that appear in every file, which is useful for tagging an export with a product, version or source value your site generator can use.
Also check the Panel / Callout Style setting. Confluence info, note, warning and tip panels are exported as GitHub-style callouts (> [!NOTE]) by default. GitHub and Obsidian render those natively. If your site generator doesn't, choose Blockquote or Plain text instead.
Step 4: Export and Commit (Manual)
- On the root page you want to publish, click ••• (More actions) and select Export to Markdown. Or open Markdown Exporter from the Apps menu and use the space selector to choose a space and parent page.
- In the page tree, tick the pages to include. Ticking a parent selects all of its children.
- In the right-hand panel, set the filename pattern, front matter and panel style.
- Click Export Selected. The ZIP downloads when the progress bar finishes.
- Extract the ZIP over your
docs/folder in the repository. - Commit:
git add docs/ && git commit -m "docs: sync from Confluence"
On the next CI run, your pipeline picks up the updated Markdown and publishes the new docs.
Step 5: Automate It with the REST API
Manual exports are fine once a sprint. For a weekly or nightly sync, Markdown Exporter has a REST API that a CI job can call. It has two endpoints:
| Endpoint | What it returns |
|---|---|
POST …/bulkExport |
A space, or a page and all its descendants, as a base64-encoded ZIP inside JSON |
POST …/singleExport |
One page as plain Markdown text (or a ZIP if you include attachments) |
One-time setup
The API uses OAuth 2.0 (3LO) through Atlassian's platform, so there is some setup before the first call. The getting started guide has the full walkthrough:
- A site or org admin turns on App REST APIs for Markdown Exporter for Confluence in Atlassian admin.
- In the Atlassian developer console, create an OAuth 2.0 integration, connect it to Markdown Exporter for Confluence on your site, and add the
export:markdown:customandread:forge-app:confluencescopes. - Authorize once in a browser with
offline_accessadded to the scope, then exchange the code for an access token and a refresh token. - Find your Cloud ID at
https://your-site.atlassian.net/_edge/tenant_info.
Access tokens last one hour, so a CI job refreshes one at the start of each run. Atlassian rotates refresh tokens: every refresh returns a new refresh token, and reusing an old one invalidates the chain. Your job has to save the new refresh token each time. Refresh tokens also expire after 90 days without use, which a weekly job will never hit.
A scheduled GitHub Actions export
This workflow runs every Monday, exports a page tree, and commits the result if anything changed. It stores the rotated refresh token back into the repository's secrets with the GitHub CLI, which needs a fine-grained personal access token with read and write access to the repository's secrets (saved here as SECRETS_PAT).
name: Export Confluence docs
on:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC
workflow_dispatch:
permissions:
contents: write
concurrency:
group: confluence-export # never refresh tokens in two runs at once
cancel-in-progress: false
jobs:
export:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Export from Confluence
env:
CLIENT_ID: ${{ secrets.ATLASSIAN_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.ATLASSIAN_CLIENT_SECRET }}
REFRESH_TOKEN: ${{ secrets.ATLASSIAN_REFRESH_TOKEN }}
CLOUD_ID: ${{ vars.CONFLUENCE_CLOUD_ID }}
ROOT_PAGE_ID: ${{ vars.CONFLUENCE_ROOT_PAGE_ID }}
GH_TOKEN: ${{ secrets.SECRETS_PAT }}
run: |
set -euo pipefail
APP_ID="0def1d4f-1593-4dd0-99a3-a098b3959067"
ENV_ID="796457c9-9c03-4382-b44f-d19571068ece" # production
# 1. Refresh the access token and save the new refresh token straight away
jq -n --arg id "$CLIENT_ID" --arg secret "$CLIENT_SECRET" --arg rt "$REFRESH_TOKEN" \
'{grant_type: "refresh_token", client_id: $id, client_secret: $secret, refresh_token: $rt}' |
curl -sS -X POST https://auth.atlassian.com/oauth/token \
-H "Content-Type: application/json" --data-binary @- > tokens.json
ACCESS_TOKEN=$(jq -r '.access_token // empty' tokens.json)
NEW_REFRESH=$(jq -r '.refresh_token // empty' tokens.json)
rm tokens.json
if [ -z "$ACCESS_TOKEN" ] || [ -z "$NEW_REFRESH" ]; then
echo "Token refresh failed"; exit 1
fi
echo "::add-mask::$ACCESS_TOKEN"
echo "::add-mask::$NEW_REFRESH"
gh secret set ATLASSIAN_REFRESH_TOKEN --body "$NEW_REFRESH" --repo "$GITHUB_REPOSITORY"
# 2. Export the page tree (up to 100 pages per request)
status=$(curl -sS -o export.json -w '%{http_code}' -X POST \
"https://api.atlassian.com/svc/confluence/${CLOUD_ID}/apps/${APP_ID}_${ENV_ID}/bulkExport" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"pageId\": \"${ROOT_PAGE_ID}\", \"bulkExport\": true, \"maxPages\": 100}")
if [ "$status" != "200" ]; then
echo "Export failed with HTTP $status"; cat export.json; exit 1
fi
echo "Exported $(jq -r '.pageCount' export.json) pages"
# 3. Replace the exported folder with the new content
jq -r '.zipBase64' export.json | base64 -d > export.zip
rm -rf docs/confluence
mkdir -p docs/confluence
unzip -q export.zip -d docs/confluence
rm export.json export.zip
- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "41898283+github-actions[bot]@users.noreply.github.com"
git add docs/confluence
git diff --cached --quiet || git commit -m "docs: sync from Confluence"
git push
Before you put the workflow on a schedule:
- Seed the secrets. Add
ATLASSIAN_CLIENT_ID,ATLASSIAN_CLIENT_SECRETand the firstATLASSIAN_REFRESH_TOKENfrom the setup above as repository secrets, and the Cloud ID and root page ID as variables. - Know the limits. One request exports at most 100 pages, the response can't exceed 5 MB, and the call has a 25-second limit. Attachments are capped at 300 KB per file and 2 MB per export, which is why this example leaves them out. For a larger space, add one
bulkExportcall per section using that section's page ID. - Expect API naming. API exports use slugified file and folder names (lowercase, hyphens). The UI options for filename patterns, front matter and panel style are not part of the API request body, so check one API export before pointing your site generator at it.
- Watch the export history. Every API export is recorded in the app's export history, which Confluence admins can see in the app's configuration page.
If the job fails with 401, the token is missing or lacks a scope. A 403 usually means App REST APIs isn't enabled or the integration is connected to the wrong environment. The troubleshooting page covers the rest.
Works With MkDocs and Docusaurus
MkDocs
MkDocs nav paths are relative to docs_dir, so don't repeat the docs/ prefix. For the UI export layout in Step 1, extracted into docs/, a minimal mkdocs.yml looks like this:
site_name: Platform Docs
docs_dir: docs
nav:
- Getting Started:
- getting-started.md
- Getting Started/installation.md
- Getting Started/quick-start.md
- Architecture:
- architecture.md
- Architecture/overview.md
- Architecture/api-reference.md
Or leave out nav and let MkDocs build navigation from the folders, optionally with a plugin such as awesome-pages for ordering. Set the panel style to Blockquote unless you've added a Markdown extension that understands GitHub-style callouts.
Docusaurus
Place the extracted files under docs/ in your Docusaurus project. Docusaurus reads YAML front matter, so the exported title is used for the page title. With autogenerated sidebars, the folder hierarchy becomes the sidebar. Docusaurus uses its own :::note syntax for admonitions, so choose the Blockquote panel style for clean output.
Keeping Content in Sync
The loop, whether manual or automated:
- Export on a schedule: a full section weekly, or after each sprint
- Review the diff:
git diff docs/(or the bot's commit) shows exactly what changed in Confluence since the last export - Let CI publish: build and deploy the updated site
Keep the direction one-way. If people edit the Markdown in Git, the next export overwrites their changes. If you'd rather write in Git and publish to Confluence, flip the flow and use Markdown Importer's API instead, as described in how to sync GitHub docs to Confluence automatically.
What the Export Handles
- Text formatting: headings, bold, italic, strikethrough, inline code
- Tables: header rows and column alignment
- Code blocks: fenced, with the language tag kept
- Lists: ordered, unordered, nested, and task lists
- Images and attachments: downloaded and referenced with relative paths
- Internal links: converted to relative
.mdpaths between files - Panels: GitHub-style callouts by default, or blockquotes or plain text
Some Confluence elements don't have a Markdown equivalent. Jira issue macros come through as the issue key, inline comments and page properties macros are not included, and whiteboards are not supported. See the known issues for the full list.
Getting Started
Install Markdown Exporter for Confluence free from the Atlassian Marketplace. For a tour of the UI, read the Markdown Exporter launch post.
Full documentation is at /docs/markdown-exporter-for-confluence.
Questions or feedback? Reach out via our support portal.


