Skip to content

Commit ec44abb

Browse files
committed
feat: add ToC sidebar, footer, quest subtitles, and SEO/accessibility fixes
Add right-side table of contents with scroll-aware active state, auto-generated from h2/h3 headings, hidden on mobile and print. Add site footer with credits, @Andralax thanks, and dual licensing (MIT + CC BY-SA 4.0). Add technical subtitles to all 23 quest cards on both FR/EN homepages. Fix color contrast issues flagged by Google (gold-dark, brown-medium, inline opacity replaced by .quest-subtitle-hint class). Fix hreflang tags to use absolute URLs instead of relative. Defer JS scripts and lazy-load Pagefind CSS to reduce render-blocking. Update homepage tip to reflect that Quest 01 handles Git installation.
1 parent 0e263dd commit ec44abb

File tree

7 files changed

+365
-106
lines changed

7 files changed

+365
-106
lines changed

src/_includes/layouts/base.njk

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>{{ title }} - {% if lang == "fr" %}Les Chroniques du Versionneur{% else %}The Git Chronicles{% endif %}</title>
77
<link rel="stylesheet" href="/assets/css/style.css">
8-
<link rel="stylesheet" href="/pagefind/pagefind-ui.css">
8+
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" media="print" onload="this.media='all'">
99
{% if extra_css %}<link rel="stylesheet" href="{{ extra_css }}" media="print">{% endif %}
1010
{% if quest_id %}
11-
<link rel="alternate" hreflang="fr" href="/fr/quetes/{{ quest_slug_fr }}/">
12-
<link rel="alternate" hreflang="en" href="/en/quests/{{ quest_slug_en }}/">
11+
<link rel="alternate" hreflang="fr" href="{{ site.url }}/fr/quetes/{{ quest_slug_fr }}/">
12+
<link rel="alternate" hreflang="en" href="{{ site.url }}/en/quests/{{ quest_slug_en }}/">
1313
{% elif cheatsheet_slug_fr %}
14-
<link rel="alternate" hreflang="fr" href="/fr/cheatsheets/{{ cheatsheet_slug_fr }}/">
15-
<link rel="alternate" hreflang="en" href="/en/cheatsheets/{{ cheatsheet_slug_en }}/">
14+
<link rel="alternate" hreflang="fr" href="{{ site.url }}/fr/cheatsheets/{{ cheatsheet_slug_fr }}/">
15+
<link rel="alternate" hreflang="en" href="{{ site.url }}/en/cheatsheets/{{ cheatsheet_slug_en }}/">
1616
{% else %}
17-
<link rel="alternate" hreflang="fr" href="/fr/">
18-
<link rel="alternate" hreflang="en" href="/en/">
17+
<link rel="alternate" hreflang="fr" href="{{ site.url }}/fr/">
18+
<link rel="alternate" hreflang="en" href="{{ site.url }}/en/">
1919
{% endif %}
20-
<link rel="alternate" hreflang="x-default" href="/fr/">
20+
<link rel="alternate" hreflang="x-default" href="{{ site.url }}/fr/">
2121
<link rel="prefetch" href="/ai.txt">
2222
<link rel="prefetch" href="/llms.txt">
2323
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large">
@@ -118,13 +118,46 @@
118118
{{ content | safe }}
119119
<nav class="nav-footer" id="nav-footer"></nav>
120120
</main>
121+
<aside class="toc-sidebar" id="toc-sidebar">
122+
<nav class="toc-nav" aria-label="{% if lang == 'fr' %}Sommaire de la page{% else %}On this page{% endif %}">
123+
<div class="toc-title">{% if lang == "fr" %}Sur cette page{% else %}On this page{% endif %}</div>
124+
<ul class="toc-list" id="toc-list"></ul>
125+
</nav>
126+
</aside>
121127
</div>
122128

129+
<footer class="site-footer">
130+
<div class="footer-content">
131+
<p>
132+
{% if lang == "fr" %}
133+
<a href="https://github.com/Dxsk/git-chronicles">Les Chroniques du Versionneur</a>, un projet libre et open source par <a href="https://github.com/Dxsk" target="_blank" rel="noopener">🇫🇷 Dxsk</a>.
134+
{% else %}
135+
<a href="https://github.com/Dxsk/git-chronicles">The Git Chronicles</a>, a free and open source project by <a href="https://github.com/Dxsk" target="_blank" rel="noopener">🇫🇷 Dxsk</a>.
136+
{% endif %}
137+
</p>
138+
<p class="footer-thanks">
139+
{% if lang == "fr" %}
140+
Merci à <a href="https://github.com/Andralax" target="_blank" rel="noopener">@Andralax</a> pour ses idées et sa relecture.
141+
{% else %}
142+
Thanks to <a href="https://github.com/Andralax" target="_blank" rel="noopener">@Andralax</a> for ideas and proofreading.
143+
{% endif %}
144+
</p>
145+
<p class="footer-license">
146+
{% if lang == "fr" %}
147+
Code sous licence <a href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener">MIT</a> · Contenu sous licence <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">CC BY-SA 4.0</a>
148+
{% else %}
149+
Code licensed under <a href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener">MIT</a> · Content licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">CC BY-SA 4.0</a>
150+
{% endif %}
151+
</p>
152+
</div>
153+
</footer>
154+
123155
<script>
124156
window.__lang = "{{ lang }}";
125157
</script>
126-
<script src="/assets/js/nav.js"></script>
127-
<script src="/assets/js/highlight.js"></script>
158+
<script src="/assets/js/nav.js" defer></script>
159+
<script src="/assets/js/highlight.js" defer></script>
160+
<script src="/assets/js/toc.js" defer></script>
128161

129162
<!-- Search FAB + modal -->
130163
<button class="search-fab" id="search-fab" aria-label="{% if lang == 'fr' %}Rechercher{% else %}Search{% endif %}">
@@ -144,7 +177,7 @@
144177
</div>
145178
</div>
146179

147-
<script src="/pagefind/pagefind-ui.js"></script>
180+
<script src="/pagefind/pagefind-ui.js" defer></script>
148181
<script>
149182
(function() {
150183
var fab = document.getElementById("search-fab");

src/assets/css/style.css

Lines changed: 148 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
--color-parchment-dark: #e8d5a8;
1414
--color-parchment-darker: #d4bf8a;
1515
--color-brown-dark: #3b2a1a;
16-
--color-brown-medium: #5c3d2e;
16+
--color-brown-medium: #4a3020;
1717
--color-brown-light: #8b6914;
1818
--color-gold: #c8a951;
1919
--color-gold-light: #e8d48b;
20-
--color-gold-dark: #a08030;
20+
--color-gold-dark: #7a6020;
2121
--color-green-dark: #2d5016;
2222
--color-green-light: #e8f5e0;
2323
--color-green-bg: #f0f8ea;
@@ -62,7 +62,7 @@
6262
--transition-base: 250ms ease;
6363

6464
/* Layout */
65-
--content-max-width: 860px;
65+
--content-max-width: 960px;
6666
--nav-width: 280px;
6767
--header-height: 64px;
6868
}
@@ -1230,7 +1230,6 @@ tbody tr:hover {
12301230
font-weight: 700;
12311231
min-width: 28px;
12321232
text-align: center;
1233-
opacity: 0.8;
12341233
}
12351234

12361235
.quest-card .quest-name {
@@ -1261,6 +1260,11 @@ tbody tr:hover {
12611260
display: none;
12621261
}
12631262

1263+
.quest-subtitle-hint {
1264+
color: var(--color-brown-medium);
1265+
font-size: 0.85em;
1266+
}
1267+
12641268
.quest-card.disabled::after {
12651269
content: "a venir";
12661270
margin-left: auto;
@@ -1485,12 +1489,151 @@ tbody tr:hover {
14851489
}
14861490

14871491

1492+
/* ============================================================
1493+
Site Footer
1494+
============================================================ */
1495+
1496+
.site-footer {
1497+
background: var(--color-brown-dark);
1498+
color: var(--color-parchment-dark);
1499+
text-align: center;
1500+
padding: var(--spacing-xl) var(--spacing-lg);
1501+
font-size: 0.85rem;
1502+
line-height: 1.6;
1503+
border-top: 3px solid var(--color-gold);
1504+
}
1505+
1506+
.footer-content p {
1507+
margin: 0.3rem 0;
1508+
}
1509+
1510+
.site-footer a {
1511+
color: var(--color-gold-light);
1512+
text-decoration: none;
1513+
border-bottom: 1px solid transparent;
1514+
transition: border-color var(--transition-fast);
1515+
}
1516+
1517+
.site-footer a:hover {
1518+
border-bottom-color: var(--color-gold-light);
1519+
}
1520+
1521+
.footer-thanks {
1522+
font-style: italic;
1523+
opacity: 0.85;
1524+
}
1525+
1526+
.footer-license {
1527+
font-size: 0.75rem;
1528+
opacity: 0.6;
1529+
margin-top: var(--spacing-sm) !important;
1530+
}
1531+
1532+
@media print {
1533+
.site-footer { display: none !important; }
1534+
}
1535+
1536+
1537+
/* ============================================================
1538+
Table of Contents - Right Sidebar
1539+
============================================================ */
1540+
1541+
.toc-sidebar {
1542+
width: 260px;
1543+
min-width: 260px;
1544+
position: sticky;
1545+
top: var(--header-height);
1546+
height: calc(100vh - var(--header-height));
1547+
overflow-y: auto;
1548+
padding: var(--spacing-lg) var(--spacing-sm) var(--spacing-lg) 0;
1549+
scrollbar-width: thin;
1550+
scrollbar-color: var(--color-parchment-darker) transparent;
1551+
}
1552+
1553+
.toc-nav {
1554+
border-left: 3px solid var(--color-parchment-darker);
1555+
padding-left: var(--spacing-md);
1556+
}
1557+
1558+
.toc-title {
1559+
font-family: var(--font-heading);
1560+
font-size: 0.82rem;
1561+
font-weight: 700;
1562+
text-transform: uppercase;
1563+
letter-spacing: 0.08em;
1564+
color: var(--color-brown-dark);
1565+
padding: 0 var(--spacing-sm);
1566+
margin-bottom: var(--spacing-md);
1567+
padding-bottom: var(--spacing-sm);
1568+
border-bottom: 1px solid var(--color-parchment-dark);
1569+
}
1570+
1571+
.toc-list {
1572+
list-style: none;
1573+
margin: 0;
1574+
padding: 0;
1575+
}
1576+
1577+
.toc-item {
1578+
margin: 0;
1579+
}
1580+
1581+
.toc-item.toc-sub {
1582+
padding-left: var(--spacing-md);
1583+
}
1584+
1585+
.toc-link {
1586+
display: block;
1587+
padding: 0.35rem var(--spacing-sm);
1588+
font-size: 0.85rem;
1589+
line-height: 1.5;
1590+
color: var(--color-brown-medium);
1591+
text-decoration: none;
1592+
border-left: 3px solid transparent;
1593+
margin-left: -3px;
1594+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
1595+
transition: color var(--transition-fast), border-color var(--transition-fast), background var(--transition-fast);
1596+
overflow: hidden;
1597+
text-overflow: ellipsis;
1598+
white-space: nowrap;
1599+
}
1600+
1601+
.toc-link:hover {
1602+
color: var(--color-gold-dark);
1603+
background: rgba(200, 169, 81, 0.08);
1604+
}
1605+
1606+
.toc-link.active {
1607+
color: var(--color-gold-dark);
1608+
border-left-color: var(--color-gold);
1609+
background: rgba(200, 169, 81, 0.12);
1610+
font-weight: 600;
1611+
}
1612+
1613+
.toc-item.toc-sub .toc-link {
1614+
font-size: 0.8rem;
1615+
padding-top: 0.25rem;
1616+
padding-bottom: 0.25rem;
1617+
}
1618+
1619+
14881620
/* --- Responsive --- */
1621+
@media (max-width: 1200px) {
1622+
.toc-sidebar {
1623+
width: 200px;
1624+
min-width: 200px;
1625+
}
1626+
}
1627+
14891628
@media (max-width: 900px) {
14901629
:root {
14911630
--nav-width: 240px;
14921631
}
14931632

1633+
.toc-sidebar {
1634+
display: none;
1635+
}
1636+
14941637
h1 {
14951638
font-size: 1.8rem;
14961639
}
@@ -1964,7 +2107,7 @@ mark.pagefind-ui__result-highlight {
19642107
}
19652108

19662109
@media print {
1967-
.search-fab, .search-overlay { display: none !important; }
2110+
.search-fab, .search-overlay, .toc-sidebar { display: none !important; }
19682111
}
19692112

19702113
@media (max-width: 480px) {

src/assets/js/toc.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// SPDX-License-Identifier: MIT
2+
// Table of Contents - right sidebar with heading anchors
3+
(function () {
4+
var tocList = document.getElementById("toc-list");
5+
var tocSidebar = document.getElementById("toc-sidebar");
6+
if (!tocList || !tocSidebar) return;
7+
8+
var main = document.querySelector(".main-content");
9+
if (!main) return;
10+
11+
var headings = main.querySelectorAll("h2, h3");
12+
if (headings.length < 2) {
13+
tocSidebar.style.display = "none";
14+
return;
15+
}
16+
17+
// Build ToC entries and assign IDs to headings
18+
var items = [];
19+
headings.forEach(function (h) {
20+
if (!h.id) {
21+
h.id = h.textContent
22+
.trim()
23+
.toLowerCase()
24+
.replace(/[^\w\s-]/g, "")
25+
.replace(/\s+/g, "-")
26+
.replace(/-+/g, "-")
27+
.replace(/^-|-$/g, "");
28+
}
29+
30+
var li = document.createElement("li");
31+
li.className = "toc-item" + (h.tagName === "H3" ? " toc-sub" : "");
32+
33+
var a = document.createElement("a");
34+
a.href = "#" + h.id;
35+
a.textContent = h.textContent.trim();
36+
a.className = "toc-link";
37+
38+
li.appendChild(a);
39+
tocList.appendChild(li);
40+
items.push({ el: h, link: a, li: li });
41+
});
42+
43+
// Intersection Observer for active state
44+
var currentActive = null;
45+
46+
var observer = new IntersectionObserver(
47+
function (entries) {
48+
entries.forEach(function (entry) {
49+
if (entry.isIntersecting) {
50+
if (currentActive) currentActive.classList.remove("active");
51+
var match = items.find(function (item) {
52+
return item.el === entry.target;
53+
});
54+
if (match) {
55+
match.link.classList.add("active");
56+
currentActive = match.link;
57+
}
58+
}
59+
});
60+
},
61+
{
62+
rootMargin: "-80px 0px -70% 0px",
63+
threshold: 0
64+
}
65+
);
66+
67+
items.forEach(function (item) {
68+
observer.observe(item.el);
69+
});
70+
71+
// Smooth scroll on click
72+
tocList.addEventListener("click", function (e) {
73+
var link = e.target.closest(".toc-link");
74+
if (!link) return;
75+
e.preventDefault();
76+
var id = link.getAttribute("href").slice(1);
77+
var target = document.getElementById(id);
78+
if (target) {
79+
target.scrollIntoView({ behavior: "smooth", block: "start" });
80+
history.replaceState(null, "", "#" + id);
81+
}
82+
});
83+
})();

0 commit comments

Comments
 (0)