jekyll-theme-zer0 1.19.1 → 1.20.2

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +395 -0
  3. data/README.md +27 -19
  4. data/_data/authors.yml +154 -5
  5. data/_data/backlog.yml +5 -5
  6. data/_data/content_statistics.yml +273 -297
  7. data/_data/features.yml +4 -25
  8. data/_data/navigation/README.md +24 -0
  9. data/_data/navigation/about.yml +2 -0
  10. data/_data/navigation/main.yml +2 -7
  11. data/_data/roadmap.yml +86 -12
  12. data/_includes/components/author-avatar-url.html +28 -0
  13. data/_includes/components/author-bio.html +86 -0
  14. data/_includes/components/author-card.html +184 -121
  15. data/_includes/components/author-eeat.html +10 -4
  16. data/_includes/components/info-section.html +1 -1
  17. data/_includes/components/mermaid.html +0 -3
  18. data/_includes/components/post-card.html +19 -9
  19. data/_includes/content/giscus.html +3 -2
  20. data/_includes/core/footer-fabs.html +28 -0
  21. data/_includes/core/footer.html +7 -17
  22. data/_includes/core/head.html +2 -2
  23. data/_includes/navigation/breadcrumbs.html +20 -2
  24. data/_includes/navigation/local-graph.html +18 -2
  25. data/_includes/obsidian/full-graph.html +4 -6
  26. data/_layouts/article.html +44 -74
  27. data/_layouts/author.html +274 -0
  28. data/_layouts/authors.html +55 -0
  29. data/_layouts/news.html +3 -3
  30. data/_layouts/note.html +21 -6
  31. data/_layouts/notebook.html +21 -6
  32. data/_layouts/root.html +31 -17
  33. data/_layouts/section.html +3 -3
  34. data/_plugins/author_pages_generator.rb +121 -0
  35. data/_sass/components/_author.scss +219 -0
  36. data/_sass/components/_content-tables.scss +16 -1
  37. data/_sass/components/_notes-index.scss +102 -0
  38. data/_sass/components/_search-modal.scss +40 -0
  39. data/_sass/components/_ui-enhancements.scss +570 -0
  40. data/_sass/core/_docs-code-examples.scss +463 -0
  41. data/_sass/core/_docs-layout.scss +0 -453
  42. data/_sass/core/_navbar.scss +253 -0
  43. data/_sass/core/_sidebar-extras.scss +79 -0
  44. data/_sass/core/_toc.scss +87 -0
  45. data/_sass/core/_variables.scss +7 -142
  46. data/_sass/custom.scss +24 -1122
  47. data/_sass/layouts/_global-chrome.scss +59 -0
  48. data/assets/css/main.scss +19 -2
  49. data/assets/js/author-profile.js +190 -0
  50. data/assets/js/modules/navigation/navbar.js +104 -0
  51. data/assets/js/obsidian-graph.js +2 -2
  52. data/assets/js/obsidian-local-graph.js +11 -5
  53. data/assets/vendor/cytoscape/cytoscape.min.js +32 -0
  54. data/scripts/README.md +39 -0
  55. data/scripts/bin/validate +11 -1
  56. data/scripts/dev/css-diff.sh +49 -0
  57. data/scripts/dev/shot.js +37 -0
  58. data/scripts/features/generate-preview-images +110 -6
  59. data/scripts/features/pixelate-preview-images +126 -0
  60. data/scripts/features/pixelate_images.py +662 -0
  61. data/scripts/github-setup.sh +0 -0
  62. data/scripts/lib/preview_generator.py +47 -3
  63. data/scripts/pixelate-preview-images.sh +12 -0
  64. data/scripts/test/integration/auto-version +10 -8
  65. data/scripts/test/lib/run_tests.sh +2 -0
  66. data/scripts/test/lib/test_content_review.sh +205 -0
  67. data/scripts/test/lib/test_pixelate_images.sh +108 -0
  68. metadata +25 -20
  69. data/_data/hub.yml +0 -68
  70. data/_data/hub_index.yml +0 -203
  71. data/_data/navigation/hub.yml +0 -110
  72. data/assets/vendor/font-awesome/css/all.min.css +0 -9
  73. data/assets/vendor/font-awesome/webfonts/fa-brands-400.ttf +0 -0
  74. data/assets/vendor/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
  75. data/assets/vendor/font-awesome/webfonts/fa-regular-400.ttf +0 -0
  76. data/assets/vendor/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
  77. data/assets/vendor/font-awesome/webfonts/fa-solid-900.ttf +0 -0
  78. data/assets/vendor/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
  79. data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.ttf +0 -0
  80. data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 +0 -0
  81. data/assets/vendor/jquery/jquery-3.7.1.min.js +0 -2
  82. data/scripts/lib/hub.rb +0 -208
  83. data/scripts/provision-org-sites.rb +0 -252
  84. data/scripts/provision-org-sites.sh +0 -23
  85. data/scripts/sync-hub-metadata.rb +0 -184
  86. data/scripts/sync-hub-metadata.sh +0 -22
@@ -1,175 +1,238 @@
1
- <!--
1
+ {% comment %}
2
2
  ===================================================================
3
- AUTHOR CARD COMPONENT - Display author information
3
+ AUTHOR CARD COMPONENT - Canonical author rendering primitive
4
4
  ===================================================================
5
-
5
+
6
6
  File: author-card.html
7
7
  Path: _includes/components/author-card.html
8
- Purpose: Reusable author profile display for posts and author pages
9
-
8
+ Purpose: Single source of truth for rendering an author across the theme.
9
+ Used by article/note/notebook bylines, the "About the Author"
10
+ section (components/author-bio.html), post-card footers, and the
11
+ author profile/index layouts.
12
+
10
13
  Parameters:
11
- - author (required): Author key from _data/authors.yml OR author name string
12
- - style (optional): "compact" | "full" | "inline" (default: "compact")
13
- - show_bio (optional): Show author bio (default: true for full, false for compact)
14
- - show_social (optional): Show social links (default: true for full, false for compact)
15
-
16
- Usage:
17
- {% include components/author-card.html author=page.author %}
18
- {% include components/author-card.html author="bamr87" style="full" %}
19
- {% include components/author-card.html author=page.author style="inline" %}
20
-
14
+ - author (required): Author key from _data/authors.yml OR a plain name string.
15
+ - style (optional): "inline" | "compact" | "full" (default: "compact")
16
+ - link (optional): Link the name/avatar to the author profile page when the
17
+ author resolves to a known key (default: true).
18
+ - show_avatar (optional): Render the avatar / fallback icon (default: true).
19
+ - show_bio (optional): Show bio (default: true for full, false otherwise).
20
+ - show_social (optional): Show social links (default: true for full, false otherwise).
21
+ - show_expertise (optional): Show expertise chips in full style (default: true).
22
+ - avatar_size (optional): Avatar size in px (default: 24 inline, 48 compact, 80 full).
23
+ - name_itemprop (optional): Wrap the visible name in this microdata itemprop
24
+ (e.g. "name") when the caller already opened a
25
+ schema.org/Person scope. Inline style only.
26
+
27
+ Usage (wrap each line in Liquid include tags — braces omitted here because
28
+ Liquid parses tags even inside HTML comments, which would self-recurse):
29
+ include components/author-card.html author=page.author style="inline"
30
+ include components/author-card.html author="bamr87" style="full"
31
+ include components/author-card.html author=post.author style="inline" show_avatar=false
32
+
21
33
  Dependencies:
22
- - _data/authors.yml: Author data definitions
23
- - Bootstrap 5 card and utility classes
24
- - Bootstrap Icons
34
+ - _data/authors.yml: Author data definitions (key -> profile)
35
+ - Bootstrap 5 card and utility classes; Bootstrap Icons
36
+ - _sass/components/_author.scss: Styling hooks (.author-*)
25
37
  ===================================================================
26
- -->
27
-
28
- {% comment %} Parameter defaults {% endcomment %}
38
+ {% endcomment %}
39
+ {% comment %} ---------------------------------------------------------------
40
+ Parameter defaults. NOTE: `| default: true` returns true when the value is
41
+ literally `false` (Liquid treats false as "missing"), so boolean params that
42
+ default to true are resolved with explicit `== false` checks instead.
43
+ --------------------------------------------------------------------------- {% endcomment %}
29
44
  {% assign style = include.style | default: "compact" %}
30
45
 
31
- {% comment %}
32
- Resolve author: can be a key to _data/authors.yml or a plain name string
33
- {% endcomment %}
34
- {% assign author_key = include.author %}
35
- {% assign author_data = site.data.authors[author_key] %}
36
-
37
- {% comment %} If no match in authors.yml, use string as display name {% endcomment %}
38
- {% unless author_data %}
39
- {% assign author_data = site.data.authors.default %}
40
- {% if include.author and include.author != "" %}
41
- {% capture author_name %}{{ include.author }}{% endcapture %}
42
- {% else %}
43
- {% assign author_name = author_data.name %}
44
- {% endif %}
46
+ {% assign do_link = true %}
47
+ {% if include.link == false %}{% assign do_link = false %}{% endif %}
48
+
49
+ {% assign show_avatar = true %}
50
+ {% if include.show_avatar == false %}{% assign show_avatar = false %}{% endif %}
51
+
52
+ {% assign show_expertise = true %}
53
+ {% if include.show_expertise == false %}{% assign show_expertise = false %}{% endif %}
54
+
55
+ {% comment %} ---------------------------------------------------------------
56
+ Resolve author: the input may be a key into _data/authors.yml or a plain
57
+ display-name string. A matched key is "known" and gets a profile link.
58
+ --------------------------------------------------------------------------- {% endcomment %}
59
+ {% assign raw_author = include.author %}
60
+ {% assign author_data = site.data.authors[raw_author] %}
61
+ {% assign is_known = false %}
62
+ {% assign author_key = nil %}
63
+ {% if author_data %}
64
+ {% assign is_known = true %}
65
+ {% assign author_key = raw_author %}
66
+ {% assign author_name = author_data.name | default: raw_author %}
45
67
  {% else %}
46
- {% assign author_name = author_data.name %}
47
- {% endunless %}
68
+ {% comment %} Not a direct key — match by display name / display_name so front
69
+ matter that uses the name (e.g. "Zer0-Mistakes Team") still resolves to its
70
+ key (default) and links to the profile. {% endcomment %}
71
+ {% if raw_author and raw_author != "" %}
72
+ {% for entry in site.data.authors %}
73
+ {% if entry[1].name == raw_author or entry[1].display_name == raw_author %}
74
+ {% assign author_data = entry[1] %}
75
+ {% assign author_key = entry[0] %}
76
+ {% assign is_known = true %}
77
+ {% assign author_name = entry[1].name | default: raw_author %}
78
+ {% break %}
79
+ {% endif %}
80
+ {% endfor %}
81
+ {% endif %}
82
+ {% unless is_known %}
83
+ {% assign author_data = site.data.authors.default %}
84
+ {% if raw_author and raw_author != "" %}
85
+ {% assign author_name = raw_author %}
86
+ {% else %}
87
+ {% assign author_name = author_data.name %}
88
+ {% endif %}
89
+ {% endunless %}
90
+ {% endif %}
91
+
92
+ {% comment %} Profile URL only when the author is a known key, linking is on, AND
93
+ the author's profile page actually exists in the build. Author pages are
94
+ generated by a _plugin and aren't delivered by remote_theme, so on a Pages
95
+ consumer the byline would otherwise link to a 404. See issue #204. {% endcomment %}
96
+ {% assign profile_url = nil %}
97
+ {% if do_link and is_known %}
98
+ {% assign author_slug = author_key | slugify %}
99
+ {% capture author_path %}/authors/{{ author_slug }}/{% endcapture %}
100
+ {% assign author_page = site.html_pages | where: "url", author_path | first %}
101
+ {% if author_page %}
102
+ {% capture profile_url %}{{ site.baseurl }}{{ author_path }}{% endcapture %}
103
+ {% endif %}
104
+ {% endif %}
48
105
 
49
106
  {% comment %} Style-specific defaults {% endcomment %}
50
107
  {% if style == "full" %}
51
- {% assign show_bio = include.show_bio | default: true %}
52
- {% assign show_social = include.show_social | default: true %}
108
+ {% assign show_bio = true %}
109
+ {% if include.show_bio == false %}{% assign show_bio = false %}{% endif %}
110
+ {% assign show_social = true %}
111
+ {% if include.show_social == false %}{% assign show_social = false %}{% endif %}
112
+ {% assign avatar_size = include.avatar_size | default: 80 %}
53
113
  {% elsif style == "inline" %}
54
114
  {% assign show_bio = false %}
55
115
  {% assign show_social = false %}
116
+ {% assign avatar_size = include.avatar_size | default: 24 %}
56
117
  {% else %}
57
- {% assign show_bio = include.show_bio | default: false %}
58
- {% assign show_social = include.show_social | default: false %}
118
+ {% assign show_bio = false %}
119
+ {% if include.show_bio == true %}{% assign show_bio = true %}{% endif %}
120
+ {% assign show_social = false %}
121
+ {% if include.show_social == true %}{% assign show_social = true %}{% endif %}
122
+ {% assign avatar_size = include.avatar_size | default: 48 %}
59
123
  {% endif %}
60
124
 
61
- <!-- ========================== -->
62
- <!-- INLINE STYLE -->
63
- <!-- ========================== -->
125
+ {% comment %} Resolve avatar: full URL (e.g. GitHub) as-is, relative path prefixed,
126
+ or derived from the author's github handle. See components/author-avatar-url.html {% endcomment %}
127
+ {% capture avatar_url %}{% include components/author-avatar-url.html data=author_data %}{% endcapture %}
128
+ {% assign avatar_url = avatar_url | strip %}
129
+ {% assign has_avatar = false %}{% if avatar_url != "" %}{% assign has_avatar = true %}{% endif %}
130
+
131
+ {% comment %} ===========================================================
132
+ INLINE STYLE — byline use (meta rows, card footers)
133
+ =========================================================== {% endcomment %}
64
134
  {% if style == "inline" %}
65
135
  <span class="author-inline d-inline-flex align-items-center">
66
- {% if author_data.avatar %}
67
- <img src="{{ site.baseurl }}/{{ site.public_folder }}{{ author_data.avatar }}"
68
- alt="{{ author_name }}"
69
- class="rounded-circle me-2"
70
- width="24" height="24"
71
- style="object-fit: cover;">
136
+ {%- if profile_url -%}<a href="{{ profile_url }}" class="author-inline__link text-decoration-none text-reset d-inline-flex align-items-center">{%- endif -%}
137
+ {% if show_avatar %}
138
+ {% if has_avatar %}
139
+ <img src="{{ avatar_url }}" alt="" class="author-inline__avatar rounded-circle me-1" width="{{ avatar_size }}" height="{{ avatar_size }}" style="object-fit: cover;" loading="lazy">
140
+ {% else %}
141
+ <i class="bi bi-person-circle me-1"></i>
142
+ {% endif %}
72
143
  {% else %}
73
- <i class="bi bi-person-circle me-1"></i>
144
+ <i class="bi bi-person me-1"></i>
74
145
  {% endif %}
75
- <span class="author-name">{{ author_name }}</span>
146
+ {%- if include.name_itemprop -%}
147
+ <span class="author-name" itemprop="{{ include.name_itemprop }}">{{ author_name }}</span>
148
+ {%- else -%}
149
+ <span class="author-name">{{ author_name }}</span>
150
+ {%- endif -%}
151
+ {%- if profile_url -%}</a>{%- endif -%}
152
+ {%- if author_data.ai -%}<span class="badge author-ai-badge ms-1" title="AI author persona">AI</span>{%- endif -%}
76
153
  </span>
77
154
 
78
- <!-- ========================== -->
79
- <!-- COMPACT STYLE (default) -->
80
- <!-- ========================== -->
155
+ {% comment %} ===========================================================
156
+ COMPACT STYLE — list rows, author index
157
+ =========================================================== {% endcomment %}
81
158
  {% elsif style == "compact" %}
82
- <div class="author-card-compact d-flex align-items-center">
83
- {% if author_data.avatar %}
84
- <img src="{{ site.baseurl }}/{{ site.public_folder }}{{ author_data.avatar }}"
85
- alt="{{ author_name }}"
86
- class="rounded-circle me-3"
87
- width="48" height="48"
88
- style="object-fit: cover;">
89
- {% else %}
90
- <div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-3"
91
- style="width: 48px; height: 48px;">
92
- <i class="bi bi-person fs-5"></i>
93
- </div>
159
+ <div class="author-card-compact d-flex align-items-center" itemscope itemtype="https://schema.org/Person">
160
+ {%- if profile_url -%}<a href="{{ profile_url }}" class="text-decoration-none text-reset d-flex align-items-center" itemprop="url">{%- endif -%}
161
+ {% if show_avatar %}
162
+ {% if has_avatar %}
163
+ <img src="{{ avatar_url }}" alt="{{ author_name }}" class="rounded-circle me-3" width="{{ avatar_size }}" height="{{ avatar_size }}" style="object-fit: cover;" itemprop="image" loading="lazy">
164
+ {% else %}
165
+ <div class="author-avatar-fallback rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-3" style="width: {{ avatar_size }}px; height: {{ avatar_size }}px;">
166
+ <i class="bi bi-person fs-5"></i>
167
+ </div>
168
+ {% endif %}
94
169
  {% endif %}
95
170
  <div>
96
- <strong class="d-block">{{ author_name }}</strong>
97
- {% if author_data.role %}
98
- <small class="text-muted">{{ author_data.role }}</small>
99
- {% endif %}
100
- {% if show_bio and author_data.bio %}
101
- <p class="text-muted small mb-0 mt-1">{{ author_data.bio | truncate: 100 }}</p>
102
- {% endif %}
171
+ <strong class="d-block" itemprop="name">{{ author_name }}{% if author_data.ai %} <span class="badge author-ai-badge align-middle" title="AI author persona"><i class="bi bi-robot" aria-hidden="true"></i> AI</span>{% endif %}</strong>
172
+ {% if author_data.role %}<small class="text-muted" itemprop="jobTitle">{{ author_data.role }}</small>{% endif %}
173
+ {% if show_bio and author_data.bio %}<p class="text-muted small mb-0 mt-1" itemprop="description">{{ author_data.bio | truncate: 100 }}</p>{% endif %}
103
174
  </div>
175
+ {%- if profile_url -%}</a>{%- endif -%}
104
176
  </div>
105
177
 
106
- <!-- ========================== -->
107
- <!-- FULL STYLE -->
108
- <!-- ========================== -->
178
+ {% comment %} ===========================================================
179
+ FULL STYLE — "About the Author" / profile hero
180
+ =========================================================== {% endcomment %}
109
181
  {% elsif style == "full" %}
110
- <div class="author-card card border-0 shadow-sm">
182
+ <div class="author-card card border-0 shadow-sm" itemscope itemtype="https://schema.org/Person">
111
183
  <div class="card-body">
112
184
  <div class="d-flex align-items-start">
113
- {% if author_data.avatar %}
114
- <img src="{{ site.baseurl }}/{{ site.public_folder }}{{ author_data.avatar }}"
115
- alt="{{ author_name }}"
116
- class="rounded-circle me-3"
117
- width="80" height="80"
118
- style="object-fit: cover;">
119
- {% else %}
120
- <div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-3"
121
- style="width: 80px; height: 80px;">
122
- <i class="bi bi-person fs-1"></i>
123
- </div>
185
+ {% if show_avatar %}
186
+ {% if has_avatar %}
187
+ <img src="{{ avatar_url }}" alt="{{ author_name }}" class="author-card__avatar rounded-circle me-3" width="{{ avatar_size }}" height="{{ avatar_size }}" style="object-fit: cover;" itemprop="image" loading="lazy">
188
+ {% else %}
189
+ <div class="author-avatar-fallback rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-3" style="width: {{ avatar_size }}px; height: {{ avatar_size }}px;">
190
+ <i class="bi bi-person fs-1"></i>
191
+ </div>
192
+ {% endif %}
124
193
  {% endif %}
125
194
  <div class="flex-grow-1">
126
- <h5 class="card-title mb-1">{{ author_name }}</h5>
127
- {% if author_data.role %}
128
- <p class="text-primary mb-2">{{ author_data.role }}</p>
129
- {% endif %}
130
- {% if show_bio and author_data.bio %}
131
- <p class="card-text text-muted">{{ author_data.bio }}</p>
195
+ <h5 class="card-title mb-1" itemprop="name">
196
+ {%- if profile_url -%}<a href="{{ profile_url }}" class="text-decoration-none text-reset" itemprop="url">{{ author_name }}</a>{%- else -%}{{ author_name }}{%- endif -%}
197
+ {%- if author_data.ai -%} <span class="badge author-ai-badge align-middle" title="AI author persona"><i class="bi bi-robot me-1" aria-hidden="true"></i>AI</span>{%- endif -%}
198
+ </h5>
199
+ {% if author_data.role %}<p class="text-primary mb-2" itemprop="jobTitle">{{ author_data.role }}</p>{% endif %}
200
+ {% if author_data.tagline %}<p class="author-tagline fst-italic text-body-secondary mb-2">{{ author_data.tagline }}</p>{% endif %}
201
+ {% if show_bio and author_data.bio %}<p class="card-text text-muted" itemprop="description">{{ author_data.bio }}</p>{% endif %}
202
+
203
+ {% if show_expertise and author_data.expertise %}
204
+ <div class="author-expertise d-flex flex-wrap gap-1 mb-2">
205
+ {% for skill in author_data.expertise %}
206
+ <span class="badge rounded-pill text-bg-light border" itemprop="knowsAbout">{{ skill }}</span>
207
+ {% endfor %}
208
+ </div>
132
209
  {% endif %}
133
-
210
+
134
211
  {% if show_social %}
135
212
  <div class="author-social d-flex gap-2 mt-2">
136
213
  {% if author_data.github %}
137
- <a href="https://github.com/{{ author_data.github }}"
138
- class="btn btn-sm btn-outline-secondary"
139
- target="_blank" rel="noopener" title="GitHub">
140
- <i class="bi bi-github"></i>
141
- </a>
214
+ <a href="https://github.com/{{ author_data.github }}" class="btn btn-sm btn-outline-secondary" target="_blank" rel="noopener" title="GitHub" itemprop="sameAs"><i class="bi bi-github"></i></a>
142
215
  {% endif %}
143
216
  {% if author_data.twitter %}
144
- <a href="https://x.com/{{ author_data.twitter }}"
145
- class="btn btn-sm btn-outline-secondary"
146
- target="_blank" rel="noopener" title="X" aria-label="X">
147
- <i class="bi bi-twitter-x"></i>
148
- </a>
217
+ <a href="https://x.com/{{ author_data.twitter }}" class="btn btn-sm btn-outline-secondary" target="_blank" rel="noopener" title="X" aria-label="X" itemprop="sameAs"><i class="bi bi-twitter-x"></i></a>
149
218
  {% endif %}
150
219
  {% if author_data.linkedin %}
151
- <a href="https://linkedin.com/in/{{ author_data.linkedin }}"
152
- class="btn btn-sm btn-outline-secondary"
153
- target="_blank" rel="noopener" title="LinkedIn">
154
- <i class="bi bi-linkedin"></i>
155
- </a>
220
+ <a href="https://linkedin.com/in/{{ author_data.linkedin }}" class="btn btn-sm btn-outline-secondary" target="_blank" rel="noopener" title="LinkedIn" itemprop="sameAs"><i class="bi bi-linkedin"></i></a>
156
221
  {% endif %}
157
222
  {% if author_data.website %}
158
- <a href="{{ author_data.website }}"
159
- class="btn btn-sm btn-outline-secondary"
160
- target="_blank" rel="noopener" title="Website">
161
- <i class="bi bi-globe"></i>
162
- </a>
223
+ <a href="{{ author_data.website }}" class="btn btn-sm btn-outline-secondary" target="_blank" rel="noopener" title="Website" itemprop="url"><i class="bi bi-globe"></i></a>
163
224
  {% endif %}
164
225
  {% if author_data.email %}
165
- <a href="mailto:{{ author_data.email }}"
166
- class="btn btn-sm btn-outline-secondary"
167
- title="Email">
168
- <i class="bi bi-envelope"></i>
169
- </a>
226
+ <a href="mailto:{{ author_data.email }}" class="btn btn-sm btn-outline-secondary" title="Email"><i class="bi bi-envelope"></i></a>
170
227
  {% endif %}
171
228
  </div>
172
229
  {% endif %}
230
+
231
+ {% if author_data.ai and author_data.persona.disclosure %}
232
+ <p class="author-ai-disclosure small text-body-secondary mt-3 mb-0 d-flex align-items-start">
233
+ <i class="bi bi-robot me-2 mt-1 flex-shrink-0" aria-hidden="true"></i><span>{{ author_data.persona.disclosure }}</span>
234
+ </p>
235
+ {% endif %}
173
236
  </div>
174
237
  </div>
175
238
  </div>
@@ -26,6 +26,12 @@
26
26
  {% assign author = site.data.authors[author_key] %}
27
27
  {% assign eeat_style = include.style | default: "banner" %}
28
28
 
29
+ {% comment %} Resolve avatar: full URL (e.g. GitHub) as-is, relative path prefixed,
30
+ or derived from the github handle. See components/author-avatar-url.html {% endcomment %}
31
+ {% capture avatar_url %}{% include components/author-avatar-url.html data=author %}{% endcapture %}
32
+ {% assign avatar_url = avatar_url | strip %}
33
+ {% assign has_avatar = false %}{% if avatar_url != "" %}{% assign has_avatar = true %}{% endif %}
34
+
29
35
  {% if author %}
30
36
 
31
37
  {% if eeat_style == "banner" %}
@@ -34,8 +40,8 @@
34
40
  <div class="container-xl px-4 px-md-5">
35
41
  <div class="row align-items-center g-3">
36
42
  <div class="col-auto">
37
- {% if author.avatar %}
38
- <img src="{{ site.baseurl }}/{{ site.public_folder }}{{ author.avatar }}"
43
+ {% if has_avatar %}
44
+ <img src="{{ avatar_url }}"
39
45
  alt="{{ author.display_name | default: author.name }}"
40
46
  class="rounded-circle border border-2 border-primary"
41
47
  width="64" height="64"
@@ -88,8 +94,8 @@
88
94
  <div class="card border-0 shadow-sm" itemscope itemtype="https://schema.org/Person">
89
95
  <div class="card-body p-4">
90
96
  <div class="d-flex align-items-start">
91
- {% if author.avatar %}
92
- <img src="{{ site.baseurl }}/{{ site.public_folder }}{{ author.avatar }}"
97
+ {% if has_avatar %}
98
+ <img src="{{ avatar_url }}"
93
99
  alt="{{ author.display_name | default: author.name }}"
94
100
  class="rounded-circle me-3 border border-2 border-primary"
95
101
  width="72" height="72"
@@ -224,7 +224,7 @@
224
224
 
225
225
  <!-- Background Tab -->
226
226
  <div class="tab-pane fade" id="background-pane" role="tabpanel" aria-labelledby="background-tab" tabindex="0">
227
- {% include components/background-settings.html %}
227
+ {% include_cached components/background-settings.html %}
228
228
  </div>
229
229
 
230
230
  </div>
@@ -320,9 +320,6 @@
320
320
  });
321
321
  </script>
322
322
 
323
- <!-- Font Awesome for Mermaid icon support (bundled) -->
324
- <link rel="stylesheet" href="{{ '/assets/vendor/font-awesome/css/all.min.css' | relative_url }}">
325
-
326
323
  <!-- Custom CSS for Mermaid diagrams -->
327
324
  <style>
328
325
  /* Mermaid container styling */
@@ -16,10 +16,11 @@
16
16
  - show_post_type (optional): Show post_type badge (default: true)
17
17
  - card_class (optional): Additional CSS classes for the card
18
18
 
19
- Usage:
20
- {% include components/post-card.html post=post %}
21
- {% include components/post-card.html post=post show_category=false %}
22
- {% include components/post-card.html post=post card_class="shadow-lg" %}
19
+ Usage (wrap each line in Liquid include tags — braces omitted here because
20
+ Liquid parses tags even inside HTML comments, which would self-recurse):
21
+ include components/post-card.html post=post
22
+ include components/post-card.html post=post show_category=false
23
+ include components/post-card.html post=post card_class="shadow-lg"
23
24
 
24
25
  Features:
25
26
  - Breaking news badge (red, top-left)
@@ -50,6 +51,12 @@
50
51
  {% assign show_post_type = include.show_post_type | default: true %}
51
52
  {% assign card_class = include.card_class | default: "" %}
52
53
 
54
+ {% comment %} Optional: omit the Bootstrap .col wrapper so a caller (e.g. the
55
+ author profile grid) can supply its own wrapper + data attributes. Defaults
56
+ to true so every existing caller is unchanged. {% endcomment %}
57
+ {% assign render_col = true %}
58
+ {% if include.col == false %}{% assign render_col = false %}{% endif %}
59
+
53
60
  {% comment %}
54
61
  Reading time: Use estimated_reading_time from front matter if available,
55
62
  otherwise skip to avoid accessing post.content which causes nesting issues
@@ -60,7 +67,7 @@
60
67
  {% assign reading_time = "2 min" %}
61
68
  {% endif %}
62
69
 
63
- <div class="col">
70
+ {% if render_col %}<div class="col">{% endif %}
64
71
  <div class="card h-100 post-card border-0 shadow-sm {{ card_class }}">
65
72
 
66
73
  <!-- ====================== -->
@@ -101,10 +108,13 @@
101
108
  <div class="card-body d-flex flex-column">
102
109
 
103
110
  <!-- Category Badge -->
111
+ {% comment %} Category base is configurable (`category_base`, default `/news`)
112
+ to avoid hardcoded 404s for remote-theme consumers. See issue #204. {% endcomment %}
104
113
  {% if show_category and include.post.categories.size > 0 %}
114
+ {% assign _category_base = site.category_base | default: '/news' %}
105
115
  <div class="mb-2">
106
116
  {% assign primary_category = include.post.categories | first %}
107
- <a href="{{ site.baseurl }}/news/{{ primary_category | slugify }}/"
117
+ <a href="{{ site.baseurl }}{{ _category_base }}/{{ primary_category | slugify }}/"
108
118
  class="badge bg-primary text-decoration-none">
109
119
  {{ primary_category }}
110
120
  </a>
@@ -145,7 +155,7 @@
145
155
  <div>
146
156
  {% if show_author and include.post.author %}
147
157
  <span class="me-2">
148
- <i class="bi bi-person me-1"></i>{{ include.post.author }}
158
+ {% include components/author-card.html author=include.post.author style="inline" show_avatar=false %}
149
159
  </span>
150
160
  {% endif %}
151
161
  <span>
@@ -174,6 +184,6 @@
174
184
  </div>
175
185
  {% endif %}
176
186
  </div>
177
-
187
+
178
188
  </div>
179
- </div>
189
+ {% if render_col %}</div>{% endif %}
@@ -5,8 +5,9 @@
5
5
  ╠═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
6
6
  ║ ║
7
7
  ║ Purpose: Integrates Giscus comment system powered by GitHub Discussions ║
8
- ║ Location: /_includes/giscus.html
9
- ║ Usage: {% include giscus.html %} (typically at bottom of posts/pages)
8
+ ║ Location: /_includes/content/giscus.html
9
+ ║ Usage: include content/giscus.html (Liquid tag; braces omitted — Liquid
10
+ ║ parses include tags even inside HTML comments, which would error) ║
10
11
  ║ Dependencies: GitHub repository with Discussions enabled, site.repository, site.giscus configuration ║
11
12
  ║ ║
12
13
  ║ Features: ║
@@ -0,0 +1,28 @@
1
+ {%- comment -%}
2
+ ===================================================================
3
+ FOOTER FABS — page-dependent floating action buttons + local graph
4
+ ===================================================================
5
+
6
+ File: footer-fabs.html
7
+ Path: _includes/core/footer-fabs.html
8
+ Purpose: The page-front-matter-dependent tail extracted out of
9
+ core/footer.html so the footer chrome itself stays page-invariant
10
+ and cacheable via include_cached (see _layouts/root.html).
11
+
12
+ These pieces read per-page front matter (page.sidebar, page.post_type,
13
+ page.local_graph, page.layout) and therefore MUST render fresh per page:
14
+ - navigation/toc-fab.html (table-of-contents FAB)
15
+ - navigation/local-graph-fab.html (local-graph FAB)
16
+ - navigation/local-graph.html (local-graph offcanvas panel)
17
+
18
+ All are position:fixed with explicit z-index tokens
19
+ (_sass/components/_back-to-top.scss), so emitting them as siblings right
20
+ after </footer> is visually identical to the previous nesting.
21
+ ===================================================================
22
+ {%- endcomment -%}
23
+ {%- assign _root_only_layouts = "landing,welcome,stats,sitemap-collection,setup,section,news,index,home,admin" | split: "," -%}
24
+ {%- unless _root_only_layouts contains page.layout -%}
25
+ {% include navigation/toc-fab.html %}
26
+ {% include navigation/local-graph-fab.html %}
27
+ {% include navigation/local-graph.html %}
28
+ {%- endunless -%}
@@ -130,7 +130,7 @@
130
130
  {% endfor %}
131
131
  {% else %}
132
132
  <li><a href="{{ '/' | relative_url }}" class="text-light text-decoration-none">Home</a></li>
133
- {% assign _candidate_links = "About,/about/|Statistics,/about/stats/|Services,/services/|News,/news/|Contact,/contact/" | split: "|" %}
133
+ {% assign _candidate_links = "About,/about/|Authors,/authors/|Statistics,/about/stats/|Services,/services/|News,/news/|Contact,/contact/" | split: "|" %}
134
134
  {% for entry in _candidate_links %}
135
135
  {% assign _parts = entry | split: "," %}
136
136
  {% assign _label = _parts[0] %}
@@ -249,15 +249,13 @@
249
249
  </div>
250
250
 
251
251
  {%- comment -%}
252
- Floating action buttons live in the footer (not #main-content) so fixed
253
- positioning shares the footer stacking context with back-to-top and does
254
- not cover the powered-by row when the user scrolls to the page bottom.
252
+ Page-dependent floating action buttons (TOC / local-graph FABs) and the
253
+ local-graph offcanvas were moved to core/footer-fabs.html so THIS file is
254
+ fully page-invariant and can be rendered via include_cached (see
255
+ _layouts/root.html). The FABs are position:fixed with explicit z-index
256
+ tokens (_sass/components/_back-to-top.scss), so rendering them as a sibling
257
+ immediately after </footer> is visually identical to nesting them inside.
255
258
  {%- endcomment -%}
256
- {%- assign _root_only_layouts = "landing,welcome,stats,sitemap-collection,setup,section,news,index,home,admin" | split: "," -%}
257
- {%- unless _root_only_layouts contains page.layout -%}
258
- {% include navigation/toc-fab.html %}
259
- {% include navigation/local-graph-fab.html %}
260
- {%- endunless -%}
261
259
 
262
260
  <!-- Back to Top Button (controlled by assets/js/back-to-top.js) -->
263
261
  <!-- Positioning/stacking handled by _sass/components/_back-to-top.scss
@@ -275,11 +273,3 @@
275
273
  <span class="visually-hidden">Back to top</span>
276
274
  </a>
277
275
  </footer>
278
-
279
- {%- comment -%}
280
- Local graph panel is a body-level sibling (after footer) so Bootstrap offcanvas
281
- escapes the #main-content z-index stacking context and sits above the footer band.
282
- {%- endcomment -%}
283
- {%- unless _root_only_layouts contains page.layout -%}
284
- {% include navigation/local-graph.html %}
285
- {%- endunless -%}
@@ -54,8 +54,8 @@
54
54
  {% include components/mermaid.html %}
55
55
  {% endif %}
56
56
 
57
- <!-- Nano Progress Bar - Visual loading indicator (config-driven) -->
58
- {% include components/nanobar.html %}
57
+ <!-- Nano Progress Bar - Visual loading indicator (config-driven) — page-invariant, cached. -->
58
+ {% include_cached components/nanobar.html %}
59
59
 
60
60
  <!-- MathJax - Conditional loading based on page front matter (like Mermaid) -->
61
61
  {% if page.mathjax %}
@@ -35,10 +35,28 @@
35
35
  {%- assign known_sections = "posts,notebooks,notes,docs" | split: "," -%}
36
36
  {%- if known_sections contains section -%}
37
37
  {%- assign section_label = section | capitalize -%}
38
+ {%- comment -%}
39
+ Only link the collection-root crumb when that index page actually exists
40
+ in the build (check both standalone pages and collection docs). A pure
41
+ remote-theme Pages consumer doesn't get e.g. a `/posts/` index, so we
42
+ render the section name as plain text rather than a link that 404s. #204
43
+ {%- endcomment -%}
44
+ {%- assign _section_url = '/' | append: section | append: '/' -%}
45
+ {%- assign _section_page = site.html_pages | where: "url", _section_url | first -%}
46
+ {%- unless _section_page -%}
47
+ {%- for col in site.collections -%}
48
+ {%- assign _section_page = col.docs | where: "url", _section_url | first -%}
49
+ {%- if _section_page -%}{%- break -%}{%- endif -%}
50
+ {%- endfor -%}
51
+ {%- endunless -%}
38
52
  <li class="breadcrumb-item" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
39
- <a href="{{ '/' | append: section | append: '/' | relative_url }}" itemprop="item">
53
+ {%- if _section_page -%}
54
+ <a href="{{ _section_url | relative_url }}" itemprop="item">
55
+ <span itemprop="name">{{ section_label }}</span>
56
+ </a>
57
+ {%- else -%}
40
58
  <span itemprop="name">{{ section_label }}</span>
41
- </a>
59
+ {%- endif -%}
42
60
  <meta itemprop="position" content="2" />
43
61
  </li>
44
62
  <li class="breadcrumb-item active" aria-current="page" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">