jekyll-theme-zer0 1.27.0 → 1.29.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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +812 -13
  3. data/_data/backlog.yml +356 -2
  4. data/_data/consumers.yml +252 -0
  5. data/_data/features.yml +234 -5
  6. data/_data/i18n/fr.yml +103 -0
  7. data/_data/i18n/manifest.yml +1529 -0
  8. data/_data/ingredient_densities.yml +122 -0
  9. data/_data/navigation/docs.yml +2 -0
  10. data/_data/navigation/main.yml +22 -0
  11. data/_data/recipe_courses.yml +64 -0
  12. data/_data/series.yml +19 -0
  13. data/_data/theme-manifest.yml +387 -277
  14. data/_data/ui-text.yml +8 -0
  15. data/_includes/README.md +36 -3
  16. data/_includes/analytics/google-tag-manager-body.html +11 -2
  17. data/_includes/analytics/google-tag-manager-head.html +16 -6
  18. data/_includes/analytics/posthog.html +33 -4
  19. data/_includes/components/abc-letter.html +43 -0
  20. data/_includes/components/background-customizer.html +2 -2
  21. data/_includes/components/book-card.html +42 -0
  22. data/_includes/components/book-nav.html +80 -0
  23. data/_includes/components/book-plate.html +31 -0
  24. data/_includes/components/book-toc.html +47 -0
  25. data/_includes/components/bookshelf.html +68 -0
  26. data/_includes/components/card-grid.html +60 -0
  27. data/_includes/components/cookie-consent.html +4 -4
  28. data/_includes/components/data-card.html +95 -0
  29. data/_includes/components/halfmoon.html +5 -1
  30. data/_includes/components/info-section.html +8 -3
  31. data/_includes/components/language-toggle.html +168 -21
  32. data/_includes/components/page-feedback.html +65 -2
  33. data/_includes/components/page-views-init.html +55 -0
  34. data/_includes/components/page-views.html +33 -0
  35. data/_includes/components/recipe-card.html +67 -0
  36. data/_includes/components/recipe-duration.html +50 -0
  37. data/_includes/components/recipe-grams.html +58 -0
  38. data/_includes/components/recipe-index.html +96 -0
  39. data/_includes/components/recipe-ingredients.html +90 -0
  40. data/_includes/components/recipe-meta.html +96 -0
  41. data/_includes/components/recipe-nutrition.html +57 -0
  42. data/_includes/components/recipe-qty.html +73 -0
  43. data/_includes/components/recipe-ratio.html +151 -0
  44. data/_includes/components/recipe-scaler.html +73 -0
  45. data/_includes/components/recipe-steps.html +86 -0
  46. data/_includes/components/recipe-temp.html +45 -0
  47. data/_includes/components/search-modal.html +2 -2
  48. data/_includes/components/shortcuts-modal.html +3 -0
  49. data/_includes/components/theme-controls-bar.html +10 -2
  50. data/_includes/components/theme-customizer.html +8 -2
  51. data/_includes/components/theme-info.html +8 -1
  52. data/_includes/content/jsonld-software.html +22 -3
  53. data/_includes/content/seo.html +9 -3
  54. data/_includes/core/color-mode-init.html +13 -4
  55. data/_includes/core/favicon.html +46 -0
  56. data/_includes/core/footer.html +18 -16
  57. data/_includes/core/head.html +21 -0
  58. data/_includes/core/header.html +7 -4
  59. data/_includes/custom/body-end.html +18 -0
  60. data/_includes/custom/body-start.html +17 -0
  61. data/_includes/custom/footer.html +18 -0
  62. data/_includes/custom/head.html +18 -0
  63. data/_includes/navigation/local-graph.html +28 -2
  64. data/_includes/navigation/nav-tree.html +3 -3
  65. data/_includes/navigation/section-sidebar.html +93 -11
  66. data/_includes/navigation/sidebar-config.html +21 -0
  67. data/_includes/navigation/sidebar-left.html +2 -1
  68. data/_includes/navigation/sidebar-nav.html +4 -0
  69. data/_includes/navigation/sidebar-pagetree.html +150 -0
  70. data/_includes/navigation/sidebar-right.html +2 -1
  71. data/_includes/navigation/unified-drawer.html +8 -2
  72. data/_includes/obsidian/full-graph.html +165 -136
  73. data/_includes/setup/wizard.html +173 -86
  74. data/_includes/stats/stats-metrics.html +2 -2
  75. data/_layouts/404.html +260 -0
  76. data/_layouts/README.md +4 -0
  77. data/_layouts/admin.html +2 -2
  78. data/_layouts/article.html +5 -0
  79. data/_layouts/book-abc.html +106 -0
  80. data/_layouts/book-story.html +91 -0
  81. data/_layouts/book.html +113 -0
  82. data/_layouts/collection.html +17 -5
  83. data/_layouts/cookbook.html +88 -0
  84. data/_layouts/default.html +4 -4
  85. data/_layouts/home.html +5 -2
  86. data/_layouts/landing.html +9 -0
  87. data/_layouts/news.html +155 -31
  88. data/_layouts/recipe.html +274 -0
  89. data/_layouts/root.html +38 -5
  90. data/_layouts/section.html +96 -25
  91. data/_sass/components/_book.scss +423 -0
  92. data/_sass/components/_callout.scss +1 -1
  93. data/_sass/components/_footer.scss +37 -1
  94. data/_sass/components/_page-views.scss +36 -0
  95. data/_sass/components/_recipe.scss +506 -0
  96. data/_sass/components/_setup-wizard.scss +235 -0
  97. data/_sass/components/_ui-enhancements.scss +6 -6
  98. data/_sass/core/_navbar.scss +263 -15
  99. data/_sass/core/_obsidian.scss +286 -6
  100. data/_sass/layouts/_landing.scss +2 -2
  101. data/_sass/theme/_backgrounds.scss +21 -8
  102. data/_sass/tokens/_color.scss +6 -0
  103. data/_sass/tokens/_index.scss +2 -0
  104. data/_sass/tokens/_radius.scss +21 -0
  105. data/_sass/tokens/_typography.scss +4 -0
  106. data/_sass/utilities/_focus.scss +14 -0
  107. data/assets/css/main.scss +4 -0
  108. data/assets/js/auto-hide-nav.js +5 -1
  109. data/assets/js/halfmoon.js +26 -0
  110. data/assets/js/modules/navigation/navbar.js +55 -0
  111. data/assets/js/obsidian-graph.js +702 -264
  112. data/assets/js/obsidian-local-graph.js +161 -54
  113. data/assets/js/page-views.js +372 -0
  114. data/assets/js/recipe-scaler.js +501 -0
  115. data/assets/js/search-modal.js +14 -1
  116. data/assets/js/setup-wizard.js +322 -20
  117. data/scripts/README.md +29 -0
  118. data/scripts/bin/audit-consumer +39 -7
  119. data/scripts/bin/giscus-discussions +213 -14
  120. data/scripts/bin/manifest +66 -16
  121. data/scripts/bin/validate +5 -1
  122. data/scripts/ci/agent_review_result.py +164 -0
  123. data/scripts/ci/test_agent_review_result.py +172 -0
  124. data/scripts/design-system-check.rb +170 -0
  125. data/scripts/install/README.md +47 -6
  126. data/scripts/install/ai/client.sh +302 -93
  127. data/scripts/install/ai/prompts/spec.schema.json +1 -1
  128. data/scripts/install/ai/wizard.sh +10 -5
  129. data/scripts/install/apply.sh +7 -3
  130. data/scripts/install/cli.sh +54 -4
  131. data/scripts/install/config.sh +167 -0
  132. data/scripts/install/doctor.sh +38 -0
  133. data/scripts/install/plan.sh +10 -0
  134. data/scripts/install/spec.sh +15 -7
  135. data/scripts/install/template.sh +4 -0
  136. data/scripts/lib/audit.sh +42 -2
  137. data/scripts/lint-liquid-raw.rb +137 -0
  138. data/scripts/propagate.rb +277 -0
  139. data/scripts/test/lib/run_tests.sh +2 -1
  140. data/scripts/test/lib/test_agent_review_result.sh +27 -0
  141. data/scripts/translate.rb +161 -15
  142. metadata +56 -2
@@ -0,0 +1,88 @@
1
+ ---
2
+ # Feature: ZER0-084
3
+ layout: default
4
+ ---
5
+ {%- comment -%}
6
+ Layout: cookbook.html
7
+ Extends: default.html
8
+ Purpose: Landing page for one cookbook — cover, introduction, a jump-nav of
9
+ its courses, and the full recipe index grouped by course.
10
+ Front matter required: layout, title, cookbook (slug shared with its recipes)
11
+ Front matter optional: subtitle, description, image/preview, author,
12
+ grouped (false for one flat grid)
13
+ Notes: Recipes belong to a cookbook by matching its `cookbook:` slug. Omit
14
+ `cookbook:` here and the page indexes EVERY recipe on the site, which
15
+ is what a single-cookbook site wants.
16
+ {%- endcomment -%}
17
+
18
+ {%- assign _cb_slug = page.cookbook -%}
19
+ {%- if _cb_slug -%}
20
+ {%- assign _cb_recipes = site.recipes | where_exp: "doc", "doc.layout != 'cookbook'" | where: "cookbook", _cb_slug -%}
21
+ {%- else -%}
22
+ {%- assign _cb_recipes = site.recipes | where_exp: "doc", "doc.layout != 'cookbook'" -%}
23
+ {%- endif -%}
24
+ {%- assign _cb_courses = _cb_recipes | map: "course" | compact | uniq -%}
25
+ {%- assign _cb_image = page.image | default: page.preview -%}
26
+
27
+ <div class="cookbook">
28
+ <!-- ================================ -->
29
+ <!-- COVER -->
30
+ <!-- ================================ -->
31
+ <header class="cookbook-header">
32
+ <div class="row g-4 align-items-center">
33
+ {%- if _cb_image -%}
34
+ <div class="col-8 col-sm-5 col-md-4 col-lg-3">
35
+ <div class="cookbook-cover">
36
+ {%- include components/preview-image.html src=_cb_image alt=page.title class="" loading="eager" -%}
37
+ </div>
38
+ </div>
39
+ {%- endif -%}
40
+ <div class="col">
41
+ <h1 id="page-title" class="cookbook-title">{{ page.title }}</h1>
42
+ {%- if page.subtitle -%}
43
+ <p class="lead mb-2">{{ page.subtitle }}</p>
44
+ {%- endif -%}
45
+ {%- if page.author -%}
46
+ <p class="text-body-secondary small mb-2">By {{ page.author }}</p>
47
+ {%- endif -%}
48
+ <p class="text-body-secondary mb-3">
49
+ <i class="bi bi-egg-fried me-1" aria-hidden="true"></i>{{ _cb_recipes.size }} {% if _cb_recipes.size == 1 %}recipe{% else %}recipes{% endif %}
50
+ {%- if _cb_courses.size > 0 %} <span class="mx-1" aria-hidden="true">•</span> {{ _cb_courses.size }} {% if _cb_courses.size == 1 %}course{% else %}courses{% endif %}{% endif -%}
51
+ </p>
52
+ {%- if page.description -%}
53
+ <p class="cookbook-description">{{ page.description }}</p>
54
+ {%- endif -%}
55
+ </div>
56
+ </div>
57
+ </header>
58
+
59
+ <!-- ================================ -->
60
+ <!-- ABOUT (the markdown body) -->
61
+ <!-- ================================ -->
62
+ {%- assign _cb_body = content | strip -%}
63
+ {%- if _cb_body != "" -%}
64
+ <section class="cookbook-about">
65
+ {{ content }}
66
+ </section>
67
+ {%- endif -%}
68
+
69
+ <!-- ================================ -->
70
+ <!-- COURSE JUMP NAV -->
71
+ <!-- ================================ -->
72
+ {%- if _cb_courses.size > 1 and page.grouped != false -%}
73
+ <nav class="cookbook-jump" aria-label="Jump to a course">
74
+ {%- for _cb_pair in site.data.recipe_courses -%}
75
+ {%- if _cb_courses contains _cb_pair[0] -%}
76
+ <a class="btn btn-sm btn-outline-secondary" href="#course-{{ _cb_pair[0] }}">
77
+ <i class="bi {{ _cb_pair[1].icon | default: 'bi-journal-bookmark' }} me-1" aria-hidden="true"></i>{{ _cb_pair[1].title | default: _cb_pair[0] | escape }}
78
+ </a>
79
+ {%- endif -%}
80
+ {%- endfor -%}
81
+ </nav>
82
+ {%- endif -%}
83
+
84
+ <!-- ================================ -->
85
+ <!-- RECIPE INDEX -->
86
+ <!-- ================================ -->
87
+ {%- include components/recipe-index.html cookbook=_cb_slug grouped=page.grouped heading_level="h2" -%}
88
+ </div>
@@ -93,12 +93,12 @@ layout: root
93
93
 
94
94
  {%- comment -%}
95
95
  Page introduction: title, breadcrumbs, metadata.
96
- Skipped on layouts that render their own page header (article, note)
97
- to avoid duplicate H1s. Authors can also opt-out per page via
98
- `hide_intro: true` in front matter.
96
+ Skipped on layouts that render their own page header (article, note,
97
+ recipe, cookbook) to avoid duplicate H1s. Authors can also opt-out
98
+ per page via `hide_intro: true` in front matter.
99
99
  {%- endcomment -%}
100
100
  {% assign _skip_intro = false %}
101
- {% if page.layout == "article" or page.layout == "note" or page.hide_intro %}
101
+ {% if page.layout == "article" or page.layout == "note" or page.layout == "recipe" or page.layout == "cookbook" or page.hide_intro %}
102
102
  {% assign _skip_intro = true %}
103
103
  {% endif %}
104
104
  {% unless _skip_intro %}
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,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