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
data/_layouts/home.html CHANGED
@@ -34,8 +34,11 @@ layout: root
34
34
  - title: displayed as <h1> (and used for SEO) when present
35
35
  - hide_title:true keep `title` for SEO but suppress the visible <h1>
36
36
  (useful when a custom hero provides its own heading)
37
+ - hide_intro:true accepted as an alias of hide_title — other layouts use
38
+ `hide_intro` for "suppress the generated page header",
39
+ and consumers reasonably reach for it here too
37
40
  - rss_subscribe:false hide the RSS subscribe link on landing/showcase homepages
38
- Both default to the original behavior, so existing homepages are unaffected.
41
+ All default to the original behavior, so existing homepages are unaffected.
39
42
 
40
43
  Design Philosophy:
41
44
  - Minimal structure allows for maximum content flexibility
@@ -56,7 +59,7 @@ layout: root
56
59
  <!-- Landing/showcase pages can keep `title` for SEO but suppress the -->
57
60
  <!-- visible heading (e.g. when a custom hero supplies its own <h1>) -->
58
61
  <!-- with `hide_title: true`. -->
59
- {%- if page.title and page.hide_title != true -%}
62
+ {%- if page.title and page.hide_title != true and page.hide_intro != true -%}
60
63
  <h1 class="page-heading">{{ page.title }}</h1>
61
64
  {%- endif -%}
62
65
 
@@ -182,6 +182,14 @@ layout: root
182
182
  <!-- ================================ -->
183
183
  <!-- QUICK START SECTION (data-driven)-->
184
184
  <!-- ================================ -->
185
+ {%- comment -%}
186
+ Rendered only when the site ships a `get_started` block in
187
+ _data/landing.yml. The install cards advertise THEME installation
188
+ (gem / Docker / fork), which is right for the theme's own site but wrong
189
+ boilerplate on a consumer landing page — a data-less consumer gets a
190
+ clean page instead of another site's marketing.
191
+ {%- endcomment -%}
192
+ {%- if landing.get_started -%}
185
193
  <section id="get-started" class="zer0-section" aria-labelledby="get-started-heading">
186
194
  <div class="container-xl py-5 px-4 px-md-5">
187
195
  <div class="row">
@@ -193,6 +201,7 @@ layout: root
193
201
  </div>
194
202
  </div>
195
203
  </section>
204
+ {%- endif -%}
196
205
 
197
206
  <!-- ================================ -->
198
207
  <!-- AUTHOR / E-E-A-T SECTION -->
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,25 +105,35 @@ 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>
78
131
  </div>
79
132
  </div>
80
133
 
81
- <main class="container">
134
+ <!-- Not a <main>: root.html already provides the single <main id="main-content">
135
+ landmark; a nested <main> here would duplicate it (issue #299). -->
136
+ <div class="container">
82
137
 
83
138
  {% if section_style == "magazine" %}
84
139
  <!-- ================================ -->
@@ -90,11 +145,22 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
90
145
  <!-- ================================ -->
91
146
  {% assign breaking_posts = all_posts | where: "breaking", true %}
92
147
  {% assign hero_post = breaking_posts | first %}
93
-
148
+
94
149
  {% unless hero_post %}
95
150
  {% assign hero_post = all_posts | where: "featured", true | first %}
96
151
  {% endunless %}
97
-
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
+
98
164
  {% if hero_post %}
99
165
  <section class="hero-section mb-5">
100
166
  <div class="row g-0 bg-body-secondary rounded-4 overflow-hidden shadow">
@@ -126,7 +192,8 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
126
192
  </small>
127
193
  {% endif %}
128
194
  {% if hero_post.categories.size > 0 %}
129
- <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">
130
197
  {{ hero_post.categories | first }}
131
198
  </a>
132
199
  {% endif %}
@@ -153,14 +220,20 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
153
220
  <!-- ================================ -->
154
221
  <section class="section-nav mb-5">
155
222
  <div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3">
156
- {% for nav_item in site.data.navigation.posts %}
157
- {% 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 %}
158
231
  <div class="col">
159
- <a href="{{ nav_item.url | replace: '/posts/', '/news/' | relative_url }}"
232
+ <a href="{{ _sec_url | relative_url }}"
160
233
  class="card text-center h-100 text-decoration-none border-0 shadow-sm hover-lift">
161
234
  <div class="card-body py-4">
162
- <i class="bi {{ nav_item.icon | default: 'bi-folder' }} fs-2 text-primary mb-2 d-block"></i>
163
- <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>
164
237
  <small class="text-muted">{{ section_posts.size }} articles</small>
165
238
  </div>
166
239
  </a>
@@ -202,7 +275,8 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
202
275
  </div>
203
276
  <div class="card-body">
204
277
  {% if main_featured.categories.size > 0 %}
205
- <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 }}"
206
280
  class="badge bg-primary text-decoration-none mb-2">
207
281
  {{ main_featured.categories | first }}
208
282
  </a>
@@ -263,16 +337,22 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
263
337
  <!-- ================================ -->
264
338
  <!-- POSTS BY SECTION -->
265
339
  <!-- ================================ -->
266
- {% for nav_item in site.data.navigation.posts limit: 3 %}
267
- {% 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 %}
268
348
  {% if section_posts.size > 0 %}
269
349
  <section class="section-posts mb-5 pb-4 border-bottom">
270
350
  <div class="d-flex justify-content-between align-items-center mb-4">
271
351
  <h2 class="h4 mb-0">
272
- <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 }}
273
353
  </h2>
274
- <a href="{{ nav_item.url | replace: '/posts/', '/news/' | relative_url }}" class="btn btn-outline-secondary btn-sm">
275
- 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>
276
356
  </a>
277
357
  </div>
278
358
 
@@ -349,7 +429,8 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
349
429
  </div>
350
430
  <div class="card-body">
351
431
  {% if gpost.categories.size > 0 %}
352
- <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">
353
434
  {{ gpost.categories | first }}
354
435
  </a>
355
436
  {% endif %}
@@ -392,7 +473,8 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
392
473
  <div class="card-body">
393
474
  <div class="d-flex gap-2 mb-2 flex-wrap">
394
475
  {% if lpost.categories.size > 0 %}
395
- <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">
396
478
  {{ lpost.categories | first }}
397
479
  </a>
398
480
  {% endif %}
@@ -509,15 +591,21 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
509
591
  {% assign unique_tags = all_tags | uniq | slice: 0, 15 %}
510
592
  <div class="d-flex flex-wrap gap-2">
511
593
  {% for tag in unique_tags %}
512
- <a href="{{ site.baseurl }}/tags/#{{ tag | slugify }}"
594
+ {% if _tags_linkable %}
595
+ <a href="{{ tags_base | relative_url }}#{{ tag | slugify }}"
513
596
  class="badge bg-secondary text-decoration-none fs-6">
514
597
  {{ tag }}
515
598
  </a>
599
+ {% else %}
600
+ <span class="badge bg-secondary fs-6">{{ tag }}</span>
601
+ {% endif %}
516
602
  {% endfor %}
517
603
  </div>
518
- <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">
519
606
  Browse All Tags <i class="bi bi-arrow-right ms-1"></i>
520
607
  </a>
608
+ {% endif %}
521
609
  </div>
522
610
  </div>
523
611
  </div>
@@ -529,6 +617,17 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
529
617
  <h5 class="card-title mb-3">
530
618
  <i class="bi bi-archive me-2"></i>Archives
531
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 %}
532
631
  {% assign posts_by_year = all_posts | group_by_exp: "post", "post.date | date: '%Y'" %}
533
632
  <div class="row">
534
633
  {% for year in posts_by_year limit: 2 %}
@@ -538,11 +637,18 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
538
637
  <ul class="list-unstyled small">
539
638
  {% for month in months limit: 6 %}
540
639
  <li class="mb-1">
541
- <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 }}"
542
642
  class="text-decoration-none">
543
643
  {{ month.name }}
544
644
  <span class="badge bg-secondary text-body">{{ month.items.size }}</span>
545
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 %}
546
652
  </li>
547
653
  {% endfor %}
548
654
  </ul>
@@ -558,15 +664,28 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
558
664
  <!-- ================================ -->
559
665
  <!-- NEWSLETTER SIGNUP -->
560
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 %}
561
677
  <section class="newsletter-section mb-5">
562
678
  <div class="card border-0 bg-primary text-white">
563
679
  <div class="card-body p-4 p-lg-5 text-center">
564
680
  <i class="bi bi-envelope-paper fs-1 mb-3 d-block"></i>
565
- <h3 class="mb-3">Stay Updated</h3>
566
- <p class="mb-4 opacity-75">Subscribe to get the latest news and updates delivered to your inbox.</p>
567
- <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 }}">
568
685
  <div class="col-md-4">
569
- <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">
570
689
  </div>
571
690
  <div class="col-auto">
572
691
  <button type="submit" class="btn btn-light btn-lg">
@@ -574,11 +693,18 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
574
693
  </button>
575
694
  </div>
576
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 %}
577
701
  </div>
578
702
  </div>
579
703
  </section>
704
+ {% endif %}
705
+ {% endunless %}
580
706
 
581
- </main>
707
+ </div>
582
708
 
583
709
  <style>
584
710
  .hover-lift {
data/_layouts/root.html CHANGED
@@ -46,7 +46,7 @@
46
46
  {%- else -%}
47
47
  {%- assign _bs_theme = 'dark' -%}
48
48
  {%- endif -%}
49
- <html lang="{{ 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 %}
@@ -82,11 +86,13 @@
82
86
  Enable with `navigation.unified_mobile_drawer: true` in _config.yml. -->
83
87
  {% include navigation/unified-drawer.html %}
84
88
 
85
- <!-- Search modal (site-wide) — page-invariant, cached once per build. -->
86
- {% include_cached components/search-modal.html %}
89
+ <!-- Search modal (site-wide) — cached once per build PER LANGUAGE
90
+ (the lang param keys the include-cache entry; page.lang is nil
91
+ on English pages, so they all still share a single render). -->
92
+ {% include_cached components/search-modal.html lang=page.lang %}
87
93
 
88
- <!-- Keyboard shortcuts help modal (triggered by `?`) — page-invariant. -->
89
- {% include_cached components/shortcuts-modal.html %}
94
+ <!-- Keyboard shortcuts help modal (triggered by `?`) — cached per language. -->
95
+ {% include_cached components/shortcuts-modal.html lang=page.lang %}
90
96
 
91
97
  <!-- Settings offcanvas (Appearance / Site / Developer tabs) -->
92
98
  {% include components/info-section.html %}
@@ -98,10 +104,14 @@
98
104
  <!-- MAIN CONTENT AREA -->
99
105
  <!-- ======================== -->
100
106
  <!-- Skip-link target: keep a single, consistent #main-content anchor site-wide.
101
- Semantic <main> landmark (consistent with the default/section/news layouts)
102
- so assistive tech, search engines, and AI content extractors can isolate the
107
+ This is the ONE <main> landmark per page — child layouts (default/section/
108
+ news) must use non-landmark wrappers (<div>) inside it (issue #299) — so
109
+ assistive tech, search engines, and AI content extractors can isolate the
103
110
  primary content from the surrounding header/nav/footer chrome. -->
104
111
  <main id="main-content">
112
+ <!-- Machine-translation disclosure — renders only on generated
113
+ translations (page.machine_translated); no-op elsewhere. -->
114
+ {%- include components/translation-notice.html -%}
105
115
  {{ content }}
106
116
  </main>
107
117
 
@@ -116,9 +126,15 @@
116
126
  fresh: jekyll-include-cache stores in memory only (Jekyll 3.x has no
117
127
  Jekyll::Cache) and clears on every build's :pre_render hook — it never
118
128
  persists to .jekyll-cache. -->
119
- {%- include_cached core/footer.html -%}
129
+ {%- include_cached core/footer.html lang=page.lang -%}
120
130
  {%- include core/footer-fabs.html -%}
121
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
+
122
138
  <!-- Privacy-compliant cookie consent banner — page-invariant. -->
123
139
  {%- include_cached components/cookie-consent.html -%}
124
140
 
@@ -142,5 +158,9 @@
142
158
 
143
159
  <!-- Analytics Integration -->
144
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 -%}
145
165
  </body>
146
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>
@@ -156,7 +190,9 @@ layout: root
156
190
  <!-- ================================ -->
157
191
  <!-- MAIN CONTENT AREA -->
158
192
  <!-- ================================ -->
159
- <main class="col-lg-9 col-xl-10 section-layout-main">
193
+ <!-- Not a <main>: root.html already provides the single <main id="main-content">
194
+ landmark; a nested <main> here would duplicate it (issue #299). -->
195
+ <div class="col-lg-9 col-xl-10 section-layout-main">
160
196
 
161
197
  <!-- Section Header -->
162
198
  <header class="section-header mb-4">
@@ -253,9 +289,11 @@ layout: root
253
289
  <h3 class="h4 mb-0">
254
290
  <i class="bi bi-tag text-primary me-2"></i>{{ subcat | capitalize }}
255
291
  </h3>
256
- <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">
257
294
  View All <i class="bi bi-arrow-right ms-1"></i>
258
295
  </a>
296
+ {% endif %}
259
297
  </div>
260
298
 
261
299
  <div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4">
@@ -494,7 +532,7 @@ layout: root
494
532
  <div class="text-center py-5">
495
533
  <i class="bi bi-inbox fs-1 text-muted"></i>
496
534
  <p class="text-muted mt-3">No articles in this section yet.</p>
497
- <a href="{{ site.baseurl }}/news/" class="btn btn-outline-primary">
535
+ <a href="{{ news_index_url | relative_url }}" class="btn btn-outline-primary">
498
536
  Browse all news
499
537
  </a>
500
538
  </div>
@@ -508,15 +546,21 @@ layout: root
508
546
  <i class="bi bi-compass me-2"></i>Explore Other Sections
509
547
  </h3>
510
548
  <div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3">
511
- {% for nav_item in site.data.navigation.posts %}
512
- {% unless nav_item.title == page.category or nav_item.title == page.title %}
513
- {% 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 %}
514
558
  <div class="col">
515
- <a href="{{ nav_item.url | replace: '/posts/', '/news/' | relative_url }}"
559
+ <a href="{{ _sec_url | relative_url }}"
516
560
  class="card text-center h-100 text-decoration-none border-0 shadow-sm hover-lift">
517
561
  <div class="card-body py-3">
518
- <i class="bi {{ nav_item.icon | default: 'bi-folder' }} fs-4 text-primary mb-2 d-block"></i>
519
- <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>
520
564
  <small class="text-muted">{{ other_section_posts.size }} articles</small>
521
565
  </div>
522
566
  </a>
@@ -526,7 +570,7 @@ layout: root
526
570
  </div>
527
571
  </aside>
528
572
 
529
- </main>
573
+ </div>
530
574
  </div>
531
575
  </div>
532
576