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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +395 -0
- data/README.md +27 -19
- data/_data/authors.yml +154 -5
- data/_data/backlog.yml +5 -5
- data/_data/content_statistics.yml +273 -297
- data/_data/features.yml +4 -25
- data/_data/navigation/README.md +24 -0
- data/_data/navigation/about.yml +2 -0
- data/_data/navigation/main.yml +2 -7
- data/_data/roadmap.yml +86 -12
- data/_includes/components/author-avatar-url.html +28 -0
- data/_includes/components/author-bio.html +86 -0
- data/_includes/components/author-card.html +184 -121
- data/_includes/components/author-eeat.html +10 -4
- data/_includes/components/info-section.html +1 -1
- data/_includes/components/mermaid.html +0 -3
- data/_includes/components/post-card.html +19 -9
- data/_includes/content/giscus.html +3 -2
- data/_includes/core/footer-fabs.html +28 -0
- data/_includes/core/footer.html +7 -17
- data/_includes/core/head.html +2 -2
- data/_includes/navigation/breadcrumbs.html +20 -2
- data/_includes/navigation/local-graph.html +18 -2
- data/_includes/obsidian/full-graph.html +4 -6
- data/_layouts/article.html +44 -74
- data/_layouts/author.html +274 -0
- data/_layouts/authors.html +55 -0
- data/_layouts/news.html +3 -3
- data/_layouts/note.html +21 -6
- data/_layouts/notebook.html +21 -6
- data/_layouts/root.html +31 -17
- data/_layouts/section.html +3 -3
- data/_plugins/author_pages_generator.rb +121 -0
- data/_sass/components/_author.scss +219 -0
- data/_sass/components/_content-tables.scss +16 -1
- data/_sass/components/_notes-index.scss +102 -0
- data/_sass/components/_search-modal.scss +40 -0
- data/_sass/components/_ui-enhancements.scss +570 -0
- data/_sass/core/_docs-code-examples.scss +463 -0
- data/_sass/core/_docs-layout.scss +0 -453
- data/_sass/core/_navbar.scss +253 -0
- data/_sass/core/_sidebar-extras.scss +79 -0
- data/_sass/core/_toc.scss +87 -0
- data/_sass/core/_variables.scss +7 -142
- data/_sass/custom.scss +24 -1122
- data/_sass/layouts/_global-chrome.scss +59 -0
- data/assets/css/main.scss +19 -2
- data/assets/js/author-profile.js +190 -0
- data/assets/js/modules/navigation/navbar.js +104 -0
- data/assets/js/obsidian-graph.js +2 -2
- data/assets/js/obsidian-local-graph.js +11 -5
- data/assets/vendor/cytoscape/cytoscape.min.js +32 -0
- data/scripts/README.md +39 -0
- data/scripts/bin/validate +11 -1
- data/scripts/dev/css-diff.sh +49 -0
- data/scripts/dev/shot.js +37 -0
- data/scripts/features/generate-preview-images +110 -6
- data/scripts/features/pixelate-preview-images +126 -0
- data/scripts/features/pixelate_images.py +662 -0
- data/scripts/github-setup.sh +0 -0
- data/scripts/lib/preview_generator.py +47 -3
- data/scripts/pixelate-preview-images.sh +12 -0
- data/scripts/test/integration/auto-version +10 -8
- data/scripts/test/lib/run_tests.sh +2 -0
- data/scripts/test/lib/test_content_review.sh +205 -0
- data/scripts/test/lib/test_pixelate_images.sh +108 -0
- metadata +25 -20
- data/_data/hub.yml +0 -68
- data/_data/hub_index.yml +0 -203
- data/_data/navigation/hub.yml +0 -110
- data/assets/vendor/font-awesome/css/all.min.css +0 -9
- data/assets/vendor/font-awesome/webfonts/fa-brands-400.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-regular-400.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-solid-900.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.ttf +0 -0
- data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 +0 -0
- data/assets/vendor/jquery/jquery-3.7.1.min.js +0 -2
- data/scripts/lib/hub.rb +0 -208
- data/scripts/provision-org-sites.rb +0 -252
- data/scripts/provision-org-sites.sh +0 -23
- data/scripts/sync-hub-metadata.rb +0 -184
- data/scripts/sync-hub-metadata.sh +0 -22
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
- Renders the current page as a single-node graph when it has no neighbors
|
|
18
18
|
- Hides itself if the current page is not in the wiki-index or the index
|
|
19
19
|
can't be loaded
|
|
20
|
-
- Lazily loads cytoscape.js
|
|
20
|
+
- Lazily loads the vendored cytoscape.js (assets/vendor/cytoscape/, no CDN)
|
|
21
21
|
|
|
22
22
|
Opt-out: set `local_graph: false` in page front matter.
|
|
23
23
|
|
|
@@ -53,13 +53,29 @@
|
|
|
53
53
|
</div>
|
|
54
54
|
<div class="offcanvas-body">
|
|
55
55
|
<div class="obsidian-local-graph-widget" aria-label="Local graph">
|
|
56
|
+
{%- comment -%}
|
|
57
|
+
Only show the "Full graph" link when that docs page exists in the build
|
|
58
|
+
(configurable via `obsidian_graph_url`, default `/docs/obsidian/graph/`).
|
|
59
|
+
The page lives under pages/ and isn't delivered by remote_theme, so a
|
|
60
|
+
Pages consumer would otherwise get a 404 link. See issue #204.
|
|
61
|
+
{%- endcomment -%}
|
|
62
|
+
{%- assign _graph_url = site.obsidian_graph_url | default: '/docs/obsidian/graph/' -%}
|
|
63
|
+
{%- assign _graph_page = site.html_pages | where: "url", _graph_url | first -%}
|
|
64
|
+
{%- unless _graph_page -%}
|
|
65
|
+
{%- for col in site.collections -%}
|
|
66
|
+
{%- assign _graph_page = col.docs | where: "url", _graph_url | first -%}
|
|
67
|
+
{%- if _graph_page -%}{%- break -%}{%- endif -%}
|
|
68
|
+
{%- endfor -%}
|
|
69
|
+
{%- endunless -%}
|
|
56
70
|
<div class="obsidian-local-graph-meta d-flex align-items-center justify-content-between gap-2 mb-3">
|
|
57
71
|
<span class="badge text-bg-secondary">Depth {{ lg_depth }}</span>
|
|
58
|
-
|
|
72
|
+
{%- if _graph_page -%}
|
|
73
|
+
<a href="{{ _graph_url | relative_url }}"
|
|
59
74
|
class="btn btn-outline-primary btn-sm"
|
|
60
75
|
title="Open the full site graph">
|
|
61
76
|
<i class="bi bi-arrows-fullscreen me-1" aria-hidden="true"></i>Full graph
|
|
62
77
|
</a>
|
|
78
|
+
{%- endif -%}
|
|
63
79
|
</div>
|
|
64
80
|
<div id="obsidian-local-graph"
|
|
65
81
|
data-depth="{{ lg_depth }}"
|
|
@@ -160,18 +160,16 @@ and [backlinks panel]({{ "/docs/obsidian/syntax-reference/#backlinks-panel" | re
|
|
|
160
160
|
| --- | --- |
|
|
161
161
|
| Build-time index (nodes + outgoing edges) | [`assets/data/wiki-index.json`]({{ "/assets/data/wiki-index.json" | relative_url }}) (Liquid template at [`assets/data/wiki-index.json`](https://github.com/bamr87/zer0-mistakes/blob/main/assets/data/wiki-index.json)) |
|
|
162
162
|
| Renderer | `assets/js/obsidian-graph.js` |
|
|
163
|
-
| Layout engine | [cytoscape.js](https://js.cytoscape.org/) (
|
|
163
|
+
| Layout engine | [cytoscape.js](https://js.cytoscape.org/) (vendored under `assets/vendor/`, loaded only on this page) |
|
|
164
164
|
|
|
165
165
|
Outgoing edges come from the same `[[…]]` syntax the resolver handles —
|
|
166
166
|
unresolved targets show up as dashed red nodes so you can find dangling
|
|
167
167
|
links at a glance. The graph is regenerated every Jekyll build; nothing
|
|
168
168
|
runs client-side except cytoscape's force layout.
|
|
169
169
|
|
|
170
|
-
<!-- Cytoscape.js (only loaded on this page)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
crossorigin="anonymous"
|
|
174
|
-
defer></script>
|
|
170
|
+
<!-- Cytoscape.js (vendored, only loaded on this page) — no CDN so the graph
|
|
171
|
+
works under strict CSP and offline. See assets/vendor/cytoscape/. -->
|
|
172
|
+
<script src="{{ '/assets/vendor/cytoscape/cytoscape.min.js' | relative_url }}" defer></script>
|
|
175
173
|
<script src="{{ '/assets/js/obsidian-graph.js' | relative_url }}" defer></script>
|
|
176
174
|
|
|
177
175
|
## See also
|
data/_layouts/article.html
CHANGED
|
@@ -126,8 +126,7 @@ layout: default
|
|
|
126
126
|
<!-- Author -->
|
|
127
127
|
{% if page.author %}
|
|
128
128
|
<span class="post-author" itemprop="author" itemscope itemtype="https://schema.org/Person">
|
|
129
|
-
|
|
130
|
-
<span itemprop="name">{{ page.author }}</span>
|
|
129
|
+
{% include components/author-card.html author=page.author style="inline" name_itemprop="name" %}
|
|
131
130
|
</span>
|
|
132
131
|
<span class="mx-2">•</span>
|
|
133
132
|
{% endif %}
|
|
@@ -161,31 +160,59 @@ layout: default
|
|
|
161
160
|
</div>
|
|
162
161
|
|
|
163
162
|
<!-- Category Link -->
|
|
163
|
+
{% comment %} Category base is configurable (`category_base`, default `/news`)
|
|
164
|
+
so remote-theme consumers whose category index lives elsewhere
|
|
165
|
+
(e.g. `/categories`) don't get a hardcoded 404. See issue #204. {% endcomment %}
|
|
164
166
|
{% if page.categories.size > 0 %}
|
|
167
|
+
{% assign _category_base = site.category_base | default: '/news' %}
|
|
165
168
|
<div class="mb-3">
|
|
166
|
-
<a href="{{ site.baseurl }}/
|
|
169
|
+
<a href="{{ site.baseurl }}{{ _category_base }}/{{ page.categories | first | slugify }}/"
|
|
167
170
|
class="badge bg-primary text-decoration-none fs-6">
|
|
168
171
|
<i class="bi bi-folder me-1"></i>{{ page.categories | first }}
|
|
169
172
|
</a>
|
|
170
173
|
</div>
|
|
171
174
|
{% endif %}
|
|
172
|
-
|
|
175
|
+
|
|
173
176
|
<!-- Tags -->
|
|
177
|
+
{% comment %} Only link tag badges when a tags index page actually exists in
|
|
178
|
+
the build (configurable via `tags_page`, default `/tags/`). On a
|
|
179
|
+
remote-theme Pages consumer the tags page is plugin/page-generated and
|
|
180
|
+
absent, so we render plain badges instead of links that 404. See #204. {% endcomment %}
|
|
174
181
|
{% if page.tags and page.tags.size > 0 %}
|
|
182
|
+
{% assign _tags_base = site.tags_page | default: '/tags/' %}
|
|
183
|
+
{% assign _tags_page = site.html_pages | where: "url", _tags_base | first %}
|
|
175
184
|
<div class="post-tags mb-3">
|
|
176
185
|
{% for tag in page.tags %}
|
|
177
|
-
|
|
178
|
-
<
|
|
179
|
-
|
|
186
|
+
{% if _tags_page %}
|
|
187
|
+
<a href="{{ site.baseurl }}{{ _tags_base }}#{{ tag | slugify }}" class="badge bg-secondary text-decoration-none me-1">
|
|
188
|
+
<i class="bi bi-tag me-1"></i>{{ tag }}
|
|
189
|
+
</a>
|
|
190
|
+
{% else %}
|
|
191
|
+
<span class="badge bg-secondary me-1"><i class="bi bi-tag me-1"></i>{{ tag }}</span>
|
|
192
|
+
{% endif %}
|
|
180
193
|
{% endfor %}
|
|
181
194
|
</div>
|
|
182
195
|
{% endif %}
|
|
183
196
|
</header>
|
|
184
197
|
|
|
185
198
|
{% comment %} ================================ {% endcomment %}
|
|
186
|
-
{% comment %} FEATURED HERO IMAGE
|
|
199
|
+
{% comment %} FEATURED / BREAKING HERO IMAGE {% endcomment %}
|
|
187
200
|
{% comment %} ================================ {% endcomment %}
|
|
188
|
-
{%
|
|
201
|
+
{% comment %}
|
|
202
|
+
Render the hero image for prominent post types (featured + breaking) when a
|
|
203
|
+
preview image is set. Breaking stories were previously skipped because the
|
|
204
|
+
condition checked only post_type == "featured"; since breaking is mapped
|
|
205
|
+
before featured (see top of file), breaking+featured posts showed no hero.
|
|
206
|
+
Nested ifs keep the "only when page.preview exists" guard intact (Liquid has
|
|
207
|
+
no parentheses, so a flat `a or b and c` would misgroup).
|
|
208
|
+
{% endcomment %}
|
|
209
|
+
{% assign show_hero = false %}
|
|
210
|
+
{% if page.preview %}
|
|
211
|
+
{% if post_type == "featured" or post_type == "breaking" %}
|
|
212
|
+
{% assign show_hero = true %}
|
|
213
|
+
{% endif %}
|
|
214
|
+
{% endif %}
|
|
215
|
+
{% if show_hero %}
|
|
189
216
|
<figure class="featured-hero mb-5">
|
|
190
217
|
{% include components/preview-image.html
|
|
191
218
|
src=page.preview
|
|
@@ -366,71 +393,14 @@ layout: default
|
|
|
366
393
|
<!-- ================================ -->
|
|
367
394
|
<!-- AUTHOR BIO SECTION -->
|
|
368
395
|
<!-- ================================ -->
|
|
369
|
-
{%
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
{% endunless %}
|
|
378
|
-
|
|
379
|
-
<aside class="author-section my-5 pt-4 border-top">
|
|
380
|
-
<h3 class="h5 mb-3">
|
|
381
|
-
<i class="bi bi-person-badge me-2"></i>About the Author
|
|
382
|
-
</h3>
|
|
383
|
-
<div class="author-card card border-0 shadow-sm">
|
|
384
|
-
<div class="card-body">
|
|
385
|
-
<div class="d-flex align-items-start">
|
|
386
|
-
{% if author_data.avatar %}
|
|
387
|
-
<img src="{{ site.baseurl }}/{{ site.public_folder }}{{ author_data.avatar }}"
|
|
388
|
-
alt="{{ author_name }}"
|
|
389
|
-
class="rounded-circle me-3"
|
|
390
|
-
width="80" height="80"
|
|
391
|
-
style="object-fit: cover;">
|
|
392
|
-
{% else %}
|
|
393
|
-
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-3"
|
|
394
|
-
style="width: 80px; height: 80px;">
|
|
395
|
-
<i class="bi bi-person fs-1"></i>
|
|
396
|
-
</div>
|
|
397
|
-
{% endif %}
|
|
398
|
-
<div class="flex-grow-1">
|
|
399
|
-
<h5 class="card-title mb-1">{{ author_name }}</h5>
|
|
400
|
-
{% if author_data.role %}
|
|
401
|
-
<p class="text-primary mb-2">{{ author_data.role }}</p>
|
|
402
|
-
{% endif %}
|
|
403
|
-
{% if author_data.bio %}
|
|
404
|
-
<p class="card-text text-muted">{{ author_data.bio }}</p>
|
|
405
|
-
{% endif %}
|
|
406
|
-
<div class="author-social d-flex gap-2 mt-2">
|
|
407
|
-
{% if author_data.github %}
|
|
408
|
-
<a href="https://github.com/{{ author_data.github }}"
|
|
409
|
-
class="btn btn-sm btn-outline-secondary"
|
|
410
|
-
target="_blank" rel="noopener" title="GitHub">
|
|
411
|
-
<i class="bi bi-github"></i>
|
|
412
|
-
</a>
|
|
413
|
-
{% endif %}
|
|
414
|
-
{% if author_data.twitter %}
|
|
415
|
-
<a href="https://x.com/{{ author_data.twitter }}"
|
|
416
|
-
class="btn btn-sm btn-outline-secondary"
|
|
417
|
-
target="_blank" rel="noopener" title="X" aria-label="X">
|
|
418
|
-
<i class="bi bi-twitter-x"></i>
|
|
419
|
-
</a>
|
|
420
|
-
{% endif %}
|
|
421
|
-
{% if author_data.website %}
|
|
422
|
-
<a href="{{ author_data.website }}"
|
|
423
|
-
class="btn btn-sm btn-outline-secondary"
|
|
424
|
-
target="_blank" rel="noopener" title="Website">
|
|
425
|
-
<i class="bi bi-globe"></i>
|
|
426
|
-
</a>
|
|
427
|
-
{% endif %}
|
|
428
|
-
</div>
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
</div>
|
|
432
|
-
</div>
|
|
433
|
-
</aside>
|
|
396
|
+
{% comment %}
|
|
397
|
+
"About the Author" is honored when the post has an author and the
|
|
398
|
+
`author_profile` front-matter flag is not false (defaults to true for the
|
|
399
|
+
posts collection in _config.yml). Rendering is delegated to the shared
|
|
400
|
+
components/author-bio.html → components/author-card.html primitives.
|
|
401
|
+
{% endcomment %}
|
|
402
|
+
{% if page.author and page.author_profile != false %}
|
|
403
|
+
{% include components/author-bio.html author=page.author %}
|
|
434
404
|
{% endif %}
|
|
435
405
|
|
|
436
406
|
<!-- ================================ -->
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
hide_intro: true
|
|
4
|
+
---
|
|
5
|
+
{% comment %}
|
|
6
|
+
===================================================================
|
|
7
|
+
AUTHOR LAYOUT - Interactive author profile page (content hub)
|
|
8
|
+
===================================================================
|
|
9
|
+
|
|
10
|
+
File: author.html
|
|
11
|
+
Path: _layouts/author.html
|
|
12
|
+
Inherits: default.html (which inherits root.html)
|
|
13
|
+
Purpose: A dedicated, interactive profile for one author — bio/blurb, social,
|
|
14
|
+
expertise, an at-a-glance stats dashboard, and a filterable /
|
|
15
|
+
searchable / sortable grid of every piece of content they've written
|
|
16
|
+
across ALL collections (posts, docs, notes, notebooks, quickstart,
|
|
17
|
+
about, quests, hobbies, …).
|
|
18
|
+
|
|
19
|
+
Front Matter Variables:
|
|
20
|
+
- author_key (required): Key into _data/authors.yml. Set automatically by
|
|
21
|
+
_plugins/author_pages_generator.rb, or by hand for a committed page.
|
|
22
|
+
- hide_intro: true (set above) — this layout renders its own hero/breadcrumb,
|
|
23
|
+
so the generic content/intro.html header is suppressed (no duplicate H1).
|
|
24
|
+
|
|
25
|
+
Interactivity is progressive-enhancement: with JS off, all content renders as
|
|
26
|
+
a plain grid (every item visible, crawlable). assets/js/author-profile.js
|
|
27
|
+
activates on the [data-author-profile] container to power the filters.
|
|
28
|
+
|
|
29
|
+
Dependencies:
|
|
30
|
+
- _data/authors.yml: Author data
|
|
31
|
+
- components/post-card.html (col=false): Content grid cards
|
|
32
|
+
- assets/js/author-profile.js: filter / search / sort controller
|
|
33
|
+
- _sass/components/_author.scss: styling
|
|
34
|
+
===================================================================
|
|
35
|
+
{% endcomment %}
|
|
36
|
+
|
|
37
|
+
{% assign author_key = page.author_key %}
|
|
38
|
+
{% assign author_data = site.data.authors[author_key] %}
|
|
39
|
+
{% assign author_name = author_data.name | default: author_key %}
|
|
40
|
+
{% comment %} Resolve avatar: full URL (e.g. GitHub) as-is, relative path prefixed,
|
|
41
|
+
or derived from the github handle. See components/author-avatar-url.html {% endcomment %}
|
|
42
|
+
{% capture avatar_url %}{% include components/author-avatar-url.html data=author_data %}{% endcapture %}
|
|
43
|
+
{% assign avatar_url = avatar_url | strip %}
|
|
44
|
+
{% assign has_avatar = false %}{% if avatar_url != "" %}{% assign has_avatar = true %}{% endif %}
|
|
45
|
+
|
|
46
|
+
{% comment %} ---------------------------------------------------------------
|
|
47
|
+
Aggregate this author's content across every collection. A document matches
|
|
48
|
+
when its `author` equals the key, the name, or the display_name.
|
|
49
|
+
--------------------------------------------------------------------------- {% endcomment %}
|
|
50
|
+
{% assign aliases = "" | split: "" %}
|
|
51
|
+
{% assign aliases = aliases | push: author_key %}
|
|
52
|
+
{% if author_data.name %}{% assign aliases = aliases | push: author_data.name %}{% endif %}
|
|
53
|
+
{% if author_data.display_name %}{% assign aliases = aliases | push: author_data.display_name %}{% endif %}
|
|
54
|
+
{% assign author_docs = site.documents | where_exp: "d", "aliases contains d.author" | where_exp: "d", "d.title" | sort: "date" | reverse %}
|
|
55
|
+
|
|
56
|
+
{% comment %} Distinct collection labels present, unique tags, latest dated item {% endcomment %}
|
|
57
|
+
{% assign collection_labels = "" | split: "" %}
|
|
58
|
+
{% assign all_tags = "" | split: "" %}
|
|
59
|
+
{% for doc in author_docs %}
|
|
60
|
+
{% unless collection_labels contains doc.collection %}{% assign collection_labels = collection_labels | push: doc.collection %}{% endunless %}
|
|
61
|
+
{% for t in doc.tags %}{% assign all_tags = all_tags | push: t %}{% endfor %}
|
|
62
|
+
{% endfor %}
|
|
63
|
+
{% assign collection_labels = collection_labels | sort %}
|
|
64
|
+
{% assign unique_tags = all_tags | uniq | sort %}
|
|
65
|
+
{% assign latest_date = nil %}
|
|
66
|
+
{% for doc in author_docs %}{% if doc.date %}{% assign latest_date = doc.date %}{% break %}{% endif %}{% endfor %}
|
|
67
|
+
|
|
68
|
+
<!-- ========================== -->
|
|
69
|
+
<!-- BREADCRUMB -->
|
|
70
|
+
<!-- ========================== -->
|
|
71
|
+
<nav aria-label="breadcrumb" class="mb-3">
|
|
72
|
+
<ol class="breadcrumb">
|
|
73
|
+
<li class="breadcrumb-item"><a href="{{ '/' | relative_url }}">Home</a></li>
|
|
74
|
+
<li class="breadcrumb-item"><a href="{{ '/authors/' | relative_url }}">Authors</a></li>
|
|
75
|
+
<li class="breadcrumb-item active" aria-current="page">{{ author_name }}</li>
|
|
76
|
+
</ol>
|
|
77
|
+
</nav>
|
|
78
|
+
|
|
79
|
+
<!-- ========================== -->
|
|
80
|
+
<!-- PROFILE HERO -->
|
|
81
|
+
<!-- ========================== -->
|
|
82
|
+
<header class="author-hero card border-0 shadow-sm mb-4" itemscope itemtype="https://schema.org/Person">
|
|
83
|
+
<div class="card-body p-4 p-md-5">
|
|
84
|
+
<div class="d-flex flex-column flex-sm-row align-items-center align-items-sm-start gap-4">
|
|
85
|
+
{% if has_avatar %}
|
|
86
|
+
<img src="{{ avatar_url }}" alt="{{ author_name }}" class="author-hero__avatar rounded-circle flex-shrink-0" width="128" height="128" style="object-fit: cover;" itemprop="image">
|
|
87
|
+
{% else %}
|
|
88
|
+
<div class="author-avatar-fallback rounded-circle bg-primary text-white d-flex align-items-center justify-content-center flex-shrink-0" style="width: 128px; height: 128px;">
|
|
89
|
+
<i class="bi bi-person fs-1" aria-hidden="true"></i>
|
|
90
|
+
</div>
|
|
91
|
+
{% endif %}
|
|
92
|
+
|
|
93
|
+
<div class="flex-grow-1 text-center text-sm-start">
|
|
94
|
+
<h1 class="author-hero__name h2 mb-1" itemprop="name">{{ author_name }}{% if author_data.ai %} <span class="badge author-ai-badge align-middle"><i class="bi bi-robot me-1" aria-hidden="true"></i>AI author</span>{% endif %}</h1>
|
|
95
|
+
{% if author_data.role %}<p class="text-primary fw-semibold mb-2" itemprop="jobTitle">{{ author_data.role }}</p>{% endif %}
|
|
96
|
+
{% if author_data.tagline %}<p class="author-hero__tagline lead mb-2" itemprop="slogan">{{ author_data.tagline }}</p>{% endif %}
|
|
97
|
+
{% if author_data.bio %}<p class="text-body-secondary mb-3" itemprop="description">{{ author_data.bio }}</p>{% endif %}
|
|
98
|
+
|
|
99
|
+
<!-- Hero meta: location + latest activity -->
|
|
100
|
+
<div class="author-hero__meta d-flex flex-wrap justify-content-center justify-content-sm-start gap-3 text-body-secondary small mb-3">
|
|
101
|
+
{% if author_data.location %}<span itemprop="homeLocation"><i class="bi bi-geo-alt me-1" aria-hidden="true"></i>{{ author_data.location }}</span>{% endif %}
|
|
102
|
+
<span><i class="bi bi-collection me-1" aria-hidden="true"></i>{{ author_docs.size }} contribution{% unless author_docs.size == 1 %}s{% endunless %}</span>
|
|
103
|
+
{% if latest_date %}<span><i class="bi bi-clock-history me-1" aria-hidden="true"></i>Last active {{ latest_date | date: "%b %Y" }}</span>{% endif %}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<!-- Expertise chips -->
|
|
107
|
+
{% assign hero_chips = author_data.topics | default: author_data.expertise %}
|
|
108
|
+
{% if hero_chips %}
|
|
109
|
+
<div class="author-expertise d-flex flex-wrap justify-content-center justify-content-sm-start gap-1 mb-3">
|
|
110
|
+
{% for skill in hero_chips %}
|
|
111
|
+
<span class="badge rounded-pill text-bg-light border" itemprop="knowsAbout">{{ skill }}</span>
|
|
112
|
+
{% endfor %}
|
|
113
|
+
</div>
|
|
114
|
+
{% endif %}
|
|
115
|
+
|
|
116
|
+
<!-- Social links -->
|
|
117
|
+
<div class="author-social d-flex flex-wrap justify-content-center justify-content-sm-start gap-2">
|
|
118
|
+
{% if author_data.github %}<a href="https://github.com/{{ author_data.github }}" class="btn btn-sm btn-outline-secondary" target="_blank" rel="noopener" itemprop="sameAs"><i class="bi bi-github me-1" aria-hidden="true"></i>GitHub</a>{% endif %}
|
|
119
|
+
{% if author_data.twitter %}<a href="https://x.com/{{ author_data.twitter }}" class="btn btn-sm btn-outline-secondary" target="_blank" rel="noopener" aria-label="X" itemprop="sameAs"><i class="bi bi-twitter-x me-1" aria-hidden="true"></i>X</a>{% endif %}
|
|
120
|
+
{% if author_data.linkedin %}<a href="https://linkedin.com/in/{{ author_data.linkedin }}" class="btn btn-sm btn-outline-secondary" target="_blank" rel="noopener" itemprop="sameAs"><i class="bi bi-linkedin me-1" aria-hidden="true"></i>LinkedIn</a>{% endif %}
|
|
121
|
+
{% if author_data.website %}<a href="{{ author_data.website }}" class="btn btn-sm btn-outline-secondary" target="_blank" rel="noopener" itemprop="url"><i class="bi bi-globe me-1" aria-hidden="true"></i>Website</a>{% endif %}
|
|
122
|
+
{% if author_data.email %}<a href="mailto:{{ author_data.email }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-envelope me-1" aria-hidden="true"></i>Email</a>{% endif %}
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{% if author_data.ai and author_data.persona.disclosure %}
|
|
126
|
+
<p class="author-ai-disclosure small text-body-secondary mt-3 mb-0 d-flex align-items-start">
|
|
127
|
+
<i class="bi bi-robot me-2 mt-1 flex-shrink-0" aria-hidden="true"></i><span>{{ author_data.persona.disclosure }}</span>
|
|
128
|
+
</p>
|
|
129
|
+
{% endif %}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</header>
|
|
134
|
+
|
|
135
|
+
{% if author_docs.size > 0 %}
|
|
136
|
+
<!-- ========================== -->
|
|
137
|
+
<!-- INTERACTIVE CONTENT HUB -->
|
|
138
|
+
<!-- ========================== -->
|
|
139
|
+
<section class="author-content" data-author-profile>
|
|
140
|
+
|
|
141
|
+
<h2 class="visually-hidden">Contributions by {{ author_name }}</h2>
|
|
142
|
+
|
|
143
|
+
<!-- Stats dashboard = interactive type filters (each card filters the grid) -->
|
|
144
|
+
<div class="author-stats" role="group" aria-label="Filter contributions by type">
|
|
145
|
+
<button type="button" class="author-stat is-active" data-filter="all" aria-pressed="true">
|
|
146
|
+
<span class="author-stat__num">{{ author_docs.size }}</span>
|
|
147
|
+
<span class="author-stat__label"><i class="bi bi-grid-3x3-gap me-1" aria-hidden="true"></i>All</span>
|
|
148
|
+
</button>
|
|
149
|
+
{% for clabel in collection_labels %}
|
|
150
|
+
{% assign cdocs = author_docs | where_exp: "d", "d.collection == clabel" %}
|
|
151
|
+
{% case clabel %}
|
|
152
|
+
{% when 'posts' %}{% assign cicon = 'bi-newspaper' %}
|
|
153
|
+
{% when 'docs' %}{% assign cicon = 'bi-book' %}
|
|
154
|
+
{% when 'notes' %}{% assign cicon = 'bi-journal-text' %}
|
|
155
|
+
{% when 'notebooks' %}{% assign cicon = 'bi-journal-code' %}
|
|
156
|
+
{% when 'quickstart' %}{% assign cicon = 'bi-rocket-takeoff' %}
|
|
157
|
+
{% when 'about' %}{% assign cicon = 'bi-person-lines-fill' %}
|
|
158
|
+
{% when 'quests' %}{% assign cicon = 'bi-compass' %}
|
|
159
|
+
{% when 'hobbies' %}{% assign cicon = 'bi-controller' %}
|
|
160
|
+
{% else %}{% assign cicon = 'bi-file-earmark-text' %}
|
|
161
|
+
{% endcase %}
|
|
162
|
+
<button type="button" class="author-stat" data-filter="{{ clabel }}" aria-pressed="false">
|
|
163
|
+
<span class="author-stat__num">{{ cdocs.size }}</span>
|
|
164
|
+
<span class="author-stat__label"><i class="bi {{ cicon }} me-1" aria-hidden="true"></i>{{ clabel | capitalize }}</span>
|
|
165
|
+
</button>
|
|
166
|
+
{% endfor %}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<!-- Controls: search + sort + live result count -->
|
|
170
|
+
<div class="author-controls d-flex flex-column flex-md-row gap-2 align-items-stretch align-items-md-center mt-3">
|
|
171
|
+
<div class="author-search flex-grow-1 position-relative">
|
|
172
|
+
<i class="bi bi-search author-search__icon" aria-hidden="true"></i>
|
|
173
|
+
<label class="visually-hidden" for="author-search-input">Search {{ author_name }}'s contributions</label>
|
|
174
|
+
<input type="search" id="author-search-input" class="form-control" placeholder="Search titles and tags…" data-author-search autocomplete="off" aria-controls="author-grid">
|
|
175
|
+
</div>
|
|
176
|
+
<div class="d-flex gap-2">
|
|
177
|
+
<label class="visually-hidden" for="author-sort">Sort contributions</label>
|
|
178
|
+
<select id="author-sort" class="form-select w-auto" data-author-sort aria-label="Sort contributions">
|
|
179
|
+
<option value="newest">Newest first</option>
|
|
180
|
+
<option value="oldest">Oldest first</option>
|
|
181
|
+
<option value="az">Title A–Z</option>
|
|
182
|
+
</select>
|
|
183
|
+
<button type="button" class="btn btn-outline-secondary" data-author-clear hidden>
|
|
184
|
+
<i class="bi bi-x-lg me-1" aria-hidden="true"></i>Clear
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!-- Topic / tag cloud (clickable filters) -->
|
|
190
|
+
{% if unique_tags.size > 0 %}
|
|
191
|
+
<div class="author-tagcloud mt-3" role="group" aria-label="Filter by topic">
|
|
192
|
+
<span class="author-tagcloud__label text-body-secondary small me-1"><i class="bi bi-tags me-1" aria-hidden="true"></i>Topics:</span>
|
|
193
|
+
{% for tag in unique_tags limit: 24 %}
|
|
194
|
+
<button type="button" class="badge rounded-pill author-tag" data-tag-filter="{{ tag | downcase | escape }}" aria-pressed="false">{{ tag }}</button>
|
|
195
|
+
{% endfor %}
|
|
196
|
+
</div>
|
|
197
|
+
{% endif %}
|
|
198
|
+
|
|
199
|
+
<!-- Live result count (announced to screen readers) -->
|
|
200
|
+
<p class="author-resultcount text-body-secondary small mt-3 mb-2" data-author-count aria-live="polite">
|
|
201
|
+
Showing {{ author_docs.size }} of {{ author_docs.size }}
|
|
202
|
+
</p>
|
|
203
|
+
|
|
204
|
+
<!-- Content grid -->
|
|
205
|
+
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" id="author-grid" data-author-grid>
|
|
206
|
+
{% for doc in author_docs %}
|
|
207
|
+
{% assign dtags = doc.tags | join: "|" | downcase %}
|
|
208
|
+
<div class="col author-item"
|
|
209
|
+
data-collection="{{ doc.collection }}"
|
|
210
|
+
data-date="{{ doc.date | date: '%s' }}"
|
|
211
|
+
data-title="{{ doc.title | downcase | escape }}"
|
|
212
|
+
data-tags="{{ dtags | escape }}">
|
|
213
|
+
{% include components/post-card.html post=doc show_author=false col=false %}
|
|
214
|
+
</div>
|
|
215
|
+
{% endfor %}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<!-- No-results state (shown by JS when filters match nothing) -->
|
|
219
|
+
<div class="author-noresults text-center py-5 d-none" data-author-noresults>
|
|
220
|
+
<i class="bi bi-search fs-1 text-muted" aria-hidden="true"></i>
|
|
221
|
+
<p class="text-muted mt-3 mb-2">No contributions match your filters.</p>
|
|
222
|
+
<button type="button" class="btn btn-outline-primary btn-sm" data-author-clear>Clear filters</button>
|
|
223
|
+
</div>
|
|
224
|
+
</section>
|
|
225
|
+
|
|
226
|
+
<!-- Structured data: this author's contributions as a CollectionPage / ItemList -->
|
|
227
|
+
<script type="application/ld+json">
|
|
228
|
+
{
|
|
229
|
+
"@context": "https://schema.org",
|
|
230
|
+
"@type": "CollectionPage",
|
|
231
|
+
"name": {{ page.title | default: author_name | jsonify }},
|
|
232
|
+
"url": {{ page.url | absolute_url | jsonify }},
|
|
233
|
+
"about": {
|
|
234
|
+
"@type": "Person",
|
|
235
|
+
"name": {{ author_name | jsonify }}{% if author_data.website %},
|
|
236
|
+
"url": {{ author_data.website | jsonify }}{% endif %}
|
|
237
|
+
},
|
|
238
|
+
"mainEntity": {
|
|
239
|
+
"@type": "ItemList",
|
|
240
|
+
"numberOfItems": {{ author_docs.size }},
|
|
241
|
+
"itemListElement": [
|
|
242
|
+
{% for doc in author_docs limit: 100 %}{
|
|
243
|
+
"@type": "ListItem",
|
|
244
|
+
"position": {{ forloop.index }},
|
|
245
|
+
"url": {{ doc.url | absolute_url | jsonify }},
|
|
246
|
+
"name": {{ doc.title | jsonify }}
|
|
247
|
+
}{% unless forloop.last %},{% endunless %}
|
|
248
|
+
{% endfor %}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
</script>
|
|
253
|
+
|
|
254
|
+
<script defer src="{{ '/assets/js/author-profile.js' | relative_url }}?v={{ site.time | date: '%s' }}"></script>
|
|
255
|
+
|
|
256
|
+
{% else %}
|
|
257
|
+
{% comment %} Suppress the generic empty state when the page supplies its own
|
|
258
|
+
body content (e.g. the Guest profile, which is a contribution guide). {% endcomment %}
|
|
259
|
+
{% assign page_body = content | strip %}
|
|
260
|
+
{% if page_body == "" %}
|
|
261
|
+
<!-- Empty state -->
|
|
262
|
+
<div class="text-center py-5">
|
|
263
|
+
<i class="bi bi-journal-x fs-1 text-muted" aria-hidden="true"></i>
|
|
264
|
+
<p class="text-muted mt-3">No published content from this author yet.</p>
|
|
265
|
+
<a href="{{ '/' | relative_url }}" class="btn btn-outline-primary">Back to home</a>
|
|
266
|
+
</div>
|
|
267
|
+
{% endif %}
|
|
268
|
+
{% endif %}
|
|
269
|
+
|
|
270
|
+
{% if content and content != "" %}
|
|
271
|
+
<section class="author-page-body mt-4">
|
|
272
|
+
{{ content }}
|
|
273
|
+
</section>
|
|
274
|
+
{% endif %}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
---
|
|
4
|
+
<!--
|
|
5
|
+
===================================================================
|
|
6
|
+
AUTHORS INDEX LAYOUT - Directory of all author profiles
|
|
7
|
+
===================================================================
|
|
8
|
+
|
|
9
|
+
File: authors.html
|
|
10
|
+
Path: _layouts/authors.html
|
|
11
|
+
Inherits: default.html (which inherits root.html)
|
|
12
|
+
Purpose: List every author defined in _data/authors.yml, each linking to
|
|
13
|
+
their individual profile page (/authors/:key/).
|
|
14
|
+
|
|
15
|
+
Front Matter Variables:
|
|
16
|
+
- title (optional): Page title (default: "Authors")
|
|
17
|
+
- description (optional): Intro text
|
|
18
|
+
|
|
19
|
+
Opt-out: an author entry with `profile: false` in _data/authors.yml is
|
|
20
|
+
hidden from this directory (and skipped by the page generator).
|
|
21
|
+
|
|
22
|
+
Dependencies:
|
|
23
|
+
- _data/authors.yml
|
|
24
|
+
- components/author-card.html (style="compact")
|
|
25
|
+
===================================================================
|
|
26
|
+
-->
|
|
27
|
+
|
|
28
|
+
<header class="authors-index-header mb-5">
|
|
29
|
+
<div class="d-flex align-items-center mb-3">
|
|
30
|
+
<i class="bi bi-people-fill fs-1 me-3 text-primary" aria-hidden="true"></i>
|
|
31
|
+
<div>
|
|
32
|
+
<h1 class="display-5 mb-0">{{ page.title | default: "Authors" }}</h1>
|
|
33
|
+
<p class="text-muted mb-0">{{ page.description | default: "Meet the people behind the content." }}</p>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
{{ content }}
|
|
37
|
+
</header>
|
|
38
|
+
|
|
39
|
+
<section class="authors-index">
|
|
40
|
+
<div class="row row-cols-1 row-cols-md-2 g-4">
|
|
41
|
+
{% for author in site.data.authors %}
|
|
42
|
+
{% assign author_key = author[0] %}
|
|
43
|
+
{% assign author_data = author[1] %}
|
|
44
|
+
{% unless author_data.profile == false %}
|
|
45
|
+
<div class="col">
|
|
46
|
+
<div class="card h-100 border-0 shadow-sm">
|
|
47
|
+
<div class="card-body">
|
|
48
|
+
{% include components/author-card.html author=author_key style="compact" show_bio=true avatar_size=64 %}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
{% endunless %}
|
|
53
|
+
{% endfor %}
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
data/_layouts/news.html
CHANGED
|
@@ -121,7 +121,7 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
|
|
|
121
121
|
</small>
|
|
122
122
|
{% if hero_post.author %}
|
|
123
123
|
<small class="text-muted">
|
|
124
|
-
|
|
124
|
+
{% include components/author-card.html author=hero_post.author style="inline" show_avatar=false %}
|
|
125
125
|
</small>
|
|
126
126
|
{% endif %}
|
|
127
127
|
{% if hero_post.categories.size > 0 %}
|
|
@@ -218,7 +218,7 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
|
|
|
218
218
|
<i class="bi bi-calendar me-1"></i>{{ main_featured.date | date: "%b %d, %Y" }}
|
|
219
219
|
{% if main_featured.author %}
|
|
220
220
|
<span class="mx-2">•</span>
|
|
221
|
-
|
|
221
|
+
{% include components/author-card.html author=main_featured.author style="inline" show_avatar=false %}
|
|
222
222
|
{% endif %}
|
|
223
223
|
</small>
|
|
224
224
|
</div>
|
|
@@ -414,7 +414,7 @@ source: "https://getbootstrap.com/docs/5.3/examples/blog/#"
|
|
|
414
414
|
<div class="d-flex gap-3 text-muted small">
|
|
415
415
|
<span><i class="bi bi-calendar me-1"></i>{{ lpost.date | date: "%B %d, %Y" }}</span>
|
|
416
416
|
{% if lpost.author %}
|
|
417
|
-
<span
|
|
417
|
+
<span>{% include components/author-card.html author=lpost.author style="inline" show_avatar=false %}</span>
|
|
418
418
|
{% endif %}
|
|
419
419
|
</div>
|
|
420
420
|
</div>
|
data/_layouts/note.html
CHANGED
|
@@ -52,8 +52,7 @@ layout: default
|
|
|
52
52
|
<!-- Author -->
|
|
53
53
|
{% if page.author %}
|
|
54
54
|
<span class="note-author" itemprop="author" itemscope itemtype="https://schema.org/Person">
|
|
55
|
-
|
|
56
|
-
<span itemprop="name">{{ page.author }}</span>
|
|
55
|
+
{% include components/author-card.html author=page.author style="inline" name_itemprop="name" %}
|
|
57
56
|
</span>
|
|
58
57
|
<span class="mx-2">•</span>
|
|
59
58
|
{% endif %}
|
|
@@ -104,12 +103,21 @@ layout: default
|
|
|
104
103
|
</div>
|
|
105
104
|
|
|
106
105
|
<!-- Tags -->
|
|
106
|
+
{% comment %} Link tag badges only when a tags index exists (configurable via
|
|
107
|
+
`tags_page`); otherwise render plain badges so remote-theme consumers
|
|
108
|
+
without a /tags/ page don't get 404 links. See issue #204. {% endcomment %}
|
|
107
109
|
{% if page.tags and page.tags.size > 0 %}
|
|
110
|
+
{% assign _tags_base = site.tags_page | default: '/tags/' %}
|
|
111
|
+
{% assign _tags_page = site.html_pages | where: "url", _tags_base | first %}
|
|
108
112
|
<div class="note-tags mb-3">
|
|
109
113
|
{% for tag in page.tags %}
|
|
110
|
-
|
|
111
|
-
{{ tag }}
|
|
112
|
-
|
|
114
|
+
{% if _tags_page %}
|
|
115
|
+
<a href="{{ _tags_base | relative_url }}#{{ tag | slugify }}" class="badge bg-secondary text-decoration-none me-1">
|
|
116
|
+
{{ tag }}
|
|
117
|
+
</a>
|
|
118
|
+
{% else %}
|
|
119
|
+
<span class="badge bg-secondary me-1">{{ tag }}</span>
|
|
120
|
+
{% endif %}
|
|
113
121
|
{% endfor %}
|
|
114
122
|
</div>
|
|
115
123
|
{% endif %}
|
|
@@ -141,7 +149,14 @@ layout: default
|
|
|
141
149
|
<div class="note-content e-content" itemprop="articleBody">
|
|
142
150
|
{{ content }}
|
|
143
151
|
</div>
|
|
144
|
-
|
|
152
|
+
|
|
153
|
+
<!-- ================================ -->
|
|
154
|
+
<!-- AUTHOR BIO SECTION -->
|
|
155
|
+
<!-- ================================ -->
|
|
156
|
+
{% if page.author and page.author_profile != false %}
|
|
157
|
+
{% include components/author-bio.html author=page.author %}
|
|
158
|
+
{% endif %}
|
|
159
|
+
|
|
145
160
|
<!-- ================================ -->
|
|
146
161
|
<!-- NOTE FOOTER -->
|
|
147
162
|
<!-- ================================ -->
|