home / skills / a5c-ai / babysitter / confluence-docs

This skill synchronizes and migrates documentation to Confluence by automating page creation, updates, and format conversions across spaces.

npx playbooks add skill a5c-ai/babysitter --skill confluence-docs

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
12.1 KB
---
name: confluence-docs
description: Atlassian Confluence integration for enterprise documentation. Create and update pages via API, manage spaces and permissions, handle content migration, and sync between Markdown and Confluence.
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
backlog-id: SK-013
metadata:
  author: babysitter-sdk
  version: "1.0.0"
---

# Confluence Integration Skill

Atlassian Confluence integration for enterprise documentation.

## Capabilities

- Page creation and updates via API
- Space management and permissions
- Macro and template management
- Content migration (Markdown to Confluence)
- Attachment handling
- Label and metadata management
- Confluence Cloud and Server support
- Confluence-to-Markdown export

## Usage

Invoke this skill when you need to:
- Sync documentation to Confluence
- Migrate content between formats
- Manage Confluence spaces programmatically
- Automate page updates from CI/CD
- Export Confluence to Markdown

## Inputs

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| action | string | Yes | create, update, migrate, export |
| baseUrl | string | Yes | Confluence instance URL |
| spaceKey | string | Yes | Target space key |
| sourcePath | string | No | Source Markdown files |
| pageId | string | No | Specific page ID for updates |
| parentPageId | string | No | Parent page for hierarchy |

### Input Example

```json
{
  "action": "migrate",
  "baseUrl": "https://company.atlassian.net/wiki",
  "spaceKey": "DOCS",
  "sourcePath": "./docs",
  "parentPageId": "123456"
}
```

## Configuration

### confluence.config.json

```json
{
  "baseUrl": "https://company.atlassian.net/wiki",
  "auth": {
    "type": "token",
    "email": "${CONFLUENCE_EMAIL}",
    "token": "${CONFLUENCE_TOKEN}"
  },
  "space": {
    "key": "DOCS",
    "name": "Documentation"
  },
  "migration": {
    "preserveStructure": true,
    "convertTables": true,
    "uploadImages": true,
    "macroMapping": {
      "note": "info",
      "warning": "warning",
      "code": "code"
    }
  },
  "sync": {
    "dryRun": false,
    "updateExisting": true,
    "createMissing": true,
    "archiveRemoved": false
  }
}
```

## API Integration

### Confluence REST API Client

```javascript
const ConfluenceClient = require('confluence-api');

class ConfluenceManager {
  constructor(config) {
    this.client = new ConfluenceClient({
      username: config.email,
      password: config.token,
      baseUrl: config.baseUrl
    });
  }

  // Create a new page
  async createPage(spaceKey, title, content, parentId = null) {
    const page = {
      type: 'page',
      title,
      space: { key: spaceKey },
      body: {
        storage: {
          value: content,
          representation: 'storage'
        }
      }
    };

    if (parentId) {
      page.ancestors = [{ id: parentId }];
    }

    return await this.client.postContent(page);
  }

  // Update existing page
  async updatePage(pageId, title, content, version) {
    const page = {
      id: pageId,
      type: 'page',
      title,
      version: { number: version + 1 },
      body: {
        storage: {
          value: content,
          representation: 'storage'
        }
      }
    };

    return await this.client.putContent(page);
  }

  // Get page by title
  async getPageByTitle(spaceKey, title) {
    const result = await this.client.getContentBySpaceKey(spaceKey, {
      title,
      expand: 'version,body.storage'
    });
    return result.results[0] || null;
  }

  // Upload attachment
  async uploadAttachment(pageId, filePath, comment = '') {
    const form = new FormData();
    form.append('file', fs.createReadStream(filePath));
    form.append('comment', comment);

    return await this.client.createAttachment(pageId, form);
  }

  // Add labels
  async addLabels(pageId, labels) {
    const labelPayload = labels.map(name => ({
      prefix: 'global',
      name
    }));

    return await this.client.postLabels(pageId, labelPayload);
  }
}
```

## Markdown to Confluence Conversion

### Converter

```javascript
const marked = require('marked');

class MarkdownToConfluence {
  constructor(options = {}) {
    this.options = options;
    this.attachments = [];
  }

  convert(markdown, metadata = {}) {
    // Parse front matter
    const { content, frontMatter } = this.parseFrontMatter(markdown);

    // Convert markdown to HTML
    let html = marked.parse(content);

    // Convert to Confluence storage format
    html = this.convertToStorageFormat(html);

    // Handle macros
    html = this.convertMacros(html);

    // Handle code blocks
    html = this.convertCodeBlocks(html);

    // Handle images
    html = this.convertImages(html);

    // Handle tables
    html = this.convertTables(html);

    return {
      title: frontMatter.title || metadata.title,
      content: html,
      labels: frontMatter.tags || [],
      attachments: this.attachments
    };
  }

  convertMacros(html) {
    // Convert admonitions to Confluence macros
    const macroMap = {
      'note': 'info',
      'warning': 'warning',
      'tip': 'tip',
      'danger': 'warning'
    };

    for (const [mdType, confType] of Object.entries(macroMap)) {
      const regex = new RegExp(`<div class="${mdType}">([\\s\\S]*?)</div>`, 'g');
      html = html.replace(regex, (match, content) => {
        return `<ac:structured-macro ac:name="${confType}">
          <ac:rich-text-body>${content}</ac:rich-text-body>
        </ac:structured-macro>`;
      });
    }

    return html;
  }

  convertCodeBlocks(html) {
    // Convert code blocks to Confluence code macro
    return html.replace(
      /<pre><code class="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g,
      (match, language, code) => {
        const decodedCode = this.decodeHtml(code);
        return `<ac:structured-macro ac:name="code">
          <ac:parameter ac:name="language">${language}</ac:parameter>
          <ac:plain-text-body><![CDATA[${decodedCode}]]></ac:plain-text-body>
        </ac:structured-macro>`;
      }
    );
  }

  convertImages(html) {
    // Convert images to Confluence attachments
    return html.replace(
      /<img src="([^"]+)" alt="([^"]*)"[^>]*>/g,
      (match, src, alt) => {
        if (src.startsWith('http')) {
          // External image
          return `<ac:image><ri:url ri:value="${src}" /></ac:image>`;
        } else {
          // Local attachment
          const filename = path.basename(src);
          this.attachments.push({ src, filename });
          return `<ac:image><ri:attachment ri:filename="${filename}" /></ac:image>`;
        }
      }
    );
  }

  convertTables(html) {
    // Confluence uses standard HTML tables but needs specific attributes
    return html.replace(/<table>/g, '<table class="wrapped">');
  }
}
```

## Confluence to Markdown Export

### Exporter

```javascript
class ConfluenceToMarkdown {
  constructor(client) {
    this.client = client;
  }

  async exportSpace(spaceKey, outputDir) {
    const pages = await this.getAllPages(spaceKey);
    const structure = this.buildHierarchy(pages);

    for (const page of pages) {
      const markdown = await this.exportPage(page);
      const filePath = this.getFilePath(page, structure, outputDir);

      await fs.mkdir(path.dirname(filePath), { recursive: true });
      await fs.writeFile(filePath, markdown);
    }

    return { exported: pages.length };
  }

  async exportPage(page) {
    const content = page.body.storage.value;

    // Convert Confluence storage format to Markdown
    let markdown = this.convertToMarkdown(content);

    // Add front matter
    const frontMatter = {
      title: page.title,
      confluence_id: page.id,
      last_modified: page.version.when
    };

    return `---
${yaml.stringify(frontMatter)}---

${markdown}`;
  }

  convertToMarkdown(storage) {
    let md = storage;

    // Convert code macro
    md = md.replace(
      /<ac:structured-macro ac:name="code"[^>]*>[\s\S]*?<ac:parameter ac:name="language">(\w+)<\/ac:parameter>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g,
      (match, lang, code) => `\`\`\`${lang}\n${code}\n\`\`\``
    );

    // Convert info macro
    md = md.replace(
      /<ac:structured-macro ac:name="(info|warning|tip)"[^>]*>[\s\S]*?<ac:rich-text-body>([\s\S]*?)<\/ac:rich-text-body>[\s\S]*?<\/ac:structured-macro>/g,
      (match, type, content) => `> **${type.toUpperCase()}:** ${this.stripHtml(content)}`
    );

    // Convert headings, lists, etc.
    md = this.convertHtmlToMarkdown(md);

    return md;
  }
}
```

## Sync Workflow

### Bidirectional Sync

```javascript
async function syncDocumentation(config) {
  const confluence = new ConfluenceManager(config);
  const converter = new MarkdownToConfluence(config.migration);

  // Get local files
  const localFiles = await glob('docs/**/*.md');

  // Get Confluence pages
  const pages = await confluence.getSpaceContent(config.space.key);

  const results = {
    created: [],
    updated: [],
    skipped: [],
    errors: []
  };

  for (const file of localFiles) {
    try {
      const markdown = await fs.readFile(file, 'utf8');
      const converted = converter.convert(markdown, { file });

      // Check if page exists
      const existing = await confluence.getPageByTitle(
        config.space.key,
        converted.title
      );

      if (existing) {
        if (config.sync.updateExisting) {
          await confluence.updatePage(
            existing.id,
            converted.title,
            converted.content,
            existing.version.number
          );
          results.updated.push(file);
        } else {
          results.skipped.push(file);
        }
      } else if (config.sync.createMissing) {
        await confluence.createPage(
          config.space.key,
          converted.title,
          converted.content,
          config.parentPageId
        );
        results.created.push(file);
      }

      // Upload attachments
      for (const attachment of converted.attachments) {
        await confluence.uploadAttachment(
          existing?.id || results.created[results.created.length - 1].id,
          attachment.src
        );
      }
    } catch (error) {
      results.errors.push({ file, error: error.message });
    }
  }

  return results;
}
```

## Space Management

### Create Space

```javascript
async function createDocumentationSpace(config) {
  const client = new ConfluenceManager(config);

  const space = await client.client.postSpace({
    key: config.space.key,
    name: config.space.name,
    description: {
      plain: { value: config.space.description, representation: 'plain' }
    },
    permissions: [
      {
        subjects: { group: { name: 'confluence-users' } },
        operation: { key: 'read', target: 'space' }
      }
    ]
  });

  // Create home page
  await client.createPage(
    config.space.key,
    'Home',
    '<h1>Welcome to Documentation</h1>',
    null
  );

  return space;
}
```

## Workflow

1. **Configure** - Set up Confluence credentials and space
2. **Convert** - Transform Markdown to Confluence format
3. **Sync** - Upload/update pages via API
4. **Attachments** - Upload images and files
5. **Labels** - Apply labels for organization
6. **Verify** - Check page rendering

## Dependencies

```json
{
  "devDependencies": {
    "confluence-api": "^1.4.0",
    "marked": "^12.0.0",
    "gray-matter": "^4.0.0",
    "form-data": "^4.0.0"
  }
}
```

## CLI Commands

```bash
# Sync Markdown to Confluence
node scripts/confluence-sync.js --config confluence.config.json

# Export Confluence to Markdown
node scripts/confluence-export.js --space DOCS --output ./exported

# Create new space
node scripts/confluence-space.js create --key NEWDOCS --name "New Documentation"
```

## Best Practices Applied

- Use page templates for consistency
- Organize with parent pages
- Apply labels for discoverability
- Keep source of truth in Git
- Sync on merge to main branch
- Handle attachments properly

## References

- Confluence REST API: https://developer.atlassian.com/cloud/confluence/rest/
- Storage Format: https://confluence.atlassian.com/doc/confluence-storage-format-790796544.html
- confluence-api npm: https://www.npmjs.com/package/confluence-api

## Target Processes

- knowledge-base-setup.js
- docs-pr-workflow.js
- content-strategy.js

Overview

This skill provides an Atlassian Confluence integration for enterprise documentation workflows. It creates and updates pages, manages spaces and permissions, and synchronizes content between Markdown and Confluence. It supports Confluence Cloud and Server and includes migration and export utilities.

How this skill works

The skill uses a Confluence REST API client to create, update, and fetch pages, manage attachments and labels, and handle space creation. A Markdown-to-Confluence converter transforms front matter, code blocks, images, macros, and tables into Confluence storage format and collects attachments for upload. A reverse exporter converts Confluence storage format back to Markdown with front matter for offline editing or source control.

When to use it

  • Migrate a Markdown docs site into a Confluence space while preserving structure and attachments.
  • Keep Confluence pages in sync with a Git-based source of truth via CI/CD.
  • Programmatically create or configure documentation spaces and permissions.
  • Export Confluence content to Markdown for backups, reviews, or repo sync.
  • Automate label, template, and macro management across many pages.

Best practices

  • Keep canonical content in Git and run sync on merges to main to prevent divergence.
  • Use page templates and parent pages to maintain a consistent hierarchy.
  • Enable dry-run mode during initial migrations to inspect changes before writing.
  • Map common macros and admonitions to Confluence equivalents in configuration.
  • Upload attachments via the API and reference them as Confluence attachments, not raw URLs.

Example use cases

  • Run a migration to import ./docs into the DOCS Confluence space, preserving directories as page hierarchies.
  • Trigger an update job from CI to push rendered docs whenever a release branch is merged.
  • Create a new documentation space with a predefined home page and read permissions for a user group.
  • Export an entire Confluence space to a Markdown tree for auditing or content reviews.
  • Batch-apply labels and upload image attachments when converting blog-style markdown into Confluence pages.

FAQ

Does this support both Cloud and Server Confluence?

Yes. The API client supports Cloud and Server endpoints; adjust baseUrl and auth in configuration.

How are images handled during migration?

Local images are collected as attachments and uploaded to the target page; external images are preserved as external URLs.