Show article TOC below author in post sidebar
This commit is contained in:
@@ -3,8 +3,12 @@
|
|||||||
{# Each widget is wrapped in a collapsible container with localStorage persistence #}
|
{# Each widget is wrapped in a collapsible container with localStorage persistence #}
|
||||||
{% from "components/icon.njk" import icon %}
|
{% from "components/icon.njk" import icon %}
|
||||||
|
|
||||||
|
{% set isArticlePost = (postType == "article") or (page.url and page.url.startsWith('/articles/') and page.url != '/articles/') %}
|
||||||
|
{% set showArticleToc = isArticlePost %}
|
||||||
|
|
||||||
{% if homepageConfig and homepageConfig.blogPostSidebar and homepageConfig.blogPostSidebar.length %}
|
{% if homepageConfig and homepageConfig.blogPostSidebar and homepageConfig.blogPostSidebar.length %}
|
||||||
{# === Data-driven mode: render configured widgets === #}
|
{# === Data-driven mode: render configured widgets === #}
|
||||||
|
{% set hasConfiguredToc = '"toc"' in (homepageConfig.blogPostSidebar | dump) %}
|
||||||
{% for widget in homepageConfig.blogPostSidebar %}
|
{% for widget in homepageConfig.blogPostSidebar %}
|
||||||
|
|
||||||
{# Resolve widget title #}
|
{# Resolve widget title #}
|
||||||
@@ -160,6 +164,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if showArticleToc and not hasConfiguredToc and (widget.type == "author-card" or widget.type == "author-card-compact") %}
|
||||||
|
{% set widgetKey = "post-widget-toc-article" %}
|
||||||
|
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
|
||||||
|
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
|
||||||
|
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
|
||||||
|
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("list", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Table of Contents</h3>
|
||||||
|
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
|
||||||
|
{% include "components/widgets/toc.njk" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{# === Fallback: aligned with rmendes.net article sidebar === #}
|
{# === Fallback: aligned with rmendes.net article sidebar === #}
|
||||||
@@ -178,6 +197,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if showArticleToc %}
|
||||||
|
{# Table of Contents (articles only) #}
|
||||||
|
{% set widgetKey = "post-widget-toc-article" %}
|
||||||
|
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
|
||||||
|
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
|
||||||
|
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
|
||||||
|
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("list", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Table of Contents</h3>
|
||||||
|
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
|
||||||
|
{% include "components/widgets/toc.njk" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# Share #}
|
{# Share #}
|
||||||
{% set widgetKey = "post-widget-share-1" %}
|
{% set widgetKey = "post-widget-share-1" %}
|
||||||
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
|
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{# Table of Contents Widget (for articles with headings) #}
|
{# Table of Contents Widget (for articles with headings) #}
|
||||||
{% if toc and toc.length %}
|
|
||||||
<is-land on:visible>
|
<is-land on:visible>
|
||||||
<div class="widget">
|
<div class="widget">
|
||||||
<h3 class="widget-title">Contents</h3>
|
<h3 class="widget-title">Contents</h3>
|
||||||
<nav class="toc" aria-label="Table of contents">
|
<nav class="toc" aria-label="Table of contents">
|
||||||
|
{% if toc and toc.length %}
|
||||||
<ul class="space-y-1 text-sm">
|
<ul class="space-y-1 text-sm">
|
||||||
{% for item in toc %}
|
{% for item in toc %}
|
||||||
<li class="{% if item.level == 3 %}ml-3{% elif item.level == 4 %}ml-6{% elif item.level == 5 %}ml-9{% elif item.level == 6 %}ml-12{% endif %}">
|
<li class="{% if item.level == 3 %}ml-3{% elif item.level == 4 %}ml-6{% elif item.level == 5 %}ml-9{% elif item.level == 6 %}ml-12{% endif %}">
|
||||||
@@ -13,7 +13,47 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<ul class="space-y-1 text-sm" data-toc-fallback-list></ul>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const script = document.currentScript;
|
||||||
|
if (!script) return;
|
||||||
|
|
||||||
|
const widget = script.closest(".widget");
|
||||||
|
const fallbackList = widget ? widget.querySelector("[data-toc-fallback-list]") : null;
|
||||||
|
if (!widget || !fallbackList) return;
|
||||||
|
|
||||||
|
const shell = widget.closest(".widget-collapsible");
|
||||||
|
const contentRoot = document.querySelector("article .e-content");
|
||||||
|
if (!contentRoot) {
|
||||||
|
if (shell) shell.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headings = Array.from(contentRoot.querySelectorAll("h2[id], h3[id], h4[id], h5[id], h6[id]"));
|
||||||
|
if (!headings.length) {
|
||||||
|
if (shell) shell.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const heading of headings) {
|
||||||
|
const level = Number.parseInt(heading.tagName.slice(1), 10);
|
||||||
|
const item = document.createElement("li");
|
||||||
|
if (Number.isFinite(level) && level > 2) {
|
||||||
|
item.style.marginLeft = `${(level - 2) * 0.75}rem`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = `#${heading.id}`;
|
||||||
|
link.className = "text-surface-600 dark:text-surface-400 hover:text-accent-600 dark:hover:text-accent-400 hover:underline transition-colors";
|
||||||
|
link.textContent = heading.textContent ? heading.textContent.trim() : heading.id;
|
||||||
|
item.appendChild(link);
|
||||||
|
fallbackList.appendChild(item);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</is-land>
|
</is-land>
|
||||||
{% endif %}
|
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
{# Each widget is wrapped in a collapsible container with localStorage persistence #}
|
{# Each widget is wrapped in a collapsible container with localStorage persistence #}
|
||||||
{% from "components/icon.njk" import icon %}
|
{% from "components/icon.njk" import icon %}
|
||||||
|
|
||||||
|
{% set isArticlePost = (postType == "article") or (page.url and page.url.startsWith('/articles/') and page.url != '/articles/') %}
|
||||||
|
{% set showArticleToc = isArticlePost %}
|
||||||
|
|
||||||
{% if homepageConfig and homepageConfig.blogPostSidebar and homepageConfig.blogPostSidebar.length %}
|
{% if homepageConfig and homepageConfig.blogPostSidebar and homepageConfig.blogPostSidebar.length %}
|
||||||
{# === Data-driven mode: render configured widgets === #}
|
{# === Data-driven mode: render configured widgets === #}
|
||||||
|
{% set hasConfiguredToc = '"toc"' in (homepageConfig.blogPostSidebar | dump) %}
|
||||||
{% for widget in homepageConfig.blogPostSidebar %}
|
{% for widget in homepageConfig.blogPostSidebar %}
|
||||||
|
|
||||||
{# Resolve widget title #}
|
{# Resolve widget title #}
|
||||||
@@ -160,6 +164,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if showArticleToc and not hasConfiguredToc and (widget.type == "author-card" or widget.type == "author-card-compact") %}
|
||||||
|
{% set widgetKey = "post-widget-toc-article" %}
|
||||||
|
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
|
||||||
|
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
|
||||||
|
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
|
||||||
|
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("list", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Table of Contents</h3>
|
||||||
|
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
|
||||||
|
{% include "components/widgets/toc.njk" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{# === Fallback: aligned with rmendes.net article sidebar === #}
|
{# === Fallback: aligned with rmendes.net article sidebar === #}
|
||||||
@@ -178,6 +197,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if showArticleToc %}
|
||||||
|
{# Table of Contents (articles only) #}
|
||||||
|
{% set widgetKey = "post-widget-toc-article" %}
|
||||||
|
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
|
||||||
|
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
|
||||||
|
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
|
||||||
|
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("list", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Table of Contents</h3>
|
||||||
|
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
|
||||||
|
{% include "components/widgets/toc.njk" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# Share #}
|
{# Share #}
|
||||||
{% set widgetKey = "post-widget-share-1" %}
|
{% set widgetKey = "post-widget-share-1" %}
|
||||||
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
|
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{# Table of Contents Widget (for articles with headings) #}
|
{# Table of Contents Widget (for articles with headings) #}
|
||||||
{% if toc and toc.length %}
|
|
||||||
<is-land on:visible>
|
<is-land on:visible>
|
||||||
<div class="widget">
|
<div class="widget">
|
||||||
<h3 class="widget-title">Contents</h3>
|
<h3 class="widget-title">Contents</h3>
|
||||||
<nav class="toc">
|
<nav class="toc" aria-label="Table of contents">
|
||||||
|
{% if toc and toc.length %}
|
||||||
<ul class="space-y-1 text-sm">
|
<ul class="space-y-1 text-sm">
|
||||||
{% for item in toc %}
|
{% for item in toc %}
|
||||||
<li class="{% if item.level > 2 %}ml-{{ (item.level - 2) * 3 }}{% endif %}">
|
<li class="{% if item.level > 2 %}ml-{{ (item.level - 2) * 3 }}{% endif %}">
|
||||||
@@ -13,7 +13,47 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<ul class="space-y-1 text-sm" data-toc-fallback-list></ul>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const script = document.currentScript;
|
||||||
|
if (!script) return;
|
||||||
|
|
||||||
|
const widget = script.closest(".widget");
|
||||||
|
const fallbackList = widget ? widget.querySelector("[data-toc-fallback-list]") : null;
|
||||||
|
if (!widget || !fallbackList) return;
|
||||||
|
|
||||||
|
const shell = widget.closest(".widget-collapsible");
|
||||||
|
const contentRoot = document.querySelector("article .e-content");
|
||||||
|
if (!contentRoot) {
|
||||||
|
if (shell) shell.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headings = Array.from(contentRoot.querySelectorAll("h2[id], h3[id], h4[id], h5[id], h6[id]"));
|
||||||
|
if (!headings.length) {
|
||||||
|
if (shell) shell.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const heading of headings) {
|
||||||
|
const level = Number.parseInt(heading.tagName.slice(1), 10);
|
||||||
|
const item = document.createElement("li");
|
||||||
|
if (Number.isFinite(level) && level > 2) {
|
||||||
|
item.style.marginLeft = `${(level - 2) * 0.75}rem`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = `#${heading.id}`;
|
||||||
|
link.className = "text-surface-600 dark:text-surface-400 hover:text-accent-600 dark:hover:text-accent-400 transition-colors";
|
||||||
|
link.textContent = heading.textContent ? heading.textContent.trim() : heading.id;
|
||||||
|
item.appendChild(link);
|
||||||
|
fallbackList.appendChild(item);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</is-land>
|
</is-land>
|
||||||
{% endif %}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user