skills/shared/md-to-confluence/SKILL.md
Push markdown files to Confluence - update existing pages or create new ones under a parent page. Use this skill whenever the user wants to publish markdown to Confluence, update a Confluence page from a local .md file, create new Confluence pages, or batch-create pages. Also triggers on "push to confluence", "update confluence page", "create confluence page", "publish to confluence", "sync to confluence", or any mention of uploading/pushing markdown content to Confluence. Use this even when the user says "put this on confluence" or "make a confluence page for this".
npx skillsauth add qa-aman/claude-skills md-to-confluenceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Push local markdown to Confluence - update existing pages or create new ones. The bundled script handles pandoc conversion, image uploads, Confluence macro formatting, and wide mode. It avoids the many pitfalls of Confluence's storage format so you don't have to build HTML manually.
# Update existing page
python3 .claude/skills/md-to-confluence/scripts/md-to-confluence.py <page_id> <markdown_file>
# Create new page under a parent
python3 .claude/skills/md-to-confluence/scripts/md-to-confluence.py --create --parent-id <parent_id> --space <SPACE_KEY> <markdown_file> --wide
# Batch create
python3 .claude/skills/md-to-confluence/scripts/md-to-confluence.py --create --parent-id <parent_id> --space <SPACE_KEY> file1.md file2.md --wide
The script auto-loads credentials from .env at the project root.
Before updating an existing page, ALWAYS run the preflight. It detects whether anyone edited the page directly on Confluence since your last sync — so you don't silently overwrite their changes.
python3 .claude/skills/md-to-confluence/scripts/confluence-preflight.py <page_id> <local.md>
Exit codes:
How snapshots work (Git-style 3-way diff):
.local/confluence-snapshots/<page_id>.md (auto-created, gitignored)First-time push (no baseline yet): preflight falls back to a 2-way diff and prints a warning. The baseline is auto-created after the push succeeds, so subsequent pushes have the full 3-way protection.
Use --show-diff to see full diffs inline; use --force only if you have
explicitly verified the Confluence-side changes and decided to overwrite.
Update - user has an existing page URL:
https://[your-instance].atlassian.net/wiki/spaces/[SPACE]/pages/1234567890/Page+Title
^^^^^^^^^^ page ID
Create - user wants a new page. You need:
--parent-id (from the parent page URL)--space key (the Confluence space key for your project)# Update with title change and wide mode
python3 .claude/skills/md-to-confluence/scripts/md-to-confluence.py 1234567890 my-doc.md --title "New Title" --wide
# Create under parent with auto-title (extracted from first H1)
python3 .claude/skills/md-to-confluence/scripts/md-to-confluence.py --create --parent-id 9876543210 --space [SPACE_KEY] guide.md --wide
# Dry run first
python3 .claude/skills/md-to-confluence/scripts/md-to-confluence.py 1234567890 my-doc.md --dry-run
After every push, verify these automatically:
# Title, the
script strips it. Verify: fetch body.storage and confirm no <h1> tag exists (Confluence adds
its own page title separately).> Source: [Confluence](...) line in markdown is a local-only reference.
The script strips it. Verify: fetch body.storage and confirm no "Source:" info panel or blockquote.body.storage
and confirm <th class="numberingColumn"> exists in every <table>.data-table-width="1800" and data-layout="wide" on every <table>.<ri:attachment ri:filename="..."> have
matching attachments on the page.@Name to <ac:link><ri:user> macros by looking up
account IDs from .local/team/users.md. Handles pandoc's citation span wrapping (<span class="citation" data-cites="Name">@Name</span>).
Verify: no @Name plain text or class="citation" remains.ac:name="toc" present.data-highlight-colour="#b3bac5" on versioning table <th> cells.
Verify: attribute present on non-numberingColumn headers.<time datetime="YYYY-MM-DD" />
macros (styled date pill). Verify: <time datetime= in HTML.# Quick eval: fetch page and check for common issues (9 checks)
python3 -c "
import http.client, json, os, base64
email = os.environ.get('CONFLUENCE_EMAIL', '')
token = os.environ.get('CONFLUENCE_TOKEN', '')
auth = base64.b64encode(f'{email}:{token}'.encode()).decode()
base_url = os.environ.get('CONFLUENCE_BASE_URL', '').replace('https://', '').rstrip('/')
conn = http.client.HTTPSConnection(base_url)
conn.request('GET', '/wiki/rest/api/content/PAGE_ID?expand=body.storage',
headers={'Authorization': f'Basic {auth}'})
html = json.loads(conn.getresponse().read())['body']['storage']['value']
checks = []
checks.append(('No duplicate H1', '<h1' not in html))
checks.append(('Numbering columns', 'numberingColumn' in html))
checks.append(('Wide tables', 'data-table-width' in html))
checks.append(('No plain @mentions', 'citation' not in html))
checks.append(('TOC macro', 'ac:name=\"toc\"' in html))
checks.append(('Versioning subtext', 'version control and releases management' in html))
checks.append(('Grey headers', 'data-highlight-colour' in html))
checks.append(('Date macros', '<time datetime' in html))
checks.append(('No broken images', 'UNKNOWN_ATTACHMENT' not in html))
for name, passed in checks:
print(f'{\"PASS\" if passed else \"FAIL\"}: {name}')
print(f'{sum(1 for _,p in checks if p)}/{len(checks)} checks passed')
"
--wrap=none - prevents premature line breaks that Confluence renders literally refs, uploads missing attachments, converts to <ac:image> macros at ac:width="1350" (fills the wide content area). After creating a page, re-pushes HTML so image refs resolve against uploaded attachments (prevents UNKNOWN_ATTACHMENT bug).**bold label:** become Confluence info/note panels<pre><code> becomes Confluence code macro with CDATAdata-table-width="1800" data-layout="wide" and data-number-column="true" for full width with numbered rows# column stripping - auto-strips manual | # |, | Sr |, | S# | first columns from tables before adding Confluence numbering (prevents duplicate row numbers). Keep # columns in markdown for local readability — the script handles it.numberingColumn class) to all tables — header gets empty <th>, data rows get sequential <td> numbers<h1> from HTML output since Confluence uses the page title as H1 (avoids duplicate heading)> Source: [Confluence](...) info panels/blockquotes that are only meaningful in local markdown<th> content is wrapped in <strong> so header row text renders bold--wide flag sets both draft and published content appearance properties@Name to Confluence user mention macros using .local/team/users.md lookup. Handles pandoc's citation span wrapping automatically.<time> date pills. Keep plain dates in markdown for local readability — the script converts them on push.Pandoc column widths are the #1 table gotcha. Pandoc generates equal-percentage <col> widths
(e.g., 7 columns = 14% each). This makes ALL columns the same width regardless of content — a
short "Type" column gets the same width as a long "Rationale" column. The script now replaces
pandoc's <colgroup> with proportional pixel-based widths using a heuristic: columns with short
header names (< 10 chars) like "Type", "#", "Count" get narrow widths; columns with known long-
content names like "Rationale", "Summary", "Reason", "Description" get wide widths. If the auto-
heuristic is wrong, fix column widths post-push by replacing the <colgroup> block via REST API
with <col style="width: Npx;" /> values that match the content. Confluence uses pixel widths
in <col> tags inside <colgroup>, NOT data-colwidth on <td>/<th> cells.
Manual # columns are auto-stripped. The script detects manual numbering columns (| # |,
| Sr |, | S# |, | S.No |) and removes them before adding Confluence's built-in
numberingColumn. Keep # columns in your markdown for local readability — the script handles
the deduplication automatically.
Pandoc line wrapping is the #2 gotcha. Without --wrap=none, pandoc breaks lines at ~72 chars
and Confluence renders those as actual line breaks. The script already handles this, but if you ever
call pandoc directly, always include --wrap=none.
Paragraph width is a Confluence design decision. Even in wide mode, paragraph text constrains to
~740px readable width. Only tables expand with data-layout="wide". This is how Confluence works -
it's not a bug.
Single newlines in markdown merge into one paragraph. Pandoc treats a single newline as a space,
not a line break. If you have Q&A pairs (**Q:**\nA:) or intro text followed by a numbered list,
they'll render on the same line in Confluence. Always use blank lines between elements that
should be separate paragraphs - e.g., between a question and its answer, or between intro text
and a numbered/bulleted list.
Duplicate titles cause errors. Confluence rejects creating a page if another page with the same title exists in the space. The script reports the 400 error clearly.
Fabric editor rejects inline code in tables. Confluence's new Fabric editor throws
BAD_REQUEST: Content contains unsupported extensions when HTML contains <code> tags inside
<table> cells. The fix: replace all backtick code spans in markdown with bold (**text**)
before running pandoc. The script should strip <code> tags from table cells and wrap the content
in <strong> instead. If the script fails with this error, manually replace backticks in the
markdown source and retry.
Fabric editor rejects --create but accepts updates. The v2 page creation API
(POST /wiki/api/v2/pages) uses the Fabric editor which rejects <code> tags in tables,
numberingColumn, and colgroup. However, updating an existing page via the v1 API
(PUT /wiki/rest/api/content/{id}) works fine and preserves all formatting. Workaround for
--create failures: create the page manually via v1 API with minimal HTML, then immediately
update it using the script. The script's update path handles numbering, colgroups, and all
post-processing correctly.
Relative links MUST be fixed in the source .md, not in post-processing HTML. When markdown
references other spec files via relative paths (e.g., ../011-p2p-peer-to-peer-sync/p2p-peer-to-peer-sync.md),
these render as broken links on Confluence. Fixing them only in the pushed HTML is NOT sufficient
because the next full push regenerates HTML from the unchanged .md, bringing broken links back.
Before pushing, grep the markdown for ../ relative links and replace them IN THE .MD FILE with
the full Confluence URL from each target spec's header line (> Source: [Confluence](url) or
> Confluence: url). This is a one-time fix per file - once the .md has absolute Confluence URLs,
all future pushes will be correct.
Check: grep '\.\.\/' <file.md> - if any matches, fix them before pushing.
For the full list of Confluence storage format details (image format, callout panels, code blocks,
status badges, @mentions, colored cells, TOC macros), read references/confluence-format-guide.md.
Whether a name appears as plain text ("Jane Doe") or with @ prefix ("@Jane Doe") in the
local .md file, it MUST render as a Confluence @mention macro on the pushed page. Plain text names
are not acceptable on Confluence.
The script handles @Name patterns automatically. For plain text names (e.g., in Attendees lines
of decisions.md), do a post-push find-replace:
body.storage via REST API.local/team/users.md<ac:link><ri:user ri:account-id="ACCOUNT_ID" /></ac:link>Sort names by length (longest first) to avoid partial matches (e.g., "Jane Doe" before "Jane").
This script does a full page replacement. Pandoc CANNOT preserve Confluence-native macros:
<ac:structured-macro ac:name="jira">) become broken UUID text<ac:image>) are lost<ac:structured-macro ac:name="status">) become plain textBefore pushing to ANY existing page, check:
confluence-update.py for surgical updates:
add-section — add a new section before/after a headingfind-replace — replace specific textadd-table-row — add a versioning rowinsert — add an info callout before specific textThis script is ONLY safe for:
--create)Real incident: A full push to a dashboard page destroyed 12 images, 2 status badges, and 15 Jira smart links. Required reverting to a previous version and re-adding content surgically.
Use Step 0 (confluence-preflight.py) — it's the enforced version of this check.
The preflight does a 3-way diff against the snapshot baseline and fails loudly
if anyone has edited the page on Confluence since your last sync.
Real incident: a full-page push via this script removed answers that a reviewer had typed directly on the Confluence page. The snapshot + preflight system exists to make this class of failure detectable before the push.
Scripts live INSIDE the skill folder, NOT in a shared .claude/scripts/ folder:
.claude/skills/md-to-confluence/scripts/md-to-confluence.py~/.claude/skills/md-to-confluence/scripts/md-to-confluence.pyNEVER use symlinks — always copy actual Python files. Symlinks break when the target moves.
Keep both locations in sync when modifying the script.
brew install pandoc).env with: CONFLUENCE_EMAIL, CONFLUENCE_TOKEN, CONFLUENCE_BASE_URLdevelopment
Plan a webinar end-to-end using April Dunford's Obviously Awesome positioning framework to find the topic angle that makes the webinar obviously valuable to the right audience. Produces topic positioning, abstract, speaker brief, registration page, promotion sequence, day-of run-of-show, and post-webinar follow-up. Use when the user asks to plan a webinar, virtual event, online workshop, "we need a webinar on X", host a webinar, online masterclass, or any live virtual event with promotion and follow-up. Reads ICP, services, and brand voice from knowledge/.
development
Write long-form thought leadership articles, opinion pieces, industry POV essays, and CEO/founder bylines using the Made to Stick SUCCESs framework (Chip and Dan Heath). Use when the user asks for a long-form article, executive byline, opinion piece, industry POV, manifesto, "explain our point of view on X", or wants to publish an authority-building piece (1200-2500 words). Reads brand voice and positioning from knowledge/.
development
Plan a monthly content calendar across channels using the Content Marketing Matrix (Dave Chaffey, Smart Insights) - Entertain/Inspire/Educate/Convince. Every post gets a quadrant label. The monthly calendar must hit 40% Educate, 40% Inspire+Convince, 20% Entertain. Produces a week-by-week posting schedule with topics, formats, channels, and asset links. Use when the user says "content calendar", "social calendar", "plan next month's content", "what should we post", "content plan", "editorial calendar", "schedule posts for the month", or wants a structured posting plan for LinkedIn, Twitter, email, or blog. Reads brand voice, ICP, and past learnings from knowledge/.
development
Write SEO-optimized long-form articles targeting specific keywords using the They Ask You Answer Big 5 framework (Marcus Sheridan). Articles are categorized by Big 5 type (Cost, Problems, Versus, Best/Reviews, How-To) and structured accordingly. The "answer first" rule applies to every article. Use when the user asks for an SEO article, blog post for ranking, "rank for keyword X", organic content, search-optimized post, pillar page, or content for organic traffic. Includes keyword targeting, search intent matching, internal linking suggestions, and meta tags.