Confluence API Doc Sync
Sync API documentation from a multi-file docs/api/ directory to Confluence — one endpoint file = one Confluence page. The directory structure maps directly to a Confluence page tree, so no heading-based splitting is needed.
Uses acli for authentication verification and page reading (to get current version), and Confluence REST API via curl for page updates.
Core Principle: Directory Structure = Confluence Page Tree
The docs/api/ directory (generated by api-doc-gen) already organizes endpoints as individual files grouped by domain. This skill maps that structure directly to Confluence:
docs/api/ Confluence Page Tree
├── index.md → Parent Page (overview + common errors)
├── consent/ → ├── Consent (domain group page)
│ ├── accept-consent.md → │ ├── Accept Consent
│ ├── get-consent.md → │ ├── Get Consent
│ └── revoke-consent.md → │ └── Revoke Consent
├── channel/ → ├── Channel (domain group page)
│ ├── create-channel.md → │ ├── Create Channel
│ └── get-all-channels.md → │ └── Get All Channels
└── purpose/ → └── Purpose (domain group page)
├── create-purpose.md → ├── Create Purpose
└── get-purposes.md → └── Get Purposes
Each .md endpoint file becomes exactly one Confluence page. Group directories become parent pages.
Step 1: Gather Required Information
Ask the user for the following (if not already provided):
- API doc directory path — e.g.,
docs/api/(relative to project root, or absolute). Default:docs/api/ - Parent page URL — the Confluence page/folder under which API doc pages live (or will be created), e.g.,
https://company.atlassian.net/wiki/spaces/PROJ/pages/123456789/API+Referenceor a folder URL likehttps://company.atlassian.net/wiki/spaces/PROJ/folder/123456789
Extract the page ID directly from the URL (the numeric segment, e.g., 123456789).
Validate the directory:
index.mdmust exist in the provided path- At least one subdirectory with
.mdfiles must exist
Do NOT ask for Confluence URL, email, or API token — those are resolved automatically in the next step.
Step 2: Verify Authentication and Resolve Credentials
acli auth status
If not authenticated or acli not found:
- Not installed → tell user:
brew install atlassian/tap/aclior visit https://developer.atlassian.com/cloud/acli/install/ - Not authenticated → guide user to run:
acli auth login
From the output, extract:
- CONFLUENCE_URL —
Site:field prefixed withhttps://, e.g.,company.atlassian.net→https://company.atlassian.net - EMAIL —
Email:field, e.g.,user@company.com
Why REST API for writes?
acli confluence pagecurrently only supportsview. For page create/update we use Confluence REST API viacurl. URL and email come fromacli auth status; only the API token needs to be resolved (at write time).
Step 3: Read Directory Structure
Scan the docs/api/ directory to build the page list. No heading-based parsing needed — the file/directory structure is the source of truth.
Scanning Steps
- Read
index.md— extract service name (from H1), overview paragraph, and Common Error Responses section - List subdirectories — each subdirectory = one domain group (e.g.,
consent/→ "Consent"). Skiphealth/— health check endpoints are infrastructure-only and not synced to Confluence. - List
.mdfiles per subdirectory — each file = one API endpoint page - Extract page title per endpoint — read the Method and Path fields from the file, then format as
METHOD: /path(e.g.,POST: /api/v1/consents). This is the Confluence page title — not the H1 heading. - Extract page content per endpoint — use the full file content, but:
- Strip the breadcrumb line (first line starting with
>) - Strip the H1 heading (used as in-page heading, not page title)
- Strip the breadcrumb line (first line starting with
Page Types
| Source | Page Type | Title | Content |
|---|---|---|---|
| Group directory | Domain group | Directory name → Title Case (e.g., consent → "Consent") |
Brief intro or empty |
Endpoint .md file |
API page | METHOD: /path from Method + Path fields (e.g., POST: /api/v1/consents) |
File content (minus breadcrumb) |
index.md overview |
Parent page content | Service name from H1 | Overview + Common Errors (appended to parent page) |
Summary Output
After scanning, show the user a structured summary:
Found N domain groups, M individual API endpoints:
Consent (5 APIs)
POST: /api/v1/consents
GET: /api/v1/consents/:citizen_id
GET: /api/v1/consents/:id
GET: /api/v1/consents/:id/history
DELETE: /api/v1/consents/:id/revoke
Channel (5 APIs)
POST: /api/v1/channels
GET: /api/v1/channels
...
Purpose (11 APIs)
POST: /api/v1/purposes
GET: /api/v1/purposes
...
Total pages to create/update: N (domain groups) + M (APIs) = T pages
Ask the user to confirm or specify which sections to sync (all, specific domains, or specific APIs).
Step 4: Map Sections to Confluence Page Hierarchy
The page structure in Confluence mirrors the directory structure:
Parent page (provided by user)
├── Domain Group pages (directories) ← first-level children
│ └── Individual API pages (files) ← second-level children
Discover Existing Pages
First, fetch all children (and grandchildren) under the parent page to find existing pages:
# Get direct children of parent page
curl -s "${CONFLUENCE_URL}/wiki/rest/api/content/${PARENT_PAGE_ID}?expand=space,children.page" \
-u "${EMAIL}:${API_TOKEN}"
From the response extract:
space.key→ save asSPACE_KEY(needed for creating new pages)children.page.results[]→ list of{id, title}for direct children
For each direct child that looks like a domain group, also fetch its children:
curl -s "${CONFLUENCE_URL}/wiki/rest/api/content/${DOMAIN_GROUP_PAGE_ID}/child/page" \
-u "${EMAIL}:${API_TOKEN}"
Build the Mapping
Match existing page titles against scanned sections to build the mapping:
| Source | Page Title | Type | Matched Page ID | Action |
|---|---|---|---|---|
| consent/ | Consent | Domain group | 456789 | Update |
| consent/accept-consent.md | POST: /api/v1/consents | API page | 567890 | Update |
| consent/revoke-consent.md | DELETE: /api/v1/consents/:id/revoke | API page | — | Create (under 456789) |
| purpose/ | Purpose | Domain group | — | Create (under parent) |
| purpose/create-purpose.md | POST: /api/v1/purposes | API page | — | Create (under new domain group page) |
Important ordering: When creating new pages, domain group pages must be created before their child API pages (because child pages need the parent's page ID as ancestor).
Matching Strategy
- Match by exact page title (e.g.,
POST: /api/v1/consentsmatches existing page with same title) - If ambiguous, show the user and ask them to confirm the mapping
- For unmatched sections → mark as "Create new"
Step 5: Get Current Page Versions
For each page in the mapping, fetch its current version number (required for updates):
acli confluence page view --id <PAGE_ID> --include-version --json
Extract version.number from the JSON output. Store as CURRENT_VERSION per page.
Step 6: Convert Markdown to Confluence Storage Format
For each endpoint file, convert the Markdown content to Confluence storage format (XHTML-based).
Pre-processing
Before conversion, strip from each endpoint file:
- Breadcrumb line — first line starting with
>(e.g.,> [API Documentation](../index.md) > ...) - H1 heading — first
#line (used as page title, not body content)
Conversion Rules
CRITICAL — Processing order matters. Follow these phases strictly:
Phase 1: Extract code blocks FIRST (before any other conversion)
Scan the entire Markdown content for fenced code blocks (``` pairs). For each block:
- Capture the language identifier (e.g.,
json,bash,yaml) - Capture ALL lines between the opening and closing
```verbatim — do NOT apply any HTML conversion to these lines - Replace the entire block (including fences) with the Confluence code macro:
<ac:structured-macro ac:name="code">
<ac:parameter ac:name="language">LANG</ac:parameter>
<ac:plain-text-body><![CDATA[...entire code content verbatim...]]></ac:plain-text-body>
</ac:structured-macro>
Rules for code blocks:
- Content inside
<![CDATA[...]]>must be the raw text — never wrap lines in<p>,<br/>, or any HTML tags - Preserve original indentation and newlines exactly as-is
- Language mapping:
sh→bash,js→javascript; all others use their name directly - Code blocks without a language identifier: omit the
<ac:parameter>line
Phase 2: Convert remaining Markdown (non-code-block content only)
| Markdown | Confluence Storage |
|---|---|
**bold** |
<strong>bold</strong> |
*italic* |
<em>italic</em> |
[[text](url)] |
<a href="url">text</a> — process before normal links (used in type columns like array[[Type](#anchor)]) |
[text](url) |
<a href="url">text</a> |
## Heading |
<h2>Heading</h2> |
| col | col | table |
<table><tbody><tr><td>...</td></tr></tbody></table> |
| Plain text paragraphs (separated by blank lines) | <p>text</p> |
- item / * item unordered list |
<ul><li>item</li></ul> |
1. item ordered list |
<ol><li>item</li></ol> |
`inline code` |
<code>inline code</code> |
Nested list handling (critical):
When an ordered list item contains indented sub-items (e.g., - bullets indented under a 1. item), the sub-items become a <ul> inside that <li>, and the <li> is then closed. Subsequent numbered items (5. , 6. , etc.) must continue as siblings in the same parent <ol> — do NOT nest them inside the previous item's sub-list.
Example input:
4. Validate collection point
- Check active status
- Check purpose mapping
5. Create consent record
6. Return response
Correct output:
<ol>
<li>Validate collection point
<ul><li>Check active status</li><li>Check purpose mapping</li></ul>
</li>
<li>Create consent record</li>
<li>Return response</li>
</ol>
Wrong output (items 5-6 nested inside item 4's sub-list):
<ol>
<li>Validate collection point
<ul><li>Check active status</li><li>Check purpose mapping</li>
<li>Create consent record</li><li>Return response</li>
</ul>
</li>
</ol>
Step 7: Sync Pages (Create + Update) via REST API
Before writing, resolve the API token:
echo $CONFLUENCE_API_TOKEN
- If set → use it silently, no need to ask
- If empty → ask the user once:
"ต้องการ API token สำหรับ write ผ่าน Confluence REST API (acli ยังไม่ support page write) — generate ได้ที่ https://id.atlassian.com/manage-profile/security/api-tokens"
Use EMAIL extracted from acli auth status in Step 2.
Execution Order (critical for parent-child hierarchy)
- First pass — Domain group pages: Create or update all domain group pages as children of the parent page. This ensures parent page IDs exist before creating child API pages.
- Second pass — Individual API pages: Create or update all endpoint pages as children of their respective domain group pages.
Creating a New Page
curl -s -X POST \
"${CONFLUENCE_URL}/wiki/rest/api/content" \
-u "${EMAIL}:${API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"page\",
\"title\": \"${PAGE_TITLE}\",
\"ancestors\": [{\"id\": \"${ANCESTOR_PAGE_ID}\"}],
\"space\": {\"key\": \"${SPACE_KEY}\"},
\"body\": {
\"storage\": {
\"value\": \"${ESCAPED_HTML}\",
\"representation\": \"storage\"
}
}
}"
- For domain group pages:
ANCESTOR_PAGE_ID= user-provided parent page ID - For individual API pages:
ANCESTOR_PAGE_ID= the domain group page ID (created in first pass)
Extract id from the response to use as ancestor for child pages.
Updating an Existing Page
curl -s -X PUT \
"${CONFLUENCE_URL}/wiki/rest/api/content/${PAGE_ID}" \
-u "${EMAIL}:${API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"version\": {\"number\": $((CURRENT_VERSION + 1))},
\"title\": \"${PAGE_TITLE}\",
\"type\": \"page\",
\"body\": {
\"storage\": {
\"value\": \"${ESCAPED_HTML}\",
\"representation\": \"storage\"
}
}
}"
Check HTTP status — 200 means success.
Content comparison tip: Before updating, compare normalized content (collapse whitespace) to skip pages with no real changes. This avoids unnecessary version bumps.
Step 8: Report Results
Print a summary table after all operations:
| Page Title | Type | Page ID | Status |
|-----------------------------------------|----------------|------------|-------------------------|
| Consent | Domain group | 456789 | Updated (v3 → v4) |
| POST: /api/v1/consents | API page | 567890 | Updated (v2 → v3) |
| DELETE: /api/v1/consents/:id/revoke | API page | 678901 | Created |
| GET: /api/v1/consents/:citizen_id | API page | 567890 | Skipped (no changes) |
| Purpose | Domain group | 789012 | Created |
| POST: /api/v1/purposes | API page | 890123 | Created |
Total: N domain groups, M API pages updated, K created, J skipped, F failed.
Error Reference
| Scenario | Action |
|---|---|
acli not found |
Install via brew install atlassian/tap/acli |
acli auth status fails |
Run acli auth login |
API doc directory not found or missing index.md |
Re-ask for correct directory path |
| HTTP 401 on REST call | Check API token — re-check $CONFLUENCE_API_TOKEN or ask user |
| HTTP 404 on page | Verify page ID is correct; page may have been deleted |
| HTTP 409 version conflict | Re-fetch version with acli and retry |
| Section title has no match | Ask user to manually provide page ID |