jekyll-theme-zer0 1.26.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +558 -5
  3. data/README.md +10 -27
  4. data/_data/authors.yml +4 -3
  5. data/_data/backlog.yml +28 -0
  6. data/_data/consumers.yml +147 -0
  7. data/_data/features.yml +142 -23
  8. data/_data/i18n/fr.yml +98 -0
  9. data/_data/i18n/languages.yml +36 -0
  10. data/_data/i18n/manifest.yml +1502 -0
  11. data/_data/navigation/docs.yml +2 -0
  12. data/_data/navigation/main.yml +6 -0
  13. data/_data/series.yml +19 -0
  14. data/_data/theme-manifest.yml +348 -268
  15. data/_data/ui-text.yml +36 -246
  16. data/_includes/README.md +25 -2
  17. data/_includes/analytics/google-tag-manager-body.html +11 -2
  18. data/_includes/analytics/google-tag-manager-head.html +16 -6
  19. data/_includes/analytics/posthog.html +33 -4
  20. data/_includes/components/abc-letter.html +43 -0
  21. data/_includes/components/author-avatar-url.html +4 -2
  22. data/_includes/components/book-card.html +42 -0
  23. data/_includes/components/book-nav.html +80 -0
  24. data/_includes/components/book-plate.html +31 -0
  25. data/_includes/components/book-toc.html +47 -0
  26. data/_includes/components/bookshelf.html +68 -0
  27. data/_includes/components/card-grid.html +60 -0
  28. data/_includes/components/data-card.html +95 -0
  29. data/_includes/components/env-switcher.html +3 -1
  30. data/_includes/components/halfmoon.html +5 -1
  31. data/_includes/components/language-toggle.html +81 -0
  32. data/_includes/components/page-feedback.html +65 -2
  33. data/_includes/components/search-modal.html +2 -2
  34. data/_includes/components/shortcuts-modal.html +1 -1
  35. data/_includes/components/theme-controls-bar.html +10 -2
  36. data/_includes/components/theme-customizer.html +8 -2
  37. data/_includes/components/translation-notice.html +27 -0
  38. data/_includes/content/intro.html +16 -15
  39. data/_includes/content/seo.html +9 -3
  40. data/_includes/core/color-mode-init.html +13 -4
  41. data/_includes/core/favicon.html +46 -0
  42. data/_includes/core/footer.html +9 -4
  43. data/_includes/core/head.html +18 -0
  44. data/_includes/core/header.html +9 -4
  45. data/_includes/core/hreflang.html +33 -0
  46. data/_includes/core/i18n.html +36 -0
  47. data/_includes/custom/body-end.html +18 -0
  48. data/_includes/custom/body-start.html +17 -0
  49. data/_includes/custom/footer.html +18 -0
  50. data/_includes/custom/head.html +18 -0
  51. data/_includes/navigation/breadcrumbs.html +1 -1
  52. data/_includes/navigation/local-graph.html +28 -2
  53. data/_includes/navigation/nav-tree.html +3 -3
  54. data/_includes/navigation/navbar.html +5 -4
  55. data/_includes/navigation/sidebar-config.html +21 -0
  56. data/_includes/navigation/sidebar-nav.html +4 -0
  57. data/_includes/navigation/sidebar-pagetree.html +150 -0
  58. data/_includes/navigation/sidebar-right.html +3 -2
  59. data/_includes/navigation/unified-drawer.html +9 -3
  60. data/_includes/obsidian/full-graph.html +165 -136
  61. data/_layouts/404.html +260 -0
  62. data/_layouts/README.md +2 -0
  63. data/_layouts/article.html +7 -1
  64. data/_layouts/book-abc.html +106 -0
  65. data/_layouts/book-story.html +91 -0
  66. data/_layouts/book.html +113 -0
  67. data/_layouts/collection.html +17 -5
  68. data/_layouts/default.html +5 -3
  69. data/_layouts/home.html +5 -2
  70. data/_layouts/landing.html +9 -0
  71. data/_layouts/news.html +159 -33
  72. data/_layouts/root.html +29 -9
  73. data/_layouts/section.html +61 -17
  74. data/_sass/components/_book.scss +423 -0
  75. data/_sass/core/_navbar.scss +13 -0
  76. data/_sass/core/_obsidian.scss +295 -7
  77. data/_sass/layouts/_navbar-extras.scss +6 -1
  78. data/_sass/theme/_backgrounds.scss +21 -8
  79. data/assets/css/main.scss +1 -0
  80. data/assets/js/auto-hide-nav.js +5 -1
  81. data/assets/js/halfmoon.js +26 -0
  82. data/assets/js/obsidian-graph.js +707 -265
  83. data/assets/js/obsidian-local-graph.js +161 -54
  84. data/assets/js/search-modal.js +4 -1
  85. data/scripts/README.md +20 -26
  86. data/scripts/bin/audit-consumer +1 -1
  87. data/scripts/bin/manifest +31 -5
  88. data/scripts/bin/sync-plugins +0 -1
  89. data/scripts/bin/validate +5 -1
  90. data/scripts/dev/rasterize-svg.js +65 -0
  91. data/scripts/features/generate-preview-images +49 -1390
  92. data/scripts/features/install-preview-generator +55 -33
  93. data/scripts/install/README.md +55 -25
  94. data/scripts/install/ai/client.sh +302 -93
  95. data/scripts/install/ai/prompts/spec.schema.json +1 -1
  96. data/scripts/install/ai/prompts/wizard.system.md +8 -17
  97. data/scripts/install/ai/wizard.sh +10 -5
  98. data/scripts/install/apply.sh +7 -3
  99. data/scripts/install/cli.sh +54 -4
  100. data/scripts/install/config.sh +167 -0
  101. data/scripts/install/doctor.sh +38 -0
  102. data/scripts/install/plan.sh +10 -0
  103. data/scripts/install/spec.sh +15 -7
  104. data/scripts/install/template.sh +4 -0
  105. data/scripts/lib/README.md +1 -5
  106. data/scripts/lib/install/deploy/README.md +3 -9
  107. data/scripts/lib/preview_generator.py +2261 -1341
  108. data/scripts/propagate.rb +277 -0
  109. data/scripts/translate.rb +1204 -0
  110. metadata +34 -3
  111. data/_plugins/preview_image_generator.rb +0 -351
@@ -14,8 +14,14 @@
14
14
  ===================================================================
15
15
  -->
16
16
 
17
- {% assign current_skin = site.theme_skin | default: site.data.theme_skins.default | default: "dark" %}
18
- {% assign skins = site.data.theme_skins.order %}
17
+ {%- comment -%}
18
+ _data/theme_skins.yml is optional for consumers (remote_theme cannot ship
19
+ _data), so fall back to the compiled skin registry. Keep this list in sync
20
+ with the [data-theme-skin] blocks in _sass/theme/_skins.scss.
21
+ {%- endcomment -%}
22
+ {% assign _skin_fallback = "air|aqua|dirt|neon|mint|plum|sunrise" | split: "|" %}
23
+ {% assign current_skin = site.theme_skin | default: site.data.theme_skins.default | default: "air" %}
24
+ {% assign skins = site.data.theme_skins.order | default: _skin_fallback %}
19
25
 
20
26
  <p class="text-body-secondary mb-3">
21
27
  Current skin: <strong>{{ current_skin }}</strong>. Click a card to preview (page-level only — does not persist).
@@ -0,0 +1,27 @@
1
+ {% comment %} Feature: ZER0-078 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: translation-notice
4
+ Path: _includes/components/translation-notice.html
5
+ Purpose: Disclosure banner shown at the top of machine-generated
6
+ translations: tells the reader the page was translated
7
+ automatically and links back to the English original.
8
+ Params: none — renders only when page.machine_translated is true
9
+ (set by scripts/translate.rb on generated pages).
10
+ Depends on: core/i18n.html, Bootstrap 5 alert, Bootstrap Icons
11
+ Notes: No-op on every human-authored page, so it is safe to include
12
+ unconditionally from the root layout.
13
+ {%- endcomment -%}
14
+ {%- if page.machine_translated -%}
15
+ {%- include core/i18n.html -%}
16
+ <div class="container-xl mt-3 translation-notice">
17
+ <div class="alert alert-info d-flex align-items-start gap-2 mb-0" role="note">
18
+ <i class="bi bi-translate flex-shrink-0 mt-1" aria-hidden="true"></i>
19
+ <span>
20
+ {{ ui.machine_translation_notice | default: "This page was translated automatically from the English original." }}
21
+ {%- if page.translation_source_url %}
22
+ <a class="alert-link" href="{{ page.translation_source_url | relative_url }}" hreflang="{{ site.translation.source_lang | default: 'en' }}">{{ ui.machine_translation_view_source | default: "View the original" }}</a>
23
+ {%- endif %}
24
+ </span>
25
+ </div>
26
+ </div>
27
+ {%- endif -%}
@@ -12,7 +12,8 @@
12
12
  - Responsive flex layout: full-width content block, frosted metadata footer with reading time and action buttons
13
13
  - Balanced padding and typography hierarchy via bd-intro-* classes
14
14
  - Metadata row: author, dates, category, difficulty, tags, view source
15
- -->
15
+ -->
16
+ {%- include core/i18n.html -%}
16
17
  {% if page.collection %}
17
18
  {% capture page_dir %}{{ site.collections_dir }}{% endcapture %}
18
19
  {% assign testvar = "collection exists" %}
@@ -133,7 +134,7 @@
133
134
  {% endif %}
134
135
 
135
136
  <div class="bd-intro" style="
136
- background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('{{ preview_path | relative_url }}') no-repeat center center / cover;">
137
+ background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('{{ preview_path }}') no-repeat center center / cover;">
137
138
 
138
139
  <div class="bd-intro-inner">
139
140
  <div class="bd-intro-content">
@@ -161,7 +162,7 @@
161
162
  <li class="bd-intro-meta-item">
162
163
  <i class="bi bi-calendar3 bd-intro-meta-icon" aria-hidden="true"></i>
163
164
  <time datetime="{{ intro_published | date_to_xmlschema }}" itemprop="datePublished">
164
- Published {{ intro_published | date: "%b %d, %Y" }}
165
+ {{ ui.published_label | default: "Published" }} {{ intro_published | date: "%b %d, %Y" }}
165
166
  </time>
166
167
  </li>
167
168
  {% endif %}
@@ -170,7 +171,7 @@
170
171
  <li class="bd-intro-meta-item">
171
172
  <i class="bi bi-pencil bd-intro-meta-icon" aria-hidden="true"></i>
172
173
  <time datetime="{{ intro_updated | date_to_xmlschema }}" itemprop="dateModified">
173
- Updated {{ intro_updated | date: "%b %d, %Y" }}
174
+ {{ ui.updated_label | default: "Updated" }} {{ intro_updated | date: "%b %d, %Y" }}
174
175
  </time>
175
176
  </li>
176
177
  {% endif %}
@@ -212,7 +213,7 @@
212
213
  {% if intro_source_url %}
213
214
  <li class="bd-intro-meta-item bd-intro-meta-item--source d-none d-md-inline-flex">
214
215
  <i class="bi bi-file-earmark-code bd-intro-meta-icon" aria-hidden="true"></i>
215
- <a class="bd-intro-meta-link" href="{{ intro_source_url }}" target="_blank" rel="noopener noreferrer">View source</a>
216
+ <a class="bd-intro-meta-link" href="{{ intro_source_url }}" target="_blank" rel="noopener noreferrer">{{ ui.view_source | default: "View source" }}</a>
216
217
  </li>
217
218
  {% endif %}
218
219
  </ul>
@@ -221,9 +222,9 @@
221
222
  <p class="bd-intro-meta">
222
223
  <i class="bi bi-clock me-1" aria-hidden="true"></i>
223
224
  {% if page.estimated_reading_time %}
224
- Estimated reading time: {{ page.estimated_reading_time }}
225
+ {{ ui.reading_time_label | default: "Estimated reading time:" }} {{ page.estimated_reading_time }}
225
226
  {% else %}
226
- Estimated reading time: {{ intro_read_mins }} minute{% if intro_read_mins != 1 %}s{% endif %}
227
+ {{ ui.reading_time_label | default: "Estimated reading time:" }} {{ intro_read_mins }} {{ ui.reading_time_minute | default: "minute" }}{% if intro_read_mins != 1 %}s{% endif %}
227
228
  {% endif %}
228
229
  </p>
229
230
 
@@ -233,12 +234,12 @@
233
234
  <div class="dropdown">
234
235
  <button class="btn btn-sm btn-info dropdown-toggle" type="button" id="shareDropdownBottom" data-bs-toggle="dropdown" aria-expanded="false">
235
236
  <i class="bi bi-share" aria-hidden="true"></i>
236
- <span class="d-none d-sm-inline ms-1">Share</span>
237
+ <span class="d-none d-sm-inline ms-1">{{ ui.share_label | default: "Share" }}</span>
237
238
  </button>
238
239
  <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="shareDropdownBottom">
239
240
  <li>
240
241
  <a class="dropdown-item" href="https://reddit.com/submit?url={{ page_absolute_url | uri_escape }}&amp;title={{ page.title | uri_escape }}" target="_blank" rel="noopener noreferrer">
241
- <i class="bi bi-reddit me-2"></i>Share on Reddit
242
+ <i class="bi bi-reddit me-2"></i>{{ ui.share_on_label | default: "Share on" }} Reddit
242
243
  </a>
243
244
  </li>
244
245
  <li>
@@ -250,12 +251,12 @@
250
251
  data-share-url="{{ page_absolute_url | escape }}"
251
252
  data-share-title="{{ linkedin_share_title | escape }}"
252
253
  data-share-description="{{ linkedin_share_description | escape }}">
253
- <i class="bi bi-linkedin me-2"></i>Share on LinkedIn
254
+ <i class="bi bi-linkedin me-2"></i>{{ ui.share_on_label | default: "Share on" }} LinkedIn
254
255
  </a>
255
256
  </li>
256
257
  <li>
257
- <a class="dropdown-item" href="https://x.com/intent/tweet?url={{ site.url | append: page.url | url_encode }}&text={{ page.title | url_encode }}" target="_blank" aria-label="Share on X">
258
- <i class="bi bi-twitter-x me-2"></i>Share on X
258
+ <a class="dropdown-item" href="https://x.com/intent/tweet?url={{ site.url | append: page.url | url_encode }}&text={{ page.title | url_encode }}" target="_blank" aria-label="{{ ui.share_on_label | default: 'Share on' }} X">
259
+ <i class="bi bi-twitter-x me-2"></i>{{ ui.share_on_label | default: "Share on" }} X
259
260
  </a>
260
261
  </li>
261
262
  <li><hr class="dropdown-divider"></li>
@@ -266,7 +267,7 @@
266
267
  the blocking alert() the legacy markup used.
267
268
  {%- endcomment -%}
268
269
  <button class="dropdown-item" type="button" data-copy="{{ site.url | append: page.url }}">
269
- <i class="bi bi-clipboard me-2" aria-hidden="true"></i>Copy Link
270
+ <i class="bi bi-clipboard me-2" aria-hidden="true"></i>{{ ui.copy_link_label | default: "Copy Link" }}
270
271
  </button>
271
272
  </li>
272
273
  </ul>
@@ -284,7 +285,7 @@
284
285
  rel="noopener noreferrer"
285
286
  title="Report a problem or suggest an improvement for this page">
286
287
  <i class="bi bi-megaphone" aria-hidden="true"></i>
287
- <span class="d-none d-sm-inline ms-1">Improve</span>
288
+ <span class="d-none d-sm-inline ms-1">{{ ui.improve_label | default: "Improve" }}</span>
288
289
  </a>
289
290
  {% endif %}
290
291
 
@@ -294,7 +295,7 @@
294
295
  target="_blank"
295
296
  rel="noopener noreferrer">
296
297
  <i class="bi bi-github" aria-hidden="true"></i>
297
- <span class="d-none d-sm-inline ms-1">Edit on GitHub</span>
298
+ <span class="d-none d-sm-inline ms-1">{{ ui.edit_on_github | default: "Edit on GitHub" }}</span>
298
299
  </a>
299
300
  </div>
300
301
  </div>
@@ -37,17 +37,23 @@
37
37
  {%- assign assets_prefix = site.preview_images.assets_prefix | default: '/assets' -%}
38
38
  {%- assign auto_prefix = site.preview_images.auto_prefix | default: true -%}
39
39
 
40
+ {%- comment -%}
41
+ Guard with truthiness AND blank: in Liquid 4 `nil != blank` is TRUE, so a
42
+ bare `!= blank` check on an unset variable takes the branch and assigns a
43
+ nil seo_image — og:image was silently dropped on every page without a
44
+ page.header image, defeating the site.og_image fallback entirely.
45
+ {%- endcomment -%}
40
46
  {%- if page_teaser_image contains '://' -%}
41
47
  {%- comment -%} External URL - use as-is {%- endcomment -%}
42
- {%- elsif auto_prefix and page_teaser_image != blank -%}
48
+ {%- elsif auto_prefix and page_teaser_image and page_teaser_image != blank -%}
43
49
  {%- unless page_teaser_image contains assets_prefix -%}
44
50
  {%- assign page_teaser_image = assets_prefix | append: page_teaser_image -%}
45
51
  {%- endunless -%}
46
52
  {%- endif -%}
47
53
 
48
- {%- if page_large_image != blank -%}
54
+ {%- if page_large_image and page_large_image != blank -%}
49
55
  {%- assign seo_image = page_large_image | absolute_url | escape -%}
50
- {%- elsif page_teaser_image != blank -%}
56
+ {%- elsif page_teaser_image and page_teaser_image != blank -%}
51
57
  {%- assign seo_image = page_teaser_image | absolute_url | escape -%}
52
58
  {%- endif -%}
53
59
 
@@ -13,9 +13,12 @@
13
13
  when Bootstrap's [data-bs-theme] selectors are first evaluated.
14
14
 
15
15
  Priority order (highest → lowest):
16
- 1. localStorage["theme"] — user's explicit override via Appearance panel
17
- 2. data-color-mode-default — site.color_mode_default config value
18
- 3. "auto" fallback — resolves via prefers-color-scheme
16
+ 1. site.color_mode_lock — when true, config wins unconditionally:
17
+ visitor preference and OS scheme are ignored
18
+ (data-color-mode-lock attribute, set by root.html)
19
+ 2. localStorage["theme"] — user's explicit override via Appearance panel
20
+ 3. data-color-mode-default — site.color_mode_default config value
21
+ 4. "auto" fallback — resolves via prefers-color-scheme
19
22
  {%- endcomment -%}
20
23
  <script>
21
24
  (function () {
@@ -23,8 +26,14 @@
23
26
  try {
24
27
  var root = document.documentElement;
25
28
  var configDefault = root.getAttribute('data-color-mode-default') || 'auto';
26
- var stored = window.localStorage.getItem('theme');
29
+ var locked = root.getAttribute('data-color-mode-lock') === 'true';
30
+ var stored = locked ? null : window.localStorage.getItem('theme');
27
31
  var mode = stored || configDefault;
32
+ if (mode === 'auto' && locked) {
33
+ {%- comment -%} A locked "auto" is contradictory; treat it as dark so
34
+ the brand-pinned mode never flips with the OS. {%- endcomment %}
35
+ mode = 'dark';
36
+ }
28
37
  if (mode === 'auto') {
29
38
  root.setAttribute('data-bs-theme',
30
39
  window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
@@ -0,0 +1,46 @@
1
+ {% comment %} Feature: ZER0-079 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: favicon
4
+ Path: _includes/core/favicon.html
5
+ Purpose: Emit the browser-identity tags (favicon, SVG icon, Apple touch
6
+ icon, web manifest, theme-color) for every page. Before this
7
+ include existed, sites relied on the browser's implicit
8
+ /favicon.ico probe — which silently 404s on sites without the
9
+ file and never works for baseurl (project-page) deployments.
10
+ Params: none — reads the optional `favicon:` block in _config.yml:
11
+
12
+ favicon:
13
+ ico : /favicon.ico # legacy .ico (default)
14
+ svg : /assets/images/favicon.svg # scalable icon (optional)
15
+ png : /assets/images/favicon-32.png # PNG icon (optional)
16
+ png_size : 32x32 # sizes attr for png (optional)
17
+ apple_touch : /assets/images/apple-touch.png # iOS home-screen icon (optional)
18
+ manifest : /site.webmanifest # PWA manifest (optional)
19
+ theme_color : "#0d1117" # browser chrome color (optional)
20
+
21
+ With NO config at all this still emits the explicit
22
+ /favicon.ico link (every consumer site carries one at root),
23
+ and theme_color falls back to site.theme_color.main so the
24
+ mobile address bar matches the site's design tokens.
25
+ Notes: Page-invariant — safe to render via include_cached. All paths
26
+ run through relative_url so baseurl deployments resolve.
27
+ {%- endcomment -%}
28
+ {%- assign _fav = site.favicon -%}
29
+ {%- assign _fav_ico = _fav.ico | default: '/favicon.ico' -%}
30
+ <link rel="icon" href="{{ _fav_ico | relative_url }}" sizes="32x32">
31
+ {%- if _fav.svg %}
32
+ <link rel="icon" type="image/svg+xml" href="{{ _fav.svg | relative_url }}">
33
+ {%- endif %}
34
+ {%- if _fav.png %}
35
+ <link rel="icon" type="image/png" href="{{ _fav.png | relative_url }}"{% if _fav.png_size %} sizes="{{ _fav.png_size }}"{% endif %}>
36
+ {%- endif %}
37
+ {%- if _fav.apple_touch %}
38
+ <link rel="apple-touch-icon" href="{{ _fav.apple_touch | relative_url }}">
39
+ {%- endif %}
40
+ {%- if _fav.manifest %}
41
+ <link rel="manifest" href="{{ _fav.manifest | relative_url }}">
42
+ {%- endif %}
43
+ {%- assign _fav_theme_color = _fav.theme_color | default: site.theme_color.main -%}
44
+ {%- if _fav_theme_color %}
45
+ <meta name="theme-color" content="{{ _fav_theme_color }}">
46
+ {%- endif %}
@@ -22,15 +22,20 @@
22
22
  - site.powered_by: Framework and service credits
23
23
  - site.links: Social media and affiliate links
24
24
  - site.default_icon: Icon framework (Bootstrap Icons)
25
- - site.data.ui-text: Internationalization support for RSS label
25
+ - core/i18n.html: UI strings (per-page language via the `lang` param)
26
26
  - Bootstrap 5 classes provided by the theme
27
-
27
+
28
+ Params:
29
+ - lang (optional): page language. Root layout renders this include via
30
+ include_cached, so it MUST pass lang=page.lang for per-language caching.
31
+
28
32
  Notes:
29
33
  - Guard optional fields (address/phone/email) to avoid empty output
30
34
  - Use relative_url for internal links to respect baseurl
31
35
  ===================================================================
32
36
  -->
33
37
 
38
+ {%- include core/i18n.html lang=include.lang -%}
34
39
  <footer class="bd-footer border-top" role="contentinfo">
35
40
  {% assign footer_page_urls = site.html_pages | map: "url" | join: "|" | prepend: "|" | append: "|" %}
36
41
  <!-- Powered by Row -->
@@ -40,7 +45,7 @@
40
45
  </p>
41
46
  <div class="footer-powered-by-row">
42
47
  <span class="footer-powered-by-label" id="footer-powered-by-label">
43
- {{ site.data.ui-text[site.locale].powered_by | default: "Powered by" }}:
48
+ {{ ui.powered_by | default: "Powered by" }}:
44
49
  </span>
45
50
  <ul class="footer-powered-by-links list-unstyled mb-0"
46
51
  aria-labelledby="footer-powered-by-label">
@@ -197,7 +202,7 @@
197
202
  <li class="list-inline-item">
198
203
  <a class="text-light text-decoration-none" href="{{ '/feed.xml' | relative_url }}">
199
204
  <i class="{{ site.default_icon }} bi-rss" aria-hidden="true"></i>
200
- <span class="d-none d-md-inline">{{ site.data.ui-text[site.locale].feed_label | default: "Feed" }}</span>
205
+ <span class="d-none d-md-inline">{{ ui.feed_label | default: "Feed" }}</span>
201
206
  </a>
202
207
  </li>
203
208
  </ul>
@@ -19,6 +19,7 @@
19
19
  - seo.html: SEO meta tags and Open Graph data
20
20
  - google-analytics.html: Google Analytics tracking
21
21
  - google-tag-manager-head.html: GTM head section
22
+ - posthog.html: PostHog product analytics (opt-in via site.posthog.enabled)
22
23
 
23
24
  Performance Notes:
24
25
  - Scripts loaded in head for immediate availability
@@ -86,6 +87,10 @@ window.MathJax = {
86
87
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
87
88
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
88
89
 
90
+ <!-- Favicon + browser identity (config-driven via `favicon:`; explicit
91
+ /favicon.ico fallback so baseurl deployments resolve) — page-invariant, cached. -->
92
+ {% include_cached core/favicon.html %}
93
+
89
94
  <!-- ================================ -->
90
95
  <!-- SEO SUPPLEMENT -->
91
96
  <!-- ================================ -->
@@ -93,6 +98,10 @@ window.MathJax = {
93
98
  Outputs custom og:image with assets_prefix normalisation and non-Google site verification. -->
94
99
  {% include content/seo.html %}
95
100
 
101
+ <!-- hreflang alternates for machine-generated translations (no-op when the
102
+ page has none) — see _data/i18n/manifest.yml + scripts/translate.rb -->
103
+ {% include core/hreflang.html %}
104
+
96
105
  <!-- ========================== -->
97
106
  <!-- STRUCTURED DATA MARKUP -->
98
107
  <!-- ========================== -->
@@ -127,6 +136,10 @@ window.MathJax = {
127
136
  <!-- Only load in production AND when an ID is configured; the include also guards against dev hostnames -->
128
137
  {% if jekyll.environment == "production" and site.google_analytics %}{% include analytics/google-analytics.html %}{% endif %}
129
138
 
139
+ <!-- PostHog analytics - Configured via the `posthog:` block in _config.yml -->
140
+ <!-- Only load in production AND when explicitly enabled; the include itself also respects Do Not Track -->
141
+ {% if jekyll.environment == "production" and site.posthog.enabled %}{% include analytics/posthog.html %}{% endif %}
142
+
130
143
 
131
144
  <!-- ================================ -->
132
145
  <!-- CSS FRAMEWORKS AND LIBRARIES -->
@@ -177,6 +190,11 @@ window.MathJax = {
177
190
  <link rel="stylesheet" href="{{ '/assets/css/stats.css' | relative_url }}">
178
191
  {% endif %}
179
192
 
193
+ <!-- Consumer hook: end of <head>. Inject JSON-LD, font preloads, verification
194
+ tags, or extra meta by shadowing _includes/custom/head.html in your site —
195
+ no fork of this file needed. The theme ships the stub empty. -->
196
+ {% include custom/head.html %}
197
+
180
198
  <!-- ========================== -->
181
199
  <!-- EXPERIMENTAL FRAMEWORKS -->
182
200
  <!-- ========================== -->
@@ -41,9 +41,11 @@
41
41
  <!-- ================================ -->
42
42
  <!-- SKIP TO CONTENT LINK -->
43
43
  <!-- ================================ -->
44
+ <!-- UI strings resolve per page language (core/i18n.html sets `ui`) -->
45
+ {%- include core/i18n.html -%}
44
46
  <!-- Accessibility: allows keyboard users to skip navigation -->
45
47
  <a href="#main-content" class="visually-hidden-focusable position-absolute top-0 start-0 z-3 m-3 btn btn-primary">
46
- Skip to main content
48
+ {{ ui.skip_main_content | default: "Skip to main content" }}
47
49
  </a>
48
50
 
49
51
  <header id="navbar" class="container-fluid text-center fixed-top" role="banner">
@@ -149,17 +151,20 @@
149
151
  <!-- UTILITY CONTROLS -->
150
152
  <!-- ========================== -->
151
153
  <div class="navbar-utility-controls d-flex align-items-center flex-shrink-0" role="group" aria-label="Utility controls">
154
+ <!-- Language Toggle (renders only when translation.languages is set) -->
155
+ {% include components/language-toggle.html %}
156
+
152
157
  <!-- Search Modal Toggle -->
153
158
  <button
154
159
  class="btn nav-search-button"
155
160
  type="button"
156
161
  data-search-toggle
157
- aria-label="Open search"
162
+ aria-label="{{ ui.shortcuts_search | default: 'Open search' }}"
158
163
  aria-haspopup="dialog"
159
- title="Search site content"
164
+ title="{{ ui.search_button_label | default: 'Search' }}"
160
165
  >
161
166
  <i class="{{site.default_icon}} bi-search" aria-hidden="true"></i>
162
- <span class="nav-link-text d-none d-xl-inline ms-1">Search</span>
167
+ <span class="nav-link-text d-none d-xl-inline ms-1">{{ ui.search_button_label | default: "Search" }}</span>
163
168
  </button>
164
169
 
165
170
  <!-- Settings Offcanvas Toggle -->
@@ -0,0 +1,33 @@
1
+ {% comment %} Feature: ZER0-078 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: hreflang
4
+ Path: _includes/core/hreflang.html
5
+ Purpose: Emit <link rel="alternate" hreflang> tags for pages that have
6
+ machine-generated translations, so search engines associate
7
+ the English original with its /fr/ (etc.) alternates.
8
+ Params: none — reads page.lang / page.translation_source_url and the
9
+ generated _data/i18n/manifest.yml (scripts/translate.rb).
10
+ Depends on: site.translation.* config, site.data.i18n.manifest
11
+ Notes: Renders nothing when translation is disabled, the manifest is
12
+ absent, or the current page has no translations. x-default
13
+ always points at the English original.
14
+ {%- endcomment -%}
15
+ {%- if site.translation.enabled -%}
16
+ {%- assign _hl_source = site.translation.source_lang | default: 'en' -%}
17
+ {%- if page.lang and page.lang != _hl_source -%}
18
+ {%- assign _hl_src_url = page.translation_source_url -%}
19
+ {%- else -%}
20
+ {%- assign _hl_src_url = page.url -%}
21
+ {%- endif -%}
22
+ {%- assign _hl_entry = site.data.i18n.manifest.pages[_hl_src_url] -%}
23
+ {%- if _hl_src_url and _hl_entry %}
24
+ <link rel="alternate" hreflang="{{ _hl_source }}" href="{{ _hl_src_url | absolute_url }}">
25
+ {%- for _hl_lang in site.translation.languages -%}
26
+ {%- assign _hl_t = _hl_entry[_hl_lang] -%}
27
+ {%- if _hl_t %}
28
+ <link rel="alternate" hreflang="{{ _hl_lang }}" href="{{ _hl_t.url | absolute_url }}">
29
+ {%- endif -%}
30
+ {%- endfor %}
31
+ <link rel="alternate" hreflang="x-default" href="{{ _hl_src_url | absolute_url }}">
32
+ {%- endif -%}
33
+ {%- endif -%}
@@ -0,0 +1,36 @@
1
+ {% comment %} Feature: ZER0-078 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: i18n
4
+ Path: _includes/core/i18n.html
5
+ Purpose: Resolve the active UI-string set into the `ui` variable,
6
+ per-page-language aware. English strings come from
7
+ _data/ui-text.yml (the human-maintained source of truth);
8
+ alternate languages come from _data/i18n/<lang>.yml, which is
9
+ GENERATED by scripts/translate.rb — never hand-edited.
10
+ Params: lang (string, optional) — overrides page.lang. REQUIRED when
11
+ the caller is rendered via {% include_cached %} (pass
12
+ lang=page.lang there) so each language gets its own cache
13
+ entry instead of sharing one page-invariant render.
14
+ Depends on: _data/ui-text.yml, _data/i18n/<lang>.yml (generated),
15
+ site.translation.* config
16
+ Notes: Liquid assigns made inside an include are visible to the
17
+ caller, so after {% include core/i18n.html %} simply use
18
+ {{ ui.some_key | default: "English fallback" }}. Missing
19
+ languages or keys degrade to English/literal defaults.
20
+ {%- endcomment -%}
21
+ {%- comment -%}
22
+ Resolve using the FULL configured language code (e.g. "fr", "pt-BR"), not a
23
+ 2-char slice: scripts/translate.rb writes _data/i18n/<code>.yml and sets
24
+ page.lang to that exact code, so a regional code like "pt-BR" must look up
25
+ site.data.i18n["pt-BR"] — slicing to "pt" would silently miss the file and
26
+ fall back to English.
27
+ {%- endcomment -%}
28
+ {%- assign _i18n_lang = include.lang | default: page.lang | default: site.translation.source_lang | default: site.locale | default: 'en' -%}
29
+ {%- assign _i18n_source = site.translation.source_lang | default: 'en' -%}
30
+ {%- assign ui = site.data.ui-text[site.locale] | default: site.data.ui-text.en -%}
31
+ {%- unless _i18n_lang == _i18n_source -%}
32
+ {%- assign _i18n_generated = site.data.i18n[_i18n_lang] -%}
33
+ {%- if _i18n_generated -%}
34
+ {%- assign ui = _i18n_generated -%}
35
+ {%- endif -%}
36
+ {%- endunless -%}
@@ -0,0 +1,18 @@
1
+ {%- comment -%}
2
+ ===================================================================
3
+ CUSTOM HOOK — body-end
4
+ ===================================================================
5
+
6
+ Path: _includes/custom/body-end.html
7
+ Purpose: Consumer extension point rendered as the LAST thing before
8
+ </body> — after the theme's JS bundle and analytics. Use it
9
+ for site-specific scripts, third-party widgets, or anything
10
+ that must load after the theme's JavaScript.
11
+
12
+ The theme ships this file EMPTY. Create _includes/custom/body-end.html
13
+ in YOUR site to shadow this stub. No theme layout needs forking.
14
+
15
+ Companion hooks: custom/head.html, custom/body-start.html,
16
+ custom/footer.html. See _includes/README.md § Custom hooks.
17
+ ===================================================================
18
+ {%- endcomment -%}
@@ -0,0 +1,17 @@
1
+ {%- comment -%}
2
+ ===================================================================
3
+ CUSTOM HOOK — body-start
4
+ ===================================================================
5
+
6
+ Path: _includes/custom/body-start.html
7
+ Purpose: Consumer extension point rendered immediately AFTER the
8
+ opening <body> tag — the required position for snippets like
9
+ Google Tag Manager's <noscript> iframe.
10
+
11
+ The theme ships this file EMPTY. Create _includes/custom/body-start.html
12
+ in YOUR site to shadow this stub. No theme layout needs forking.
13
+
14
+ Companion hooks: custom/head.html, custom/footer.html,
15
+ custom/body-end.html. See _includes/README.md § Custom hooks.
16
+ ===================================================================
17
+ {%- endcomment -%}
@@ -0,0 +1,18 @@
1
+ {%- comment -%}
2
+ ===================================================================
3
+ CUSTOM HOOK — footer
4
+ ===================================================================
5
+
6
+ Path: _includes/custom/footer.html
7
+ Purpose: Consumer extension point rendered directly AFTER the theme
8
+ footer (and its FAB tail), before the cookie-consent banner.
9
+ Use it for an extra footer strip, badges, or legal links the
10
+ theme footer doesn't model.
11
+
12
+ The theme ships this file EMPTY. Create _includes/custom/footer.html
13
+ in YOUR site to shadow this stub. No theme layout needs forking.
14
+
15
+ Companion hooks: custom/head.html, custom/body-start.html,
16
+ custom/body-end.html. See _includes/README.md § Custom hooks.
17
+ ===================================================================
18
+ {%- endcomment -%}
@@ -0,0 +1,18 @@
1
+ {%- comment -%}
2
+ ===================================================================
3
+ CUSTOM HOOK — head
4
+ ===================================================================
5
+
6
+ Path: _includes/custom/head.html
7
+ Purpose: Consumer extension point rendered at the END of the theme's
8
+ <head> (after main.css, tokens-inline, and user-overrides).
9
+
10
+ The theme ships this file EMPTY. To inject site-specific markup —
11
+ structured data (JSON-LD), font preloads, verification tags, extra
12
+ meta — create _includes/custom/head.html in YOUR site and it will
13
+ shadow this stub. No theme layout or core include needs forking.
14
+
15
+ Companion hooks: custom/body-start.html, custom/footer.html,
16
+ custom/body-end.html. See _includes/README.md § Custom hooks.
17
+ ===================================================================
18
+ {%- endcomment -%}
@@ -14,7 +14,7 @@
14
14
  ===================================================================
15
15
  {%- endcomment -%}
16
16
  {% if page.url != "/" and site.breadcrumbs %}
17
- {%- assign ui = site.data.ui-text[site.locale] | default: site.data.ui-text.en -%}
17
+ {%- include core/i18n.html -%}
18
18
  {%- assign label_root = ui.breadcrumbs_root | default: "Home" -%}
19
19
  {%- assign label_aria = ui.breadcrumbs_aria | default: "Breadcrumb" -%}
20
20
  {%- assign crumbs = page.url | remove:'/index.html' | split: '/' -%}
@@ -23,6 +23,8 @@
23
23
  Opt-out: set `local_graph: false` in page front matter.
24
24
 
25
25
  Configurable depth: `local_graph_depth: 2` in page front matter (default 1).
26
+ The panel's depth selector (1–3) and incoming/outgoing switches override it
27
+ per-browser via localStorage (zer0.obsidianLocalGraph.v1).
26
28
  ===================================================================
27
29
  {%- endcomment -%}
28
30
  {%- comment -%} Shared visibility resolver — sets sidebar_enabled {%- endcomment -%}
@@ -63,8 +65,18 @@
63
65
  {%- if _graph_page -%}{%- break -%}{%- endif -%}
64
66
  {%- endfor -%}
65
67
  {%- endunless -%}
66
- <div class="obsidian-local-graph-meta d-flex align-items-center justify-content-between gap-2 mb-3">
67
- <span class="badge text-bg-secondary">Depth {{ lg_depth }}</span>
68
+ <div class="obsidian-local-graph-meta d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
69
+ <div class="d-flex align-items-center gap-2">
70
+ <span class="small text-secondary">Depth</span>
71
+ <div class="btn-group btn-group-sm" role="group" aria-label="Graph depth">
72
+ {%- for d in (1..3) -%}
73
+ <input type="radio" class="btn-check" name="obsidianLgDepth"
74
+ id="obsidian-lg-depth-{{ d }}" autocomplete="off"
75
+ {% if d == lg_depth %}checked{% endif %} />
76
+ <label class="btn btn-outline-secondary" for="obsidian-lg-depth-{{ d }}">{{ d }}</label>
77
+ {%- endfor -%}
78
+ </div>
79
+ </div>
68
80
  {%- if _graph_page -%}
69
81
  <a href="{{ _graph_url | relative_url }}"
70
82
  class="btn btn-outline-primary btn-sm"
@@ -73,6 +85,20 @@
73
85
  </a>
74
86
  {%- endif -%}
75
87
  </div>
88
+ <div class="d-flex flex-wrap column-gap-3 mb-2"
89
+ role="group"
90
+ aria-label="Link directions shown in the local graph">
91
+ <div class="form-check form-switch mb-0">
92
+ <input class="form-check-input" type="checkbox" role="switch"
93
+ id="obsidian-lg-outgoing" checked />
94
+ <label class="form-check-label small" for="obsidian-lg-outgoing">Outgoing</label>
95
+ </div>
96
+ <div class="form-check form-switch mb-0">
97
+ <input class="form-check-input" type="checkbox" role="switch"
98
+ id="obsidian-lg-incoming" checked />
99
+ <label class="form-check-label small" for="obsidian-lg-incoming">Incoming</label>
100
+ </div>
101
+ </div>
76
102
  <div id="obsidian-local-graph"
77
103
  data-depth="{{ lg_depth }}"
78
104
  data-index-url="{{ '/assets/data/wiki-index.json' | relative_url }}"
@@ -36,7 +36,7 @@
36
36
  <ul class="list-unstyled fw-normal pb-2 small nav-tree-root">
37
37
  {% for item in navigation %}
38
38
  {% assign item_id = item.title | slugify | prepend: "nav-" %}
39
- {% assign is_active = page.url == item.url %}
39
+ {% assign is_active = false %}{% if page.url == item.url %}{% assign is_active = true %}{% endif %}
40
40
  {% assign is_expanded = item.expanded | default: false %}
41
41
  {% assign has_children = item.children and item.children.size > 0 %}
42
42
 
@@ -78,7 +78,7 @@
78
78
  <ul class="list-unstyled fw-normal ps-3 nav-tree-children">
79
79
  {% for child in item.children %}
80
80
  {% assign child_id = child.title | slugify | prepend: "nav-" %}
81
- {% assign child_active = page.url == child.url %}
81
+ {% assign child_active = false %}{% if page.url == child.url %}{% assign child_active = true %}{% endif %}
82
82
  {% assign child_expanded = child.expanded | default: false %}
83
83
  {% assign child_has_children = child.children and child.children.size > 0 %}
84
84
 
@@ -118,7 +118,7 @@
118
118
  <div class="collapse{% if child_expanded %} show{% endif %}" id="{{ child_id }}">
119
119
  <ul class="list-unstyled fw-normal ps-3 nav-tree-children">
120
120
  {% for grandchild in child.children %}
121
- {% assign gc_active = page.url == grandchild.url %}
121
+ {% assign gc_active = false %}{% if page.url == grandchild.url %}{% assign gc_active = true %}{% endif %}
122
122
  <li class="mb-1 nav-tree-item" data-depth="2">
123
123
  {% if grandchild.url %}
124
124
  <a href="{{ grandchild.url | relative_url }}"