Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save psycho0verload/b0057b5b3bba480cd8d80c3f4eab6822 to your computer and use it in GitHub Desktop.
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment