jekyll-theme-zer0 1.27.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +386 -4
  3. data/_data/consumers.yml +147 -0
  4. data/_data/features.yml +77 -5
  5. data/_data/i18n/fr.yml +98 -0
  6. data/_data/i18n/manifest.yml +1502 -0
  7. data/_data/navigation/docs.yml +2 -0
  8. data/_data/navigation/main.yml +6 -0
  9. data/_data/series.yml +19 -0
  10. data/_data/theme-manifest.yml +348 -266
  11. data/_includes/README.md +21 -2
  12. data/_includes/analytics/google-tag-manager-body.html +11 -2
  13. data/_includes/analytics/google-tag-manager-head.html +16 -6
  14. data/_includes/analytics/posthog.html +33 -4
  15. data/_includes/components/abc-letter.html +43 -0
  16. data/_includes/components/book-card.html +42 -0
  17. data/_includes/components/book-nav.html +80 -0
  18. data/_includes/components/book-plate.html +31 -0
  19. data/_includes/components/book-toc.html +47 -0
  20. data/_includes/components/bookshelf.html +68 -0
  21. data/_includes/components/card-grid.html +60 -0
  22. data/_includes/components/data-card.html +95 -0
  23. data/_includes/components/halfmoon.html +5 -1
  24. data/_includes/components/page-feedback.html +65 -2
  25. data/_includes/components/theme-controls-bar.html +10 -2
  26. data/_includes/components/theme-customizer.html +8 -2
  27. data/_includes/content/seo.html +9 -3
  28. data/_includes/core/color-mode-init.html +13 -4
  29. data/_includes/core/favicon.html +46 -0
  30. data/_includes/core/head.html +9 -0
  31. data/_includes/custom/body-end.html +18 -0
  32. data/_includes/custom/body-start.html +17 -0
  33. data/_includes/custom/footer.html +18 -0
  34. data/_includes/custom/head.html +18 -0
  35. data/_includes/navigation/local-graph.html +28 -2
  36. data/_includes/navigation/nav-tree.html +3 -3
  37. data/_includes/navigation/sidebar-config.html +21 -0
  38. data/_includes/navigation/sidebar-nav.html +4 -0
  39. data/_includes/navigation/sidebar-pagetree.html +150 -0
  40. data/_includes/navigation/unified-drawer.html +8 -2
  41. data/_includes/obsidian/full-graph.html +165 -136
  42. data/_layouts/404.html +260 -0
  43. data/_layouts/README.md +2 -0
  44. data/_layouts/book-abc.html +106 -0
  45. data/_layouts/book-story.html +91 -0
  46. data/_layouts/book.html +113 -0
  47. data/_layouts/collection.html +17 -5
  48. data/_layouts/home.html +5 -2
  49. data/_layouts/landing.html +9 -0
  50. data/_layouts/news.html +155 -31
  51. data/_layouts/root.html +16 -2
  52. data/_layouts/section.html +57 -15
  53. data/_sass/components/_book.scss +423 -0
  54. data/_sass/core/_navbar.scss +13 -0
  55. data/_sass/core/_obsidian.scss +286 -6
  56. data/_sass/theme/_backgrounds.scss +21 -8
  57. data/assets/css/main.scss +1 -0
  58. data/assets/js/auto-hide-nav.js +5 -1
  59. data/assets/js/halfmoon.js +26 -0
  60. data/assets/js/obsidian-graph.js +702 -264
  61. data/assets/js/obsidian-local-graph.js +161 -54
  62. data/assets/js/search-modal.js +4 -1
  63. data/scripts/bin/manifest +31 -4
  64. data/scripts/bin/validate +5 -1
  65. data/scripts/install/README.md +47 -6
  66. data/scripts/install/ai/client.sh +302 -93
  67. data/scripts/install/ai/prompts/spec.schema.json +1 -1
  68. data/scripts/install/ai/wizard.sh +10 -5
  69. data/scripts/install/apply.sh +7 -3
  70. data/scripts/install/cli.sh +54 -4
  71. data/scripts/install/config.sh +167 -0
  72. data/scripts/install/doctor.sh +38 -0
  73. data/scripts/install/plan.sh +10 -0
  74. data/scripts/install/spec.sh +15 -7
  75. data/scripts/install/template.sh +4 -0
  76. data/scripts/propagate.rb +277 -0
  77. data/scripts/translate.rb +90 -0
  78. metadata +27 -2
data/_layouts/news.html CHANGED
@@ -15,11 +15,27 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
15
15
 
16
16
  Front Matter Variables:
17
17
  - section_style: Display style (magazine, grid, list) - default: magazine
18
-
18
+
19
19
  Section Styles:
20
20
  - magazine (default): Hero + category blocks + featured grid
21
21
  - grid: All posts in unified card grid
22
22
  - list: Chronological list view across all sections
23
+
24
+ Site Config (`news:` block in _config.yml — every key optional):
25
+ - news.category_base : base path for per-category section pages
26
+ (default "/news/"; e.g. "/categories/")
27
+ - news.hero : false to disable the latest-post hero fallback
28
+ (hero still shows for breaking/featured posts)
29
+ - news.archives_url : path of an archives page to deep-link months to;
30
+ defaults to "/archives/" WHEN that page exists,
31
+ otherwise month entries render unlinked
32
+ - news.newsletter : { enabled: false } hides the subscribe band;
33
+ { endpoint: "https://..." } posts the form there;
34
+ with no endpoint it falls back to a mailto:
35
+ subscribe link (site.email) or hides itself
36
+ Sections come from _data/navigation/posts.yml when present; otherwise
37
+ they are derived automatically from site.categories, so the layout
38
+ renders fully on a data-less consumer site.
23
39
 
24
40
  Layout Structure (NO SIDEBAR - Full Width):
25
41
  1. News Header: Dark header with title and section navigation
@@ -41,12 +57,41 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
41
57
  {% endcomment %}
42
58
  {% assign section_style = page.section_style %}
43
59
 
44
- {% comment %}
60
+ {% comment %}
45
61
  Filter posts: exclude index files
46
62
  Index files are marked with index: true in front matter
47
63
  {% endcomment %}
48
64
  {% assign all_posts = site.posts | where_exp: "post", "post.index != true" %}
49
65
 
66
+ {% comment %}
67
+ Resolve the news config surface once (see the header block above).
68
+ news_sections is a list of section NAMES: data-driven from
69
+ _data/navigation/posts.yml when that file exists, otherwise derived from
70
+ site.categories so a consumer without vendored nav data still gets a
71
+ fully working news homepage. Per-section url/icon are resolved at each
72
+ use site via section-nav lookup with a derived fallback.
73
+ {% endcomment %}
74
+ {% assign news_cfg = site.news %}
75
+ {% assign news_base = news_cfg.category_base | default: "/news/" %}
76
+ {% capture _sec_names %}{% for item in site.data.navigation.posts %}{{ item.title }}{% unless forloop.last %}|{% endunless %}{% endfor %}{% endcapture %}
77
+ {% if _sec_names == "" %}{% capture _sec_names %}{% for cat in site.categories limit: 8 %}{{ cat[0] }}{% unless forloop.last %}|{% endunless %}{% endfor %}{% endcapture %}{% endif %}
78
+ {% assign news_sections = _sec_names | split: "|" %}
79
+
80
+ {% comment %}
81
+ Tags index: honor the same site.tags_page convention as article.html.
82
+ When the key is not set explicitly, only link to /tags/ if such a page
83
+ actually exists in the build — a consumer without one gets unlinked tag
84
+ badges instead of site-wide 404 links.
85
+ {% endcomment %}
86
+ {% assign tags_base = site.tags_page | default: "/tags/" %}
87
+ {% assign _tags_linkable = false %}
88
+ {% if site.tags_page %}
89
+ {% assign _tags_linkable = true %}
90
+ {% else %}
91
+ {% assign _tags_index_page = site.html_pages | where: "url", tags_base | first %}
92
+ {% if _tags_index_page %}{% assign _tags_linkable = true %}{% endif %}
93
+ {% endif %}
94
+
50
95
  <!-- ================================ -->
51
96
  <!-- NEWS HEADER -->
52
97
  <!-- ================================ -->
@@ -60,18 +105,26 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
60
105
  </div>
61
106
  <div class="col-md-8">
62
107
  <nav class="nav nav-pills justify-content-md-end flex-wrap">
63
- <a class="nav-link text-white active" href="{{ site.baseurl }}/news/">
108
+ <a class="nav-link text-white active" href="{{ page.url | relative_url }}">
64
109
  <i class="bi bi-house me-1"></i>Home
65
110
  </a>
66
- {% for item in site.data.navigation.posts %}
67
- <a class="nav-link text-white-50" href="{{ item.url | replace: '/posts/', '/news/' | relative_url }}">
68
- {% if item.icon %}<i class="bi {{ item.icon }} me-1"></i>{% endif %}
69
- {{ item.title }}
111
+ {% for sec_name in news_sections %}
112
+ {% assign _sec_nav = site.data.navigation.posts | where: "title", sec_name | first %}
113
+ {% if _sec_nav %}
114
+ {% assign _sec_url = _sec_nav.url | replace: '/posts/', news_base %}
115
+ {% else %}
116
+ {% capture _sec_url %}{{ news_base }}{{ sec_name | slugify }}/{% endcapture %}
117
+ {% endif %}
118
+ <a class="nav-link text-white-50" href="{{ _sec_url | relative_url }}">
119
+ {% if _sec_nav.icon %}<i class="bi {{ _sec_nav.icon }} me-1"></i>{% endif %}
120
+ {{ sec_name }}
70
121
  </a>
71
122
  {% endfor %}
72
- <a class="nav-link text-white-50" href="{{ site.baseurl }}/tags/">
123
+ {% if _tags_linkable %}
124
+ <a class="nav-link text-white-50" href="{{ tags_base | relative_url }}">
73
125
  <i class="bi bi-tags me-1"></i>Tags
74
126
  </a>
127
+ {% endif %}
75
128
  </nav>
76
129
  </div>
77
130
  </div>
@@ -92,11 +145,22 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
92
145
  <!-- ================================ -->
93
146
  {% assign breaking_posts = all_posts | where: "breaking", true %}
94
147
  {% assign hero_post = breaking_posts | first %}
95
-
148
+
96
149
  {% unless hero_post %}
97
150
  {% assign hero_post = all_posts | where: "featured", true | first %}
98
151
  {% endunless %}
99
-
152
+
153
+ {% comment %}
154
+ No post is marked breaking/featured: fall back to the newest post so the
155
+ hero never silently vanishes on sites that don't curate front matter.
156
+ Opt out with `news: { hero: false }` in _config.yml.
157
+ {% endcomment %}
158
+ {% unless hero_post %}
159
+ {% unless news_cfg.hero == false %}
160
+ {% assign hero_post = all_posts | first %}
161
+ {% endunless %}
162
+ {% endunless %}
163
+
100
164
  {% if hero_post %}
101
165
  <section class="hero-section mb-5">
102
166
  <div class="row g-0 bg-body-secondary rounded-4 overflow-hidden shadow">
@@ -128,7 +192,8 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
128
192
  </small>
129
193
  {% endif %}
130
194
  {% if hero_post.categories.size > 0 %}
131
- <a href="{{ site.baseurl }}/news/{{ hero_post.categories | first | slugify }}/" class="badge bg-primary text-decoration-none">
195
+ {% capture _hero_cat_url %}{{ news_base }}{{ hero_post.categories | first | slugify }}/{% endcapture %}
196
+ <a href="{{ _hero_cat_url | relative_url }}" class="badge bg-primary text-decoration-none">
132
197
  {{ hero_post.categories | first }}
133
198
  </a>
134
199
  {% endif %}
@@ -155,14 +220,20 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
155
220
  <!-- ================================ -->
156
221
  <section class="section-nav mb-5">
157
222
  <div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3">
158
- {% for nav_item in site.data.navigation.posts %}
159
- {% assign section_posts = all_posts | where_exp: "post", "post.categories contains nav_item.title" %}
223
+ {% for sec_name in news_sections %}
224
+ {% assign section_posts = all_posts | where_exp: "post", "post.categories contains sec_name" %}
225
+ {% assign _sec_nav = site.data.navigation.posts | where: "title", sec_name | first %}
226
+ {% if _sec_nav %}
227
+ {% assign _sec_url = _sec_nav.url | replace: '/posts/', news_base %}
228
+ {% else %}
229
+ {% capture _sec_url %}{{ news_base }}{{ sec_name | slugify }}/{% endcapture %}
230
+ {% endif %}
160
231
  <div class="col">
161
- <a href="{{ nav_item.url | replace: '/posts/', '/news/' | relative_url }}"
232
+ <a href="{{ _sec_url | relative_url }}"
162
233
  class="card text-center h-100 text-decoration-none border-0 shadow-sm hover-lift">
163
234
  <div class="card-body py-4">
164
- <i class="bi {{ nav_item.icon | default: 'bi-folder' }} fs-2 text-primary mb-2 d-block"></i>
165
- <h6 class="card-title mb-1">{{ nav_item.title }}</h6>
235
+ <i class="bi {{ _sec_nav.icon | default: 'bi-folder' }} fs-2 text-primary mb-2 d-block"></i>
236
+ <h6 class="card-title mb-1">{{ sec_name }}</h6>
166
237
  <small class="text-muted">{{ section_posts.size }} articles</small>
167
238
  </div>
168
239
  </a>
@@ -204,7 +275,8 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
204
275
  </div>
205
276
  <div class="card-body">
206
277
  {% if main_featured.categories.size > 0 %}
207
- <a href="{{ site.baseurl }}/news/{{ main_featured.categories | first | slugify }}/"
278
+ {% capture _mf_cat_url %}{{ news_base }}{{ main_featured.categories | first | slugify }}/{% endcapture %}
279
+ <a href="{{ _mf_cat_url | relative_url }}"
208
280
  class="badge bg-primary text-decoration-none mb-2">
209
281
  {{ main_featured.categories | first }}
210
282
  </a>
@@ -265,16 +337,22 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
265
337
  <!-- ================================ -->
266
338
  <!-- POSTS BY SECTION -->
267
339
  <!-- ================================ -->
268
- {% for nav_item in site.data.navigation.posts limit: 3 %}
269
- {% assign section_posts = all_posts | where_exp: "post", "post.categories contains nav_item.title" %}
340
+ {% for sec_name in news_sections limit: 3 %}
341
+ {% assign section_posts = all_posts | where_exp: "post", "post.categories contains sec_name" %}
342
+ {% assign _sec_nav = site.data.navigation.posts | where: "title", sec_name | first %}
343
+ {% if _sec_nav %}
344
+ {% assign _sec_url = _sec_nav.url | replace: '/posts/', news_base %}
345
+ {% else %}
346
+ {% capture _sec_url %}{{ news_base }}{{ sec_name | slugify }}/{% endcapture %}
347
+ {% endif %}
270
348
  {% if section_posts.size > 0 %}
271
349
  <section class="section-posts mb-5 pb-4 border-bottom">
272
350
  <div class="d-flex justify-content-between align-items-center mb-4">
273
351
  <h2 class="h4 mb-0">
274
- <i class="bi {{ nav_item.icon | default: 'bi-folder' }} text-primary me-2"></i>{{ nav_item.title }}
352
+ <i class="bi {{ _sec_nav.icon | default: 'bi-folder' }} text-primary me-2"></i>{{ sec_name }}
275
353
  </h2>
276
- <a href="{{ nav_item.url | replace: '/posts/', '/news/' | relative_url }}" class="btn btn-outline-secondary btn-sm">
277
- View All {{ nav_item.title }} <i class="bi bi-arrow-right ms-1"></i>
354
+ <a href="{{ _sec_url | relative_url }}" class="btn btn-outline-secondary btn-sm">
355
+ View All {{ sec_name }} <i class="bi bi-arrow-right ms-1"></i>
278
356
  </a>
279
357
  </div>
280
358
 
@@ -351,7 +429,8 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
351
429
  </div>
352
430
  <div class="card-body">
353
431
  {% if gpost.categories.size > 0 %}
354
- <a href="{{ site.baseurl }}/news/{{ gpost.categories | first | slugify }}/" class="badge bg-primary text-decoration-none mb-2">
432
+ {% capture _gpost_cat_url %}{{ news_base }}{{ gpost.categories | first | slugify }}/{% endcapture %}
433
+ <a href="{{ _gpost_cat_url | relative_url }}" class="badge bg-primary text-decoration-none mb-2">
355
434
  {{ gpost.categories | first }}
356
435
  </a>
357
436
  {% endif %}
@@ -394,7 +473,8 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
394
473
  <div class="card-body">
395
474
  <div class="d-flex gap-2 mb-2 flex-wrap">
396
475
  {% if lpost.categories.size > 0 %}
397
- <a href="{{ site.baseurl }}/news/{{ lpost.categories | first | slugify }}/" class="badge bg-primary text-decoration-none">
476
+ {% capture _lpost_cat_url %}{{ news_base }}{{ lpost.categories | first | slugify }}/{% endcapture %}
477
+ <a href="{{ _lpost_cat_url | relative_url }}" class="badge bg-primary text-decoration-none">
398
478
  {{ lpost.categories | first }}
399
479
  </a>
400
480
  {% endif %}
@@ -511,15 +591,21 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
511
591
  {% assign unique_tags = all_tags | uniq | slice: 0, 15 %}
512
592
  <div class="d-flex flex-wrap gap-2">
513
593
  {% for tag in unique_tags %}
514
- <a href="{{ site.baseurl }}/tags/#{{ tag | slugify }}"
594
+ {% if _tags_linkable %}
595
+ <a href="{{ tags_base | relative_url }}#{{ tag | slugify }}"
515
596
  class="badge bg-secondary text-decoration-none fs-6">
516
597
  {{ tag }}
517
598
  </a>
599
+ {% else %}
600
+ <span class="badge bg-secondary fs-6">{{ tag }}</span>
601
+ {% endif %}
518
602
  {% endfor %}
519
603
  </div>
520
- <a href="{{ site.baseurl }}/tags/" class="btn btn-sm btn-outline-primary mt-3">
604
+ {% if _tags_linkable %}
605
+ <a href="{{ tags_base | relative_url }}" class="btn btn-sm btn-outline-primary mt-3">
521
606
  Browse All Tags <i class="bi bi-arrow-right ms-1"></i>
522
607
  </a>
608
+ {% endif %}
523
609
  </div>
524
610
  </div>
525
611
  </div>
@@ -531,6 +617,17 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
531
617
  <h5 class="card-title mb-3">
532
618
  <i class="bi bi-archive me-2"></i>Archives
533
619
  </h5>
620
+ {% comment %}
621
+ Month entries deep-link into an archives page only when one is
622
+ configured (news.archives_url) or a /archives/ page actually
623
+ exists — the theme ships no archives page, so an unconditional
624
+ link 404s on every consumer site.
625
+ {% endcomment %}
626
+ {% assign archives_url = news_cfg.archives_url %}
627
+ {% unless archives_url %}
628
+ {% assign _archives_page = site.html_pages | where: "url", "/archives/" | first %}
629
+ {% if _archives_page %}{% assign archives_url = "/archives/" %}{% endif %}
630
+ {% endunless %}
534
631
  {% assign posts_by_year = all_posts | group_by_exp: "post", "post.date | date: '%Y'" %}
535
632
  <div class="row">
536
633
  {% for year in posts_by_year limit: 2 %}
@@ -540,11 +637,18 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
540
637
  <ul class="list-unstyled small">
541
638
  {% for month in months limit: 6 %}
542
639
  <li class="mb-1">
543
- <a href="{{ '/archives/' | relative_url }}#{{ month.name | slugify }}-{{ year.name }}"
640
+ {% if archives_url %}
641
+ <a href="{{ archives_url | relative_url }}#{{ month.name | slugify }}-{{ year.name }}"
544
642
  class="text-decoration-none">
545
643
  {{ month.name }}
546
644
  <span class="badge bg-secondary text-body">{{ month.items.size }}</span>
547
645
  </a>
646
+ {% else %}
647
+ <span>
648
+ {{ month.name }}
649
+ <span class="badge bg-secondary text-body">{{ month.items.size }}</span>
650
+ </span>
651
+ {% endif %}
548
652
  </li>
549
653
  {% endfor %}
550
654
  </ul>
@@ -560,15 +664,28 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
560
664
  <!-- ================================ -->
561
665
  <!-- NEWSLETTER SIGNUP -->
562
666
  <!-- ================================ -->
667
+ {% comment %}
668
+ Renders only when it can actually do something: a configured form
669
+ endpoint (news.newsletter.endpoint) posts for real; otherwise a
670
+ mailto: subscribe link on site.email. With neither — or with
671
+ news.newsletter.enabled: false — the band is omitted entirely
672
+ (the old markup shipped a form that submitted nowhere).
673
+ {% endcomment %}
674
+ {% assign _nl = news_cfg.newsletter %}
675
+ {% unless _nl.enabled == false %}
676
+ {% if _nl.endpoint or site.email %}
563
677
  <section class="newsletter-section mb-5">
564
678
  <div class="card border-0 bg-primary text-white">
565
679
  <div class="card-body p-4 p-lg-5 text-center">
566
680
  <i class="bi bi-envelope-paper fs-1 mb-3 d-block"></i>
567
- <h3 class="mb-3">Stay Updated</h3>
568
- <p class="mb-4 opacity-75">Subscribe to get the latest news and updates delivered to your inbox.</p>
569
- <form class="row g-2 justify-content-center">
681
+ <h3 class="mb-3">{{ _nl.heading | default: "Stay Updated" }}</h3>
682
+ <p class="mb-4 opacity-75">{{ _nl.lead | default: "Subscribe to get the latest news and updates delivered to your inbox." }}</p>
683
+ {% if _nl.endpoint %}
684
+ <form class="row g-2 justify-content-center" method="post" action="{{ _nl.endpoint }}">
570
685
  <div class="col-md-4">
571
- <input type="email" class="form-control form-control-lg" placeholder="Enter your email">
686
+ <label for="news-newsletter-email" class="visually-hidden">Email address</label>
687
+ <input type="email" id="news-newsletter-email" name="email" required
688
+ class="form-control form-control-lg" placeholder="Enter your email">
572
689
  </div>
573
690
  <div class="col-auto">
574
691
  <button type="submit" class="btn btn-light btn-lg">
@@ -576,9 +693,16 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
576
693
  </button>
577
694
  </div>
578
695
  </form>
696
+ {% else %}
697
+ <a class="btn btn-light btn-lg" href="mailto:{{ site.email }}?subject=Subscribe%20to%20{{ site.title | uri_escape }}">
698
+ Subscribe <i class="bi bi-send ms-1"></i>
699
+ </a>
700
+ {% endif %}
579
701
  </div>
580
702
  </div>
581
703
  </section>
704
+ {% endif %}
705
+ {% endunless %}
582
706
 
583
707
  </div>
584
708
 
data/_layouts/root.html CHANGED
@@ -46,7 +46,7 @@
46
46
  {%- else -%}
47
47
  {%- assign _bs_theme = 'dark' -%}
48
48
  {%- endif -%}
49
- <html lang="{{ page.lang | default: site.locale | slice: 0, 2 | default: 'en' }}" class="no-js" data-bs-theme="{{ _bs_theme }}" data-color-mode-default="{{ _cm }}" data-theme-skin="{{ site.theme_skin | default: 'dark' }}" data-zer0-bg="{{ site.theme_background.enabled | default: true }}">
49
+ <html lang="{{ page.lang | default: site.locale | slice: 0, 2 | default: 'en' }}" class="no-js" data-bs-theme="{{ _bs_theme }}" data-color-mode-default="{{ _cm }}" data-color-mode-lock="{{ site.color_mode_lock | default: false }}" data-theme-skin="{{ site.theme_skin | default: 'air' }}" data-zer0-bg="{{ site.theme_background.enabled | default: true }}">
50
50
  <head>
51
51
  <!-- =============================================== -->
52
52
  <!-- HEAD SECTION: Meta tags, styles, and SEO setup -->
@@ -67,7 +67,11 @@
67
67
  <!-- ============================================== -->
68
68
  <!-- BODY SECTION: Site structure and content area -->
69
69
  <!-- ============================================== -->
70
-
70
+
71
+ <!-- Consumer hook: first thing inside <body> (e.g. GTM noscript).
72
+ Empty stub shipped by the theme; shadow _includes/custom/body-start.html to use. -->
73
+ {% include custom/body-start.html %}
74
+
71
75
  <!-- Inline SVG symbols for consistent iconography throughout the site -->
72
76
  <!-- Page-invariant: cached once per build via jekyll-include-cache. -->
73
77
  {% include_cached components/svg.html %}
@@ -125,6 +129,12 @@
125
129
  {%- include_cached core/footer.html lang=page.lang -%}
126
130
  {%- include core/footer-fabs.html -%}
127
131
 
132
+ <!-- Consumer hook: after the theme footer, before the consent banner.
133
+ Empty stub shipped by the theme; shadow _includes/custom/footer.html to use.
134
+ Deliberately OUTSIDE the cached footer include so page-dependent
135
+ consumer markup renders per page. -->
136
+ {%- include custom/footer.html -%}
137
+
128
138
  <!-- Privacy-compliant cookie consent banner — page-invariant. -->
129
139
  {%- include_cached components/cookie-consent.html -%}
130
140
 
@@ -148,5 +158,9 @@
148
158
 
149
159
  <!-- Analytics Integration -->
150
160
  {%- include analytics/posthog.html -%}
161
+
162
+ <!-- Consumer hook: last thing before </body> (site scripts, widgets).
163
+ Empty stub shipped by the theme; shadow _includes/custom/body-end.html to use. -->
164
+ {%- include custom/body-end.html -%}
151
165
  </body>
152
166
  </html>
@@ -46,6 +46,34 @@ layout: root
46
46
  {% comment %} SECTION DISCOVERY LOGIC {% endcomment %}
47
47
  {% comment %} ================================ {% endcomment %}
48
48
 
49
+ {% comment %}
50
+ Same `news:` config surface as _layouts/news.html:
51
+ - news.category_base (default "/news/") — base path for section pages
52
+ - news.index_url (default "/news/") — the news homepage this section
53
+ belongs to (breadcrumb + "Browse all news")
54
+ Section pills fall back to site.categories when _data/navigation/posts.yml
55
+ is absent, so the layout works on a data-less consumer site.
56
+ {% endcomment %}
57
+ {% assign news_cfg = site.news %}
58
+ {% assign news_base = news_cfg.category_base | default: "/news/" %}
59
+ {% assign news_index_url = news_cfg.index_url | default: "/news/" %}
60
+ {% capture _sec_names %}{% for item in site.data.navigation.posts %}{{ item.title }}{% unless forloop.last %}|{% endunless %}{% endfor %}{% endcapture %}
61
+ {% if _sec_names == "" %}{% capture _sec_names %}{% for cat in site.categories limit: 8 %}{{ cat[0] }}{% unless forloop.last %}|{% endunless %}{% endfor %}{% endcapture %}{% endif %}
62
+ {% assign news_sections = _sec_names | split: "|" %}
63
+
64
+ {% comment %}
65
+ Tags index link policy — identical to news.html: honor site.tags_page when
66
+ set, otherwise only link when a /tags/ page exists in the build.
67
+ {% endcomment %}
68
+ {% assign tags_base = site.tags_page | default: "/tags/" %}
69
+ {% assign _tags_linkable = false %}
70
+ {% if site.tags_page %}
71
+ {% assign _tags_linkable = true %}
72
+ {% else %}
73
+ {% assign _tags_index_page = site.html_pages | where: "url", tags_base | first %}
74
+ {% if _tags_index_page %}{% assign _tags_linkable = true %}{% endif %}
75
+ {% endif %}
76
+
49
77
  {% assign path_parts = page.path | split: "/" %}
50
78
  {% assign section_name = "" %}
51
79
  {% for part in path_parts %}
@@ -99,7 +127,7 @@ layout: root
99
127
  <nav aria-label="breadcrumb">
100
128
  <ol class="breadcrumb mb-0">
101
129
  <li class="breadcrumb-item">
102
- <a href="{{ site.baseurl }}/news/" class="text-white-50 text-decoration-none">
130
+ <a href="{{ news_index_url | relative_url }}" class="text-white-50 text-decoration-none">
103
131
  <i class="bi bi-newspaper me-1"></i>News
104
132
  </a>
105
133
  </li>
@@ -123,15 +151,21 @@ layout: root
123
151
  <div class="container-fluid bg-body-tertiary border-bottom py-2 mb-4">
124
152
  <div class="container-fluid px-4">
125
153
  <nav class="nav nav-pills flex-nowrap overflow-auto" style="scrollbar-width: none;">
126
- {% for item in site.data.navigation.posts %}
154
+ {% for sec_name in news_sections %}
127
155
  {% assign is_active = false %}
128
- {% if item.title == page.category or item.title == page.title %}
156
+ {% if sec_name == page.category or sec_name == page.title %}
129
157
  {% assign is_active = true %}
130
158
  {% endif %}
131
- <a class="nav-link text-nowrap {% if is_active %}active{% else %}text-body-secondary{% endif %}"
132
- href="{{ item.url | replace: '/posts/', '/news/' | relative_url }}">
133
- {% if item.icon %}<i class="bi {{ item.icon }} me-1"></i>{% endif %}
134
- {{ item.title }}
159
+ {% assign _sec_nav = site.data.navigation.posts | where: "title", sec_name | first %}
160
+ {% if _sec_nav %}
161
+ {% assign _sec_url = _sec_nav.url | replace: '/posts/', news_base %}
162
+ {% else %}
163
+ {% capture _sec_url %}{{ news_base }}{{ sec_name | slugify }}/{% endcapture %}
164
+ {% endif %}
165
+ <a class="nav-link text-nowrap {% if is_active %}active{% else %}text-body-secondary{% endif %}"
166
+ href="{{ _sec_url | relative_url }}">
167
+ {% if _sec_nav.icon %}<i class="bi {{ _sec_nav.icon }} me-1"></i>{% endif %}
168
+ {{ sec_name }}
135
169
  </a>
136
170
  {% endfor %}
137
171
  </nav>
@@ -255,9 +289,11 @@ layout: root
255
289
  <h3 class="h4 mb-0">
256
290
  <i class="bi bi-tag text-primary me-2"></i>{{ subcat | capitalize }}
257
291
  </h3>
258
- <a href="{{ site.baseurl }}/tags/#{{ subcat | slugify }}" class="btn btn-outline-secondary btn-sm">
292
+ {% if _tags_linkable %}
293
+ <a href="{{ tags_base | relative_url }}#{{ subcat | slugify }}" class="btn btn-outline-secondary btn-sm">
259
294
  View All <i class="bi bi-arrow-right ms-1"></i>
260
295
  </a>
296
+ {% endif %}
261
297
  </div>
262
298
 
263
299
  <div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4">
@@ -496,7 +532,7 @@ layout: root
496
532
  <div class="text-center py-5">
497
533
  <i class="bi bi-inbox fs-1 text-muted"></i>
498
534
  <p class="text-muted mt-3">No articles in this section yet.</p>
499
- <a href="{{ site.baseurl }}/news/" class="btn btn-outline-primary">
535
+ <a href="{{ news_index_url | relative_url }}" class="btn btn-outline-primary">
500
536
  Browse all news
501
537
  </a>
502
538
  </div>
@@ -510,15 +546,21 @@ layout: root
510
546
  <i class="bi bi-compass me-2"></i>Explore Other Sections
511
547
  </h3>
512
548
  <div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3">
513
- {% for nav_item in site.data.navigation.posts %}
514
- {% unless nav_item.title == page.category or nav_item.title == page.title %}
515
- {% assign other_section_posts = all_site_posts | where_exp: "post", "post.categories contains nav_item.title" %}
549
+ {% for sec_name in news_sections %}
550
+ {% unless sec_name == page.category or sec_name == page.title %}
551
+ {% assign other_section_posts = all_site_posts | where_exp: "post", "post.categories contains sec_name" %}
552
+ {% assign _sec_nav = site.data.navigation.posts | where: "title", sec_name | first %}
553
+ {% if _sec_nav %}
554
+ {% assign _sec_url = _sec_nav.url | replace: '/posts/', news_base %}
555
+ {% else %}
556
+ {% capture _sec_url %}{{ news_base }}{{ sec_name | slugify }}/{% endcapture %}
557
+ {% endif %}
516
558
  <div class="col">
517
- <a href="{{ nav_item.url | replace: '/posts/', '/news/' | relative_url }}"
559
+ <a href="{{ _sec_url | relative_url }}"
518
560
  class="card text-center h-100 text-decoration-none border-0 shadow-sm hover-lift">
519
561
  <div class="card-body py-3">
520
- <i class="bi {{ nav_item.icon | default: 'bi-folder' }} fs-4 text-primary mb-2 d-block"></i>
521
- <h6 class="card-title mb-1 small">{{ nav_item.title }}</h6>
562
+ <i class="bi {{ _sec_nav.icon | default: 'bi-folder' }} fs-4 text-primary mb-2 d-block"></i>
563
+ <h6 class="card-title mb-1 small">{{ sec_name }}</h6>
522
564
  <small class="text-muted">{{ other_section_posts.size }} articles</small>
523
565
  </div>
524
566
  </a>