This macro dynamically fetches and displays a collapsible tree view of pages, where each title is a clickable link to its corresponding page.
## 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>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
An overview of all pages within one space which contains the title, the version, and the last updated date
Space Information macro by space Id
This macro allows user to retrieve all pages from current space despite limitation of 250 pages per request
Shows page creation date
Find image within page attachments. Handy for reuse files and update them all at once
Show filtered issues and their relations
Based on CQL it shows a table with: Page title, Author, Updated, Status
Display a custom list of recently updated content