Skip to content

Latest commit

 

History

1,185 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mark

All Contributors

Mark — a tool for syncing your markdown documentation with Atlassian Confluence pages.

This is very useful if you store documentation to your software in a Git repository and don't want to do an extra job of updating Confluence page using a tinymce wysiwyg enterprise core editor which always breaks everything.

Mark does the same but in a different way. Mark reads your markdown file, creates a Confluence page if it's not found by its name, uploads attachments, translates Markdown into HTML and updates the contents of the page via REST API. It's like you don't even need to create sections/pages in your Confluence anymore, just use them in your Markdown documentation.

Mark uses an extended file format, which, still being valid markdown, contains several HTML-ish metadata headers, which can be used to locate page inside Confluence instance and update it accordingly.

File metadata can be written as YAML front matter when the frontmatter feature is enabled. Because specifying --features replaces the defaults, include the default features explicitly if needed:

mark --features=mermaid --features=mention --features=frontmatter

YAML front matter uses the following format:

---
space: <space key>
parents:
  - <parent 1>
  - <parent 2>
folders:
  - <folder 1>
  - <folder 2>
title: <title>
attachments:
  - <local path>
labels:
  - <label 1>
  - <label 2>
image-align: <left|center|right>
order: <whole number>
---

<page contents>

A list key given a single value, as in labels: release, reads it as a list of one. A value that is neither a string nor a list of strings is ignored, and warned about.

The legacy HTML header format is also supported:

When both formats are present, HTML headers override matching scalar front matter values. Repeatable parents, folders, attachments, and labels are appended to the values from front matter.

Note that the front matter keys are plural where the HTML headers are singular: attachments, parents, folders, labels and properties. A key mark does not read is ignored, and warned about:

WARNING doc.md: front matter key "attachment" is not read by mark and was ignored

Front matter shared with another tool -- a static site generator's date and draft in the same block -- is reported the same way, since mark cannot tell a key meant for something else from one meant for mark and misspelled.

<!-- Space: <space key> -->
<!-- Parent: <parent 1> -->
<!-- Parent: <parent 2> -->
<!-- Folder: <folder 1> -->
<!-- Folder: <folder 2> -->
<!-- Title: <title> -->
<!-- Attachment: <local path> -->
<!-- Label: <label 1> -->
<!-- Label: <label 2> -->
<!-- Property: <key>=<value> -->
<!-- Synchronized: <true|false> -->
<!-- Image-Align: <left|center|right> -->

<page contents>

Headers are read from the run of comments the document opens with, and only from there. Blank lines between them are fine, so headers may be grouped — identity, then labels, then properties — but anything else ends the run, and a header written below the page's own text is published as text rather than applied. Mark warns when it finds one there.

Only a comment naming one of the headers above is read as one. A comment that names something else — <!-- TODO: ... -->, <!-- Author: ... -->, an SPDX line, an editor modeline — is an ordinary comment: it stays in the page and the headers around it still apply. So do the ac: markers, which are storage-format markup rather than metadata, and a Macro definition, which belongs to the macro expander; the layout markers may therefore follow <!-- Layout: plain --> directly, as Customizing the page layout shows.

A comment that is nearly a header is an error: <!-- Titel: ... --> fails the run naming the header it was probably meant to be. The line is taken out of the page either way, so publishing the document without its title and exiting zero was the alternative.

An Include directive may be written among the headers or anywhere below them. It is left in the page for the include expander to find.

There can be any number of Parent headers, if Mark can't find specified parent by title, Mark creates it.

Parents from the directory a file is in

--parents-from-path puts each page under a page named after every directory between the file pattern's root and the file itself, so the page tree comes out looking like the repository:

mark --parents-from-path --files "docs/**/*.md"
docs/guides/setup.md        ->  Guides > Setup
docs/guides/deep/tuning.md  ->  Guides > Deep > Tuning

Directory names are titled the way filenames are, so getting-started becomes "Getting Started".

An index.md or README.md is its directory's page rather than a page inside it, whatever the case of its name. So docs/guides/README.md is the page that setup.md sits under. Only a document the run publishes counts: a README the --files pattern leaves out neither names its directory's page nor stands for it. The document standing for the root itself is titled after the root directory, there being no directory above it.

What that page is called is decided once, and the documents beneath it look for the same name, so the two cannot disagree:

  1. the Title header or front matter of the directory's own document
  2. its leading heading, with --title-from-h1
  3. a title: in a .pages file beside the documents, for a directory that has no document of its own
  4. the directory's name

The filename is never used. It is README in every directory that has one, which names a file rather than a page.

The root is everything in --files before the first wildcard, and --parents-from-path-root overrides that where the guess is wrong. A root given by hand that none of the files lie under is refused, and a file outside it is reported and published without derived parents. A document that names its own Parent is left where it asks to be, and --parents still prefixes everything.

One page per title

A space holds one page of a given title, so two documents wanting the same title want the same page: the second would overwrite the first and drag it under its own parents. Deriving parents from the path makes that likely rather than unlucky -- every directory tends to hold a README, and several will want an "Overview".

Mark refuses before publishing the second one, comparing titles the way Confluence does, without regard to case:

docs/api/overview.md already publishes "Overview" in space "DOCS", and a space
holds one page of a title: rename one of them, or use
--title-append-generated-hash

--title-append-generated-hash is the way to keep both titles as they are: it appends a short hash of the page's parents, space and title, taken once the path has supplied the parents, so it differs between two documents in different directories.

Directories are remembered too

Mark creates the page standing for a directory, so it remembers doing so. When the last document under a directory goes away, that page turns up as a page with no source file like any other, and --on-orphan decides what becomes of it:

mark --parents-from-path --track-pages --on-orphan delete --files "docs/**/*.md"

A directory's page is only removable once it holds nothing, since a page with children is always left alone. So a directory and everything in it takes two runs to disappear: the documents first, then the page that held them.

A directory holding its own index.md or README.md is not remembered this way, that document's own entry having the page already. Directories are remembered by their path as the run names it, so as with the files themselves, the working directory is part of what the manifest matches on.

Turning it on for pages that already exist

Every page not already where its path implies will be moved on the next run. --dry-run reports the moves without making them, and is worth reading first on a space anybody else works in.

Changing a Parent header on a page that has already been published moves that page to the new parent. Mark treats your repository as the source of truth for where a page lives, as it already does for the page's title and content, so a page somebody has moved by hand in Confluence is moved back on the next run. A page nested below its declared parent is left where it is: every parent the headers name is still in its ancestry, so nothing is contradicted.

The Folder header allows organizing pages within Confluence folders. Like Parent headers, if Mark can't find the specified folder by title, it creates it. Folders and parents can be mixed in the same document to create complex hierarchies.

Note

Folder support is currently only available on Confluence Cloud and is not supported in Confluence Server / Data Center.

Also, optional following headers are supported:

<!-- Layout: (article|plain) -->
  • (default) article: content will be put in narrow column for ease of reading;
  • plain: content will fill all page;
<!-- Type: (page|blogpost) -->
  • (default) page: normal Confluence page - defaults to this if omitted
  • blogpost: Blog post in Space. Cannot have Parent(s)
<!-- Content-Appearance: (full-width|fixed|default) -->
  • (default) full-width: content will fill the full page width
  • fixed: content will be rendered in a fixed narrow view
  • default: sets the Confluence property value to "default", which is the narrow layout as set by the Confluence UI. Note: fixed maps to a different Confluence property value and can cause misaligned page title and body content — use default instead for the narrow layout.
<!-- ac:ignore -->
content that stays out of Confluence
<!-- ac:ignore end -->

Everything between the two markers is left out of the published page, along with the markers themselves. It is for content that reads well in one place and badly in the other -- a table of contents that Confluence builds for itself, or a plain-text stand-in for a macro:

<!-- Include: ac:profile
     Name: Doe, John -->
<!-- ac:ignore -->
John Doe's profile
<!-- ac:ignore end -->

Read on GitHub the file shows the name; published to Confluence it shows the profile macro. Included files may mark regions of their own, which is stripped as each file is read. Attachments referenced only inside an ignored region are not uploaded, since nothing on the page would point at them. A marker without its pair is an error rather than a guess -- quietly publishing half a page is worse than refusing.

<!-- Order: <number> -->

Positions the page among its siblings, smaller numbers first. Pages that do not declare an order are left exactly where Confluence has them, so annotating one page does not disturb the rest.

Only pages published in the same run are arranged, and only relative to each other -- a run narrowed with --files knows nothing about the other children of those parents and will not rearrange them. Nothing is moved that is already in the right relative order, so a run that changes nothing performs no moves at all.

Note that giving Confluence explicit positions takes that branch of the tree out of its default alphabetical ordering, which is inherent to asking for a particular order.

Note

The content move endpoint mark uses for ordering and reparenting is served only by Confluence Cloud. On Server and Data Center mark moves pages through /pages/movepage.action, the action behind the Move dialog, which needs the credentials to be accepted outside /rest. Where it is not, a page moved to a different parent is moved with an update carrying the new ancestor instead, and a page that has to be repositioned among its siblings fails the run.

<!-- Sidebar: <h2>Test</h2> -->

Setting the sidebar creates a column on the right side. You're able to add any valid HTML content. Adding this property sets the layout to article.

<!-- Emoji: 🚀 -->

You can set a page emoji icon by specifying the icon in the headers.

<!-- Image-Align: center -->

You can set the alignment for all images in the page. Common values are left, center, and right. Can also be set globally via the --image-align CLI option (per-page header takes precedence).

Note: Images with width >= 760px automatically use center instead of the configured alignment, as Confluence requires this for wide images.

Mark supports Go templates, which can be included into article by using path to the template relative to current working dir, e.g.:

<!-- Include: <path> -->

If the template cannot be found relative to the current directory, a fallback directory can be defined via --include-path. This way it is possible to have global include files while local ones will still take precedence.

Whitespace inside ac:parameter

A macro parameter holding a storage-format element must hold nothing else, so Mark publishes such a parameter with the whitespace taken out of it. A template can be written to be read:

<ac:structured-macro ac:name="inc-drawio">
  <ac:parameter ac:name="name">
    <ri:attachment ri:filename="{{ .Name }}"/>
  </ac:parameter>
</ac:structured-macro>

and reaches Confluence as <ac:parameter ac:name="name"><ri:attachment ri:filename="..."/></ac:parameter>.

Left as written, the newlines would make the parameter hold text as well as an element, and Confluence resolves that by writing the element out as a string: the page ends up carrying AttachmentResourceIdentifier[...,filename=...] where the attachment should be, with nothing reported, since Mark published what it was given.

Only a parameter whose value is an element is tightened, which is decided by its content beginning with < and ending with >. A parameter holding a string keeps its spacing, because there the spacing is the value. Whitespace anywhere else is left alone and has to be: the blank lines inside <ac:rich-text-body> are what let a macro's body be read as Markdown, and a body ending in a list would otherwise swallow the closing tags.

Optionally the delimiters can be defined:

<!-- Include: <path>
     Delims: "<<", ">>"
     -->

Or they can be switched off to disable processing:

<!-- Include: <path>
     Delims: none
     -->

Note: Switching delimiters off really simply changes them to ASCII characters "\x00" and "\x01" which, usually should not occure in a template.

Templates can accept configuration data in YAML format which immediately follows the Include and Delims tag, if present:

<!-- Include: <path>
     <yaml-data> -->

Includes can be nested inside other included templates. A directive inside an included file is resolved relative to that file's own directory first, the way the links it holds are, so sub/a.md can include b.md to mean sub/b.md. If nothing of that name is there, it is looked for relative to the document and then in --include-path, as it always was, so a nested include written from the document's directory keeps working. Where both exist, the file beside the fragment is used and Mark logs a warning naming the one it did not use. Either way the file has to be inside the document's directory, the directory Mark is running in, or --include-path; a ../ that climbs out of those is refused. Includes produced by a macro, and the Template of a macro defined in an included file, are resolved relative to the document, since macros are expanded over the whole document. Furthermore, included files can define page metadata (such as Title, Space, Parent, etc.) or macro definitions. Circular inclusion loops are automatically detected and reported as an error.

Mark also supports attachments. The standard way involves declaring an Attachment along with the other items in the header, then have any links with the same path:

<!-- Attachment: <path-to-image> -->

<beginning of page content>

An attached link is [here](<path-to-image>)

A path may be a pattern, in the same syntax --files uses, which uploads every file it matches:

<!-- Attachment: images/*.png -->
<!-- Attachment: media/**/*.svg -->

Each match is attached under the path it was found at, so a link or image in the page refers to it exactly as it would have without the pattern — ![](images/logo.png). A pattern is not reported as an unused attachment when the page does not link to all of it, since uploading a directory is the point of writing one.

A name with no pattern characters in it is a path, exactly as before. So is one whose pattern matches nothing: a file really called report[2024].pdf still attaches, and a pattern that was meant to match something reports the path as written when nothing is there.

A pattern reaches no further than a path does — the document's own directory or the one mark is running in — so ../../*.pem is refused rather than swept up.

A link or image destination is a URL, so a character that means something in one is written percent-encoded: ![](a%23b.png) for a file called a#b.png, [notes](100%25.pdf) for 100%.pdf, and my%20file.png or <my file.png> for my file.png. The destination is tried exactly as written first, so a file really called my%20file.png is still found under that name.

An image is uploaded whether or not it is declared: ![](images/logo.png) attaches the file and shows it. A link to a file is not, and is published as the path the document wrote -- which means nothing once the page is on Confluence, so the reader gets a link that leads nowhere.

--attach-referenced uploads those too, and links to the attachment:

See [the report](files/report.pdf).

Only a file that is there, beside the document, and only what a document may read anyway: the same boundary an attachment is held to, so a link reaching out of the project is refused rather than published. A URL, an anchor, a mail address, a rooted path, a name with nothing behind it, and a link to another document are all left exactly as they are -- the last of those because linking to a page is not asking to publish its source as a download.

NOTE: Be careful with Attachment! If your path string is a subset of another longer string or referenced in text, you may get undesired behavior.

Mark also supports macro definitions, which are defined as regexps which will be replaced with specified template:

<!-- Macro: <regexp>
     Template: <path>
     <yaml-data> -->

NOTE: Make sure to define your macros after your metadata (Title/Space), mark will stop processing metadata if it hits a Macro.

Capture groups can be defined in the macro's which can be later referenced in the <yaml-data> using ${<number>} syntax, where <number> is number of a capture group in regexp (${0} is used for entire regexp match), for example:

  <!-- Macro: MYJIRA-\d+
       Template: ac:jira:ticket
       Ticket: ${0} -->

Macros can also use inline templates. Inline templates are templates where the template content is described in the <yaml-data>. The Template value starts with a #, followed by the key used in the <yaml-data>. The key's value must be a string which defines the template's content.

  <!-- Macro: <tblbox\s+(.*?)\s*>
       Template: #inline
       title: ${1}
       inline: |
           <table>
           <thead><tr><th>{{ .title }}</th></tr></thead>
           <tbody><tr><td>
        -->
  <!-- Macro: </tblbox>
       Template: #also_inline
       also_inline: |
           </td></tr></tbody></table>
        -->
  <tblbox with a title>
  and some
  content
  </tblbox>

An inline template can use everything a file template can: the xmlesc and cdata functions, and the built-in templates by name, such as {{ template "ac:status" . }}. A ${<number>} written in an inline template is replaced by the matched text as text, never as template code, so a match holding {{ ... }} is published as written. Inside an action, write it within a quoted string, as in {{ "${1}" | xmlesc }}.

Macro templates can also output <!-- Include: ... --> directives, allowing macros to dynamically load external template files or include other documents.

Automatic Page Title

If you don't want to specify the page title in the metadata of each file, mark provides two ways to set it automatically.

From the first H1 heading

You can use the --title-from-h1 flag to extract the page title from the first H1 heading in the markdown file. If no H1 heading is found, the title must be set in the page metadata.

From the filename

You can use the --title-from-filename flag to use the filename (without the extension) as the page title. mark will automatically convert the filename to a more readable title by:

  • Replacing underscores (_) and dashes (-) with spaces.
  • Applying title case to the filename.

For example, a file named my_awesome-page.md will have the title "My Awesome Page".

These two options are mutually exclusive. If both flags are provided, mark will produce an error.

Customizing the page layout

If you set the Layout to plain, the page layout can be customized using HTML comments inside the markdown:

<!-- Layout: plain -->
<!-- ac:layout -->

<!-- ac:layout-section type:three_with_sidebars -->
<!-- ac:layout-cell -->
More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-cell -->
More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-cell -->
Even More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-section end -->

<!-- ac:layout-section type:single -->
<!-- ac:layout-cell -->
Still More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-section end -->

<!-- ac:layout end -->

Please be aware that mark does not validate the layout, so it's your responsibility to create a valid layout.

Placeholders

You can use this to define placeholders:

<!-- ac:placeholder -->
Placeholder
<!-- ac:placeholder end -->

Code Blocks

```bash
...
some long bash code block
...
```
Parameter Default
collapse false
title none
linenumbers false
1 (any number for firstline) 1

Example:

  • bash collapse If you have long code blocks, you can make them collapsible.
  • bash collapse title Some long long bash function And you can also add a title.
  • bash linenumbers collapse title Some long long bash function And linenumbers.
  • bash 1 collapse title Some long long bash function Or directly give a number as firstline number.
  • bash 1 collapse midnight title Some long long bash function And even themes.
  • - 1 collapse midnight title Some long long code Please note that, if you want to have a code block without a language use - as the first character, if you want to have the other goodies.

More details at Confluence Code Block Macro doc.

Block Quotes

GitHub Alerts Support

You can now use GitHub-style alert syntax in your markdown, and Mark will automatically convert them to Confluence macros:

> [!NOTE]
> This creates a blue info box - perfect for helpful information!

> [!TIP]
> This creates a green tip box - great for best practices and suggestions!

> [!IMPORTANT]
> This creates a blue info box - ideal for critical information!

> [!WARNING]
> This creates a yellow warning box - use for important warnings!

> [!CAUTION]
> This creates a red warning box - perfect for dangerous situations!

Technical Details

Block Quotes are converted to Confluence Info/Warn/Note box when the following conditions are met:

  1. The BlockQuote is on the root level of the document (not nested)
  2. The first line of the BlockQuote contains one of the following patterns Info/Warn/Note or GitHub MD Alerts style [!NOTE]/[!TIP]/[!IMPORTANT]/[!WARNING]/[!CAUTION]
GitHub Alerts Confluence Description
[!TIP] (green lightbulb) Tip (green checkmark in circle) Helpful suggestions and best practices
[!NOTE] (blue I in circle) Info (blue I in circle) General information and notes
[!IMPORTANT] (purple exclamation mark in speech bubble) Info (blue I in circle) Critical information that needs attention
[!WARNING] (yellow exclamation mark in triangle) Note (yellow exclamation mark in triangle) Important warnings and cautions
[!CAUTION] (red exclamation mark in hexagon) Warning (red exclamation mark in hexagon) Dangerous situations requiring immediate attention

In any other case the default behaviour will be resumed and html <blockquote> tag will be used

Definition Lists

A definition list is published as a two-column table, with each term heading its own row:

Term
: What it means

Another term
: One definition
: And a second

Confluence storage format has no <dl>, <dt> or <dd> — its sanitiser drops all three, which left the list as an unstructured run of text with the terms no longer distinguishable from what defined them. A table is what the Confluence editor produces for this shape, and it is what the elements mean.

Several definitions of one term share its cell rather than each taking a row, since a row with no term to head it reads as though something is missing.

Task Lists

Mark supports GitHub Flavored Markdown task lists. Task lists are automatically converted to Confluence ac:task-list elements.

- [x] Finished task
- [ ] Unfinished task

A list holding both kinds is published as one Confluence task list per run of checkboxes and one ordinary list per run of bullets, in the order they were written — mixing <ac:task> and <li> inside a single container is not something the storage format allows.

An ordered mixed list is the exception: splitting it would restart the numbering at every run, so it falls back to a standard list with textual [x] and [ ] markers, which keeps the completion state visible if not actionable.

Template & Macros

By default, mark provides several built-in templates and macros:

  • template ac:status to include badge-like text, which accepts following parameters:

    • Title: text to display in the badge
    • Color: color to use as background/border for badge
      • Grey
      • Red
      • Yellow
      • Green
      • Blue
    • Subtle: specify to fill badge with background or not
      • true
      • false
  • template ac:boxto include info, tip, note, and warning text boxes. Parameters:

    • Name: select box style
      • info
      • tip
      • note
      • warning
    • Icon: show information/tip/exclamation mark/warning icon
      • true
      • false
    • Title: title text of the box
    • Body: text to display in the box

    See: https://confluence.atlassian.com/conf59/info-tip-note-and-warning-macros-792499127.html

  • template ac:jira:ticket to include JIRA ticket link. Parameters:

    • Ticket: Jira ticket number like BUGS-123.

    See: https://confluence.atlassian.com/conf59/status-macro-792499207.html

  • template ac:jira:filter to include JIRA Filters/Searches. Parameters:

    • JQL: The "JQL" query of the search
    • Server (Optional): The Jira server to fetch the query from if its not the default of "System Jira"
  • template ac:jiraissues to include a list of JIRA tickets. Parameters:

    • URL (Required), The URL of the XML view of your selected issues. (link to the filter)
    • Anonymous (Optional) If this parameter is set to 'true', your JIRA application will return only the issues which allow unrestricted viewing. That is, the issues which are visible to anonymous viewers. If this parameter is omitted or set to 'false', then the results depend on how your administrator has configured the communication between the JIRA application and Confluence. By default, Confluence will show only the issues which the user is authorised to view.
    • BaseURL (Optional) If you specify a 'baseurl', then the link in the header, pointing to your JIRA application, will use this base URL instead of the value of the 'url' parameter. This is useful when Confluence connects to JIRA with a different URL from the one used by other users.
    • Columns (Optional) A list of JIRA column names, separated by semi-colons (;). You can include many columns recognized by your JIRA application, including custom columns.
    • Count (Optional) If this parameter is set to 'true', the issue list will show the number of issues in JIRA. The count will be linked to your JIRA site.
    • Cache (Optional) The macro maintains a cache of the issues which result from the JIRA query. If the 'cache' parameter is set to 'off', the relevant part of the cache is cleared each time the macro is reloaded. (The value 'false' also works and has the same effect as 'off'.)
    • Height (Optional) The height in pixels of the table displaying the issues.
    • RenderMode (Optional) If the value is 'dynamic', the JIRA Issues macro offers an interactive display.
    • Title (Optional) You can customise the title text at the top of the issues table with this parameter. For instance, setting the title to 'Bugs-to-fix' will replace the default 'JIRA Issues' text. This can help provide more context to the list of issues displayed.
    • Width (Optional) The width of the table displaying the issues. Can be entered as a percentage (%) or in pixels (px).

    See: https://confluence.atlassian.com/doc/jira-issues-macro-139380.html

  • template: ac:emoticon to include emoticons. Parameters:

    • Name: select emoticon
      • smile
      • sad
      • cheeky
      • laugh
      • wink
      • thumbs-up
      • thumbs-down
      • information
      • tick
      • cross
      • warning
      • plus
      • minus
      • question
      • light-on
      • light-off
      • yellow-star
      • red-star
      • green-star
      • blue-star

    See: https://confluence.atlassian.com/doc/confluence-storage-format-790796544.html

  • template: ac:youtube to include YouTube Widget. Parameters:

    • URL: YouTube video endpoint
    • Width: Width in px. Defaults to "640px"
    • Height: Height in px. Defaults to "360px"

    See: https://confluence.atlassian.com/doc/widget-connector-macro-171180449.html#WidgetConnectorMacro-YouTube

  • template: ac:children to include Children Display macro

    • Reverse (Reverse Sort): Use with the Sort Children By parameter. When set, the sort order changes from ascending to descending.
      • true
      • false (Default)
    • Sort (Sort Children By):
      • creation — to sort by content creation date
      • title — to sort alphabetically on title
      • modified — to sort of last modification date.
      • If not specified, manual sorting is used if manually ordered, otherwise alphabetical.
    • Style (Heading Style): Choose the style used to display descendants.
      • from h1 to h6
      • If not specified, default style is applied.
    • Page (Parent Page):
      • / — to list the top-level pages of the current space, i.e. those without parents.
      • pagename — to list the children of the specified page.
      • spacekey:pagename — to list the children of the specified page in the specified space.
      • If not specified, the current page is used.
    • Excerpt (Include Excerpts): Allows you to include a short excerpt under each page in the list.
      • none - no excerpt will be displayed. (Default)
      • simple - displays the first line of text contained in an Excerpt macro any of the returned pages. If there is not an Excerpt macro on the page, nothing will be shown.
      • rich content - displays the contents of an Excerpt macro, or if there is not an Excerpt macro on the page, the first part of the page content, including formatted text, images and some macros.
    • First (Number of Children): Restrict the number of child pages that are displayed at the top level.
      • If not specified, no limit is applied.
    • Depth (Depth of Descendants): Enter a number to specify the depth of descendants to display. For example, if the value is 2, the macro will display 2 levels of child pages. This setting has no effect if Show Descendants is enabled.
      • If not specified, no limit is applied.
    • All (Show Descendants): Choose whether to display all the parent page's descendants.
      • true
      • false (Default)

    See: https://confluence.atlassian.com/doc/children-display-macro-139501.html

  • template: ac:iframe to include iframe macro (cloud only)

    • URL: URL to the iframe.
    • Frameborder: Choose whether to draw a border around content in the iframe.
      • show (Default)
      • hide
    • Width: Width in px. Defaults to "640px"
    • Height: Height in px. Defaults to "360px"
    • Scrolling: Allow or prevent scrolling in the iframe to see additional content.
      • yes
      • no
      • auto (Default)
    • Align: Align the iframe to the left or right of the page.
      • left (Default)
      • right

    See: https://support.atlassian.com/confluence-cloud/docs/insert-the-iframe-macro

  • template: ac:blog-poststo include blog-posts

    • Content: How much content will be shown
      • titles (default)
      • excerpts
      • entire
    • Time: Specify how much back in time Confluence should look for blog posts (default: unlimited)
    • Label: Restrict to blog posts with specific labels
    • Author: Restrict to blog posts by specific authors
    • Spaces: Restrict to blog posts in specific spaces
    • Max: Maximum number of blog posts shown (default: 15)
    • Sort: Sorting posts by
      • title
      • creation (default)
      • modified
    • Reverse: Reverses the Sort parameter from oldest to newest (default: false)

    See: https://confluence.atlassian.com/doc/blog-posts-macro-139470.html

  • template: ac:include to include a page

    • Page: the page to be included
    • Space: the space the page is in (optional, otherwise same space)
  • template: ac:excerpt-include to include the excerpt from another page

    • Page: the page the excerpt should be included from
    • Name: The specific identifier for the excerpt, allowing multiple Excerpt macros on one page to be referenced individually. If not provided, the first excerpt from the page will be used (optional, cloud only)
    • NoPanel: Determines whether Confluence will display a panel around the excerpted content (optional, default: false)
  • template: ac:excerpt to create an excerpt and include it in the page

    • Excerpt: The text you want to include
    • Name: Allows you to identify this macro so that you can add multiple Excerpt macros to one page and use a specific one on another page using the Excerpt Include macro (optional, cloud only)
    • OutputType: Determines whether the content of the Excerpt macro body is displayed on a new line or inline (optional, options: "BLOCK" or "INLINE", default: BLOCK)
    • Hidden: Hide the excerpt content (optional, default: false)
  • template: ac:anchor to set an anchor inside a page

    • Anchor: Text for the anchor
  • template: ac:expand to display an expandable/collapsible section of text on your page

    • Title: Defines the text next to the expand/collapse icon.
    • Body: The Text that it is expanded to.
  • template: ac:profile to display a short summary of a given Confluence user's profile.

    • Name: The username of the Confluence user whose profile summary you wish to show.
  • template: ac:contentbylabel to display a list of pages, blog posts or attachments that have particular labels

    • CQL: The CQL query to discover the content
  • template: ac:detailssummary to show summary information from one page on a another page

    • Headings: Column headings to show
    • FirstColumn: Name of the Title Column
    • CQL: The CQL query to discover the pages
    • SortBy: Sort by a specific column heading
  • template: ac:details to create page properties

    • Body: Must contain a table with two rows, the table headings are used as property key. The table content is the value.
  • template: ac:panel to display a block of text within a customisable panel

    • Title: Panel title (optional)
    • Body: Body text of the panel
    • BGColor: Background Color
    • TitleBGColor: Background color of the title bar
    • TitleColor: Text color of the title
    • BorderStyle: Style of the panel's border
    • BorderColor: Color of the panel's border
  • template ac:recently-updated to display a list of most recently changed content

    • Spaces: List of Spaces to watch (optional, default is current Space)
    • ShowProfilePic: Show profile picture of editor
    • Max: Maximum number of changes
    • Types: Include these content types only (comments, blogposts, pages)
    • Theme: Apperance of the macro (concise, social, sidebar)
    • HideHeading: Determines whether the macro hides or displays the text 'Recently Updated' as a title above the list of content
    • Labels: Filter the results by label. The macro will display only the pages etc which are tagged with the label(s) you specify here.
  • template: ac:pagetreesearch to add a search box to your Confluence page.

    • Root: Name of the root page whose hierarchy of pages will be searched by this macro. If this not specified, the root page is the current page.
  • template: ac:column To be used with the section macro to define the columns in a page.

    • Width: Width of the column
    • Body: The content of the column, rich text as in ac:box and ac:panel: Markdown and storage-format markup are rendered, not shown as text
  • template: ac:multimedia to embedd an attached video, animation or other multimedia files in a Confluence page

    • Name: Name of the file
    • Width: Width of the video (optional)
    • AutoPlay: Start playing the file on page load (default: false)
  • template ac:view-file

    • Name: Name of the file
    • Height: height of the view
  • macro @{...} to mention user by name specified in the braces.

Template & Macros Usecases

Insert Disclaimer

This should be in disclaimer.md.

**NOTE**: this document is generated, do not edit manually.

Add this to your article.md.

<!-- Space: TEST -->
<!-- Title: My Article -->

<!-- Include: disclaimer.md -->

This is my article.

Insert Status Badge

<!-- Space: TEST -->
<!-- Title: TODO List -->

<!-- Macro: :done:
     Template: ac:status
     Title: DONE
     Color: Green -->

<!-- Macro: :todo:
     Template: ac:status
     Title: TODO
     Color: Blue -->

* :done: Write Article
* :todo: Publish Article

Insert Colored Text Box

<!-- Space: TEST -->
<!-- Title: Announcement -->

<!-- Macro: :box:([^:]+):([^:]*):(.+):
     Template: ac:box
     Icon: true
     Name: ${1}
     Title: ${2}
     Body: ${3} -->

:box:info::Foobar:
:box:tip:Tip of day:Foobar:
:box:note::Foobar:
:box:warning:Alert!:Foobar:

Insert Table of Contents

<!-- Include: ac:toc -->

If default TOC looks don't find a way to your heart, try parametrizing it, for example:

<!-- Macro: :toc:
     Template: ac:toc
     Printable: 'false'
     MinLevel: 2 -->

# This is my nice title

:toc:

You can call the Macro as you like but the Template field must have the ac:toc value. Also, note the single quotes around 'false'.

See Confluence TOC Macro for the list of parameters - keep in mind that here they start with capital letters. Every skipped field will have the default value, so feel free to include only the ones that you require.

Insert PageTree

# My First Heading
<!-- Include: ac:pagetree -->

The pagetree macro works almost the same as the TOC above, but the tree behavior is more desirable for creating placeholder pages above collections of SOPs.

The default pagetree macro behavior is to insert a tree rooted @self.

The following parameters can be used to alter your default configuration with parameters described more in depth here:Confluence Pagetree Macro.

Parameters:

  • Title (of tree root page)
  • Sort
  • Excerpt
  • Reverse
  • SearchBox
  • ExpandCollapseAll
  • StartDepth

E.G.

<!-- Macro: :pagetree:
     Template: ac:pagetree
     Reverse: 'true'
     ExpandCollapseAll: 'true'
     StartDepth: 2 -->

# My First Heading

:pagetree:

Insert Children Display

To include Children Display (TOC displaying children pages) use following macro:

<!-- Macro: :children:
     Template: ac:children
-->

# This is my nicer title

:children:

You can use various parameters to modify Children Display:

<!-- Macro: :children:
     Template: ac:children
     Sort: title
     Style: h3
     Excerpt: simple
     First: 10
     Page: Space:Page title
     Depth: 2
     Reverse: false
     All: false -->

# This is my nicest title

:children:

Insert Jira Ticket

<!-- Space: TEST -->
<!-- Title: TODO List -->

<!-- Macro: MYJIRA-\d+
     Template: ac:jira:ticket
     Ticket: ${0} -->

See task MYJIRA-123.

Insert link to existing confluence page by title

This is a [link to an existing confluence page](ac:Pagetitle)

And this is how to link when the linktext is the same as the [Pagetitle](ac:)

Link to a [page title containing spaces](<ac:With Multiple Words>)

Link to another page in the same repository

A relative link to another Markdown file is replaced with a link to the Confluence page that file publishes:

See [the other page](./other.md) and [a heading in it](./other.md#setup).

The target file is read to find its Space and Title, and the page is looked up by those. Links are rewritten to Confluence tiny links (/x/AbCdEf), which survive page renames and moves.

The path is read the way an image's is: other%20page.md and <other page.md> both name other page.md, and other\_page.md and a&amp;b.md name other_page.md and a&b.md. The name as written is tried first, so a file whose name really contains a % still resolves to itself.

A link is left exactly as written when it has a scheme (https:, mailto:), is a bare #fragment, points at a directory or a non-text file, or names a file that has no mark metadata and so is never published. Links are resolved on the parsed document, so a link that appears inside a code span or a fenced or indented code block is left alone -- a page documenting Markdown gets to show its examples unchanged. Files pulled in with Include are resolved along with the document that includes them.

Leaving a document unpublished

A document can ask to be left out of a run:

<!-- Synchronized: false -->

or, in YAML front matter:

---
title: Runbook
synchronized: false
---

Mark skips the file before it asks Confluence anything, so a document that has opted out costs no page lookup and uploads no attachments. Saying nothing means the document is published, which is the ordinary case; opting out has to be deliberate.

A page that was published before is left exactly as it is -- Mark does not delete it, and does not report it as having lost its source file. Setting Synchronized: true again, or removing the header, resumes publishing to the same page.

Confluence content properties

A page can carry arbitrary key/value data that macros, reports and scripts read back through the API. One Property header sets one of them, as many times as you like:

<!-- Property: owner=platform-team -->
<!-- Property: reviewed=2026-08 -->

In YAML front matter the same thing is a mapping under the plural key, the way Parent headers become a parents list:

---
title: Runbook
properties:
  owner: platform-team
  reviewers: 3
  tags:
    - runbook
    - on-call
---

A content property holds JSON, so front matter can give a number, a list or a mapping as the value. A Property header can only say a string, which is the one difference between the two forms.

--global-properties points at a YAML or JSON file of properties to set on every page:

mark --global-properties confluence-properties.yaml --files "docs/**/*.md"

A document naming a property the file also names wins for its own page. A property whose value has not changed is not written again, because Confluence versions each one and rewriting it fills its history for nothing.

Labels applied in Confluence

A page ends up with exactly the labels its Label headers name: one added in the Confluence UI is removed on the next publish, because the document is taken as the whole truth about its labels.

That is often not what a team wants. Labels drive macros, searches and reports, and are frequently applied by people who are not editing the Markdown. --append-labels adds what a document asks for without removing anything else:

mark --append-labels --files "docs/**/*.md"

The cost is that a label outlives the header that introduced it -- appending cannot tell a Label header somebody deleted from a label somebody added in Confluence. That is visible on the page and can be undone by hand, which the deletion it prevents is not.

Reporting what a run did

By default Mark prints the address of each page as it publishes, which is what it has always done. --output-format offers two other shapes.

json describes the whole run as one object:

mark --output-format json --files "docs/**/*.md" | jq -r '.pages[] | select(.status=="published") | .url'
{
  "pages": [
    {
      "file": "docs/architecture.md",
      "status": "published",
      "space": "DOCS",
      "title": "Architecture",
      "pageId": "1004",
      "url": "https://example.atlassian.net/wiki/display/DOCS/Architecture"
    },
    {
      "file": "docs/draft.md",
      "status": "skipped",
      "reason": "the document is not synchronized"
    }
  ],
  "orphans": [
    {"file": "docs/old.md", "pageId": "1007", "title": "Old", "action": "delete"}
  ]
}

A page is published, unchanged (--changes-only found nothing to do), skipped (not synchronized, or edited in Confluence under --no-overwrite) or failed, with reason saying which in the last two cases. A page is only published once everything the run had to do to it -- its body, labels and properties -- has been done. Each document appears once, with what finally became of it, so one published again whose second publish failed is failed.

orphans lists the tracked pages whose source file is gone, under --track-pages, with the --on-orphan action taken: report for a page that was only reported and left where it is, archive or delete for one that was archived or trashed.

errors lists what failed that was not one document's doing: ac: links that still resolve to no page once everything is published, a page manifest that could not be saved, a failed ordering pass, an orphan that could not be handled. The report is written however the run ends, so a run that fails still says which pages it published and why it failed. A document that fails is recorded against that document, as failed with its reason, and is not repeated here.

{
  "pages": [
    {"file": "docs/guide.md", "status": "published", "title": "Guide", "pageId": "1009"}
  ],
  "errors": [
    "1 link does not resolve:\n  docs/guide.md: link \"ac:Nowhere\" does not resolve: there is no page \"Nowhere\" in space \"DOCS\""
  ]
}

github prints workflow commands, so that a failure appears against the file that caused it in a pull request:

::notice file=docs/architecture.md::published "Architecture" to https://...
::warning file=docs/draft.md::the document is not synchronized
::error file=docs/broken.md::unable to compile markdown: ...
::warning file=docs/old.md::page "Old" was deleted: its source file is gone
::error::unable to save page manifest: ...

An error that belongs to no one file, from errors above, is annotated without one, and appears in the run's summary rather than against a line of the diff.

- run: mark --output-format github --files "docs/**/*.md"

Links between pages published together

A link is resolved by finding the page it points at, so a document linking to another that the same run is creating has nothing to find yet.

Mark notes those documents and publishes them again once everything exists, so links resolve within a single run. Only the documents that were waiting are published a second time, and only on the run that creates their targets: once the pages are there, later runs find them the first time and nothing is published twice.

Nothing is published again on --dry-run or --compile-only, where no page is being created for a link to wait for.

Checking that links go somewhere

By default a link that cannot be resolved is left exactly as written, which in Confluence means a link that leads nowhere. --check-links makes that a failure instead:

mark --check-links internal,confluence --files "docs/**/*.md"
ERR unable to compile markdown: link "./architecure.md" does not resolve:
    there is no such file

It reports in every mode, including --compile-only and --dry-run, so a pull request gate can check links without Confluence credentials:

mark --compile-only --check-links all --files "docs/**/*.md"
Value Checks
internal relative links to other Markdown files in the repository
confluence ac: links, which name a Confluence page by title
external requests each URL with a scheme to see whether it answers
all all three

The values are a set, not a mode: repeat the flag or separate them with commas, and pick whichever combination suits. The three cost very different things -- internal is answered from the filesystem, confluence costs a lookup per link, and external leaves the building -- so internal,confluence in CI with no network checking is a perfectly reasonable choice, and the one above.

An internal link fails the run when the file it names is missing, is a directory, or is a document that never becomes a page -- one with no title, so there is nothing for the link to point at.

A link is looked for beside the document that contains it, and then beside each file that document includes. A fragment reads as a document in its own right, so a link inside one is written from where the fragment lives rather than from wherever it is pulled into. The document's own directory is always tried first, so this cannot change what an unambiguous link already meant. Only the files a document includes directly are considered, not what those files include in turn.

One case is reported but does not fail: a link to a document that exists and has a title, but is not in the space and is not being published by this run either. A page the run is about to create is waited for rather than complained about -- see below -- so this is left for the genuinely absent.

A confluence link is checked by looking for a page of that title in the document's own space, which is what an ac: link resolves against. The title is read the way the renderer reads it: whatever follows the colon, or the link text when nothing does, so [Some Page](ac:) is checked as Some Page.

These are checked once the run has finished rather than as each document compiles, because a page named this way is often published by another file in the same run. Each page is looked up once however many documents link to it.

external needs network access from wherever Mark runs, and makes publishing dependent on every site you link to being up. Each URL is requested once per run however many pages mention it. HEAD is tried first and a refusal is retried with GET, since plenty of servers reject HEAD while serving the URL perfectly well.

Bare #fragments, mailto: links and rooted paths are not links Mark resolves, and are never checked.

Every broken link in a document is reported, not just the first, so a page with several of them takes one run to find out rather than one run each.

Adopting it on a repository that already publishes

--check-links-warn-only reports the same links without failing the run, which is how to see the list before the build starts failing over it:

mark --check-links all --check-links-warn-only --files "docs/**/*.md"

Pages still publish exactly as they would have. It is a separate flag from --continue-on-error, which is about files rather than links and still fails the run at the end: use --check-links-warn-only to not fail at all, and --continue-on-error to attempt every file before failing.

With --output-format github each warning is annotated against the file and the line-less document it came from, so it appears in the pull request rather than only in the build log:

::notice file=docs/doc.md::published "Doc" to https://confluence.example.com/display/DOCS/1003
::warning file=docs/doc.md::link "guide" does not resolve: it is a directory, not a document

Without the flag the same link fails the run and is annotated as an ::error instead.

Upload and included inline images

![Example](../images/examples.png)

will automatically upload the inlined image as an attachment and inline the image using the ac:image template.

The path is read the way Markdown defines it: my%20file.png and <my file.png> both name my file.png, and the backslash escape in my\_file.png names my_file.png. The name as written is tried first, so a file whose name really contains a % still resolves to itself. The alt text and title are read the same way: "a \"quoted\" &amp; b" is published as a "quoted" & b.

If the file is not found, it will inline the image using the ac:image template and link to the image.

Add width for an image

Use the following macro:

<!-- Macro: \!\[.*\]\((.+)\)\<\!\-\- width=(.*) \-\-\>
     Template: ac:image
     Attachment: ${1}
     Width: ${2} -->

And attach any image with the following

![Example](../images/example.png)<!-- width=300 -->

The width will be the commented html after the image (in this case 300px).

Currently this is not compatible with the automated upload of inline images.

Where an attachment may come from

A file is attached when it is inside the directory Mark is running in, or inside the document's own directory. An upward path is ordinary — ../images/logo.png from a docs folder is how a repository refers to shared assets — and stays inside the repository when Mark is run at its root.

A path that resolves outside both, ../../../etc/passwd or a symlink pointing out of the repository, fails the file rather than being uploaded. A document says which files to publish, and on a repository that accepts pull requests a document is something a contributor writes.

If a legitimate attachment lives outside, publish from a directory that contains both it and the documents.

Use HTML img tags for sizing

Standard HTML <img> tags (inline, block, single-line, or multi-line) are converted into <ac:image> macros. This allows you to specify sizing while keeping the document readable in standard Markdown renderers like GitHub:

<img src="../images/example.png" width="300" alt="Example" title="An Example" />

Standard attributes (width, alt, and title) are supported and carried over directly. Local image files are uploaded as attachments, and remote image URLs are linked directly.

An <img> wrapped in other HTML is converted where it stands, and the markup around it is kept, so the usual centered-image idiom works as it does on GitHub:

<p align="center">
  <img src="../images/example.png" width="200">
</p>

An <img> inside a <picture> is left as written, because Confluence has nothing to turn the <source> elements beside it into. This conversion applies to the default compile path only; the legacy renderer leaves every <img> as written.

Date Badges

Interactive Confluence date badges (<time datetime="YYYY-MM-DD" />) can be generated by enabling the --features date flag. Both HTML <time> tags and @date(YYYY-MM-DD) macro directives are converted:

Release on @date(2026-07-27) or <time datetime="2026-12-31">December 31, 2026</time>.

Render Mermaid Diagram

Confluence doesn't provide mermaid.js support natively. Mark provides a convenient way to enable the feature like GitHub does. As long as you have a code block marked as "mermaid", mark will automatically render it as a PNG image and attach it to the page as a rendered version of the code block.

graph TD;
A-->B;
Loading

A diagram is published as a PNG by default, scaled by --mermaid-scale. --mermaid-output=svg publishes the drawing itself instead: one file that is sharp at any zoom and whose text stays text, on an instance that displays an SVG attachment. --mermaid-scale applies to either -- it multiplies the pixels of a PNG, and the size the page displays an SVG at. It has to be a finite number greater than 0, and a PNG that would come out wider or taller than 16384 pixels, or larger than 64 million pixels in all, is refused rather than captured: make the diagram smaller or lower the scale.

--mermaid-bundle keeps the diagram's own source inside that SVG, in its <desc> element, so what was published can be opened and edited again from the attachment without the document it came from. It needs --mermaid-output=svg: a PNG has nowhere to keep it.

Drawing without a browser

Diagrams are drawn by a headless browser running mermaid.js, which is what mermaid.js is built for and what mark has always done.

--mermaid-engine=merman draws them with merman instead, a native implementation that needs no browser -- useful where starting Chrome is awkward or slow, such as a minimal CI image. It is installed separately, and mark reports it by name if it is missing.

Experimental. merman is a reimplementation rather than mermaid.js, so a diagram may come out differently, or not at all, and which diagrams those are is not written down anywhere. mark says so once per run when it is selected. The default is unchanged.

A minimum merman version is required, and an older binary is refused by name rather than left to draw something else: merman changed how its operations work across a release, and a version from before that answers for itself perfectly well and then draws differently. The version is in mermaid/merman-version.txt, which mark embeds and which the Dockerfile and CI both read, so it is written down once.

The published Docker image carries merman on amd64, which is where it publishes a Linux build; an arm64 image draws with Chrome.

Render D2 Diagram

Optionally you can enable D2 rendering via --features="d2". This will transform the d2 diagram into a png that will be attached to Confluence, similar to how mermaid-go support works. All you need is a codeblock marked as "d2".

X -> Y

--d2-output=svg attaches the drawing itself instead: one file that is sharp at any zoom and whose text stays text, on an instance that displays an SVG attachment. --d2-scale applies to either -- it multiplies the pixels of a PNG, and the size the page displays an SVG at.

An image the diagram references is inlined into the SVG, the way d2's own --bundle does it, because Confluence serves the attachment from its own host where a path relative to the document resolves to nothing. A reference that cannot be read fails the run rather than publishing a diagram with a hole in it.

A |md | label is passed into the drawing as markup, and mark both renders that drawing through a browser and uploads it for other people to open. A diagram that would run something rather than depict something -- a <script>, an <iframe>, an event handler, a javascript: link -- is refused rather than published. Everything a label is written for, from bold text to links and images, is unaffected.

Because the file carries what it points at, what it may point at is limited:

  • A file must be inside the diagram's own directory or the one mark is running in, the same boundary an attachment is held to. One elsewhere is refused, so a diagram naming /etc/id_rsa cannot publish it.
  • A URL is not fetched unless --d2-bundle-remote says so. The request is made by the document rather than by you, to any address it names -- a loopback or cloud metadata one included -- and the answer is published inside the drawing. Set it for documents whose diagrams you trust. Without it, a diagram whose icon or shape: image names an http or https URL is refused, and the file fails with an error that names the URL and the flag, since Confluence would not resolve the URL from inside the SVG either. The flag changes nothing for a PNG.

Render PlantUML Diagrams

Optionally you can enable PlantUML diagram rendering via --features="plantuml". Unlike Mermaid and D2 which are rendered locally, code blocks marked as "plantuml" are rendered in Confluence by the PlantUML for Confluence Macro. This requires the PlantUML for Confluence macro to be installed in your Confluence instance.

@startuml
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response
@enduml

MkDocs' Admonitions

Optionally you can enable mkdocs-style Admonitions via --features="mkdocsadmonitions".

When enabled, this renders note, warning, tip, info admonitions as Confluence alerts.

!!! note

HTML Details/Summary Macro

Standard HTML <details> and <summary> tags are converted to native Confluence expand macros:

<details>
<summary>Click to expand</summary>
This is the hidden content.
</details>

LaTeX & Math Formulas

Optionally you can enable LaTeX / Math formula rendering via --features="math".

Each formula is rendered as an image, uploaded as a page attachment, and shown with <ac:image> — the same thing mark already does with mermaid and d2 diagrams. The LaTeX stays with it as the image's alt text, so the formula is still findable by search and readable to a screen reader.

Inline math, with either pair of delimiters:

Euler's identity is $e^{i\pi} + 1 = 0$, or \(e^{i\pi} + 1 = 0\).

Display math, likewise:

$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$

\[
f(x) = \int_{-\infty}^\infty \hat{f}(\xi)\,e^{2\pi i \xi x}\,d\xi
\]

A $$ alone on a line opens a formula that runs to the next $$ alone on a line, so a formula may contain blank lines — which is how aligned and gather environments are written:

$$
\begin{aligned}
  a &= b + c \\

  d &= e + f
\end{aligned}
$$

The \[ and \] markers work the same way, and a run of more than two dollars is matched by its own length, so $$$ opens and closes a formula too.

Written that way the formula is a block of its own, so it may sit inside a blockquote or a list item without the > or the indentation reaching LaTeX. A formula that opens and closes on one line — $$E = mc^2$$ — is read as it always was, wherever it is written.

\[ is CommonMark's escape for a literal bracket, so the bracket form is only read as a formula when the formula is the whole of the line it is on: use \[brackets\] here is a sentence about brackets.

A dollar sign in ordinary prose is left alone: a formula may not begin or end with a space, so it costs $5 and $7 today is a sentence about money rather than mathematics. \$5 is always literal, and anything inside a code span or a code block is quoted rather than rendered — including a $$ fence, so a document explaining this syntax publishes the example rather than a picture of it.

A formula MathJax cannot read fails the file, quoting both the formula and the complaint — unable to render formula "\frac{a": Missing close brace — rather than publishing a picture of the error message.

Typesetting is done by mathjax-go, with no CGO and no Confluence plugin on the instance. The same formula written twice on a page is uploaded once.

Choosing the image format

--math-format What you get What it costs
png (default) What Confluence certainly displays, and what the diagram renderers have always produced Rasterised through the same headless Chrome mermaid uses
svg Vector: sharp at any zoom, a few kilobytes Nothing — no browser at all — where the instance displays an SVG attachment
mark --features=math --math-format=svg -f document.md

--math-scale (2 by default) applies to PNG only, and multiplies the pixels rather than the size: the image still occupies the space the formula asked for, with more pixels in it for a display that can use them, because a formula rasterised 1:1 looks ragged beside the text it sits in. It has to be a finite number greater than 0.

Why an image

Confluence has no math of its own, and the usual answer for a Markdown tool — publishing KaTeX or MathJax HTML — does not survive the trip. That markup is a pile of positioned <span>s whose layout lives entirely in katex.css, which a Confluence page never loads, and it ships a MathML twin and the LaTeX source alongside. Stripped of the styling Confluence discards, a reader saw the formula spelled out three times:

Inline math: E=mc2E = mc^2E=mc2 and a Greek pair α+β\alpha + \betaα+β.

An SVG carries its own geometry and its glyphs as outlines, so it needs no stylesheet, no fonts, and nothing installed on the instance.

Inline Link Cards

Optionally you can render bare URLs as Confluence Cloud inline smart cards via --features="inline-link-card".

When enabled, auto-detected URLs in markdown (e.g. <https://example.com> or a bare URL on its own line) are rendered with the data-card-appearance="inline" attribute, which Confluence Cloud uses as a hint to display the link as an inline card preview (page title, Jira issue summary, GitHub repo card, etc.) instead of a plain blue hyperlink.

See <https://your-instance.atlassian.net/wiki/spaces/DOCS/pages/12345/Page+Title>
for context.

Only auto-detected URLs (bare URLs / <...> autolinks) are affected. Markdown-explicit links ([label](https://...)) keep their author-chosen display text and render as regular hyperlinks, unchanged.

Note: Inline Smart Cards (data-card-appearance="inline") are a Confluence Cloud feature. On Confluence Data Center / Server, the attribute is safely ignored and the link displays as a standard hyperlink.

Footnotes

Markdown footnotes are rendered as footnotes Confluence can actually navigate.

The estimate is optimistic[^basis], and the deadline is not[^deadline].

[^basis]: Measured on the staging cluster, which has half the nodes.
[^deadline]: Set before the scope changed.

Each marker becomes a superscript [1] linking down to the note, and each note ends with a ↩ linking back to the sentence that cited it. Notes are collected into a numbered list under a horizontal rule at the foot of the page, in the order they were first cited -- so the definitions can be written in whatever order suits the source file, and a definition nothing cites is left out rather than numbered.

Citing the same note twice gives it one entry with a numbered arrow per citation, so both ways back are distinguishable.

Why it is not a plain HTML footnote

Markdown footnote syntax has always been parsed. What did not work is the navigation. Goldmark, like every other Markdown renderer, wires the two ends together with id attributes and href="#id" links, and Confluence keeps neither: it discards the ids in the storage format and generates its own from element text. The result renders, looks right, and does nothing when clicked. There is nothing to choose between, which is why this is not a feature you turn on.

The plumbing is replaced with the two things Confluence does understand:

  • the Anchor macro for the jump targets, which is bundled with both Cloud and Data Center and so needs no marketplace plugin, and
  • <ac:link ac:anchor="..."> for the jumps, with no ri:page -- an anchor link within the current page, which scrolls rather than reloads.
<!-- at the marker -->
<ac:structured-macro ac:name="anchor"><ac:parameter ac:name="">footnote-ref-1</ac:parameter></ac:structured-macro>
<ac:link ac:anchor="footnote-1"><ac:link-body><sup>[1]</sup></ac:link-body></ac:link>

<!-- at the note -->
<li><ac:structured-macro ac:name="anchor"><ac:parameter ac:name="">footnote-1</ac:parameter></ac:structured-macro>
<p>Measured on the staging cluster, which has half the nodes.&#160;<ac:link ac:anchor="footnote-ref-1"><ac:link-body>&#x21a9;&#xfe0e;</ac:link-body></ac:link></p>
</li>

Anchor names are footnote-<n> and footnote-ref-<n>, which share a namespace with the page's heading anchors -- avoid headings that would generate the same names.

Emoji

Optionally you can write emoji as :shortcode:, via --features="emoji".

Shipped it :tada: and the build is green :white_check_mark:

The shortcodes are the GitHub ones. A shortcode no emoji answers to, one inside a code span or code block, and a bare colon in running text (10:30) are all left exactly as written.

What ends up in the page

Confluence holds an emoji in <ac:emoticon>, and its two flavours read that tag differently. Data Center goes by ac:name and knows only the couple of dozen names listed in the storage format reference; Cloud writes ac:emoji-id, ac:emoji-shortname and ac:emoji-fallback alongside it and takes the glyph from those. A name Data Center does not know renders as nothing at all.

So mark writes whichever of the two is safe on both:

  • an emoji Confluence has a legacy name for -- :smile:, :wink:, :thumbsup:, :warning:, :white_check_mark:, :x:, :heart: and the rest of that short list -- becomes the macro, with the name for Data Center and the ac:emoji-* attributes for Cloud;
  • every other emoji is written as the character itself, which both flavours render as text without having to understand a macro.
<!-- :white_check_mark: -->
<ac:emoticon ac:name="tick" ac:emoji-shortname=":white_check_mark:" ac:emoji-id="2705" ac:emoji-fallback="✅"/>

<!-- :tada: -->
🎉

Several emoji can share one legacy name -- Data Center has one smiley where Unicode has four -- which is only visible on Data Center, since Cloud picks the exact emoji from ac:emoji-id.

An emoji in a heading is part of that heading's anchor, and the anchor is built from the source text rather than the rendered emoji: ## Release :tada: gets the id Release-tada.

This feature is about emoji in the body of a document. The page's icon is set separately, with the <!-- Emoji: --> header described above, which takes the character rather than a shortcode.

Note: mark will read configuration from your environment variables or the configuration file.

Installation

Homebrew

brew tap kovetskiy/mark
brew install mark

Go Install

go install github.com/kovetskiy/mark/v16/cmd/mark@latest

Releases

Download a release from the Releases page

Docker

docker run --rm -i kovetskiy/mark:latest mark <params>

Compile and install using docker-compose

Mostly useful when you intend to enhance mark.

# Create the binary
$ docker-compose run markbuilder
# "install" the binary
$ cp mark /usr/local/bin

Usage

NAME:
   mark - A tool for updating Atlassian Confluence pages from markdown.

USAGE:
   mark [global options]

VERSION:
   v16.x.x

DESCRIPTION:
   Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark

GLOBAL OPTIONS:
   --config string, -c string                     use the specified configuration file. (default: "${HOME}/.config/mark.toml") [$MARK_CONFIG]
   --files string, -f string                      use specified markdown file(s) for converting to html. Supports file globbing patterns (needs to be quoted). [$MARK_FILES]
   --continue-on-error                            don't exit if an error occurs while processing a file, continue processing remaining files. [$MARK_CONTINUE_ON_ERROR]
   --compile-only                                 show resulting HTML and don't update Confluence page content. [$MARK_COMPILE_ONLY]
   --dry-run                                      resolve page and ancestry, show resulting HTML and exit. [$MARK_DRY_RUN]
   --edit-lock, -k                                lock page editing to current user only to prevent accidental manual edits over Confluence Web UI. [$MARK_EDIT_LOCK]
   --drop-h1                                      don't include the first H1 heading in Confluence output. [$MARK_DROP_H1]
   --strip-linebreaks, -L                         remove linebreaks inside of tags, to accommodate non-standard Confluence behavior [$MARK_STRIP_LINEBREAKS]
   --title-from-h1                                extract page title from a leading H1 heading. If no H1 heading on a page exists, then title must be set in the page metadata. Mutually exclusive with --title-from-filename. [$MARK_TITLE_FROM_H1]
   --title-from-filename                          use the filename (without extension) as the Confluence page title if no explicit page title is set in the metadata. Mutually exclusive with --title-from-h1. [$MARK_TITLE_FROM_FILENAME]
   --parents-from-path                            place each page under a page named after every directory between the file pattern's root and the file itself. An index.md or README.md is the page for its own directory rather than a page inside it. A document that names its own Parent is left alone. [$MARK_PARENTS_FROM_PATH]
   --parents-from-path-root string                the directory --parents-from-path measures from. Taken from the --files pattern when not given, which is everything before its first wildcard. [$MARK_PARENTS_FROM_PATH_ROOT]
   --title-append-generated-hash                  appends a short hash generated from the path of the page (space, parents, and title) to the title [$MARK_TITLE_APPEND_GENERATED_HASH]
   --minor-edit                                   don't send notifications while updating Confluence page. [$MARK_MINOR_EDIT]
   --version-message string                       add a message to the page version, to explain the edit (default: "") [$MARK_VERSION_MESSAGE]
   --color string                                 display logs in color. Possible values: auto, never. (default: "auto") [$MARK_COLOR]
   --log-level string                             set the log level. Possible values: TRACE, DEBUG, INFO, WARNING, ERROR, FATAL. (default: "info") [$MARK_LOG_LEVEL]
   --username string, -u string                   use specified username for updating Confluence page. [$MARK_USERNAME]
   --password string, -p string                   use specified token for updating Confluence page. Specify - as password to read password from stdin, or your Personal access token. Username is not mandatory if personal access token is provided. For more info please see: https://developer.atlassian.com/server/confluence/confluence-server-rest-api/#authentication. [$MARK_PASSWORD]
   --password-command string                      run the specified command and use the first line of its stdout as the token for updating Confluence page. Runs without a shell. Mutually exclusive with password. [$MARK_PASSWORD_COMMAND]
   --target-url string, -l string                 edit the Confluence page at this URL, as a browser shows it: .../pages/viewpage.action?pageId=ID or .../spaces/KEY/pages/ID/Title. The instance is taken from it too, context path included, unless --base-url names it. Without it, each file must name its page with Space and Title metadata headers. [$MARK_TARGET_URL]
   --base-url string, -b string                   base URL for Confluence. Alternative to the base-url config file key. [$MARK_BASE_URL]
   --ci                                           run on CI mode. It won't fail if files are not found. [$MARK_CI]
   --space string                                 use specified space key. If the space key is not specified, it must be set in the page metadata. [$MARK_SPACE]
   --parents string                               A list containing the parents of the document separated by parents-delimiter (default: '/'). These will be prepended to the ones defined in the document itself. [$MARK_PARENTS]
   --parents-delimiter string                     The delimiter used for the parents list (default: "/") [$MARK_PARENTS_DELIMITER]
   --content-appearance string                    default content appearance for pages without a Content-Appearance header. Possible values: full-width, fixed, default. [$MARK_CONTENT_APPEARANCE]
   --mermaid-scale float                          defines the scaling factor for mermaid renderings: the pixels of a png, and the size the page displays an svg at. (default: 1) [$MARK_MERMAID_SCALE]
   --mermaid-engine string                        what mermaid diagrams are drawn by: chrome (the default, a headless browser running mermaid.js) or merman (experimental, a native reimplementation that needs no browser and must be installed separately). (default: "chrome") [$MARK_MERMAID_ENGINE]
   --mermaid-output string                        image a mermaid diagram is published as: png (rasterised, and scaled by --mermaid-scale) or svg (vector and sharp at any zoom, where the instance displays an SVG attachment). (default: "png") [$MARK_MERMAID_OUTPUT]
   --mermaid-bundle                               keep the diagram's own source inside the SVG published for it, in its <desc> element, so the drawing can be edited again from the attachment. Needs --mermaid-output=svg. [$MARK_MERMAID_BUNDLE]
   --math-format string                           image a formula is published as with --features=math: png (rasterised through the same headless Chrome mermaid uses) or svg (vector and sharp at any zoom, where the instance displays an SVG attachment). (default: "png") [$MARK_MATH_FORMAT]
   --math-scale float                             defines the scaling factor for PNG formula renderings; ignored when math-format is svg. (default: 2) [$MARK_MATH_SCALE]
   --include-path string                          Path for shared includes, used as a fallback if the include doesn't exist in the current directory. [$MARK_INCLUDE_PATH]
   --changes-only                                 Avoids re-uploading pages that haven't changed since the last run. [$MARK_CHANGES_ONLY]
   --output-format string                         how to report what the run did: "url" prints the address of each published page (the default), "json" prints one object describing the whole run, "github" prints GitHub Actions workflow commands so that failures appear against the file that caused them. (default: "url") [$MARK_OUTPUT_FORMAT]
   --on-orphan string                             what to do about a page whose source file is gone: "report" says so and does nothing (the default), "archive" archives the page (Confluence Cloud only), "delete" moves it to the trash. Requires --track-pages. (default: "report") [$MARK_ON_ORPHAN]
   --orphan-under string                          limit --on-orphan, and the reporting it does, to pages below this page or folder, given by title or id. Without it, every tracked page the --files pattern would have published is in scope. [$MARK_ORPHAN_UNDER]
   --check-links string [ --check-links string ]  fail on links that do not resolve. Repeat or comma-separate any of: "internal" (relative links to other files in the repository), "confluence" (ac: links naming a page by title), "external" (requests each URL to see whether it answers), or "all". [$MARK_CHECK_LINKS]
   --global-properties string                     path to a YAML or JSON file of Confluence content properties to set on every page. A Property header or properties front matter in a document wins over the file for that page. [$MARK_GLOBAL_PROPERTIES]
   --append-labels                                add the labels a document asks for without removing any others, so that labels applied in Confluence survive a publish. Without it, a page ends up with exactly the labels its Label headers name. [$MARK_APPEND_LABELS]
   --check-links-warn-only                        report links that do not resolve without failing the run. Only meaningful together with --check-links. [$MARK_CHECK_LINKS_WARN_ONLY]
   --no-overwrite                                 Leave alone any page that has been edited in Confluence since mark last published it, instead of overwriting the edit. Requires --track-pages, which is where the last published version is remembered. [$MARK_NO_OVERWRITE]
   --track-pages                                  Remember which page each file publishes to, so renaming a file or changing its title updates the existing page instead of creating a second one. Stores the mapping in Confluence (a space property on Cloud, a homepage content property on Server/Data Center, or the page --manifest-page names); nothing is written to the repository. [$MARK_TRACK_PAGES]
   --manifest-page string                         keep the --track-pages mapping as content properties of this page, given by title or id, instead of as space properties. Needs only the right to edit that page where a space property needs space administration, and works with a scoped API token. Requires --track-pages. [$MARK_MANIFEST_PAGE]
   --manifest-prefix string                       name the --track-pages mapping's properties under this prefix, so that two projects publishing into one space keep manifests of their own instead of sharing, and reporting each other's files as gone. Letters, digits, '_', '-' and dots. Requires --track-pages. (default: "mark.manifest") [$MARK_MANIFEST_PREFIX]
   --preserve-comments                            Fetch and preserve inline comments on existing Confluence pages. [$MARK_PRESERVE_COMMENTS]
   --d2-output string                             image a d2 diagram is published as: png (rasterised) or svg (vector and sharp at any zoom, with whatever the diagram references inlined into it, where the instance displays an SVG attachment). (default: "png") [$MARK_D2_OUTPUT]
   --d2-bundle-remote                             let a d2 diagram published as svg have mark fetch the URLs it names, and publish what comes back inside the drawing. Off by default, which refuses a diagram that names a URL: the request is made by the document rather than by you, to any address it likes. [$MARK_D2_BUNDLE_REMOTE]
   --d2-scale float                               defines the scaling factor for d2 renderings: the pixels of a png, and the size the page displays an svg at. (default: 1) [$MARK_D2_SCALE]
   --features string [ --features string ]        Enables optional features, replacing the defaults (mermaid, mention) rather than adding to them. Current features: d2, date, emoji, frontmatter, inline-link-card, math, mention, mermaid, mkdocsadmonitions, plantuml (default: "mermaid", "mention") [$MARK_FEATURES]
   --insecure-skip-tls-verify                     skip TLS certificate verification (useful for self-signed certificates) [$MARK_INSECURE_SKIP_TLS_VERIFY]
   --attach-referenced                            upload a local file that a link points at, and link to the attachment. Without it the link is published as the path the document wrote, which means nothing once the page is on Confluence. Images are attached either way. [$MARK_ATTACH_REFERENCED]
   --image-align string                           set image alignment (left, center, right). Can be overridden per-file via the Image-Align header. [$MARK_IMAGE_ALIGN]
   --help, -h                                     show help
   --version, -v                                  print the version

You can store user credentials in the configuration file, which should be located in a system specific directory (or specified via -c --config <path>) with the following format (TOML):

username = "your-email"
password = "password-or-api-key-for-confluence-cloud"
# Or name a command that prints the token, instead of storing it here:
# password-command = "pass show confluence/api-token"
# If you are using Confluence Cloud add the /wiki suffix to base-url
base-url = "http://confluence.local"
title-from-h1 = true
drop-h1 = true
image-align = "center"

NOTE: password-command keeps the token out of both the configuration file and the environment. A password manager or keychain helper supplies it per run.

The command runs without a shell, so the string is split on whitespace and nothing is expanded: there is no quoting, and a program path or argument containing a space cannot be written here. Wrap such a helper in a small script and name the script instead. The command also gets no standard input, so a helper that prompts on stdin cannot be used -- one that opens the terminal itself, as pinentry does, is fine.

Only the first line of what the command prints becomes the token, so a helper that prints a whole entry with the password on top -- pass show does -- needs no wrapper to trim it.

The command has to fetch the secret rather than contain it. It is handed to the operating system as written, so anyone who can list processes can read it for as long as it runs -- which is the exposure password-command exists to avoid, and writing a token into the command itself puts it straight back.

password and password-command are mutually exclusive, and setting both -- in any combination of flag, environment variable and configuration file -- is an error rather than a choice mark makes for you. Moving to a command therefore means taking the password out of the configuration file, not leaving it there beside the new setting.

A compile resolves the command like any other run, since a relative link is followed by looking that page up. A helper that cannot produce a token there is a warning and not an error, because --compile-only has to keep validating documents on a machine -- a CI image, typically -- that has no password manager on it.

Scoped API tokens

An Atlassian scoped API token only works through the api.atlassian.com gateway, so that is what the base URL has to name:

base-url = "https://api.atlassian.com/ex/confluence/<cloud-id>"

The gateway checks the token's scopes against the endpoint being called, and the older v1 endpoints check content scopes a scoped token is not usually minted with. Through the gateway mark therefore reads, creates and updates pages with the v2 API, which needs:

scope what it is for
read:space:confluence resolving a space key and finding its home page
read:page:confluence, write:page:confluence finding, reading, creating and updating pages
read:blogpost:confluence, write:blogpost:confluence the same for blog posts (Type: blogpost)
read:attachment:confluence listing what is attached to a page
read:label:confluence reading the labels a page carries
read:content.property:confluence, write:content.property:confluence the content appearance and emoji title of a page
read:page:confluence, write:page:confluence --track-pages with --manifest-page; without it the mapping is a space property, whose creation a scoped token is refused
read:folder:confluence, write:folder:confluence, read:hierarchical-content:confluence Folder headers: reading and creating folders, and finding one among its parent's direct children

Uploading attachments, applying Label headers, --preserve-comments, --on-orphan, page restrictions, mentions, moving a page among its siblings and finding a folder left at the space root have no v2 endpoint and stay on v1. To use those with a scoped token, grant the classic scopes (read:confluence-content.all, write:confluence-content, read:confluence-user) alongside the granular ones.

NOTE: Labels aren't supported when using minor-edit!

NOTE: Attachments are always uploaded as minor edits, whether or not --minor-edit is set. Watchers are notified about the page update that carries them, which --minor-edit controls, rather than once more for every file.

NOTE: See Preserving Inline Comments for a detailed description of the --preserve-comments flag.

NOTE: The system specific locations are described in here: https://pkg.go.dev/os#UserConfigDir. Currently, these are: On Unix systems, it returns $XDG_CONFIG_HOME as specified by https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if non-empty, else $HOME/.config. On Darwin, it returns $HOME/Library/Application Support. On Windows, it returns %AppData%. On Plan 9, it returns $home/lib. Where none of these is set, as in a minimal container or a systemd unit, there is no default file, and one is read only if --config or MARK_CONFIG names it.

Tricks

Continuous Integration

It's quite trivial to integrate Mark into a CI/CD system, here is an example with Snake CI in case of self-hosted Bitbucket Server / Data Center.

stages:
  - sync

Sync documentation:
  stage: sync
  only:
    branches:
      - main
  image: kovetskiy/mark
  commands:
    - for file in $(find -type f -name '*.md'); do
        echo "> Sync $file";
        mark -u $MARK_USER -p $MARK_PASS -b $MARK_URL -f $file || exit 1;
        echo;
      done

In this example, I'm using the kovetskiy/mark image for creating a job container where the repository with documentation will be cloned to. The following command finds all *.md files and runs mark against them one by one:

for file in $(find -type f -name '*.md'); do
    echo "> Sync $file";
    mark -u $MARK_USER -p $MARK_PASS -b $MARK_URL -f $file || exit 1;
    echo;
done

The following directive tells the CI to run this particular job only if the changes are pushed into the main branch. It means you can safely push your changes into feature branches without being afraid that they have automatically shown in Confluence, then go through the reviewal process and automatically deploy them when PR got merged.

only:
  branches:
    - main

File Globbing

Rather than running mark multiple times, or looping through a list of files from find, you can use file globbing (i.e. wildcard patterns) to match files in subdirectories. For example:

mark -f "helpful_cmds/*.md"

You can also use ** to get all files recursively.

mark -f "**/docs/*.md"

Naming a Heading's Anchor

By default a heading's anchor is derived from its text. To name it yourself, use the {#custom-id} syntax:

## Release Notes {#rel}

[link to it](#rel)
<h2 id="rel">Release Notes</h2>

The braces are consumed rather than rendered, so the heading reads as written.

A custom id replaces the derived one rather than adding to it, so the slug of the heading text no longer resolves -- [link](#release-notes) above would be left exactly as written. That is what other Markdown tools do too: a custom id is the id, not an alias.

Links to Headings on the Same Page

Mark generates Confluence-compatible heading anchors, which keep their capitals and punctuation: ## My Heading becomes id="My-Heading". A link written the way every other Markdown tool expects -- [jump](#my-heading) -- would not name that anchor, and would quietly go nowhere.

Mark now points such links at the anchor it actually generated, so both spellings work:

## My Heading

[slug style](#my-heading) and [exact style](#My-Heading) both resolve.

Matching is on the letters and digits only, because the two conventions disagree about which punctuation survives: a heading API/v2 Guide becomes API/v2-Guide, while a slug of it is apiv2-guide. If two headings differ only in punctuation that matching drops, the link is left exactly as written rather than guessed at.

Both ends are published in the storage format's own terms rather than as HTML. Confluence keeps no id on a heading — it generates its own from the element's text — so a heading a link points at carries the Anchor macro, and the link becomes <ac:link ac:anchor="…">. That is what footnotes have always used, and it is the difference between a link that works and one that renders, is clickable, and does nothing.

Only a heading something links to carries an anchor; the rest are left as they were.

To mark a place no heading names, write an anchor by hand — Confluence keeps neither the name nor the id, so mark turns it into the same macro a heading gets:

<a name="details"></a>

...later: [see the details](#details)

An anchor on another page is written after the page's title:

[the setup](<ac:Other Page#Setup>)

The part after the last # is taken as an anchor only if it has no whitespace in it, which is true of every anchor mark generates — so a page whose title contains a #, like <ac:C# Guide>, is still just a title.

A relative link to a section of another document works the same way, and is written the way every other Markdown tool expects:

[the setup](./other.md#setup-guide)

The slug is folded onto the id mark gave that heading, exactly as a same-page link is, and the link is published as an anchor on that page rather than as a URL with a fragment — a fragment on a Confluence address names nothing, since Confluence keeps a heading's anchor in the macro rather than in the address.

Two limits, both deliberate. The document has to be published to the same space, because a page named without a space key is looked for in the current one; a link across spaces keeps its URL. And a fragment naming no heading in that document is left exactly as written, since guessing which section was meant would send the reader somewhere the author did not choose.

Linting markdown

We recommend to lint your markdown files with markdownlint-cli2 before publishing them to confluence to catch any conversion errors early.

Preserving Inline Comments

When collaborators leave inline comments on a Confluence page, updating the page via mark will normally erase those comments because the stored body is fully replaced. The --preserve-comments flag re-attaches inline comment markers to the new page body before uploading, so existing review threads survive updates.

mark --preserve-comments -f docs/page.md

Or via environment variable:

MARK_PRESERVE_COMMENTS=true mark -f docs/page.md

How it works:

  1. Before uploading, mark fetches the current page body and all inline comment markers from the Confluence API.
  2. For each existing <ac:inline-comment-marker> tag it records the content wrapped by that marker plus a short context window immediately before the opening tag and immediately after the closing tag in the old body (not around the raw selection text, so the context is stable even when the marker wraps additional inline markup such as <strong>).
  3. It searches the new body for the same selected text and picks the occurrence whose surrounding context best matches the original (using Levenshtein distance), so the marker lands in the right place even if nearby text has shifted.
  4. The updated body—with all markers re-embedded—is then uploaded as normal.

Limitations:

  • If the commented text was deleted from the document, the inline comment cannot be relocated and will be lost. mark logs a warning in this case.
  • Overlapping selections (two comments anchored to the same stretch of text) are detected; the earlier overlapping match is dropped with a warning, and the later one (higher byte offset) is kept, rather than producing malformed markup.
  • --preserve-comments is automatically skipped for newly created pages (there are no comments to preserve yet).
  • When combined with --changes-only, the comment-preservation API calls are skipped entirely on runs where the page content has not changed, avoiding unnecessary round-trips.

Tracking Pages Across Renames

Mark finds an existing page by its title, and that title comes from three independently changeable places: the Title header, the leading H1, and the filename when --title-from-filename is set. Edit any of them and the existing page can no longer be found, so Mark publishes a second page beside the first and leaves the original stranded under its old name.

--track-pages records which page each source file published to, keyed on the file path, and consults that record at the one moment the title lookup comes up empty -- which is where "this page is new" and "this page was renamed" are otherwise indistinguishable:

mark --track-pages --files "docs/**/*.md"

Nothing is written back to your repository: no page IDs in the Markdown, no lock file, no commit. The record lives in Confluence.

What it detects

change how it is found
Title header edited the path is the key, so the lookup still hits
leading H1 edited the same
file renamed content fingerprint, matched against paths that stopped appearing
parent page renamed the page the declared parent chain resolved to last run, whatever it is called now
folder renamed in Confluence the recorded folder ID, reused rather than duplicated
source file deleted a recorded path absent from a run whose pattern covered it
two files claiming one page the reverse index, when the second one is recorded
document changed Space the same path recorded against another space

A retitle is written even under --changes-only. The content fingerprint would otherwise match, the update would be skipped, and the page would keep its old title indefinitely.

Renaming a file is the awkward case: the path is the key, so a rename is a miss, and when the title comes from the filename the title lookup misses too. What connects them is the file's own content. Mark knows the whole file set before it publishes any of it, so a path that has stopped appearing and a new file carrying its content are matched to each other -- much as git log --follow recovers a rename after the fact rather than being told about it.

What it does not detect

case why
a file renamed and rewritten in one commit the fingerprint is exact, so any content change breaks the match. Reads as a deletion plus a new page -- Git's limit too, without an -M50% to loosen it
an ambiguous rename two unpublished documents sharing content, or a target page another file already claimed this run. A duplicate is a nuisance; a wrong rebind overwrites someone's page
anything in the first run nothing is recorded until a file publishes with the flag set, so the first run only adopts
a rename across two --files patterns matching and reporting are both scoped to the pattern that recorded the entry, so the file is a deletion in one and a new page in the other. That scoping is what lets several Mark invocations publish different folders into one space without reaching into each other's pages
whether a deletion was intended it reports; it never deletes
a page nested deeper than its headers declare every parent it declares is still somewhere in its ancestry, so there is nothing to find wrong, and moving it would tear up a hierarchy nobody asked to change. A Parent: change that does contradict the ancestry moves the page, with tracking or without it
a moved space homepage (Server/DC) the mapping is anchored to it, so tracking starts over

A page renamed by hand in Confluence is found by its ID and renamed back to what the file says. That is deliberate -- the repository is the source of truth, as it already is for titles and content -- but it is worth knowing before turning this on over a space people edit directly.

What it never does

Mark does not delete pages, and tracking does not change that. It reports source files it can no longer account for:

space "DOCS": 2 tracked page(s) had no matching source file in this run: docs/old.md, docs/removed.md

The report is suppressed on runs that had errors, where a file that failed to process is indistinguishable from one that is gone. Each deletion is reported once: having said it, Mark stops tracking that path, so the message does not repeat forever and the mapping does not accumulate files that no longer exist. What becomes of the page itself is your call.

A dry run resolves exactly as a real one does and says what it would have done, including which existing page a retitle or a rename would have updated. It writes nothing at all -- neither to Confluence nor to the mapping.

Where the mapping is stored

storage
Cloud space properties mark.manifest.0 … mark.manifest.15, mark.manifest.folders and mark.manifest.parents
Server / Data Center content properties of the same names, on the space homepage
--manifest-page, anywhere content properties of the same names, on the page it names

Space properties exist only in the v2 API, so Server and Data Center anchor to the space homepage instead.

Creating a space property on Cloud takes space administration, which is a lot to grant a publisher for the sake of a bookkeeping record, and a scoped API token is refused it outright. --manifest-page keeps the mapping as content properties of a page of your choosing instead, given by title or by id:

mark --track-pages --manifest-page "Team Handbook" --files "docs/**/*.md"

A content property needs only the right to edit the page it sits on, which a publisher has by definition, and on Cloud it is written through the v2 API, so a scoped token holding the page scopes is enough. A space holding several independent mirrors can give each its own manifest this way. A title is looked up in each space the run publishes to; an id is used as it is, and so suits a run confined to one space. The page has to exist already: Mark refuses to start rather than quietly keep the mapping somewhere else.

Two projects in one space

Two projects publishing into the same space would otherwise read and rewrite one mapping between them, and each would report the other's files as gone. --manifest-prefix names the properties differently for each:

mark --track-pages --manifest-prefix mark.manifest.api --files "api/**/*.md"
mark --track-pages --manifest-prefix mark.manifest.sdk --files "sdk/**/*.md"

Each keeps its own <prefix>.0 … <prefix>.15, <prefix>.folders and <prefix>.parents, and sees nothing of the other's. The prefix is made of letters, digits, _, - and dots, like the default. Changing it on a project that has already published starts that project's mapping afresh: the old properties stay where they were and nothing reads them, so the next run finds every page by title as a run without --track-pages would.

It is split over sixteen properties rather than held in one because Confluence bounds how large a single property value may be, and one blob would cap how many files a repository may have. Each path is assigned a shard by hash; all of them are read in a single request and only the ones that changed are written back.

Folders and parents live in properties of their own rather than among the shards. They are keyed by the chain of titles a document declares rather than by a source path, there are few enough of them that splitting them buys nothing, and a key of theirs among the page keys would be reported as a source file that had gone missing.

Removing pages whose files are gone

By default Mark reports a tracked page whose source file has disappeared and does nothing about it. --on-orphan chooses otherwise:

Value Effect
report say so and leave the page alone (the default)
archive archive the page. Confluence Cloud only
delete move the page to the trash
mark --track-pages --on-orphan delete --files "docs/**/*.md"

delete means the trash, which Confluence keeps recoverable. Mark never purges a page: that second step is left to a person.

archive exists only on Confluence Cloud, and Server and Data Center report that they have no such thing rather than appearing to have archived anything. Confluence accepts the request and archives afterwards, so Mark reports that it asked rather than that it finished; a failure after acceptance is not something Mark sees.

--orphan-under narrows it to the pages below one page or folder, named by title or by id:

mark --track-pages --on-orphan delete --orphan-under "Team Handbook" \
     --files "docs/**/*.md"

Without it, every tracked page the --files pattern would have published is in scope -- which is already narrower than the space, since a pattern is only evidence about where it was looking.

The scope applies to everything Mark does about orphans, not only to removing them. A page outside it is not reported either, and is still remembered, so a later run with a wider scope knows about it.

Flags that need other flags

A combination that would leave you believing you are protected is refused outright rather than warned about, because the belief comes from silence:

  • --on-orphan archive or delete without --track-pages
  • --no-overwrite without --track-pages
  • --check-links-warn-only without --check-links

A combination that merely does nothing -- --track-pages alongside a page ID, where the mapping cannot apply -- is a warning.

What stops a page being removed

  • --track-pages is required, and Mark refuses to start without it. Only the manifest knows which pages Mark published, and a guess from titles is not one worth making about deletion.
  • A page holding child pages is left alone and reported, because removing it would take them with it and they may be pages nobody wrote in this repository.
  • A run in which any file failed removes nothing: a file that failed to process is indistinguishable from one that was deleted.
  • A run that published nothing removes nothing, so a failed checkout cannot empty a space.
  • A document that opted out with Synchronized: false still counts as present.
  • --dry-run reports what would go and touches nothing.

A page that is left alone stays in the manifest, so the next run finds it again rather than losing sight of it.

Worth knowing

  • Paths are keyed relative to the directory Mark runs in and with forward slashes, so --files "$PWD/docs/*.md" and --files "docs/*.md" share one mapping when run from the same place, as do a Windows workstation and Linux CI. A file outside that directory has no better anchor and is keyed by its absolute path.
  • --track-pages has no effect when publishing straight to a page ID, since the mapping is per space and per file and a page ID is neither.

Leaving hand-edited pages alone

By default Mark overwrites whatever a page currently holds. --no-overwrite makes it skip any page that has been edited in Confluence since Mark last published it:

mark --track-pages --no-overwrite --files "docs/**/*.md"
WRN page "Runbook" was edited in Confluence since mark published it
    (version 9, mark wrote 7); leaving it alone

It needs --track-pages, because the version Mark last wrote is remembered in the same manifest, and Mark refuses to run without it rather than appear to guard pages it is not guarding.

A skipped page still counts as published: it is not reported as an orphan and not pruned. Nothing else about it is touched either -- no labels, no ordering, and its attachments are not re-uploaded.

The warning repeats on every run until the difference is resolved, which is deliberate: a page drifting from its source is a standing problem, not a one-off event. To resolve it, either bring the edit back into the Markdown, or publish once without --no-overwrite to let Mark reclaim the page.

Comparison is by version number, not by content. Confluence rewrites storage markup on save often enough that comparing bodies would report a difference on pages nobody had touched.

Worth knowing about --no-overwrite

  • A page Mark has no recorded version for -- first run, or one published before the version was tracked -- is published normally and recorded, rather than frozen on the suspicion that it might have changed.
  • Anything that creates a version counts as an edit, including a comment added in the web UI.
  • An edit that lands while Mark is publishing the page -- after the check, before the update -- makes the update fail rather than be retried over it. Without the flag, Mark retries such an update once against the page's current version, which overwrites the edit.

Issues, Bugs & Contributions

I've started the project to solve my own problem and open sourced the solution so anyone who has a problem like me can solve it too. I have no profits/sponsors from these projects which means I don't really prioritize working on this project in my free time. I still check the issues and do code reviews for Pull Requests which means if you encounter a bug in the program, you should not expect me to fix it as soon as possible, but I'll be very glad to merge your own contributions into the project and release the new version.

I try to label all new issues, so it's easy to find a bug or a feature request to fix/implement, if you are willing to help with the project, you can use the following labels to find issues, just make sure to reply in the issue to let everyone know you took the issue:

Contributors ✨

Thanks goes to these wonderful people (emoji key):

Manuel Rüger
Manuel Rüger

🚧 💻
Egor Kovetskiy
Egor Kovetskiy

🚧 💻
Nick Klauer
Nick Klauer

💻
Rolf Ahrenberg
Rolf Ahrenberg

💻
Charles Southerland
Charles Southerland

💻
Šarūnas Nejus
Šarūnas Nejus

💻
Alexey Baranov
Alexey Baranov

💻
Anthony Barbieri
Anthony Barbieri

💻
Devin Auclair
Devin Auclair

💻
Gezim Sejdiu
Gezim Sejdiu

💻
Josip Ćavar
Josip Ćavar

💻
Juho Saarinen
Juho Saarinen

💻
Luke Fritz
Luke Fritz

💻
Matt Radford
Matt Radford

💻
Planktonette
Planktonette

💻
Stefano Teodorani
Stefano Teodorani

💻
Tim Schrumpf
Tim Schrumpf

💻
Tyler Cole
Tyler Cole

💻
elgreco247
elgreco247

💻
emead-indeed
emead-indeed

💻
Will Hegedus
Will Hegedus

💻
Leandro Carneiro
Leandro Carneiro

💻
beeme1mr
beeme1mr

💻
Taldrain
Taldrain

💻
Hugo Cisneiros
Hugo Cisneiros

💻
jevfok
jevfok

💻
Mateus Miranda
Mateus Miranda

💻
Stephan Hradek
Stephan Hradek

💻
Dreampuf
Dreampuf

💻
Joel Andritsch
Joel Andritsch

💻
guoweis-outreach
guoweis-outreach

💻
klysunkin
klysunkin

💻
Florent Monbillard
Florent Monbillard

💻
Joey Freeland
Joey Freeland

💻
Noam Asor
Noam Asor

💻
Philipp
Philipp

💻
Pommier Vincent
Pommier Vincent

💻
Toru Kawaguchi
Toru Kawaguchi

💻
Will Gorman
Will Gorman

💻
Zackery Griesinger
Zackery Griesinger

💻
cc-chris
cc-chris

💻
datsickkunt
datsickkunt

💻
recrtl
recrtl

💻
Stanislav Seletskiy
Stanislav Seletskiy

💻
Joris Conijn
Joris Conijn

💻

This project follows the all-contributors specification. Contributions of any kind welcome!

About

Sync your markdown files with Confluence pages.

Topics

Resources

Stars

1.6k stars

Watchers

9 watching

Forks

Releases

Used by

Contributors

Languages