Create the page to use the template linktree as
---
template: linktree
title: Ideas
process:
twig: true
access:
site.login: true
---
Put the data in a directory _data as default.md as an example as frontmatter
---
title: Data Container
tree_items:
- title: Business
children:
- title: Seniors
children:
- title: "Pickup Up Stuff"
url: "https://pragprog.com"
details: "Foo Blah..."
- title: "To Watch"
children:
- title: "Grav Tutorials"
details: "Official playlist..."
---
The template listtree.html.twig loads the _data/default.md front matter as treeData and uses in javascript to create tree and details
{% extends 'partials/base.html.twig' %}
{% block content %}
{# 1. Find the sub-page relative to the current page #}
{# Note: use the folder name with the underscore #}
{% set relative_path = page.route ~ '/_data' %}
{% set data_page = page.find(relative_path) %}
{% if data_page %}
{# 2. Get the pre-parsed array from the header #}
{% set clean_array = data_page.header.tree_items %}
<script>
// 3. Convert the PHP array to a JavaScript Object
var treeData = {{ clean_array|json_encode|raw }};
console.log("Success! Data loaded:", treeData);
// Now you can build your tree using JS
// buildTree(treeData);
</script>
{% else %}
<div class="alert">Error: Could not find _data page.</div>
{% endif %}
<div class="split-container">
<aside class="sidebar-panel" id="js-sidebar"></aside>
<main class="content-panel" id="js-content">
<div class="welcome-box">
<p>Select an item from the tree to view details.</p>
</div>
</main>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
// 1. Initial Check: Ensure data exists
if (typeof treeData === 'undefined' || !treeData) {
console.error("treeData is missing. Make sure the Twig loaded correctly.");
return;
}
const sidebar = document.getElementById('js-sidebar');
const content = document.getElementById('js-content');
// 2. Start the Build
const rootList = createTreeList(treeData);
sidebar.appendChild(rootList);
// --- Core Function: Recursively Build the List --- //
function createTreeList(items) {
const ul = document.createElement('ul');
ul.className = 'tree-list';
items.forEach(item => {
const li = document.createElement('li');
li.className = 'tree-item';
// A. Create the Row (Toggle + Title)
const row = document.createElement('div');
row.className = 'tree-row';
// B. Check for Children
const hasChildren = item.children && item.children.length > 0;
// C. Create Toggle Button
if (hasChildren) {
const toggle = document.createElement('span');
toggle.className = 'tree-toggle';
toggle.innerHTML = '<i class="fa fa-chevron-right"></i>';
// Toggle Event
toggle.onclick = function(e) {
e.stopPropagation(); // Prevent triggering the link click
li.classList.toggle('open');
};
row.appendChild(toggle);
} else {
// Spacer for alignment
const spacer = document.createElement('span');
spacer.className = 'tree-spacer';
row.appendChild(spacer);
}
// D. Create the Link/Title
const link = document.createElement('span');
link.className = 'tree-link';
link.innerText = item.title;
// Link Click Event -> Render Details
link.onclick = function() {
// Highlight active state
document.querySelectorAll('.tree-link').forEach(el => el.classList.remove('active'));
link.classList.add('active');
// Render Right Panel
renderDetails(item);
};
row.appendChild(link);
// Append Row to LI
li.appendChild(row);
// E. Recursion: Build Children if they exist
if (hasChildren) {
const wrapper = document.createElement('div');
wrapper.className = 'tree-children-wrapper'; // Hidden by CSS until 'open' class added
const childUl = createTreeList(item.children); // RECURSION HERE
wrapper.appendChild(childUl);
li.appendChild(wrapper);
}
ul.appendChild(li);
});
return ul;
}
// --- Helper: Render the Right Panel --- //
function renderDetails(item) {
// Clear current content
content.innerHTML = '';
// 1. Title
const h1 = document.createElement('h1');
h1.innerText = item.title;
content.appendChild(h1);
// 2. Details (HTML/Text)
if (item.details) {
const desc = document.createElement('div');
desc.className = 'detail-body';
// Note: If your YAML has HTML tags (like <br>), use innerHTML.
// If strictly text, use innerText for security.
desc.innerHTML = item.details;
content.appendChild(desc);
}
// 3. Link Button
if (item.url) {
const btn = document.createElement('a');
btn.href = item.url;
btn.target = "_blank";
btn.className = "button button-primary"; // Use standard Grav button classes
btn.innerHTML = 'Visit Link <i class="fa fa-external-link"></i>';
btn.style.marginTop = "20px";
btn.style.display = "inline-block";
content.appendChild(btn);
}
}
});
</script>
{% endblock %}