Page Tree View — Confluence Cloud Macro
This macro dynamically fetches and displays a collapsible tree view of pages, where each title is a clickable link to its corresponding page.
User Parameters
Parent Page
Choose a parent page to generate the page tree (defaults to the current page if none is selected)
Template
## Set parent page variables (current page by default)
#set($parentPageId = $page.id)
#set($parentPageTitle = $page.title)
## If user parameter is provided, extract parent page data
#if($parameters["parentPage"])
#set($parentPageParts = $StringUtils.split($parameters["parentPage"], ":"))
#if($parentPageParts.size() > 1)
#set($parentPageTitle = $parentPageParts[1])
#set($parentPageSpace = $parentPageParts[0])
#else
#set($parentPageTitle = $parentPageParts[0])
#set($parentPageSpace = $space.key)
#end
## Search for the parent page by title and space
#set($cql = "title='$parentPageTitle' and space='$parentPageSpace'")
#set($parentSearchResult = $ConfluenceManager.get("/wiki/rest/api/content/search?cql=$cql"))
#if($parentSearchResult.results.size() > 0)
#set($parentPageId = $parentSearchResult.results[0].id)
#end
#end
<style>
ul {
display: flex;
flex-direction: column;
width: 100%;
gap: 6px;
padding-left: 10px;
min-height: 100px;
}
li {
list-style: none;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.main-container {
width: fit-content;
height: 100vh;
overflow: hidden;
}
.expand-arrow,
.list-bullet {
margin-right: 5px;
min-width: 18px;
min-height: 18px;
text-align: center;
}
.expand-arrow {
cursor: pointer;
display: inline-block;
width: 18px;
height: 18px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.child-list {
padding-left: 20px;
display: none;
opacity: 0;
transition: opacity 0.3s ease-in-out;
overflow: hidden;
}
.no-children {
margin-top: 2px;
}
</style>
<main class="main-container">
<ul id="pageList">
<li>
<span class="expand-arrow" data-page-id="$parentPageId"></span>
<a href="$baseUrl/pages/viewpage.action?pageId=$parentPageId" target="_blank">$parentPageTitle</a>
<ul class="child-list" id="children-$parentPageId"></ul>
</li>
</ul>
</main>
<script>
const childrenMaxLimit = 250;
const baseURL = "$baseUrl";
let arrowIcons = {
collapsed: "https://api.iconify.design/mdi/chevron-right.svg",
expanded: "https://api.iconify.design/mdi/chevron-down.svg"
};
let loadedChildren = new Set();
document.addEventListener("DOMContentLoaded", function () {
const rootPageId = "$parentPageId";
const rootArrow = document.querySelector('.expand-arrow[data-page-id="' + rootPageId + '"]');
checkHasChildren(rootPageId, rootArrow);
});
const setArrowIcon = (arrow, state) => {
const iconURL = arrowIcons[state];
const fallback = state === "collapsed" ? "▶": "▼";
const img = new Image();
img.src = iconURL;
img.width = 18;
img.height = 18;
img.onload = () => {
arrow.textContent = "";
arrow.style.backgroundImage = 'url("' + iconURL + '")';
};
img.onerror = () => {
arrow.style.backgroundImage = "none";
arrow.textContent = fallback;
arrow.style.fontSize = "16px";
arrow.style.lineHeight = "18px";
};
};
function checkHasChildren(parentId, arrow) {
AP.request({
url: "/api/v2/pages/" + parentId + "/children?limit=" + childrenMaxLimit,
type: "GET",
contentType: "application/json",
success: function (response) {
const data = JSON.parse(response);
const li = arrow.closest("li");
if (data.results.length > 0) {
setArrowIcon(arrow, "collapsed");
arrow.onclick = function () {
toggleChildren(parentId, arrow);
};
} else {
arrow.outerHTML = '<span class="list-bullet">•</span>';
li.classList.add("no-children");
}
},
error: function (xhr) {
console.error("Failed to check children for page " + parentId + ":", xhr.status, xhr.statusText);
}
});
}
function toggleChildren(parentId, arrow) {
const sublist = document.getElementById("children-" + parentId);
if (!sublist) return;
const isExpanded = sublist.style.display === "block";
if (isExpanded) {
sublist.style.opacity = "0";
setTimeout(function () {
sublist.style.display = "none";
setArrowIcon(arrow, "collapsed");
}, 300);
} else {
sublist.style.display = "block";
setTimeout(function () {
sublist.style.opacity = "1";
setArrowIcon(arrow, "expanded");
}, 10);
if (!loadedChildren.has(parentId)) {
loadedChildren.add(parentId);
AP.request({
url: "/api/v2/pages/" + parentId + "/children?limit=" + childrenMaxLimit,
type: "GET",
contentType: "application/json",
success: function (response) {
const data = JSON.parse(response);
renderPages(data.results, sublist);
},
error: function (xhr) {
console.error("Failed to fetch children for page " + parentId + ":", xhr.status, xhr.statusText);
}
});
}
}
}
function renderPages(items, container) {
items.forEach(function (item) {
const li = document.createElement("li");
const link = document.createElement("a");
link.href = baseURL + "/pages/viewpage.action?pageId=" + item.id;
link.textContent = item.title;
link.target = "_blank";
const sublist = document.createElement("ul");
sublist.classList.add("child-list");
sublist.id = "children-" + item.id;
const arrow = document.createElement("span");
arrow.className = "expand-arrow";
arrow.setAttribute("data-page-id", item.id);
li.appendChild(arrow);
li.appendChild(link);
container.appendChild(li);
container.appendChild(sublist);
checkHasChildren(item.id, arrow);
});
}
</script>Recommended Macros
Display page edit and view restrictions in Confluence to get essential permission details, including users, groups, and inherited access.
Read and display the fixed version of Confluence pages by checking page or ancestor labels
This macro allows user to retrieve all pages from current space despite limitation of 250 pages per request
An overview of all pages within one space which contains the title, the version, and the last updated date
Find image within page attachments. Handy for reuse files and update them all at once
Use sticky previous/next buttons to navigate through your Confluence page tree in a linear reading order
Macro that will show a "Delete me" message only for editors (in the Page Edit mode)
Display Confluence users filtered by their email addresses.