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";
const arrowColor = encodeURIComponent(
getComputedStyle(document.documentElement)
.getPropertyValue('--ds-text')
.trim() || '#292A2E'
);
let arrowIcons = {
collapsed: "https://api.iconify.design/mdi/chevron-right.svg?color=" + arrowColor,
expanded: "https://api.iconify.design/mdi/chevron-down.svg?color=" + arrowColor
};
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 Confluence page edit and view restrictions to provide essential permission details, including users, groups, and inherited access
Insert a page break in documents generated from a Confluence page
Retrieve and display all page content properties in an AUI sortable table
Read and display the fixed version of Confluence pages by checking page or ancestor labels
Display an overview of all pages in a space, including title, version, and last updated date
Allow users to retrieve all pages from the current space despite the 250-page limit per request
Display space details such as type, status, creation date, and links to permissions and calendars
Find images within page attachments. Handy for reusing files and updating them all at once.
Show filtered issues and their relations
Based on CQL, show a table with the following columns: Page Title, Author, Updated, and Status