Building Dynamic Confluence Dashboards with Page Properties and Storage Format Parsing
Use Confluence's Page Properties macro and storage format parsing to build live cross-project dashboards without custom add-ons. Includes a worked Python example fetching pages via REST API, extracting key-value pairs from XHTML, and generating a Markdown status table.
30 Apr 2026, 23:31 UTC

The Problem: Static Tables That Rot
Every Confluence space accumulates project pages with status tables that someone manually updates — or doesn't. A product manager asks for a cross-project dashboard. The instinct is to build a custom macro or reach for a Marketplace add-on. But Confluence already stores structured data in each page's XHTML storage format. The Page Properties macro writes key-value pairs into that format, and the REST API can fetch pages in bulk. You can build a living dashboard by parsing what's already there, no server-side code required.
How Page Properties Lives in Storage Format
When you add a Page Properties macro to a page and define fields like Project, Status, and Owner, Confluence serializes them as an <ac:structured-macro ac:name=\"pageproperties\"> block inside the page's XHTML. Each field becomes an <ac:parameter ac:name=\"key\"> with its value in a nested <ac:rich-text-body>. The macro renders a two-column table on the page, but the raw storage format is machine-readable.
To see it, open any page with Page Properties, choose ••• > View Source (or append ?viewSource=true to the URL), and search for pageproperties. You'll find markup like:
<ac:structured-macro ac:name="pageproperties" ac:schema-version="1">
<ac:parameter ac:name="Project">
<ac:rich-text-body>Apollo</ac:rich-text-body>
</ac:parameter>
<ac:parameter ac:name="Status">
<ac:rich-text-body>In Progress</ac:rich-text-body>
</ac:parameter>
<ac:parameter ac:name="Owner">
<ac:rich-text-body>[contact removed]</ac:rich-text-body>
</ac:parameter>
</ac:structured-macro>
This is the contract: if your pages use consistent parameter names, you have a queryable dataset embedded in every page.
Fetching Pages at Scale with the REST API
The /rest/api/content endpoint returns paginated results. Use start and limit to page through a space. A typical call from a script or CI job:
GET https://your-domain.atlassian.net/wiki/rest/api/content
?spaceKey=ENG
&expand=body.storage.version
&start=0
&limit=50
&type=page
Where to run: Any environment with network access to your Confluence Cloud instance. Permissions: The authenticating user needs VIEW permission on the target space. Auth: Bearer token (PAT) or Basic auth with API token. Expected check: Response JSON contains a results array; each entry has body.storage.value with the XHTML. Risk: Cloud instances enforce rate limits (undocumented, but observe 429 responses). Implement exponential backoff and respect Retry-After headers.
Verification step: call /rest/api/content?start=0&limit=1 and confirm the response includes size and start fields — this proves pagination works as documented.
Worked Example: A Project Status Dashboard Script
Goal: generate a Markdown table of all projects in the ENG space, keyed by the Page Properties fields Project, Status, Owner.
- Fetch all pages in the space using pagination loop (start=0, limit=50, increment start until results empty).
- Parse storage XHTML for each page. Use an XML parser (not regex) — the storage format is valid XHTML. In Python:
from lxml import etree;root = etree.fromstring(storage_xml.encode()). - Extract parameters with XPath:
//ac:structured-macro[@ac:name='pageproperties']/ac:parameter(register theacnamespace). - Build rows mapping parameter
ac:nameto the text content of itsac:rich-text-body. - Output Markdown or CSV for a static site generator, Slack message, or Confluence page via the API.
Minimal Python snippet (illustrative, not tested):
import requests, os
from lxml import etree
BASE = "https://your-domain.atlassian.net/wiki/rest/api/content"
HEADERS = {"Authorization": f"Bearer {os.environ['CONFLUENCE_TOKEN']}"}
NAMESPACES = {"ac": "http://atlassian.com/schema/confluence/4/ac/"}
def fetch_all_pages(space_key):
start, limit = 0, 50
all_pages = []
while True:
resp = requests.get(BASE, headers=HEADERS, params={
"spaceKey": space_key, "expand": "body.storage,version",
"start": start, "limit": limit, "type": "page"
})
resp.raise_for_status()
data = resp.json()
all_pages.extend(data["results"])
if len(data["results"]) < limit:
break
start += limit
return all_pages
def extract_props(storage_html):
root = etree.fromstring(storage_html.encode())
props = {}
for param in root.xpath("//ac:structured-macro[@ac:name='pageproperties']/ac:parameter", namespaces=NAMESPACES):
key = param.get("{http://atlassian.com/schema/confluence/4/ac/}name")
body = param.find("ac:rich-text-body", namespaces=NAMESPACES)
if body is not None:
props[key] = "".join(body.itertext()).strip()
return props
pages = fetch_all_pages("ENG")
rows = []
for p in pages:
props = extract_props(p["body"]["storage"]["value"])
if props:
rows.append(props)
# Print Markdown table
if rows:
keys = ["Project", "Status", "Owner"]
print("| " + " | ".join(keys) + " |")
print("| " + " | ".join(["---" for _ in keys]) + " |")
for r in rows:
print("| " + " | ".join(r.get(k, "") for k in keys) + " |")
Run this in a scheduled GitHub Action or GitLab CI job to publish a fresh dashboard daily.
Trade-offs and Limitations
- Cloud vs Data Center: Cloud instances may lack certain macros (e.g.,
pagepropertiesreportis Cloud-only; some Data Center macros don't exist in Cloud). The storage format parsing approach works on both, but the macro set differs. - Rate limiting: Atlassian doesn't publish exact limits. A full space crawl on a large instance can hit 429s. Cache ETags, use
If-None-Match, and run during off-peak hours. - Parsing fragility: Storage format evolves. Atlassian-specific tags (
ac:*,ri:*) are stable but not versioned. Pin your parser to a known-good schema or validate with a sample set before each run. - No write-back: This pattern is read-only. Updating Page Properties via API requires PUT to
/rest/api/content/{id}with incrementedversion.numberand the full modified storage XHTML — error-prone and not covered here.
Start With a Single Space Audit
Before automating, verify the data exists and is consistent. Pick one space, run the fetch_all_pages function with limit=5, and inspect the extracted props dict for each page. Look for missing keys, typos ("Statue" vs "Status"), or pages that use the Excerpt macro instead of Page Properties. The Excerpt macro (<ac:structured-macro ac:name=\"excerpt\">) stores a single block of rich text — useful for summaries but not structured fields.
If 80% of pages have clean Page Properties, you have a viable data source. If not, standardize the template first: create a page template with a pre-filled Page Properties macro, then apply it to new projects. The dashboard script becomes a maintenance-free byproduct of that discipline.