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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +812 -13
- data/_data/backlog.yml +356 -2
- data/_data/consumers.yml +252 -0
- data/_data/features.yml +234 -5
- data/_data/i18n/fr.yml +103 -0
- data/_data/i18n/manifest.yml +1529 -0
- data/_data/ingredient_densities.yml +122 -0
- data/_data/navigation/docs.yml +2 -0
- data/_data/navigation/main.yml +22 -0
- data/_data/recipe_courses.yml +64 -0
- data/_data/series.yml +19 -0
- data/_data/theme-manifest.yml +387 -277
- data/_data/ui-text.yml +8 -0
- data/_includes/README.md +36 -3
- data/_includes/analytics/google-tag-manager-body.html +11 -2
- data/_includes/analytics/google-tag-manager-head.html +16 -6
- data/_includes/analytics/posthog.html +33 -4
- data/_includes/components/abc-letter.html +43 -0
- data/_includes/components/background-customizer.html +2 -2
- data/_includes/components/book-card.html +42 -0
- data/_includes/components/book-nav.html +80 -0
- data/_includes/components/book-plate.html +31 -0
- data/_includes/components/book-toc.html +47 -0
- data/_includes/components/bookshelf.html +68 -0
- data/_includes/components/card-grid.html +60 -0
- data/_includes/components/cookie-consent.html +4 -4
- data/_includes/components/data-card.html +95 -0
- data/_includes/components/halfmoon.html +5 -1
- data/_includes/components/info-section.html +8 -3
- data/_includes/components/language-toggle.html +168 -21
- data/_includes/components/page-feedback.html +65 -2
- data/_includes/components/page-views-init.html +55 -0
- data/_includes/components/page-views.html +33 -0
- data/_includes/components/recipe-card.html +67 -0
- data/_includes/components/recipe-duration.html +50 -0
- data/_includes/components/recipe-grams.html +58 -0
- data/_includes/components/recipe-index.html +96 -0
- data/_includes/components/recipe-ingredients.html +90 -0
- data/_includes/components/recipe-meta.html +96 -0
- data/_includes/components/recipe-nutrition.html +57 -0
- data/_includes/components/recipe-qty.html +73 -0
- data/_includes/components/recipe-ratio.html +151 -0
- data/_includes/components/recipe-scaler.html +73 -0
- data/_includes/components/recipe-steps.html +86 -0
- data/_includes/components/recipe-temp.html +45 -0
- data/_includes/components/search-modal.html +2 -2
- data/_includes/components/shortcuts-modal.html +3 -0
- data/_includes/components/theme-controls-bar.html +10 -2
- data/_includes/components/theme-customizer.html +8 -2
- data/_includes/components/theme-info.html +8 -1
- data/_includes/content/jsonld-software.html +22 -3
- data/_includes/content/seo.html +9 -3
- data/_includes/core/color-mode-init.html +13 -4
- data/_includes/core/favicon.html +46 -0
- data/_includes/core/footer.html +18 -16
- data/_includes/core/head.html +21 -0
- data/_includes/core/header.html +7 -4
- data/_includes/custom/body-end.html +18 -0
- data/_includes/custom/body-start.html +17 -0
- data/_includes/custom/footer.html +18 -0
- data/_includes/custom/head.html +18 -0
- data/_includes/navigation/local-graph.html +28 -2
- data/_includes/navigation/nav-tree.html +3 -3
- data/_includes/navigation/section-sidebar.html +93 -11
- data/_includes/navigation/sidebar-config.html +21 -0
- data/_includes/navigation/sidebar-left.html +2 -1
- data/_includes/navigation/sidebar-nav.html +4 -0
- data/_includes/navigation/sidebar-pagetree.html +150 -0
- data/_includes/navigation/sidebar-right.html +2 -1
- data/_includes/navigation/unified-drawer.html +8 -2
- data/_includes/obsidian/full-graph.html +165 -136
- data/_includes/setup/wizard.html +173 -86
- data/_includes/stats/stats-metrics.html +2 -2
- data/_layouts/404.html +260 -0
- data/_layouts/README.md +4 -0
- data/_layouts/admin.html +2 -2
- data/_layouts/article.html +5 -0
- data/_layouts/book-abc.html +106 -0
- data/_layouts/book-story.html +91 -0
- data/_layouts/book.html +113 -0
- data/_layouts/collection.html +17 -5
- data/_layouts/cookbook.html +88 -0
- data/_layouts/default.html +4 -4
- data/_layouts/home.html +5 -2
- data/_layouts/landing.html +9 -0
- data/_layouts/news.html +155 -31
- data/_layouts/recipe.html +274 -0
- data/_layouts/root.html +38 -5
- data/_layouts/section.html +96 -25
- data/_sass/components/_book.scss +423 -0
- data/_sass/components/_callout.scss +1 -1
- data/_sass/components/_footer.scss +37 -1
- data/_sass/components/_page-views.scss +36 -0
- data/_sass/components/_recipe.scss +506 -0
- data/_sass/components/_setup-wizard.scss +235 -0
- data/_sass/components/_ui-enhancements.scss +6 -6
- data/_sass/core/_navbar.scss +263 -15
- data/_sass/core/_obsidian.scss +286 -6
- data/_sass/layouts/_landing.scss +2 -2
- data/_sass/theme/_backgrounds.scss +21 -8
- data/_sass/tokens/_color.scss +6 -0
- data/_sass/tokens/_index.scss +2 -0
- data/_sass/tokens/_radius.scss +21 -0
- data/_sass/tokens/_typography.scss +4 -0
- data/_sass/utilities/_focus.scss +14 -0
- data/assets/css/main.scss +4 -0
- data/assets/js/auto-hide-nav.js +5 -1
- data/assets/js/halfmoon.js +26 -0
- data/assets/js/modules/navigation/navbar.js +55 -0
- data/assets/js/obsidian-graph.js +702 -264
- data/assets/js/obsidian-local-graph.js +161 -54
- data/assets/js/page-views.js +372 -0
- data/assets/js/recipe-scaler.js +501 -0
- data/assets/js/search-modal.js +14 -1
- data/assets/js/setup-wizard.js +322 -20
- data/scripts/README.md +29 -0
- data/scripts/bin/audit-consumer +39 -7
- data/scripts/bin/giscus-discussions +213 -14
- data/scripts/bin/manifest +66 -16
- data/scripts/bin/validate +5 -1
- data/scripts/ci/agent_review_result.py +164 -0
- data/scripts/ci/test_agent_review_result.py +172 -0
- data/scripts/design-system-check.rb +170 -0
- data/scripts/install/README.md +47 -6
- data/scripts/install/ai/client.sh +302 -93
- data/scripts/install/ai/prompts/spec.schema.json +1 -1
- data/scripts/install/ai/wizard.sh +10 -5
- data/scripts/install/apply.sh +7 -3
- data/scripts/install/cli.sh +54 -4
- data/scripts/install/config.sh +167 -0
- data/scripts/install/doctor.sh +38 -0
- data/scripts/install/plan.sh +10 -0
- data/scripts/install/spec.sh +15 -7
- data/scripts/install/template.sh +4 -0
- data/scripts/lib/audit.sh +42 -2
- data/scripts/lint-liquid-raw.rb +137 -0
- data/scripts/propagate.rb +277 -0
- data/scripts/test/lib/run_tests.sh +2 -1
- data/scripts/test/lib/test_agent_review_result.sh +27 -0
- data/scripts/translate.rb +161 -15
- metadata +56 -2
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Feature: ZER0-084
|
|
3
|
+
layout: default
|
|
4
|
+
---
|
|
5
|
+
{%- comment -%}
|
|
6
|
+
Layout: recipe.html
|
|
7
|
+
Extends: default.html
|
|
8
|
+
Purpose: One recipe — headnote, fact bar, serving scaler, ingredients,
|
|
9
|
+
equipment, method, ratio table, notes, and nutrition, all driven by
|
|
10
|
+
structured front matter rather than hand-written markdown.
|
|
11
|
+
Front matter required: layout, title
|
|
12
|
+
Front matter optional (the whole cookbook schema):
|
|
13
|
+
description, image/preview, course, cookbook, cuisine, difficulty,
|
|
14
|
+
yield: {amount, unit, singular}, times: {prep, cook, rest, rest_label,
|
|
15
|
+
total}, oven: {temp_f, temp_c, mode}, ratio, ratio_basis, equipment,
|
|
16
|
+
ingredients, steps, notes, nutrition, source, source_url, scaler (false to
|
|
17
|
+
hide the controls), units (false to hide the unit switch)
|
|
18
|
+
Notes: Every amount is rendered as authored, so the page is complete and
|
|
19
|
+
correct without JavaScript; assets/js/recipe-scaler.js only rewrites
|
|
20
|
+
the numbers once a reader asks it to. Machine-readable data is emitted
|
|
21
|
+
once, as JSON-LD — the itemprops inside the components are inert
|
|
22
|
+
annotations unless a consumer adds their own itemscope.
|
|
23
|
+
{%- endcomment -%}
|
|
24
|
+
|
|
25
|
+
{%- assign _rl_id = page.slug | default: "recipe" -%}
|
|
26
|
+
{%- assign _rl_course_meta = site.data.recipe_courses[page.course] -%}
|
|
27
|
+
{%- assign _rl_cookbook = site.recipes | where: "layout", "cookbook" | where: "cookbook", page.cookbook | first -%}
|
|
28
|
+
{%- assign _rl_image = page.image | default: page.preview -%}
|
|
29
|
+
{%- assign _rl_show_scaler = true -%}
|
|
30
|
+
{%- if page.scaler == false -%}{%- assign _rl_show_scaler = false -%}{%- endif -%}
|
|
31
|
+
|
|
32
|
+
<article class="recipe" data-recipe data-recipe-id="{{ _rl_id }}">
|
|
33
|
+
|
|
34
|
+
<!-- ================================ -->
|
|
35
|
+
<!-- HEADER -->
|
|
36
|
+
<!-- ================================ -->
|
|
37
|
+
<header class="recipe-header">
|
|
38
|
+
{%- if _rl_cookbook or _rl_course_meta -%}
|
|
39
|
+
<p class="recipe-eyebrow">
|
|
40
|
+
{%- if _rl_cookbook -%}
|
|
41
|
+
<a href="{{ _rl_cookbook.url | relative_url }}"><i class="bi bi-journal-bookmark me-1" aria-hidden="true"></i>{{ _rl_cookbook.title }}</a>
|
|
42
|
+
{%- endif -%}
|
|
43
|
+
{%- if _rl_cookbook and _rl_course_meta %} <span class="mx-1" aria-hidden="true">•</span> {% endif -%}
|
|
44
|
+
{%- if _rl_course_meta -%}{{ _rl_course_meta.title | escape }}{%- endif -%}
|
|
45
|
+
</p>
|
|
46
|
+
{%- endif -%}
|
|
47
|
+
|
|
48
|
+
<h1 id="page-title" class="recipe-title">{{ page.title }}</h1>
|
|
49
|
+
|
|
50
|
+
{%- if page.description -%}
|
|
51
|
+
<p class="lead recipe-description">{{ page.description }}</p>
|
|
52
|
+
{%- endif -%}
|
|
53
|
+
|
|
54
|
+
{%- if page.source or page.author -%}
|
|
55
|
+
<p class="recipe-byline text-body-secondary small">
|
|
56
|
+
{%- if page.author %}By {{ page.author }}{% endif -%}
|
|
57
|
+
{%- if page.author and page.source %} <span class="mx-1" aria-hidden="true">•</span> {% endif -%}
|
|
58
|
+
{%- if page.source -%}
|
|
59
|
+
Adapted from
|
|
60
|
+
{%- if page.source_url %} <a href="{{ page.source_url }}" rel="noopener">{{ page.source | escape }}</a>
|
|
61
|
+
{%- else %} {{ page.source | escape }}{% endif -%}
|
|
62
|
+
{%- endif -%}
|
|
63
|
+
</p>
|
|
64
|
+
{%- endif -%}
|
|
65
|
+
|
|
66
|
+
{%- if _rl_image -%}
|
|
67
|
+
<figure class="recipe-hero-figure">
|
|
68
|
+
{%- include components/preview-image.html src=_rl_image alt=page.title class="recipe-hero-image" loading="eager" -%}
|
|
69
|
+
{%- if page.image_caption -%}
|
|
70
|
+
<figcaption class="small text-body-secondary mt-2">{{ page.image_caption | escape }}</figcaption>
|
|
71
|
+
{%- endif -%}
|
|
72
|
+
</figure>
|
|
73
|
+
{%- endif -%}
|
|
74
|
+
</header>
|
|
75
|
+
|
|
76
|
+
<!-- ================================ -->
|
|
77
|
+
<!-- HEADNOTE (the markdown body) -->
|
|
78
|
+
<!-- ================================ -->
|
|
79
|
+
{%- assign _rl_body = content | strip -%}
|
|
80
|
+
{%- if _rl_body != "" -%}
|
|
81
|
+
<div class="recipe-headnote">
|
|
82
|
+
{{ content }}
|
|
83
|
+
</div>
|
|
84
|
+
{%- endif -%}
|
|
85
|
+
|
|
86
|
+
<!-- ================================ -->
|
|
87
|
+
<!-- FACT BAR + SCALER -->
|
|
88
|
+
<!-- ================================ -->
|
|
89
|
+
{%- include components/recipe-meta.html -%}
|
|
90
|
+
|
|
91
|
+
{%- if _rl_show_scaler -%}
|
|
92
|
+
{%- include components/recipe-scaler.html recipe_id=_rl_id units=page.units -%}
|
|
93
|
+
<noscript>
|
|
94
|
+
<p class="recipe-noscript small text-body-secondary">
|
|
95
|
+
<i class="bi bi-info-circle me-1" aria-hidden="true"></i>Serving scaling and unit conversion need JavaScript.
|
|
96
|
+
Every amount below is shown exactly as the recipe was written.
|
|
97
|
+
</p>
|
|
98
|
+
</noscript>
|
|
99
|
+
{%- endif -%}
|
|
100
|
+
|
|
101
|
+
<!-- ================================ -->
|
|
102
|
+
<!-- INGREDIENTS + METHOD -->
|
|
103
|
+
<!-- ================================ -->
|
|
104
|
+
<div class="row g-4 g-lg-5 recipe-body">
|
|
105
|
+
<div class="col-lg-5">
|
|
106
|
+
{%- include components/recipe-ingredients.html id_prefix=_rl_id -%}
|
|
107
|
+
|
|
108
|
+
{%- if page.equipment and page.equipment.size > 0 -%}
|
|
109
|
+
<section class="recipe-equipment">
|
|
110
|
+
<h2 class="recipe-section-heading">
|
|
111
|
+
<i class="bi bi-tools me-2" aria-hidden="true"></i>Equipment
|
|
112
|
+
</h2>
|
|
113
|
+
<ul class="recipe-equipment-list list-unstyled">
|
|
114
|
+
{%- for _rl_e in page.equipment -%}
|
|
115
|
+
<li><i class="bi bi-dot" aria-hidden="true"></i>{{ _rl_e | escape }}</li>
|
|
116
|
+
{%- endfor -%}
|
|
117
|
+
</ul>
|
|
118
|
+
</section>
|
|
119
|
+
{%- endif -%}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="col-lg-7">
|
|
123
|
+
{%- include components/recipe-steps.html -%}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<!-- ================================ -->
|
|
128
|
+
<!-- RATIO / FORMULA -->
|
|
129
|
+
<!-- ================================ -->
|
|
130
|
+
{%- include components/recipe-ratio.html -%}
|
|
131
|
+
|
|
132
|
+
<!-- ================================ -->
|
|
133
|
+
<!-- NOTES -->
|
|
134
|
+
<!-- ================================ -->
|
|
135
|
+
{%- if page.notes and page.notes.size > 0 -%}
|
|
136
|
+
<section class="recipe-notes">
|
|
137
|
+
<h2 class="recipe-section-heading">
|
|
138
|
+
<i class="bi bi-lightbulb me-2" aria-hidden="true"></i>Cook's notes
|
|
139
|
+
</h2>
|
|
140
|
+
<ul class="recipe-notes-list">
|
|
141
|
+
{%- for _rl_note in page.notes -%}
|
|
142
|
+
<li>{{ _rl_note | markdownify | remove: "<p>" | remove: "</p>" }}</li>
|
|
143
|
+
{%- endfor -%}
|
|
144
|
+
</ul>
|
|
145
|
+
</section>
|
|
146
|
+
{%- endif -%}
|
|
147
|
+
|
|
148
|
+
<!-- ================================ -->
|
|
149
|
+
<!-- NUTRITION -->
|
|
150
|
+
<!-- ================================ -->
|
|
151
|
+
{%- include components/recipe-nutrition.html -%}
|
|
152
|
+
|
|
153
|
+
<!-- ================================ -->
|
|
154
|
+
<!-- COMMENTS -->
|
|
155
|
+
<!-- ================================ -->
|
|
156
|
+
{%- if page.comments and site.giscus.enabled -%}
|
|
157
|
+
<section class="post-comments mt-5 pt-4 border-top" id="comments">
|
|
158
|
+
<h2 class="h4 mb-4"><i class="bi bi-chat-dots me-2" aria-hidden="true"></i>Comments</h2>
|
|
159
|
+
{%- include content/giscus.html -%}
|
|
160
|
+
</section>
|
|
161
|
+
{%- endif -%}
|
|
162
|
+
</article>
|
|
163
|
+
|
|
164
|
+
{%- comment -%}
|
|
165
|
+
================================================================
|
|
166
|
+
STRUCTURED DATA — schema.org/Recipe as JSON-LD.
|
|
167
|
+
Emitted once, here, so search engines see exactly one Recipe entity.
|
|
168
|
+
================================================================
|
|
169
|
+
{%- endcomment -%}
|
|
170
|
+
{%- assign _rl_prep = page.times.prep | default: 0 -%}
|
|
171
|
+
{%- assign _rl_cook = page.times.cook | default: 0 -%}
|
|
172
|
+
{%- assign _rl_rest = page.times.rest | default: 0 -%}
|
|
173
|
+
{%- if page.times.total -%}
|
|
174
|
+
{%- assign _rl_total = page.times.total -%}
|
|
175
|
+
{%- else -%}
|
|
176
|
+
{%- assign _rl_total = _rl_prep | plus: _rl_cook | plus: _rl_rest -%}
|
|
177
|
+
{%- endif -%}
|
|
178
|
+
{%- capture _rl_prep_iso -%}{%- include components/recipe-duration.html minutes=_rl_prep format="iso" -%}{%- endcapture -%}
|
|
179
|
+
{%- capture _rl_cook_iso -%}{%- include components/recipe-duration.html minutes=_rl_cook format="iso" -%}{%- endcapture -%}
|
|
180
|
+
{%- capture _rl_total_iso -%}{%- include components/recipe-duration.html minutes=_rl_total format="iso" -%}{%- endcapture -%}
|
|
181
|
+
{%- assign _rl_flat = "" | split: "" -%}
|
|
182
|
+
{%- for _rl_entry in page.ingredients -%}
|
|
183
|
+
{%- if _rl_entry.items -%}
|
|
184
|
+
{%- for _rl_i in _rl_entry.items -%}{%- assign _rl_flat = _rl_flat | push: _rl_i -%}{%- endfor -%}
|
|
185
|
+
{%- else -%}
|
|
186
|
+
{%- assign _rl_flat = _rl_flat | push: _rl_entry -%}
|
|
187
|
+
{%- endif -%}
|
|
188
|
+
{%- endfor -%}
|
|
189
|
+
{%- assign _rl_flat_steps = "" | split: "" -%}
|
|
190
|
+
{%- for _rl_entry in page.steps -%}
|
|
191
|
+
{%- if _rl_entry.items -%}
|
|
192
|
+
{%- for _rl_s in _rl_entry.items -%}{%- assign _rl_flat_steps = _rl_flat_steps | push: _rl_s -%}{%- endfor -%}
|
|
193
|
+
{%- else -%}
|
|
194
|
+
{%- assign _rl_flat_steps = _rl_flat_steps | push: _rl_entry -%}
|
|
195
|
+
{%- endif -%}
|
|
196
|
+
{%- endfor -%}
|
|
197
|
+
<script type="application/ld+json">
|
|
198
|
+
{
|
|
199
|
+
"@context": "https://schema.org",
|
|
200
|
+
"@type": "Recipe",
|
|
201
|
+
"name": {{ page.title | jsonify }},
|
|
202
|
+
"url": {{ page.url | absolute_url | jsonify }}
|
|
203
|
+
{%- if page.description %},
|
|
204
|
+
"description": {{ page.description | strip_html | strip | jsonify }}
|
|
205
|
+
{%- endif -%}
|
|
206
|
+
{%- if _rl_image %},
|
|
207
|
+
"image": {{ _rl_image | absolute_url | jsonify }}
|
|
208
|
+
{%- endif -%}
|
|
209
|
+
{%- if page.author %},
|
|
210
|
+
"author": { "@type": "Person", "name": {{ page.author | jsonify }} }
|
|
211
|
+
{%- endif -%}
|
|
212
|
+
{%- if page.date %},
|
|
213
|
+
"datePublished": {{ page.date | date_to_xmlschema | jsonify }}
|
|
214
|
+
{%- endif -%}
|
|
215
|
+
{%- assign _rl_prep_iso = _rl_prep_iso | strip -%}
|
|
216
|
+
{%- if _rl_prep_iso != "" %},
|
|
217
|
+
"prepTime": {{ _rl_prep_iso | jsonify }}
|
|
218
|
+
{%- endif -%}
|
|
219
|
+
{%- assign _rl_cook_iso = _rl_cook_iso | strip -%}
|
|
220
|
+
{%- if _rl_cook_iso != "" %},
|
|
221
|
+
"cookTime": {{ _rl_cook_iso | jsonify }}
|
|
222
|
+
{%- endif -%}
|
|
223
|
+
{%- assign _rl_total_iso = _rl_total_iso | strip -%}
|
|
224
|
+
{%- if _rl_total_iso != "" %},
|
|
225
|
+
"totalTime": {{ _rl_total_iso | jsonify }}
|
|
226
|
+
{%- endif -%}
|
|
227
|
+
{%- if page.yield.amount %},
|
|
228
|
+
{%- capture _rl_yield_text %}{{ page.yield.amount }} {{ page.yield.unit | default: "servings" }}{% endcapture %}
|
|
229
|
+
"recipeYield": {{ _rl_yield_text | strip | jsonify }}
|
|
230
|
+
{%- endif -%}
|
|
231
|
+
{%- if _rl_course_meta %},
|
|
232
|
+
"recipeCategory": {{ _rl_course_meta.title | jsonify }}
|
|
233
|
+
{%- endif -%}
|
|
234
|
+
{%- if page.cuisine %},
|
|
235
|
+
"recipeCuisine": {{ page.cuisine | jsonify }}
|
|
236
|
+
{%- endif -%}
|
|
237
|
+
{%- if page.tags and page.tags.size > 0 %},
|
|
238
|
+
"keywords": {{ page.tags | join: ", " | jsonify }}
|
|
239
|
+
{%- endif -%}
|
|
240
|
+
{%- if _rl_flat.size > 0 %},
|
|
241
|
+
"recipeIngredient": [
|
|
242
|
+
{%- assign _rl_first = true -%}
|
|
243
|
+
{%- for _rl_i in _rl_flat -%}
|
|
244
|
+
{%- if _rl_i.item -%}
|
|
245
|
+
{%- capture _rl_line %}{% if _rl_i.qty %}{{ _rl_i.qty }} {% endif %}{% if _rl_i.unit %}{{ _rl_i.unit }} {% endif %}{{ _rl_i.item }}{% if _rl_i.prep %}, {{ _rl_i.prep }}{% endif %}{% endcapture -%}
|
|
246
|
+
{%- unless _rl_first %},{% endunless %}{%- assign _rl_first = false -%}
|
|
247
|
+
{{ _rl_line | strip | jsonify }}
|
|
248
|
+
{%- endif -%}
|
|
249
|
+
{%- endfor -%}
|
|
250
|
+
]
|
|
251
|
+
{%- endif -%}
|
|
252
|
+
{%- if _rl_flat_steps.size > 0 %},
|
|
253
|
+
"recipeInstructions": [
|
|
254
|
+
{%- assign _rl_first = true -%}
|
|
255
|
+
{%- for _rl_s in _rl_flat_steps -%}
|
|
256
|
+
{%- if _rl_s.text -%}{%- assign _rl_stext = _rl_s.text -%}{%- else -%}{%- assign _rl_stext = _rl_s -%}{%- endif -%}
|
|
257
|
+
{%- assign _rl_stext = _rl_stext | strip_html | strip -%}
|
|
258
|
+
{%- if _rl_stext != "" -%}
|
|
259
|
+
{%- unless _rl_first %},{% endunless %}{%- assign _rl_first = false -%}
|
|
260
|
+
{ "@type": "HowToStep"{% if _rl_s.title %}, "name": {{ _rl_s.title | jsonify }}{% endif %}, "text": {{ _rl_stext | jsonify }} }
|
|
261
|
+
{%- endif -%}
|
|
262
|
+
{%- endfor -%}
|
|
263
|
+
]
|
|
264
|
+
{%- endif -%}
|
|
265
|
+
{%- if page.nutrition.calories %},
|
|
266
|
+
{%- capture _rl_serving_size %}1 {{ page.yield.singular | default: "serving" }}{% endcapture %}
|
|
267
|
+
"nutrition": {
|
|
268
|
+
"@type": "NutritionInformation",
|
|
269
|
+
"servingSize": {{ _rl_serving_size | strip | jsonify }},
|
|
270
|
+
"calories": {{ page.nutrition.calories | append: " calories" | jsonify }}
|
|
271
|
+
}
|
|
272
|
+
{%- endif %}
|
|
273
|
+
}
|
|
274
|
+
</script>
|
data/_layouts/root.html
CHANGED
|
@@ -46,12 +46,23 @@
|
|
|
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: '
|
|
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
|
+
<!-- Progressive-enhancement hook: upgrade <html class="no-js"> to "js".
|
|
52
|
+
MUST be the first child of <head>, inline, and neither `defer` nor
|
|
53
|
+
`async` — it has to run before the first stylesheet is fetched or
|
|
54
|
+
`html.js` rules would flash. It cannot live in core/head.html:
|
|
55
|
+
Google Tag Manager is emitted above everything else there, so
|
|
56
|
+
"first in that include" is not "first in <head>".
|
|
57
|
+
classList.replace() is a no-op when `no-js` is absent, so this is
|
|
58
|
+
safe unconditionally. The served HTML keeps `no-js` on <html> so
|
|
59
|
+
the JS-disabled branch stays correct. -->
|
|
60
|
+
<script>document.documentElement.classList.replace("no-js", "js");</script>
|
|
61
|
+
|
|
51
62
|
<!-- =============================================== -->
|
|
52
63
|
<!-- HEAD SECTION: Meta tags, styles, and SEO setup -->
|
|
53
64
|
<!-- =============================================== -->
|
|
54
|
-
|
|
65
|
+
|
|
55
66
|
<!-- Core site metadata, stylesheets, and external dependencies -->
|
|
56
67
|
{% include core/head.html %}
|
|
57
68
|
|
|
@@ -67,7 +78,11 @@
|
|
|
67
78
|
<!-- ============================================== -->
|
|
68
79
|
<!-- BODY SECTION: Site structure and content area -->
|
|
69
80
|
<!-- ============================================== -->
|
|
70
|
-
|
|
81
|
+
|
|
82
|
+
<!-- Consumer hook: first thing inside <body> (e.g. GTM noscript).
|
|
83
|
+
Empty stub shipped by the theme; shadow _includes/custom/body-start.html to use. -->
|
|
84
|
+
{% include custom/body-start.html %}
|
|
85
|
+
|
|
71
86
|
<!-- Inline SVG symbols for consistent iconography throughout the site -->
|
|
72
87
|
<!-- Page-invariant: cached once per build via jekyll-include-cache. -->
|
|
73
88
|
{% include_cached components/svg.html %}
|
|
@@ -103,8 +118,16 @@
|
|
|
103
118
|
This is the ONE <main> landmark per page — child layouts (default/section/
|
|
104
119
|
news) must use non-landmark wrappers (<div>) inside it (issue #299) — so
|
|
105
120
|
assistive tech, search engines, and AI content extractors can isolate the
|
|
106
|
-
primary content from the surrounding header/nav/footer chrome.
|
|
107
|
-
|
|
121
|
+
primary content from the surrounding header/nav/footer chrome.
|
|
122
|
+
|
|
123
|
+
tabindex="-1" is REQUIRED, not decorative: <main> is not natively
|
|
124
|
+
focusable, and Safari/WebKit (historically Firefox too) only scroll on a
|
|
125
|
+
fragment jump to a non-focusable container — keyboard focus stays put, so
|
|
126
|
+
the next Tab returns the user to the header nav they just skipped
|
|
127
|
+
(WCAG 2.4.1 Bypass Blocks). -1 keeps it out of the sequential Tab order
|
|
128
|
+
while allowing programmatic focus. The focus ring it would otherwise
|
|
129
|
+
paint is suppressed in _sass/utilities/_focus.scss. -->
|
|
130
|
+
<main id="main-content" tabindex="-1">
|
|
108
131
|
<!-- Machine-translation disclosure — renders only on generated
|
|
109
132
|
translations (page.machine_translated); no-op elsewhere. -->
|
|
110
133
|
{%- include components/translation-notice.html -%}
|
|
@@ -125,6 +148,12 @@
|
|
|
125
148
|
{%- include_cached core/footer.html lang=page.lang -%}
|
|
126
149
|
{%- include core/footer-fabs.html -%}
|
|
127
150
|
|
|
151
|
+
<!-- Consumer hook: after the theme footer, before the consent banner.
|
|
152
|
+
Empty stub shipped by the theme; shadow _includes/custom/footer.html to use.
|
|
153
|
+
Deliberately OUTSIDE the cached footer include so page-dependent
|
|
154
|
+
consumer markup renders per page. -->
|
|
155
|
+
{%- include custom/footer.html -%}
|
|
156
|
+
|
|
128
157
|
<!-- Privacy-compliant cookie consent banner — page-invariant. -->
|
|
129
158
|
{%- include_cached components/cookie-consent.html -%}
|
|
130
159
|
|
|
@@ -148,5 +177,9 @@
|
|
|
148
177
|
|
|
149
178
|
<!-- Analytics Integration -->
|
|
150
179
|
{%- include analytics/posthog.html -%}
|
|
180
|
+
|
|
181
|
+
<!-- Consumer hook: last thing before </body> (site scripts, widgets).
|
|
182
|
+
Empty stub shipped by the theme; shadow _includes/custom/body-end.html to use. -->
|
|
183
|
+
{%- include custom/body-end.html -%}
|
|
151
184
|
</body>
|
|
152
185
|
</html>
|
data/_layouts/section.html
CHANGED
|
@@ -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="{{
|
|
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
|
|
154
|
+
{% for sec_name in news_sections %}
|
|
127
155
|
{% assign is_active = false %}
|
|
128
|
-
{% if
|
|
156
|
+
{% if sec_name == page.category or sec_name == page.title %}
|
|
129
157
|
{% assign is_active = true %}
|
|
130
158
|
{% endif %}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
{%
|
|
134
|
-
|
|
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>
|
|
@@ -148,8 +182,9 @@ layout: root
|
|
|
148
182
|
{% include navigation/section-sidebar.html
|
|
149
183
|
section_posts=section_posts
|
|
150
184
|
sub_categories=sub_categories
|
|
151
|
-
page_title=page.title
|
|
152
|
-
sidebar_id="sectionSidebar"
|
|
185
|
+
page_title=page.title
|
|
186
|
+
sidebar_id="sectionSidebar"
|
|
187
|
+
section_style=section_style
|
|
153
188
|
%}
|
|
154
189
|
</div>
|
|
155
190
|
|
|
@@ -255,9 +290,11 @@ layout: root
|
|
|
255
290
|
<h3 class="h4 mb-0">
|
|
256
291
|
<i class="bi bi-tag text-primary me-2"></i>{{ subcat | capitalize }}
|
|
257
292
|
</h3>
|
|
258
|
-
|
|
293
|
+
{% if _tags_linkable %}
|
|
294
|
+
<a href="{{ tags_base | relative_url }}#{{ subcat | slugify }}" class="btn btn-outline-secondary btn-sm">
|
|
259
295
|
View All <i class="bi bi-arrow-right ms-1"></i>
|
|
260
296
|
</a>
|
|
297
|
+
{% endif %}
|
|
261
298
|
</div>
|
|
262
299
|
|
|
263
300
|
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4">
|
|
@@ -383,7 +420,17 @@ layout: root
|
|
|
383
420
|
<section id="all-posts" class="grid-section mb-5 scroll-mt-5">
|
|
384
421
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
|
385
422
|
{% for gpost in section_posts %}
|
|
386
|
-
|
|
423
|
+
{%- comment -%}
|
|
424
|
+
Slugify each tag separately and separate with a space. Joining
|
|
425
|
+
first and slugifying the result turned the separators into
|
|
426
|
+
hyphens, making a tag boundary indistinguishable from a hyphen
|
|
427
|
+
inside a slug ("edge-ai"), which broke filtering.
|
|
428
|
+
The trailing space is deliberate: guarding the separator with a
|
|
429
|
+
last-iteration conditional gives that conditional a
|
|
430
|
+
whitespace-only body, which Liquid treats as a blank block and
|
|
431
|
+
renders as nothing — silently gluing the tags together.
|
|
432
|
+
{%- endcomment -%}
|
|
433
|
+
<div class="col" data-tags="{% for t in gpost.tags %}{{ t | slugify }} {% endfor %}">
|
|
387
434
|
<div class="card h-100 border-0 shadow-sm hover-lift">
|
|
388
435
|
<div class="position-relative">
|
|
389
436
|
{% if gpost.breaking %}
|
|
@@ -446,7 +493,8 @@ layout: root
|
|
|
446
493
|
|
|
447
494
|
<section id="all-posts" class="list-section mb-5 scroll-mt-5">
|
|
448
495
|
{% for lpost in section_posts %}
|
|
449
|
-
|
|
496
|
+
{%- comment -%} Per-tag slugify + space separator — see the grid branch above. {%- endcomment -%}
|
|
497
|
+
<article class="card border-0 shadow-sm mb-3 hover-lift" data-tags="{% for t in lpost.tags %}{{ t | slugify }} {% endfor %}">
|
|
450
498
|
<div class="row g-0">
|
|
451
499
|
<div class="col-md-3">
|
|
452
500
|
<a href="{{ lpost.url | relative_url }}">
|
|
@@ -496,7 +544,7 @@ layout: root
|
|
|
496
544
|
<div class="text-center py-5">
|
|
497
545
|
<i class="bi bi-inbox fs-1 text-muted"></i>
|
|
498
546
|
<p class="text-muted mt-3">No articles in this section yet.</p>
|
|
499
|
-
<a href="{{
|
|
547
|
+
<a href="{{ news_index_url | relative_url }}" class="btn btn-outline-primary">
|
|
500
548
|
Browse all news
|
|
501
549
|
</a>
|
|
502
550
|
</div>
|
|
@@ -510,15 +558,21 @@ layout: root
|
|
|
510
558
|
<i class="bi bi-compass me-2"></i>Explore Other Sections
|
|
511
559
|
</h3>
|
|
512
560
|
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3">
|
|
513
|
-
{% for
|
|
514
|
-
{% unless
|
|
515
|
-
{% assign other_section_posts = all_site_posts | where_exp: "post", "post.categories contains
|
|
561
|
+
{% for sec_name in news_sections %}
|
|
562
|
+
{% unless sec_name == page.category or sec_name == page.title %}
|
|
563
|
+
{% assign other_section_posts = all_site_posts | where_exp: "post", "post.categories contains sec_name" %}
|
|
564
|
+
{% assign _sec_nav = site.data.navigation.posts | where: "title", sec_name | first %}
|
|
565
|
+
{% if _sec_nav %}
|
|
566
|
+
{% assign _sec_url = _sec_nav.url | replace: '/posts/', news_base %}
|
|
567
|
+
{% else %}
|
|
568
|
+
{% capture _sec_url %}{{ news_base }}{{ sec_name | slugify }}/{% endcapture %}
|
|
569
|
+
{% endif %}
|
|
516
570
|
<div class="col">
|
|
517
|
-
<a href="{{
|
|
571
|
+
<a href="{{ _sec_url | relative_url }}"
|
|
518
572
|
class="card text-center h-100 text-decoration-none border-0 shadow-sm hover-lift">
|
|
519
573
|
<div class="card-body py-3">
|
|
520
|
-
<i class="bi {{
|
|
521
|
-
<h6 class="card-title mb-1 small">{{
|
|
574
|
+
<i class="bi {{ _sec_nav.icon | default: 'bi-folder' }} fs-4 text-primary mb-2 d-block"></i>
|
|
575
|
+
<h6 class="card-title mb-1 small">{{ sec_name }}</h6>
|
|
522
576
|
<small class="text-muted">{{ other_section_posts.size }} articles</small>
|
|
523
577
|
</div>
|
|
524
578
|
</a>
|
|
@@ -555,17 +609,34 @@ layout: root
|
|
|
555
609
|
btn.addEventListener('click', function() {
|
|
556
610
|
const filter = this.dataset.filter;
|
|
557
611
|
|
|
558
|
-
// Update active
|
|
612
|
+
// Update active state across every filter control on the page — the
|
|
613
|
+
// topic pills here and the section sidebar's topic buttons both carry
|
|
614
|
+
// [data-filter], so they stay in sync. Only pills get the btn-* colour
|
|
615
|
+
// swap; the sidebar's controls are .nav-link and style themselves off
|
|
616
|
+
// .active alone.
|
|
559
617
|
document.querySelectorAll('[data-filter]').forEach(b => {
|
|
560
|
-
b.classList.remove('
|
|
561
|
-
b.classList.
|
|
618
|
+
b.classList.remove('active');
|
|
619
|
+
if (b.classList.contains('btn')) {
|
|
620
|
+
b.classList.remove('btn-primary');
|
|
621
|
+
b.classList.add('btn-outline-secondary');
|
|
622
|
+
}
|
|
562
623
|
});
|
|
563
|
-
this.classList.
|
|
564
|
-
this.classList.
|
|
624
|
+
this.classList.add('active');
|
|
625
|
+
if (this.classList.contains('btn')) {
|
|
626
|
+
this.classList.remove('btn-outline-secondary');
|
|
627
|
+
this.classList.add('btn-primary');
|
|
628
|
+
}
|
|
565
629
|
|
|
566
630
|
// Filter cards
|
|
631
|
+
// Match whole tags, not substrings. `data-tags` is a space-separated list
|
|
632
|
+
// of individually-slugified tags, so a slug never spans a separator and
|
|
633
|
+
// filtering by "ai" no longer also matches a post tagged "edge-ai".
|
|
634
|
+
// (This used to disagree with the sidebar's own count: the badge is built
|
|
635
|
+
// in Liquid with `post.tags contains subcat` — exact membership — so a
|
|
636
|
+
// topic showing 5 posts could reveal 6 cards when clicked.)
|
|
567
637
|
document.querySelectorAll('[data-tags]').forEach(card => {
|
|
568
|
-
|
|
638
|
+
const cardTags = (card.dataset.tags || '').split(' ').filter(Boolean);
|
|
639
|
+
if (filter === 'all' || cardTags.indexOf(filter) !== -1) {
|
|
569
640
|
card.style.display = '';
|
|
570
641
|
} else {
|
|
571
642
|
card.style.display = 'none';
|