jekyll-theme-zer0 1.28.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 +430 -13
- data/_data/backlog.yml +356 -2
- data/_data/consumers.yml +109 -4
- data/_data/features.yml +157 -0
- data/_data/i18n/fr.yml +12 -7
- data/_data/i18n/manifest.yml +39 -12
- data/_data/ingredient_densities.yml +122 -0
- data/_data/navigation/main.yml +16 -0
- data/_data/recipe_courses.yml +64 -0
- data/_data/theme-manifest.yml +68 -40
- data/_data/ui-text.yml +8 -0
- data/_includes/README.md +15 -1
- data/_includes/components/background-customizer.html +2 -2
- data/_includes/components/cookie-consent.html +4 -4
- data/_includes/components/info-section.html +8 -3
- data/_includes/components/language-toggle.html +168 -21
- 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-info.html +8 -1
- data/_includes/content/jsonld-software.html +22 -3
- data/_includes/core/footer.html +18 -16
- data/_includes/core/head.html +12 -0
- data/_includes/core/header.html +7 -4
- data/_includes/navigation/section-sidebar.html +93 -11
- data/_includes/navigation/sidebar-left.html +2 -1
- data/_includes/navigation/sidebar-right.html +2 -1
- data/_includes/setup/wizard.html +173 -86
- data/_includes/stats/stats-metrics.html +2 -2
- data/_layouts/README.md +2 -0
- data/_layouts/admin.html +2 -2
- data/_layouts/article.html +5 -0
- data/_layouts/cookbook.html +88 -0
- data/_layouts/default.html +4 -4
- data/_layouts/recipe.html +274 -0
- data/_layouts/root.html +22 -3
- data/_layouts/section.html +39 -10
- 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 +250 -15
- data/_sass/layouts/_landing.scss +2 -2
- 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 +3 -0
- data/assets/js/modules/navigation/navbar.js +55 -0
- data/assets/js/page-views.js +372 -0
- data/assets/js/recipe-scaler.js +501 -0
- data/assets/js/search-modal.js +10 -0
- 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 +35 -12
- 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/lib/audit.sh +42 -2
- data/scripts/lint-liquid-raw.rb +137 -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 +71 -15
- metadata +31 -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
|
@@ -48,10 +48,21 @@
|
|
|
48
48
|
{%- endif -%}
|
|
49
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
|
|
|
@@ -107,8 +118,16 @@
|
|
|
107
118
|
This is the ONE <main> landmark per page — child layouts (default/section/
|
|
108
119
|
news) must use non-landmark wrappers (<div>) inside it (issue #299) — so
|
|
109
120
|
assistive tech, search engines, and AI content extractors can isolate the
|
|
110
|
-
primary content from the surrounding header/nav/footer chrome.
|
|
111
|
-
|
|
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">
|
|
112
131
|
<!-- Machine-translation disclosure — renders only on generated
|
|
113
132
|
translations (page.machine_translated); no-op elsewhere. -->
|
|
114
133
|
{%- include components/translation-notice.html -%}
|
data/_layouts/section.html
CHANGED
|
@@ -182,8 +182,9 @@ layout: root
|
|
|
182
182
|
{% include navigation/section-sidebar.html
|
|
183
183
|
section_posts=section_posts
|
|
184
184
|
sub_categories=sub_categories
|
|
185
|
-
page_title=page.title
|
|
186
|
-
sidebar_id="sectionSidebar"
|
|
185
|
+
page_title=page.title
|
|
186
|
+
sidebar_id="sectionSidebar"
|
|
187
|
+
section_style=section_style
|
|
187
188
|
%}
|
|
188
189
|
</div>
|
|
189
190
|
|
|
@@ -419,7 +420,17 @@ layout: root
|
|
|
419
420
|
<section id="all-posts" class="grid-section mb-5 scroll-mt-5">
|
|
420
421
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
|
421
422
|
{% for gpost in section_posts %}
|
|
422
|
-
|
|
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 %}">
|
|
423
434
|
<div class="card h-100 border-0 shadow-sm hover-lift">
|
|
424
435
|
<div class="position-relative">
|
|
425
436
|
{% if gpost.breaking %}
|
|
@@ -482,7 +493,8 @@ layout: root
|
|
|
482
493
|
|
|
483
494
|
<section id="all-posts" class="list-section mb-5 scroll-mt-5">
|
|
484
495
|
{% for lpost in section_posts %}
|
|
485
|
-
|
|
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 %}">
|
|
486
498
|
<div class="row g-0">
|
|
487
499
|
<div class="col-md-3">
|
|
488
500
|
<a href="{{ lpost.url | relative_url }}">
|
|
@@ -597,17 +609,34 @@ layout: root
|
|
|
597
609
|
btn.addEventListener('click', function() {
|
|
598
610
|
const filter = this.dataset.filter;
|
|
599
611
|
|
|
600
|
-
// 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.
|
|
601
617
|
document.querySelectorAll('[data-filter]').forEach(b => {
|
|
602
|
-
b.classList.remove('
|
|
603
|
-
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
|
+
}
|
|
604
623
|
});
|
|
605
|
-
this.classList.
|
|
606
|
-
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
|
+
}
|
|
607
629
|
|
|
608
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.)
|
|
609
637
|
document.querySelectorAll('[data-tags]').forEach(card => {
|
|
610
|
-
|
|
638
|
+
const cardTags = (card.dataset.tags || '').split(' ').filter(Boolean);
|
|
639
|
+
if (filter === 'all' || cardTags.indexOf(filter) !== -1) {
|
|
611
640
|
card.style.display = '';
|
|
612
641
|
} else {
|
|
613
642
|
card.style.display = 'none';
|
|
@@ -64,6 +64,12 @@
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Element-agnostic: applied to <a> (external credits), <span> (credits with
|
|
68
|
+
// no url) and <button> (the in-page "Info" dialog trigger — it opens the
|
|
69
|
+
// #info-section offcanvas rather than navigating, so it is a button, not a
|
|
70
|
+
// link; WCAG 4.1.2). Reboot already inherits margin/font for <button>, but
|
|
71
|
+
// not background, border or colour — reset those and restate the colours
|
|
72
|
+
// `.bd-footer a` supplies so all three elements render identically.
|
|
67
73
|
.powered-by-link {
|
|
68
74
|
display: inline-flex;
|
|
69
75
|
align-items: center;
|
|
@@ -75,10 +81,18 @@
|
|
|
75
81
|
text-decoration: none;
|
|
76
82
|
border-radius: var(--bs-border-radius-sm, 0.25rem);
|
|
77
83
|
white-space: nowrap;
|
|
84
|
+
// Reboot sets `-webkit-appearance: button`, which the unprefixed property
|
|
85
|
+
// does not cancel on older WebKit — reset both.
|
|
86
|
+
-webkit-appearance: none;
|
|
87
|
+
appearance: none;
|
|
88
|
+
background: none;
|
|
89
|
+
border: 0;
|
|
90
|
+
color: var(--bs-body-color);
|
|
78
91
|
|
|
79
92
|
&:hover,
|
|
80
93
|
&:focus-visible {
|
|
81
94
|
text-decoration: none;
|
|
95
|
+
color: var(--bs-link-hover-color);
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
98
|
|
|
@@ -132,6 +146,9 @@
|
|
|
132
146
|
// Block padding grows the tap target without changing the visual text size;
|
|
133
147
|
// icon links get an explicit minimum box. (`d-block` links — e.g. the
|
|
134
148
|
// truncated Latest Posts titles — keep their display but gain the padding.)
|
|
149
|
+
// `> button` is matched alongside `> a` because the policy row's "Cookie
|
|
150
|
+
// Preferences" control is a <button> (it opens a modal rather than
|
|
151
|
+
// navigating) and must keep the same 1.75rem tap target as its siblings.
|
|
135
152
|
.footer-dark-block {
|
|
136
153
|
ul.list-unstyled li > a,
|
|
137
154
|
p > a {
|
|
@@ -139,7 +156,8 @@
|
|
|
139
156
|
padding-block: 0.25rem;
|
|
140
157
|
}
|
|
141
158
|
|
|
142
|
-
ul.list-inline li > a
|
|
159
|
+
ul.list-inline li > a,
|
|
160
|
+
ul.list-inline li > button {
|
|
143
161
|
display: inline-flex;
|
|
144
162
|
align-items: center;
|
|
145
163
|
justify-content: center;
|
|
@@ -171,6 +189,24 @@
|
|
|
171
189
|
}
|
|
172
190
|
}
|
|
173
191
|
|
|
192
|
+
// A <button> that must sit in running text exactly like the <a> beside it —
|
|
193
|
+
// used by the "Cookie Preferences" control, which opens #cookieSettingsModal
|
|
194
|
+
// rather than navigating (WCAG 4.1.2). Reboot inherits margin/font for
|
|
195
|
+
// <button> but leaves the UA background, border and padding in place, so
|
|
196
|
+
// strip those; colour comes from the `.text-light` utility as before.
|
|
197
|
+
.footer-inline-button {
|
|
198
|
+
// Reboot sets `-webkit-appearance: button`, which the unprefixed property
|
|
199
|
+
// does not cancel on older WebKit — reset both.
|
|
200
|
+
-webkit-appearance: none;
|
|
201
|
+
appearance: none;
|
|
202
|
+
background: none;
|
|
203
|
+
border: 0;
|
|
204
|
+
padding: 0;
|
|
205
|
+
vertical-align: baseline;
|
|
206
|
+
text-align: inherit;
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
}
|
|
209
|
+
|
|
174
210
|
.footer-policy-links {
|
|
175
211
|
@media (max-width: 767.98px) {
|
|
176
212
|
font-size: 0.8125rem;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Feature: ZER0-083
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// Page-view counter badge
|
|
4
|
+
// ----------------------------------------------------------------------------
|
|
5
|
+
// Markup: _includes/components/page-views.html
|
|
6
|
+
// Behaviour: assets/js/page-views.js
|
|
7
|
+
// Config: _config.yml → page_views:
|
|
8
|
+
// Theming: inherits the surrounding meta-row color (.post-meta is
|
|
9
|
+
// .text-muted), so it follows [data-bs-theme] light/dark for free.
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
.page-views {
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: baseline;
|
|
15
|
+
gap: .25rem;
|
|
16
|
+
white-space: nowrap;
|
|
17
|
+
|
|
18
|
+
// The badge is server-rendered hidden and revealed by JS once a count is
|
|
19
|
+
// known. `!important` because a utility class on the include's `class`
|
|
20
|
+
// param (e.g. `d-inline-flex`) would otherwise win over [hidden].
|
|
21
|
+
&[hidden] {
|
|
22
|
+
display: none !important;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Digits change as the count updates — tabular figures keep the meta row
|
|
26
|
+
// from reflowing when 9 becomes 10.
|
|
27
|
+
&__count {
|
|
28
|
+
font-variant-numeric: tabular-nums;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Leading separator for meta rows; hides with the badge because it lives
|
|
32
|
+
// inside it (see the include's `separator` param).
|
|
33
|
+
&__sep {
|
|
34
|
+
display: inline;
|
|
35
|
+
}
|
|
36
|
+
}
|