Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save psycho0verload/b0057b5b3bba480cd8d80c3f4eab6822 to your computer and use it in GitHub Desktop.

Select an option

Save psycho0verload/b0057b5b3bba480cd8d80c3f4eab6822 to your computer and use it in GitHub Desktop.
This script dynamically loads and displays a hierarchical list of subpages for the current page in Wiki.js (version 2.x). It utilizes the built-in GraphQL API to fetch child pages and builds a nested tree view up to a specified depth.

📄 Auto-generate Subpage Tree for Wiki.js 2.x

Description:

This script dynamically loads and displays a hierarchical list of subpages for the current page in Wiki.js (version 2.x). It utilizes the built-in GraphQL API to fetch child pages and builds a nested tree view up to a specified depth.

The script is especially useful for wikis with structured page hierarchies and helps users navigate subpages more easily.

Usage:

The script can be included in two ways:

  1. Globally by the administrator

Navigate to: Administration → Theme → Head HTML Injection

Paste the script there to make it available on all pages.

  1. On individual pages Navigate to: Page Settings → Scripts

Paste the script into the “Script” section to apply it only to a specific page.

Requirements:

  • A <div class="children-placeholder"> must be present on the page.
  • Optionally, configure the placeholder with the following attributes:
    • data-limit="100" – Maximum number of subpages
    • data-depth="2" – Maximum tree depth
    • data-sort="title:asc" – Sorting (by title or path, asc or desc)
    • data-debug="true" – Enable debug logs in the console

Result:

Once the page is loaded, the script replaces the placeholder with a nested

    structure of links to all available subpages.

<script type="application/javascript">
window.addEventListener("load", async () => {
const placeholder = document.querySelector(".children-placeholder");
// Check if the placeholder element exists
if (!placeholder) {
console.log("🛈 No .children-placeholder found.");
return;
}
// Read configuration from data attributes
const limit = parseInt(placeholder.getAttribute("data-limit") || "100", 10);
const maxDepth = parseInt(placeholder.getAttribute("data-depth") || "1", 10);
const sortAttr = placeholder.getAttribute("data-sort") || "path:asc";
const debug = placeholder.getAttribute("data-debug") === "true";
const [sortField, sortDirection] = sortAttr.split(":");
const sortAsc = sortDirection !== "desc";
const log = (...args) => debug && console.log(...args);
// Parse the URL path to determine the base path and locale
let fullPath = window.location.pathname;
let [, locale, ...pathParts] = fullPath.split("/");
locale = locale || "de";
let path = pathParts.join("/").replace(/^\/+|\/+$/g, "");
const basePath = path ? `${path}/` : "";
log("🌍 Locale:", locale);
log("🔍 Searching for subpages of path:", basePath);
// Show loading message
placeholder.innerHTML = "Loading subpages…";
// GraphQL query to fetch pages
const query = {
query: `
query ($query: String!, $locale: String!) {
pages {
search(query: $query, locale: $locale) {
results {
title
path
description
}
}
}
}
`,
variables: {
query: basePath,
locale: locale
}
};
try {
// Send GraphQL query to server
const response = await fetch("/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(query)
});
const json = await response.json();
// Check for errors in response
if (!response.ok || json.errors) {
throw new Error("GraphQL error: " + JSON.stringify(json.errors));
}
const results = json?.data?.pages?.search?.results ?? [];
log("📄 Found pages:", results.map(p => p.path));
// Filter and sort child pages
const children = results
.filter(p => p.path !== path)
.filter(p => p.path.startsWith(path + "/"))
.sort((a, b) => {
const aVal = a[sortField]?.toLowerCase?.() || "";
const bVal = b[sortField]?.toLowerCase?.() || "";
if (aVal < bVal) return sortAsc ? -1 : 1;
if (aVal > bVal) return sortAsc ? 1 : -1;
return 0;
})
.slice(0, limit);
log("✅ Filtered & sorted subpages:", children.map(p => p.path));
// Show message if no children were found
if (children.length === 0) {
placeholder.innerHTML = "<em>No subpages available.</em>";
return;
}
// Build a tree structure from the page paths
const tree = {};
children.forEach(page => {
const relPath = page.path.slice(basePath.length).replace(/^\/+|\/+$/g, "");
const parts = relPath.split("/");
let node = tree;
parts.forEach((part, idx) => {
if (!node[part]) {
node[part] = { __meta: null, __children: {} };
}
if (idx === parts.length - 1) {
node[part].__meta = page;
}
node = node[part].__children;
});
});
// Escape HTML to prevent XSS
function escapeHtml(str) {
return str.replace(/[&<>"']/g, (m) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" }[m])
);
}
// Recursively render the tree into a nested list
function renderTree(treeObj, depth = 1) {
if (depth > maxDepth) return null;
const ul = document.createElement("ul");
ul.className = `children-tree level-${depth}`;
for (const key of Object.keys(treeObj)) {
const node = treeObj[key];
const hasChildren = Object.keys(node.__children).length > 0;
const hasMeta = !!node.__meta;
if (!hasMeta && !hasChildren) continue;
const li = document.createElement("li");
li.className = "children-item";
if (hasMeta) {
const p = node.__meta;
li.innerHTML = `<a href="/${p.path}">${escapeHtml(p.title)}</a><br><small>${escapeHtml(p.description || "")}</small>`;
} else {
li.innerHTML = `<strong>${key}</strong>`;
}
const childList = renderTree(node.__children, depth + 1);
if (childList) li.appendChild(childList);
ul.appendChild(li);
}
return ul;
}
// Create the final HTML structure and replace the placeholder
const wrapper = document.createElement("div");
wrapper.className = "children-list";
const treeHtml = renderTree(tree);
if (treeHtml) wrapper.appendChild(treeHtml);
placeholder.replaceWith(wrapper);
log("🌲 Tree structure successfully rendered.");
} catch (err) {
console.error("❌ Error loading subpages:", err);
placeholder.innerHTML = "<em>Error loading subpages.</em>";
}
});
</script>
@mormorhaxa
Copy link

I forked this and made some changes so that the script waits for Wiki.js to render the page before replacing the contents of the div (instead of replacing the entire div), which means additional text on the page doesn’t interfere with the scripts functionality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment