Each Markdown file is one Confluence page. A file has an optional YAML frontmatter block, and then the Markdown body.
---
title: My Page Title
space: ENG
parent: null
page_id: 1234567890
page_status: Ready for review
page_width: max
---
# Body starts here
...
Frontmatter is a YAML block between two --- lines. It can hold only flat
key: value pairs. A value is a single-line scalar, or a list of scalars. You
can write a list inline (labels: [a, b]) or as - lines.
markfluence reads some fields as single values: title, space, parent,
page_id, page_width, and page_status. If you write one of them as a list,
that is an error. markfluence does not read it as unset.
You cannot use nesting or multi-line values, and markfluence enforces this.
Each of these is an error that names the key: a nested value, a | block, a
duplicate key, a tab indent, or a list item on two lines. markfluence does not
read it as blank.
When markfluence writes a block again, it keeps full-line # comments and
trailing # ... comments. A list keeps the spelling that you wrote it in.
The block is real YAML. Thus you must quote a value that YAML would read as something other than a plain string:
- a colon and a space (
title: "Deploy Runbook: Part 2") - a leading
#,[,{,@,*,&,%,!,|,>,-, or? - leading or trailing whitespace
- the words that YAML gives a type:
true,false,yes,no,null,~, and anything that looks like a number
markfluence quotes each value automatically when it writes it. Thus this is important only for frontmatter that you write by hand.
null in any spelling (null, Null, ~, or an empty value) means unset.
For a page with the real title null, write title: "null".
| Field | Value domain | Notes |
|---|---|---|
space |
a space key (e.g. ENG, or a personal space like ~1234abcd) |
The target space for create. You can also give --space, or set a space: default for the whole project (see below). If --space and this field disagree, create refuses the file. create writes it back. update refuses a page that is in a different space, and does not move it. It is always a key, and never a numeric space id. |
parent |
null, a numeric page or folder id, or a relative .md path |
See parent. create uses it, or --parent. update moves the page to agree with it. |
page_id |
a numeric page id, or null |
The target page. update needs one, in the frontmatter or in the entry for the file in markfluence.yaml. Without one, update fails, and it does not search by title. create writes it back after it creates the page. null or absent means "no page yet". |
title |
text | The Confluence page title. create needs it, or --title, and writes it back. For update, an absent title keeps the live title of the page. An empty title: is an error in both. |
labels |
a list of label names, e.g. [ci/cd, howto] |
The labels of the page. See labels. |
page_width |
narrow, wide, or max |
The published page width. See page_width. |
page_status |
the display name of a status that page can be given, e.g. Ready for review |
The colored lozenge that Confluence shows next to the page title. See page_status. |
To create a page, you need only the title in the frontmatter, or --title.
nullmeans a top-level page in the Confluence space.- An id names a parent that exists. The parent can be a page id or a Cloud folder id. Nothing records which kind of thing the id is for.
- A
.mdpath names a parent page by its file. The file can be in the same run, and thencreateresolves it in dependency order. The file can also be a page that you published earlier, and thencreateuses itspage_idor itspages:entry. In both cases,createthen changes the value to<page_id> # <original.md>. - A
.mdpath must be inside the documentation root, and it cannot be a symlink. Otherwisecreaterefuses the file.
update moves a page to agree with parent. With parent: null (or ~, or
an empty value), it moves the page to the top of its space. With no parent:
line, it does not move the page. The page takes its children with it. A move
does not give the page a new version. update refuses a parent that it
cannot find, a parent in a different space, and a parent that is the page
itself or a page under it, and it does this before it writes anything.
A new page from create, and a page that update moves, go last among the
children of the parent. markfluence never changes the order of siblings after
that. To put pages in a different order, drag them in Confluence.
A present field means that markfluence asserts it exactly. markfluence
removes a label on the page that the file does not list. labels: [] removes
all of them.
An absent field means that markfluence does not touch the labels. Thus a run that never mentioned labels cannot change a page that a person labeled by hand.
markfluence manages only global: labels. page-info shows a my: or
team: label, but markfluence never writes or removes one. An unmanaged label
can have the same name as a managed label that markfluence must remove. Then
markfluence skips the removal and gives a warning. Confluence removes a label
by a name with no prefix, so it would delete the personal label instead.
markfluence changes names to lowercase and gives a warning, because Confluence does that anyway. Any other invalid name is an error before any write.
To adopt a page that a person labeled in the UI, use page-info to see its
labels, and copy them into the file.
These are the "Adjust width" options of the UI. narrow, wide, and max
map to the default, full-width, and max appearance properties.
markfluence asserts a declared width on every publish. Thus it overwrites a width that a person set in the Confluence UI, unless the file agrees.
The two verbs do different things when the field is absent. create uses
max. update leaves the live width alone and makes no width request at all.
--page-width and a page_width: default for the whole project both count as
declared (see below).
create writes the width that it used back to the file. Thus after create,
the field is not absent. A later update asserts that width, and it overwrites
a width that a person set in the UI. To let the UI decide the width, remove the
page_width: line after create.
A present field means that markfluence asserts it. An absent field means that markfluence does not touch it. "Does not touch" is exact: for a file that declares no status, no verb even reads the status of the page.
The names that you can use are not a fixed list. If a name does not agree with
the list, the file fails, and the error lists the names that would work.
page-info PAGE lists them too (page_status/available).
The match ignores case, because markfluence sends only the id of the status to
Confluence. Thus ready for review publishes, and read and export output
the spelling of the space.
markfluence refuses a custom status, also when your own Confluence picker shows it. A custom status belongs to your account, and not to the space. Thus a file that names one would publish for you and fail for all other persons.
A file cannot clear a status. page_status: with no value is an error, and
not an instruction. Every empty spelling of a scalar looks the same as an
unfinished edit. To clear a status, use the Confluence UI.
A status write gives the page a new version. Thus markfluence does not write a status that already agrees.
Confluence decides which statuses a page can have for each page and for each
account, and not for each space. The same account had 4 statuses on a page
that it created, and 3 on a page that it did not create. The write enforces
this. Thus update asks the page that it publishes to.
The page of create does not exist until create makes it. Thus create
does a check of the name only after it makes the page. If the new page does
not accept the name, the page stays created, with no status and a warning.
This is also important if you move a tree between spaces, because export
writes this field.
You can declare space and page_width one time for a whole project. Put them
in the markfluence.yaml that marks the
documentation root:
space: ENG
page_width: maxFor page_width, the sequence is the flag first, then the frontmatter, then
the project file. The answer that is nearest to the content wins.
space is different at the top. If --space and a frontmatter space:
disagree, create refuses the file, and neither wins.
update uses the space from the file, or else the space: default, as a
check. If the page is in a different space, update refuses the file, because
markfluence cannot know which of the two is the mistake. It never moves a page
between spaces.
For both settings, markfluence reads the project file only when the two levels above it say nothing. Thus the project file never disagrees with either of them.
page_status is not one of these defaults, on purpose. A width is house
style, and a whole tree can share it. A status is a claim about the maturity of
one page. A page_status: Rough draft default for the whole project would make
a false claim about most of the tree on every publish.
Every field above can be in a pages: entry in markfluence.yaml instead
of in the frontmatter of the file. With this, a Markdown file stays clean and
markfluence still publishes it:
pages:
docs/deploy-runbook.md:
title: Deploy Runbook
page_id: 12346
labels: [runbook]An entry in pages: in markfluence.yaml is the same frontmatter block but in
a different location. It has the same field names, the same value domains, and
the same canonical sequence. create writes an entry for you when the project
uses pages: and the file has no frontmatter of its own.
One value has a different spelling in the two locations: a parent: that names
a .md file. In a pages: entry, it is relative to the root. In frontmatter,
it is relative to the file (see
root-model.md).
Both locations are legal, and markfluence says nothing when they agree. A
pages: entry is not a fourth level of precedence. Frontmatter and a pages:
entry are two spellings of one level. Thus when both give a value, markfluence
uses a rule for disagreements, and not a rule for precedence. For the details,
and the rules for path keys, see
root-model.md.
The other part of this file is the reference for the body. For each Markdown construct, it tells you what the construct becomes in Confluence storage format.
The design target is semantic equivalence to valid Confluence storage format, and not byte-for-byte equality. Thus each construct here keeps its meaning through a round trip, but not always its markup. What Confluence does with the result, and the traps behind some of these constructs, is in docs/confluence/.
markfluence renders GFM fenced code blocks as Confluence code macros. They have syntax highlighting for the languages that Confluence supports.
markfluence renders GFM tables as Confluence tables.
To give a cell a background color, put an HTML comment at the start of the cell. The comment is not visible in a Markdown preview. In Confluence, the cell has the background color.
| Service | Status |
| ------- | -------------------------- |
| auth | <!-- bg:light-green --> ok |
| billing | <!-- bg:light-red --> down |The color is a swatch name from the cell background palette of the Confluence
editor. For any other color, use a literal #rrggbb hex value. There are 21
swatches. Each row here is one column of the picker in the editor:
| Light | Medium | Bold |
|---|---|---|
white #ffffff |
light-grey light-gray #f4f5f7 |
grey gray #b3bac5 |
light-blue #deebff |
blue #b3d4ff |
bold-blue #4c9aff |
light-teal #e6fcff |
teal #b3f5ff |
bold-teal #79e2f2 |
light-green #e3fcef |
green #abf5d1 |
bold-green #57d9a3 |
light-yellow #fffae6 |
yellow #fff0b3 |
bold-yellow #ffc400 |
light-red #ffebe6 |
red #ffbdad |
bold-red #ff8f73 |
light-purple #eae6ff |
purple #c0b6f2 |
bold-purple #998dd9 |
Details:
- Confluence colors cells, and not rows or columns. To color a column, put a marker in each cell of the column. To color a row, put a marker in each cell of the row.
- The marker also works in header cells.
- A cell that holds only a marker is an empty colored cell.
- The color marker must be the first thing in the cell. In any other position, markfluence ignores it and gives a warning, because otherwise a stray comment would do nothing that you can see.
- markfluence drops an unknown color name and gives a warning. The cell publishes with no color.
To break a table cell onto more than one line, use a literal <br>. You cannot
use a real newline, because a GFM table row must stay on one physical line.
| Field | Notes |
| ----- | -------------------------- |
| Key | Type: string<br>JQL: "Key" |In storage format, the editor of Confluence records a multi-line cell as
separate paragraphs, and not with <br>. read and export change that back
to the <br> form above.
That form publishes back to the same paragraphs.
For a list in a table cell, use HTML list tags directly in the cell: <ul>,
<ol>, and <li>. You write them as inline HTML in the cell, as you write
<br> for a line break. The list syntax of Markdown needs each item on its own
line, and a table row cannot do that. Thus you cannot use it here.
| Field | Values |
| ------ | ------------------------------------- |
| Status | <ul><li>open</li><li>closed</li></ul> |read and export get back the same tags. They do not change them to anything
else.
GitHub alerts become Confluence panels in the color that GitHub gives them. The
alerts are > [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and
[!CAUTION]:
| alert | colour | published as |
|---|---|---|
NOTE |
blue | info macro |
TIP |
green | tip macro |
IMPORTANT |
purple | ADF panel (no macro exists for purple) |
WARNING |
orange | note macro |
CAUTION |
red | warning macro |
Each alert maps to one panel, and each panel maps to one alert. Thus read and
export get back the original
GFM alert.
Example:
> [!NOTE]
> This is a note. uploads a local file as an attachment. It can also
reference a remote URL. A missing image, or an image of a type that is not
supported, becomes the text line N: IMAGE BROKEN: …. N is the line of the
image in the file.
An image path is relative to the Markdown file, as it is when you look at the file on GitHub. Thus a page in a subdirectory can share an asset directory above it:
docs/ ← needs a markfluence.yaml here for this to work
assets/logo.png
guide/page.md → 
That layout needs a declared
documentation root at docs/. Without
one, the root of each page is its own directory. Then guide/page.md cannot
go above itself to assets/, because that is out of bounds.
Note
An image path is a URL, and not a filename. Thus you must percent-encode a
space or another special character. For a file named my image.png, write
. GitHub and the preview of your editor use
the same rule, and they write this form when they make a link for you.
The angle-bracket form  is a different
spelling of the same image. A bare space () is
not a valid path. Thus it is not an image at all, and it stays on the page as
literal text. GitHub and your preview show the same result.
markfluence read and markfluence export write the encoded form. Thus a
page round-trips back to Markdown that still renders.
The documentation root bounds every
image. An image that resolves outside it (../../secrets/x.png) is not
uploaded. markfluence reports it as
line N: IMAGE BROKEN: … (outside the documentation root). markfluence also
refuses a symlink, also when it resolves inside the root.
A Confluence attachment name cannot contain /. Thus markfluence attaches an
image with its base name: assets/logo.png becomes logo.png. The
attachment comment records the path, relative to the root and not to the page.
markfluence read and markfluence export use that path to put the file back
where it came from. A page one directory down can reference the same file as
../assets/logo.png. That is the same attachment, because both paths resolve
to the same path relative to the root.
The name is only the base name. Thus markfluence cannot publish two images in
one file that have the same name, such as arch/diagram.png and
deploy/diagram.png. An attachment name is unique on a page, so one image
would overwrite the other. markfluence refuses the file and names both paths.
markfluence check reports it with no publish. Rename one of the files.
You can put more properties in the title as JSON:
alignisleft,center, orright.widthandheightare in pixels.
A plain title () becomes the tooltip of the image.
Examples:




markfluence changes a link to a .md file to the Confluence URL of the target
page. The path is relative to the file that has the link, as on GitHub. The
target can be any .md file under the documentation root, such as
../other/page.md. It changes heading anchors to the anchor scheme of
Confluence.
A link destination is a URL, as an image path is. For a sibling whose filename
has a space, write [see](my%20doc.md) or [see](<my doc.md>). A bare
[see](my doc.md) is not a link at all. The same rule applies to the
fragment, so a non-ASCII heading anchor can be #caf%C3%A9-section.
markfluence decodes both before it compares them with files and headings on
disk. Thus either spelling resolves.
For a link to a .md file, whether markfluence reports an unresolved link, and
how serious it is, depends on the reason:
-
A target that does not exist at all, or that resolves outside the documentation root, is Broken. markfluence replaces the whole link element with the literal text
line N: LINK BROKEN: … (not found)orline N: LINK BROKEN: … (outside the documentation root). It does the same for a broken image. -
A target that exists but has no
page_idyet gives a Warning (link not resolved: …). This is the usual state of each page in a tree that nobody published yet. The href still renders exactly as you wrote it.markfluence treats a same-page anchor (
#heading) as a link to the current file. Thus it gives this warning too when the current file has nopage_idyet. The warning looks as if the file names itself as missing. It does not: that is only this file before its first publish. -
A
#fragmentthat matches no heading on a target that resolves is also a Warning (anchor not found: …). The link still works, but it goes to the top of the page, and not to the named heading.
An attachment link or an external URL does not resolve here, and markfluence says nothing about it in both cases. A mention is also a link, but a special link. See below.
A Confluence user mention round-trips as a usual Markdown link to the profile of
the person, with an @ at the start of the link text:
Ping [@Ada Lovelace](https://home.atlassian.com/people/712020:0e5f8a21-3c4d-4e5f-a6b7-c8d9e0f1a2b3) about the deploy.read and export write that form. create and update publish it back as a
real mention. Know these 3 things:
- The
@is what makes it a mention. A link to the same URL with text that does not start with@publishes as a plain link. Thus you can link to the profile of a person without triggering a user mention. - The account id is the only critical part. On publish, markfluence
generates the user mention using the account id. On
readandexport, markfluence will lookup the user display name with the accoun id and re-generate the user mention link. Thus when a person changes their name, markfluence will create the correct user mention on publish and on the nextreadorexport, generate an updated user mention link. - An id that doesn't tie to a real, live account gives a warning, and not an error.
Confluence accepts any account id and shows it as
@Unlicensed user, and it does not fail. Thus markfluence looks up the id and tells you. Nothing else will.
A colleague who left keeps their name. A deactivated account resolves
normally, and Confluence adds the suffix itself. Thus a page after a round trip
shows [@Mark Reid (Deactivated)](…). It records who left, and it does not
lose them.
An id that really does not resolve, such as a typo or a URL that somebody
edited by hand, shows as [@Unlicensed user](…). That is what the page itself
shows. The id stays in the URL, so it is still the easiest thing to correct.
Sometimes markfluence cannot ask whether an account exists, for example with
no network or a refused token. Then it leaves the mention exactly as it was,
and does not put a placeholder in it. Otherwise one bad moment during an
export would write Unlicensed user over every real name in a tree.
<!-- confluence-toc -->: markfluence replaces it with the Confluence table-of-contents macro.<!-- bg:COLOR -->at the start of a table cell: markfluence removes it and gives the cell a background color. See Cell background colors.
There are no other directives. Any other HTML comment that you write is lost because Confluence removes every comment on write, so the comment never gets to the stored page (measured; see storage-format.md).
Don't use HTML comments to leave notes on published pages that you will read
or export in the future.
You can paste Confluence
storage format
markup into your Markdown. This is <ac:…> and <ri:…> elements, such as any
macro or layout. Copy it from ⋯ → View storage format on a page.
markfluence writes it with no change. There are two conventions:
- Put a blank line between an
ac:orri:tag and the Markdown that you want markfluence to convert. An example is the body of a macro or a layout cell. With a blank line, markfluence parses the content as Markdown. Without a blank line, markfluence does not convert the content, and passes it through as it is. - Put the opening tag on its own line, or close it in the same tag. Then markfluence does not put it in a paragraph.
This example is a two-column layout with Markdown in each cell:
<ac:layout>
<ac:layout-section ac:type="two_equal">
<ac:layout-cell>
Left column with **markdown**.
</ac:layout-cell>
<ac:layout-cell>
Right column.
</ac:layout-cell>
</ac:layout-section>
</ac:layout>
Storage markup in a fenced code block stays literal. markfluence does not activate it.