Confluence API Doc Sync
Sync API documentation from a multi-file
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
for authentication verification and page reading (to get current version), and Confluence REST API via
for page updates.
Core Principle: Directory Structure = Confluence Page Tree
The
directory (generated by
) 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
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., (relative to project root, or absolute). Default:
- 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+Reference
or a folder URL like https://company.atlassian.net/wiki/spaces/PROJ/folder/123456789
Extract the
page ID directly from the URL (the numeric segment, e.g.,
).
Validate the directory:
- must exist in the provided path
- At least one subdirectory with files 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
If not authenticated or acli not found:
- Not installed → tell user:
brew install atlassian/tap/acli
or visit https://developer.atlassian.com/cloud/acli/install/
- Not authenticated → guide user to run:
From the output, extract:
- CONFLUENCE_URL — field prefixed with , e.g., →
https://company.atlassian.net
- EMAIL — field, e.g.,
Why REST API for writes? currently only supports
. For page create/update we use Confluence REST API via
. URL and email come from
; only the API token needs to be resolved (at write time).
Step 3: Read Directory Structure
Scan the
directory to build the page list. No heading-based parsing needed — the file/directory structure is the source of truth.
Scanning Steps
- Read — extract service name (from H1), overview paragraph, and Common Error Responses section
- List subdirectories — each subdirectory = one domain group (e.g., → "Consent"). Skip — health check endpoints are infrastructure-only and not synced to Confluence.
- List files 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 (e.g., ). 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)
Page Types
| Source | Page Type | Title | Content |
|---|
| Group directory | Domain group | Directory name → Title Case (e.g., → "Consent") | Brief intro or empty |
| Endpoint file | API page | from Method + Path fields (e.g., ) | File content (minus breadcrumb) |
| 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:
bash
# 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:
- → save as (needed for creating new pages)
- → list of for direct children
For each direct child that looks like a domain group, also fetch its children:
bash
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., matches 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):
bash
acli confluence page view --id <PAGE_ID> --include-version --json
Extract
from the JSON output. Store as
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., , , )
- 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:
xml
<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 must be the raw text — never wrap lines in , , or any HTML tags
- Preserve original indentation and newlines exactly as-is
- Language mapping: → , → ; all others use their name directly
- Code blocks without a language identifier: omit the line
Phase 2: Convert remaining Markdown (non-code-block content only)
| Markdown | Confluence Storage |
|---|
| |
| |
| — process before normal links (used in type columns like ) |
| |
| |
| table | <table><tbody><tr><td>...</td></tr></tbody></table>
|
| Plain text paragraphs (separated by blank lines) | |
| / unordered list | |
| ordered list | |
| |
Nested list handling (critical):
When an ordered list item contains indented sub-items (e.g.,
bullets indented under a
item), the sub-items become a
inside that
, and the
is then closed. Subsequent numbered items (
,
, etc.) must continue as siblings in the
same parent — 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:
html
<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):
html
<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:
bash
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
extracted from
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
bash
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: = user-provided parent page ID
- For individual API pages: = the domain group page ID (created in first pass)
Extract
from the response to use as ancestor for child pages.
Updating an Existing Page
bash
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 |
|---|
| not found | Install via brew install atlassian/tap/acli
|
| fails | Run |
| API doc directory not found or missing | Re-ask for correct directory path |
| HTTP 401 on REST call | Check API token — re-check 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 |