jekyll-theme-zer0 1.26.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +558 -5
  3. data/README.md +10 -27
  4. data/_data/authors.yml +4 -3
  5. data/_data/backlog.yml +28 -0
  6. data/_data/consumers.yml +147 -0
  7. data/_data/features.yml +142 -23
  8. data/_data/i18n/fr.yml +98 -0
  9. data/_data/i18n/languages.yml +36 -0
  10. data/_data/i18n/manifest.yml +1502 -0
  11. data/_data/navigation/docs.yml +2 -0
  12. data/_data/navigation/main.yml +6 -0
  13. data/_data/series.yml +19 -0
  14. data/_data/theme-manifest.yml +348 -268
  15. data/_data/ui-text.yml +36 -246
  16. data/_includes/README.md +25 -2
  17. data/_includes/analytics/google-tag-manager-body.html +11 -2
  18. data/_includes/analytics/google-tag-manager-head.html +16 -6
  19. data/_includes/analytics/posthog.html +33 -4
  20. data/_includes/components/abc-letter.html +43 -0
  21. data/_includes/components/author-avatar-url.html +4 -2
  22. data/_includes/components/book-card.html +42 -0
  23. data/_includes/components/book-nav.html +80 -0
  24. data/_includes/components/book-plate.html +31 -0
  25. data/_includes/components/book-toc.html +47 -0
  26. data/_includes/components/bookshelf.html +68 -0
  27. data/_includes/components/card-grid.html +60 -0
  28. data/_includes/components/data-card.html +95 -0
  29. data/_includes/components/env-switcher.html +3 -1
  30. data/_includes/components/halfmoon.html +5 -1
  31. data/_includes/components/language-toggle.html +81 -0
  32. data/_includes/components/page-feedback.html +65 -2
  33. data/_includes/components/search-modal.html +2 -2
  34. data/_includes/components/shortcuts-modal.html +1 -1
  35. data/_includes/components/theme-controls-bar.html +10 -2
  36. data/_includes/components/theme-customizer.html +8 -2
  37. data/_includes/components/translation-notice.html +27 -0
  38. data/_includes/content/intro.html +16 -15
  39. data/_includes/content/seo.html +9 -3
  40. data/_includes/core/color-mode-init.html +13 -4
  41. data/_includes/core/favicon.html +46 -0
  42. data/_includes/core/footer.html +9 -4
  43. data/_includes/core/head.html +18 -0
  44. data/_includes/core/header.html +9 -4
  45. data/_includes/core/hreflang.html +33 -0
  46. data/_includes/core/i18n.html +36 -0
  47. data/_includes/custom/body-end.html +18 -0
  48. data/_includes/custom/body-start.html +17 -0
  49. data/_includes/custom/footer.html +18 -0
  50. data/_includes/custom/head.html +18 -0
  51. data/_includes/navigation/breadcrumbs.html +1 -1
  52. data/_includes/navigation/local-graph.html +28 -2
  53. data/_includes/navigation/nav-tree.html +3 -3
  54. data/_includes/navigation/navbar.html +5 -4
  55. data/_includes/navigation/sidebar-config.html +21 -0
  56. data/_includes/navigation/sidebar-nav.html +4 -0
  57. data/_includes/navigation/sidebar-pagetree.html +150 -0
  58. data/_includes/navigation/sidebar-right.html +3 -2
  59. data/_includes/navigation/unified-drawer.html +9 -3
  60. data/_includes/obsidian/full-graph.html +165 -136
  61. data/_layouts/404.html +260 -0
  62. data/_layouts/README.md +2 -0
  63. data/_layouts/article.html +7 -1
  64. data/_layouts/book-abc.html +106 -0
  65. data/_layouts/book-story.html +91 -0
  66. data/_layouts/book.html +113 -0
  67. data/_layouts/collection.html +17 -5
  68. data/_layouts/default.html +5 -3
  69. data/_layouts/home.html +5 -2
  70. data/_layouts/landing.html +9 -0
  71. data/_layouts/news.html +159 -33
  72. data/_layouts/root.html +29 -9
  73. data/_layouts/section.html +61 -17
  74. data/_sass/components/_book.scss +423 -0
  75. data/_sass/core/_navbar.scss +13 -0
  76. data/_sass/core/_obsidian.scss +295 -7
  77. data/_sass/layouts/_navbar-extras.scss +6 -1
  78. data/_sass/theme/_backgrounds.scss +21 -8
  79. data/assets/css/main.scss +1 -0
  80. data/assets/js/auto-hide-nav.js +5 -1
  81. data/assets/js/halfmoon.js +26 -0
  82. data/assets/js/obsidian-graph.js +707 -265
  83. data/assets/js/obsidian-local-graph.js +161 -54
  84. data/assets/js/search-modal.js +4 -1
  85. data/scripts/README.md +20 -26
  86. data/scripts/bin/audit-consumer +1 -1
  87. data/scripts/bin/manifest +31 -5
  88. data/scripts/bin/sync-plugins +0 -1
  89. data/scripts/bin/validate +5 -1
  90. data/scripts/dev/rasterize-svg.js +65 -0
  91. data/scripts/features/generate-preview-images +49 -1390
  92. data/scripts/features/install-preview-generator +55 -33
  93. data/scripts/install/README.md +55 -25
  94. data/scripts/install/ai/client.sh +302 -93
  95. data/scripts/install/ai/prompts/spec.schema.json +1 -1
  96. data/scripts/install/ai/prompts/wizard.system.md +8 -17
  97. data/scripts/install/ai/wizard.sh +10 -5
  98. data/scripts/install/apply.sh +7 -3
  99. data/scripts/install/cli.sh +54 -4
  100. data/scripts/install/config.sh +167 -0
  101. data/scripts/install/doctor.sh +38 -0
  102. data/scripts/install/plan.sh +10 -0
  103. data/scripts/install/spec.sh +15 -7
  104. data/scripts/install/template.sh +4 -0
  105. data/scripts/lib/README.md +1 -5
  106. data/scripts/lib/install/deploy/README.md +3 -9
  107. data/scripts/lib/preview_generator.py +2261 -1341
  108. data/scripts/propagate.rb +277 -0
  109. data/scripts/translate.rb +1204 -0
  110. metadata +34 -3
  111. data/_plugins/preview_image_generator.rb +0 -351
data/_data/ui-text.yml CHANGED
@@ -1,6 +1,16 @@
1
- # User interface text and labels
2
- # Used by: Various includes and layouts throughout the theme
3
- # Purpose: Multilingual UI strings for internationalization (i18n)
1
+ # User interface text and labels — ENGLISH SOURCE OF TRUTH
2
+ # Used by: _includes/core/i18n.html (and through it, layouts/includes)
3
+ #
4
+ # Multilingual model (ZER0-078): this file holds ONLY the English strings.
5
+ # Alternate languages are NOT hand-maintained here anymore — they are
6
+ # machine-generated into _data/i18n/<lang>.yml by scripts/translate.rb
7
+ # (Claude Code OAuth) via .github/workflows/translate.yml. To change a
8
+ # translated label, edit the English string here and let the translate
9
+ # workflow regenerate the alternates. Languages are configured under
10
+ # `translation.languages` in _config.yml.
11
+ #
12
+ # (The former hand-written es/fr/de/ar blocks were removed in favor of the
13
+ # generated pipeline — see docs/systems/multilingual-translation.md.)
4
14
 
5
15
  # English (default)
6
16
  # -----------------
@@ -8,6 +18,7 @@ en: &DEFAULT_EN
8
18
  skip_links : "Skip links"
9
19
  skip_primary_nav : "Skip to primary navigation"
10
20
  skip_content : "Skip to content"
21
+ skip_main_content : "Skip to main content"
11
22
  skip_footer : "Skip to footer"
12
23
  page : "Page"
13
24
  pagination_previous : "Previous"
@@ -79,7 +90,28 @@ en: &DEFAULT_EN
79
90
  shortcuts_toggle_sidebar : "Toggle sidebar"
80
91
  shortcuts_toggle_toc : "Toggle table of contents"
81
92
  shortcuts_show_help : "Show this help"
82
-
93
+ # Header / navbar chrome (ZER0-078)
94
+ home_label : "Home"
95
+ main_nav_label : "Main Navigation"
96
+ search_button_label : "Search"
97
+ settings_button_label : "Settings"
98
+ toc_title : "Table of Contents"
99
+ # Article intro / metadata chrome (ZER0-078)
100
+ published_label : "Published"
101
+ updated_label : "Updated"
102
+ reading_time_label : "Estimated reading time:"
103
+ reading_time_minute : "minute"
104
+ share_label : "Share"
105
+ copy_link_label : "Copy Link"
106
+ improve_label : "Improve"
107
+ edit_on_github : "Edit on GitHub"
108
+ # Language toggle & machine translation (ZER0-078)
109
+ lang_toggle_label : "Language"
110
+ lang_toggle_aria : "Change language"
111
+ lang_not_available : "Not yet translated"
112
+ machine_translation_notice : "This page was translated automatically from the English original."
113
+ machine_translation_view_source : "View the original"
114
+
83
115
  en-US:
84
116
  <<: *DEFAULT_EN
85
117
  en-CA:
@@ -88,245 +120,3 @@ en-GB:
88
120
  <<: *DEFAULT_EN
89
121
  en-AU:
90
122
  <<: *DEFAULT_EN
91
-
92
- # Spanish
93
- # -------
94
- es: &DEFAULT_ES
95
- skip_links : "Saltar enlaces"
96
- skip_primary_nav : "Saltar a navegación principal"
97
- skip_content : "Saltar a contenido"
98
- skip_footer : "Saltar a pie"
99
- page : "Página"
100
- pagination_previous : "Anterior"
101
- pagination_next : "Siguiente"
102
- breadcrumb_home_label : "Inicio"
103
- breadcrumb_separator : "/"
104
- menu_label : "Alternar menú"
105
- search_label : "Alternar búsqueda"
106
- toc_label : "En esta página"
107
- ext_link_label : "Enlace directo"
108
- less_than : "menos de"
109
- minute_read : "minuto(s) de lectura"
110
- share_on_label : "Compartir en"
111
- meta_label :
112
- tags_label : "Etiquetas:"
113
- categories_label : "Categorías:"
114
- date_label : "Actualizado:"
115
- author_label : "Autor:"
116
- comments_label : "Deja un comentario"
117
- comments_title : "Comentarios"
118
- more_label : "Ver más"
119
- related_label : "Puede que también te interese"
120
- follow_label : "Seguir:"
121
- feed_label : "Feed"
122
- powered_by : "Funciona con"
123
- website_label : "Sitio web"
124
- email_label : "Correo electrónico"
125
- recent_posts : "Entradas recientes"
126
- undefined_wpm : "El parámetro words_per_minute (palabras por minuto) no está definido en _config.yml"
127
- comment_form_info : "Tu dirección de correo electrónico no se publicará. Los campos obligatorios están marcados"
128
- comment_form_comment_label : "Comentario"
129
- comment_form_md_info : "Puedes utilizar Markdown"
130
- comment_form_name_label : "Nombre"
131
- comment_form_email_label : "Dirección de correo electrónico"
132
- comment_form_website_label : "Sitio web (opcional)"
133
- comment_btn_submit : "Enviar comentario"
134
- comment_btn_submitted : "Enviado"
135
- comment_success_msg : "¡Gracias por tu comentario! Se mostrará en el sitio una vez que haya sido aprobado."
136
- comment_error_msg : "Lo sentimos, ha habido un error con tu envío. Por favor, asegúrate de que todos los campos obligatorios se han completado e inténtalo de nuevo."
137
- loading_label : "Cargando..."
138
- search_label_text : "Introduce tu término de búsqueda..."
139
- search_placeholder_text : "Introduce tu término de búsqueda..."
140
- search_no_results : "Sin resultados"
141
- results_found : "Resultado(s) encontrado(s)"
142
- back_to_top : "Volver arriba"
143
- # Statistics labels
144
- stats_total_posts : "Total de Publicaciones"
145
- stats_total_pages : "Total de Páginas"
146
- stats_categories : "Categorías"
147
- stats_tags : "Etiquetas"
148
- stats_last_updated : "Última Actualización"
149
- stats_overview : "Resumen"
150
- stats_content_breakdown : "Desglose de Contenido"
151
- # Theme-specific labels
152
- theme_toggle : "Cambiar tema"
153
- dark_mode : "Modo oscuro"
154
- light_mode : "Modo claro"
155
- view_source : "Ver código fuente"
156
- edit_page : "Editar esta página"
157
- report_issue : "Reportar un problema"
158
-
159
- es-ES:
160
- <<: *DEFAULT_ES
161
- es-MX:
162
- <<: *DEFAULT_ES
163
-
164
- # French
165
- # ------
166
- fr: &DEFAULT_FR
167
- skip_links : "Liens de saut"
168
- skip_primary_nav : "Aller à la navigation principale"
169
- skip_content : "Aller au contenu"
170
- skip_footer : "Aller au pied de page"
171
- page : "Page"
172
- pagination_previous : "Précédent"
173
- pagination_next : "Suivant"
174
- breadcrumb_home_label : "Accueil"
175
- breadcrumb_separator : "/"
176
- menu_label : "Basculer le menu"
177
- search_label : "Basculer la recherche"
178
- toc_label : "Sur cette page"
179
- ext_link_label : "Lien direct"
180
- less_than : "moins de"
181
- minute_read : "minute(s) de lecture"
182
- share_on_label : "Partager sur"
183
- meta_label :
184
- tags_label : "Tags :"
185
- categories_label : "Catégories :"
186
- date_label : "Mis à jour :"
187
- author_label : "Auteur :"
188
- comments_label : "Laisser un commentaire"
189
- comments_title : "Commentaires"
190
- more_label : "En savoir plus"
191
- related_label : "Vous pourriez aussi aimer"
192
- follow_label : "Suivre :"
193
- feed_label : "Flux"
194
- powered_by : "Propulsé par"
195
- website_label : "Site web"
196
- email_label : "Email"
197
- recent_posts : "Articles récents"
198
- back_to_top : "Retour en haut"
199
- # Statistics labels
200
- stats_total_posts : "Total des Publications"
201
- stats_total_pages : "Total des Pages"
202
- stats_categories : "Catégories"
203
- stats_tags : "Tags"
204
- stats_last_updated : "Dernière Mise à Jour"
205
- stats_overview : "Aperçu"
206
- stats_content_breakdown : "Répartition du Contenu"
207
- # Theme-specific labels
208
- theme_toggle : "Changer de thème"
209
- dark_mode : "Mode sombre"
210
- light_mode : "Mode clair"
211
- view_source : "Voir la source"
212
- edit_page : "Modifier cette page"
213
- report_issue : "Signaler un problème"
214
-
215
- fr-FR:
216
- <<: *DEFAULT_FR
217
- fr-CA:
218
- <<: *DEFAULT_FR
219
-
220
- # German
221
- # ------
222
- de: &DEFAULT_DE
223
- skip_links : "Links überspringen"
224
- skip_primary_nav : "Zur Hauptnavigation springen"
225
- skip_content : "Zum Inhalt springen"
226
- skip_footer : "Zur Fußzeile springen"
227
- page : "Seite"
228
- pagination_previous : "Zurück"
229
- pagination_next : "Weiter"
230
- breadcrumb_home_label : "Startseite"
231
- breadcrumb_separator : "/"
232
- menu_label : "Menü umschalten"
233
- search_label : "Suche umschalten"
234
- toc_label : "Auf dieser Seite"
235
- ext_link_label : "Direktlink"
236
- less_than : "weniger als"
237
- minute_read : "Minute(n) Lesezeit"
238
- share_on_label : "Teilen auf"
239
- meta_label :
240
- tags_label : "Tags:"
241
- categories_label : "Kategorien:"
242
- date_label : "Aktualisiert:"
243
- author_label : "Autor:"
244
- comments_label : "Einen Kommentar hinterlassen"
245
- comments_title : "Kommentare"
246
- more_label : "Mehr erfahren"
247
- related_label : "Das könnte Sie auch interessieren"
248
- follow_label : "Folgen:"
249
- feed_label : "Feed"
250
- powered_by : "Betrieben mit"
251
- website_label : "Webseite"
252
- email_label : "E-Mail"
253
- recent_posts : "Neueste Beiträge"
254
- back_to_top : "Nach oben"
255
- # Statistics labels
256
- stats_total_posts : "Gesamte Beiträge"
257
- stats_total_pages : "Gesamte Seiten"
258
- stats_categories : "Kategorien"
259
- stats_tags : "Tags"
260
- stats_last_updated : "Zuletzt Aktualisiert"
261
- stats_overview : "Übersicht"
262
- stats_content_breakdown : "Inhaltsaufschlüsselung"
263
- # Theme-specific labels
264
- theme_toggle : "Thema wechseln"
265
- dark_mode : "Dunkler Modus"
266
- light_mode : "Heller Modus"
267
- view_source : "Quelle ansehen"
268
- edit_page : "Diese Seite bearbeiten"
269
- report_issue : "Ein Problem melden"
270
-
271
- de-DE:
272
- <<: *DEFAULT_DE
273
- de-AT:
274
- <<: *DEFAULT_DE
275
- de-CH:
276
- <<: *DEFAULT_DE
277
-
278
- # Arabic
279
- # ------
280
- ar: &DEFAULT_AR
281
- skip_links : "تخطي الروابط"
282
- skip_primary_nav : "انتقل إلى التنقل الرئيسي"
283
- skip_content : "انتقل إلى المحتوى"
284
- skip_footer : "انتقل إلى التذييل"
285
- page : "صفحة"
286
- pagination_previous : "السابق"
287
- pagination_next : "التالي"
288
- breadcrumb_home_label : "الرئيسية"
289
- breadcrumb_separator : "/"
290
- menu_label : "تبديل القائمة"
291
- search_label : "تبديل البحث"
292
- toc_label : "في هذه الصفحة"
293
- ext_link_label : "رابط مباشر"
294
- less_than : "أقل من"
295
- minute_read : "دقيقة للقراءة"
296
- share_on_label : "مشاركة على"
297
- meta_label :
298
- tags_label : "الوسوم:"
299
- categories_label : "الأقسام:"
300
- date_label : "تاريخ التحديث:"
301
- author_label : "الكاتب:"
302
- comments_label : "اترك تعليقاً"
303
- comments_title : "التعليقات"
304
- more_label : "اقرأ المزيد"
305
- related_label : "قد يعجبك أيضاً"
306
- follow_label : "تابعنا:"
307
- feed_label : "الخلاصة"
308
- powered_by : "يعمل بواسطة"
309
- website_label : "الموقع"
310
- email_label : "البريد الإلكتروني"
311
- recent_posts : "المنشورات الأخيرة"
312
- back_to_top : "العودة للأعلى"
313
- # Statistics labels
314
- stats_total_posts : "إجمالي المنشورات"
315
- stats_total_pages : "إجمالي الصفحات"
316
- stats_categories : "الأقسام"
317
- stats_tags : "الوسوم"
318
- stats_last_updated : "آخر تحديث"
319
- stats_overview : "نظرة عامة"
320
- stats_content_breakdown : "تفصيل المحتوى"
321
- # Theme-specific labels
322
- theme_toggle : "تبديل المظهر"
323
- dark_mode : "الوضع الداكن"
324
- light_mode : "الوضع الفاتح"
325
- view_source : "عرض المصدر"
326
- edit_page : "تحرير هذه الصفحة"
327
- report_issue : "الإبلاغ عن مشكلة"
328
-
329
- ar-SA:
330
- <<: *DEFAULT_AR
331
- ar-EG:
332
- <<: *DEFAULT_AR
data/_includes/README.md CHANGED
@@ -9,9 +9,12 @@ This directory has been reorganized for better maintainability and clarity. File
9
9
  Essential layout components that form the foundation of the site:
10
10
 
11
11
  - `head.html` - HTML document head with meta tags, scripts, and styles
12
+ - `favicon.html` - Favicon / browser-identity tags (icon links, apple-touch, manifest, theme-color) driven by the optional `favicon:` config block
12
13
  - `header.html` - Main site header with navigation
13
14
  - `footer.html` - Site footer (if exists)
14
15
  - `branding.html` - Site branding and title display
16
+ - `i18n.html` - Resolves the per-page-language UI-string set into the `ui` variable (generated translations fall back to English)
17
+ - `hreflang.html` - `<link rel="alternate" hreflang>` tags for pages with machine-generated translations
15
18
 
16
19
  ### `navigation/`
17
20
 
@@ -32,14 +35,16 @@ All navigation-related components:
32
35
  Analytics and tracking integrations:
33
36
 
34
37
  - `google-analytics.html` - Google Analytics tracking
35
- - `google-tag-manager-head.html` - GTM head section
36
- - `google-tag-manager-body.html` - GTM body section
38
+ - `google-tag-manager-head.html` - GTM head section (emits nothing unless `google_tag_manager:` is set in `_config.yml`)
39
+ - `google-tag-manager-body.html` - GTM body section (same config gate)
37
40
 
38
41
  ### `components/`
39
42
 
40
43
  Reusable UI components and widgets:
41
44
 
42
45
  - `searchbar.html` - Deprecated search stub (superseded by `search-modal.html`)
46
+ - `language-toggle.html` - Navbar dropdown switching between the English original and its machine-generated translations
47
+ - `translation-notice.html` - Disclosure banner on machine-translated pages linking back to the English original
43
48
  - `powered-by.html` - "Powered by" credits display
44
49
  - `quick-index.html` - Quick page index
45
50
  - `dev-shortcuts.html` - Developer shortcuts
@@ -51,6 +56,13 @@ Reusable UI components and widgets:
51
56
  - `js-cdn.html` - CDN JavaScript libraries
52
57
  - `preview-image.html` - Consistent preview image rendering with lazy loading
53
58
  - `post-card.html` - Reusable post card component for listings
59
+ - `data-card.html` - One generic card rendered from a plain data hash (title/url/icon/badge/meta/buttons schema in its header)
60
+ - `card-grid.html` - Responsive grid of `data-card`s from any data array — hub dashboards, fleet registries, service grids
61
+ - `bookshelf.html` - Grid of every book in the `books` collection (home-page library)
62
+ - `book-card.html` - One book's portrait cover card (bookshelf tile)
63
+ - `book-toc.html` - Ordered story/chapter list for one book
64
+ - `book-nav.html` - Prev/next-story + contents navigation inside a book
65
+ - `book-plate.html` - Full-width illustration figure with optional caption for story pages
54
66
 
55
67
  ### `content/`
56
68
 
@@ -75,6 +87,17 @@ Documentation and reference materials:
75
87
 
76
88
  - `bootstrap-docs.html` - Bootstrap documentation (moved from style.html)
77
89
 
90
+ ### `custom/` — consumer extension hooks
91
+
92
+ Empty stubs the theme includes at fixed points in the page chrome. A consumer site creates a file at the same path to inject markup there — **no fork of `root.html`, `head.html`, or `footer.html` needed**, so theme updates keep flowing without merge drift:
93
+
94
+ - `custom/head.html` - end of `<head>` (JSON-LD, font preloads, verification tags, extra meta)
95
+ - `custom/body-start.html` - immediately after `<body>` (e.g. GTM `<noscript>`)
96
+ - `custom/footer.html` - after the theme footer, before the cookie-consent banner
97
+ - `custom/body-end.html` - last thing before `</body>` (site scripts, third-party widgets)
98
+
99
+ The stubs ship empty (comment-only) and render nothing until shadowed.
100
+
78
101
  ## Usage
79
102
 
80
103
  When including files in layouts or other templates, use the full path:
@@ -1,9 +1,18 @@
1
1
  {% comment %} Feature: ZER0-039 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: google-tag-manager-body
4
+ Path: _includes/analytics/google-tag-manager-body.html
5
+ Purpose: Google Tag Manager noscript fallback (body section). Config-driven:
6
+ emits nothing unless the site sets a container ID.
7
+ Params: none — reads `google_tag_manager: "<container-id>"` from _config.yml
8
+ Depends on: google-tag-manager-head.html (loader)
9
+ {%- endcomment -%}
10
+ {%- if site.google_tag_manager -%}
2
11
  <!-- Google Tag Manager (noscript) -->
3
12
  <noscript>
4
- <iframe src="https://www.googletagmanager.com/ns.html?id=GTM-NN8P7RZ"
13
+ <iframe src="https://www.googletagmanager.com/ns.html?id={{ site.google_tag_manager }}"
5
14
  height="0" width="0" style="display:none;visibility:hidden">
6
15
  </iframe>
7
16
  </noscript>
8
17
  <!-- End Google Tag Manager (noscript) -->
9
- <!-- source https://tagmanager.google.com/#/admin/accounts/6005859084/containers/58386588/install?containerDraftId=2 -->
18
+ {%- endif -%}
@@ -1,8 +1,17 @@
1
1
  {% comment %} Feature: ZER0-039 {% endcomment %}
2
- <!--
3
- title: Google Tag Manager
4
- file:
5
- -->
2
+ {%- comment -%}
3
+ Component: google-tag-manager-head
4
+ Path: _includes/analytics/google-tag-manager-head.html
5
+ Purpose: Google Tag Manager loader (head section). Config-driven: emits
6
+ nothing unless the site sets a container ID, so consumer sites
7
+ never inherit this theme's own GTM container.
8
+ Params: none — reads `google_tag_manager: "<container-id>"` from _config.yml
9
+ Depends on: google-tag-manager-body.html (optional noscript counterpart)
10
+ Notes: Previously the container ID was hardcoded, silently enrolling
11
+ every consumer site's production build into the theme owner's
12
+ GTM container. Set your own ID (or leave unset for no GTM).
13
+ {%- endcomment -%}
14
+ {%- if site.google_tag_manager -%}
6
15
  <script>
7
16
  // Skip GTM on local/dev hostnames even if a prod build is served locally (Docker/CI preview).
8
17
  (function () {
@@ -13,7 +22,8 @@
13
22
  new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
14
23
  j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
15
24
  'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
16
- })(window,document,'script','dataLayer','GTM-NN8P7RZ');
25
+ })(window,document,'script','dataLayer','{{ site.google_tag_manager }}');
17
26
  })();
18
27
  </script>
19
- <!-- End Google Tag Manager -->
28
+ <!-- End Google Tag Manager -->
29
+ {%- endif -%}
@@ -64,6 +64,9 @@ Privacy: Respects Do Not Track and provides opt-out mechanisms
64
64
  capturePageleave: {{ site.posthog.capture_pageleave | default: true }},
65
65
  disableCookie: {{ site.posthog.disable_cookie | default: false }},
66
66
  respectDnt: {{ site.posthog.respect_dnt | default: true }},
67
+ respectGpc: {{ site.posthog.privacy.respect_gpc | default: true }},
68
+ requireConsent: {{ site.posthog.privacy.require_consent | default: false }},
69
+ geoipDisable: {{ site.posthog.privacy.geoip_disable | default: false }},
67
70
  crossSubdomainCookie: {{ site.posthog.cross_subdomain_cookie | default: false }},
68
71
  secureCookie: {{ site.posthog.secure_cookie | default: true }},
69
72
  persistence: '{{ site.posthog.persistence | default: "localStorage+cookie" }}',
@@ -74,9 +77,28 @@ Privacy: Respects Do Not Track and provides opt-out mechanisms
74
77
  }
75
78
  };
76
79
 
77
- {% comment %} Check for Do Not Track before loading {% endcomment %}
78
- if (window.posthogConfig.respectDnt && navigator.doNotTrack === '1') {
79
- console.log('PostHog: Respecting Do Not Track setting');
80
+ {% comment %} Consent gate (privacy.require_consent): until the visitor
81
+ accepts the "analytics" category in the theme's cookie-consent banner
82
+ (localStorage "zer0-cookie-consent"), PostHog starts opted OUT with
83
+ memory-only persistence — no cookies, nothing stored. The banner's
84
+ applyConsent() flips opt-in/opt-out live once a choice is made. {% endcomment %}
85
+ window.__phHasAnalyticsConsent = true;
86
+ if (window.posthogConfig.requireConsent) {
87
+ window.__phHasAnalyticsConsent = false;
88
+ try {
89
+ var __phStored = window.localStorage.getItem('zer0-cookie-consent');
90
+ if (__phStored) {
91
+ var __phConsent = JSON.parse(__phStored);
92
+ window.__phHasAnalyticsConsent = __phConsent && __phConsent.analytics === true;
93
+ }
94
+ } catch (e) { /* localStorage unavailable — stay opted out */ }
95
+ }
96
+
97
+ {% comment %} Honor browser privacy signals before loading: Do Not Track
98
+ and Global Privacy Control (GPC is a legal signal under CCPA/CPRA). {% endcomment %}
99
+ if ((window.posthogConfig.respectDnt && navigator.doNotTrack === '1') ||
100
+ (window.posthogConfig.respectGpc && window.navigator.globalPrivacyControl === true)) {
101
+ console.log('PostHog: respecting browser privacy signal (DNT/GPC)');
80
102
  } else {
81
103
  {% comment %} PostHog Loading Script {% endcomment %}
82
104
  !function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init Rr Mr fi Or Ar ci Tr Cr capture Mi calculateEventProperties Lr register register_once register_for_session unregister unregister_for_session Hr getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey displaySurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Ur jr createPersonProfile zr kr Br opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing get_explicit_consent_status is_capturing clear_opt_in_out_capturing Dr debug M Nr getPageViewId captureTraceFeedback captureTraceMetric $r".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
@@ -91,7 +113,8 @@ Privacy: Respects Do Not Track and provides opt-out mechanisms
91
113
  disable_cookie: window.posthogConfig.disableCookie,
92
114
  cross_subdomain_cookie: window.posthogConfig.crossSubdomainCookie,
93
115
  secure_cookie: window.posthogConfig.secureCookie,
94
- persistence: window.posthogConfig.persistence,
116
+ persistence: window.__phHasAnalyticsConsent ? window.posthogConfig.persistence : 'memory',
117
+ opt_out_capturing_by_default: !window.__phHasAnalyticsConsent,
95
118
  session_recording: window.posthogConfig.sessionRecording,
96
119
  {% if site.posthog.privacy.ip_anonymization %}
97
120
  ip: false,
@@ -100,6 +123,12 @@ Privacy: Respects Do Not Track and provides opt-out mechanisms
100
123
  {% comment %} PostHog loaded successfully {% endcomment %}
101
124
  console.log('PostHog analytics loaded successfully');
102
125
 
126
+ {% comment %} privacy.geoip_disable: stop PostHog from resolving
127
+ IP to location on every event {% endcomment %}
128
+ if (window.posthogConfig.geoipDisable) {
129
+ posthog.register({ '$geoip_disable': true });
130
+ }
131
+
103
132
  {% comment %} Track Jekyll-specific page properties {% endcomment %}
104
133
  posthog.register({
105
134
  'site_title': '{{ site.title }}',
@@ -0,0 +1,43 @@
1
+ {% comment %} Feature: ZER0-081 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: abc-letter
4
+ Path: _includes/components/abc-letter.html
5
+ Purpose: One card of an ABC board book — the illustration (or a friendly
6
+ "coming soon" placeholder), a big letter badge, the word, and the
7
+ read-aloud tagline. Rendered once per `alphabet:` entry by
8
+ _layouts/book-abc.html.
9
+ Params: entry (hash, required) — an alphabet entry with keys:
10
+ letter, word, tagline, image, alt, status ("planned"|"rendered")
11
+ Depends on: components/preview-image.html,
12
+ _sass/components/_book.scss (.abc-letter, .abc-letter-*)
13
+ Notes: The big letter is HTML typography, never baked into the art (so
14
+ raster models never garble it). A `status: planned` entry (no
15
+ rendered plate yet) shows a coloured placeholder tile instead of
16
+ a broken image, so the book reads end-to-end before any art runs.
17
+ {%- endcomment -%}
18
+ {%- if include.entry == nil -%}
19
+ {%- comment -%} ERROR: abc-letter requires `entry` {%- endcomment -%}
20
+ {%- else -%}
21
+ {%- assign _e = include.entry -%}
22
+ {%- assign _letter = _e.letter -%}
23
+ {%- comment -%} A plate counts as rendered only when it has an image AND is not
24
+ still "planned". Branch inline: Liquid's assign does not reliably apply the
25
+ "not-equals" comparison, so the condition lives in the if-tag below. {%- endcomment -%}
26
+ <article class="abc-letter" id="letter-{{ _letter | downcase }}">
27
+ <div class="abc-letter-plate">
28
+ {%- if _e.image and _e.status != "planned" -%}
29
+ {%- include components/preview-image.html src=_e.image alt=_e.alt class="abc-letter-img" loading="lazy" -%}
30
+ {%- else -%}
31
+ <div class="abc-letter-placeholder" role="img" aria-label="{{ _e.alt | default: _e.word | escape }} — illustration coming soon">
32
+ <span class="abc-letter-glyph" aria-hidden="true">{{ _letter }}</span>
33
+ <span class="abc-letter-planned">illustration coming soon</span>
34
+ </div>
35
+ {%- endif -%}
36
+ <span class="abc-letter-badge" aria-hidden="true">{{ _letter }}</span>
37
+ </div>
38
+ <p class="abc-letter-word">{{ _e.word }}</p>
39
+ {%- if _e.tagline -%}
40
+ <p class="abc-letter-tagline">{{ _e.tagline }}</p>
41
+ {%- endif -%}
42
+ </article>
43
+ {%- endif -%}
@@ -10,7 +10,9 @@
10
10
  reference an external avatar, e.g. a GitHub one:
11
11
  avatar: "https://avatars.githubusercontent.com/u/10567847?v=4"
12
12
  2. `avatar` is a relative path (e.g. /images/authors/me.png) — prefixed with
13
- the site's assets path ({{ site.baseurl }}/{{ site.public_folder }}).
13
+ the site's public_folder and passed through relative_url, so baseurl is
14
+ applied and the slashes never double into a protocol-relative //… URL
15
+ (even when public_folder and/or baseurl are empty or unset).
14
16
  3. No `avatar`, but a `github` handle is set — falls back to that account's
15
17
  GitHub avatar: https://github.com/<handle>.png
16
18
  4. Otherwise — empty output (the caller renders a fallback icon).
@@ -22,7 +24,7 @@
22
24
  {%- endcomment -%}
23
25
  {%- assign _avatar = include.data.avatar -%}
24
26
  {%- if _avatar and _avatar != "" -%}
25
- {%- if _avatar contains "://" -%}{{ _avatar }}{%- else -%}{{ site.baseurl }}/{{ site.public_folder }}{{ _avatar }}{%- endif -%}
27
+ {%- if _avatar contains "://" -%}{{ _avatar }}{%- else -%}{%- capture _avatar_path -%}/{{ site.public_folder }}{{ _avatar }}{%- endcapture -%}{{ _avatar_path | replace: "//", "/" | relative_url }}{%- endif -%}
26
28
  {%- elsif include.data.github and include.data.github != "" -%}
27
29
  https://github.com/{{ include.data.github }}.png
28
30
  {%- endif -%}
@@ -0,0 +1,42 @@
1
+ {% comment %} Feature: ZER0-080 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: book-card
4
+ Path: _includes/components/book-card.html
5
+ Purpose: Bookshelf card for one book — portrait cover, title, subtitle,
6
+ chapter count, and audience badge, linking to the book page.
7
+ Params: book (document, required) — a `books` collection doc with
8
+ `layout: book` (the book's landing page)
9
+ Depends on: components/preview-image.html,
10
+ _sass/components/_book.scss (.book-card, .book-cover-frame)
11
+ Notes: Chapter count is derived live from the collection so the card
12
+ never goes stale. Whole card is one link for a big tap target.
13
+ {%- endcomment -%}
14
+ {%- if include.book == nil -%}
15
+ {%- comment -%} ERROR: book-card requires `book` {%- endcomment -%}
16
+ {%- else -%}
17
+ {%- assign _book = include.book -%}
18
+ {%- assign _slug = _book.book | default: _book.slug -%}
19
+ {%- assign _is_abc = _book.layout == "book-abc" -%}
20
+ {%- assign _chapters = site.books | where: "book", _slug | where_exp: "doc", "doc.chapter" -%}
21
+ {%- assign _cover = _book.cover_image | default: _book.preview | default: site.teaser -%}
22
+ <article class="book-card h-100">
23
+ <a href="{{ _book.url | relative_url }}" class="d-block text-decoration-none text-body h-100">
24
+ <div class="book-cover-frame mb-3">
25
+ {%- include components/preview-image.html src=_cover alt=_book.title class="" -%}
26
+ </div>
27
+ <h3 class="h5 mb-1">{{ _book.title }}</h3>
28
+ {%- if _book.subtitle -%}
29
+ <p class="text-body-secondary small mb-2">{{ _book.subtitle }}</p>
30
+ {%- endif -%}
31
+ <p class="small text-body-secondary mb-0">
32
+ {%- if _is_abc -%}
33
+ {%- assign _letters = _book.alphabet | size -%}
34
+ <i class="bi bi-type me-1" aria-hidden="true"></i>{{ _letters }} {% if _letters == 1 %}letter{% else %}letters{% endif %}
35
+ {%- else -%}
36
+ <i class="bi bi-book me-1" aria-hidden="true"></i>{{ _chapters.size }} {% if _chapters.size == 1 %}story{% else %}stories{% endif %}
37
+ {%- endif -%}
38
+ {%- if _book.audience %} <span class="mx-1" aria-hidden="true">•</span> {{ _book.audience }}{% endif -%}
39
+ </p>
40
+ </a>
41
+ </article>
42
+ {%- endif -%}