pandorga 1.5.0 → 1.6.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/README.md +1 -1
  4. data/_includes/chord-tab.html +7 -7
  5. data/_includes/content-runtime/41-listing-helpers.html +7 -0
  6. data/_includes/content-runtime/43-blocks.html +4 -3
  7. data/_includes/content-runtime/60-fragments.html +77 -11
  8. data/_includes/page/site-footer.html +3 -2
  9. data/_includes/theme/critical-preload.html +35 -0
  10. data/_includes/theme/json-ld.html +35 -0
  11. data/_includes/transcription.html +5 -5
  12. data/_layouts/articles.html +8 -2
  13. data/_layouts/cv.html +20 -14
  14. data/_layouts/home.html +23 -3
  15. data/_layouts/shell.html +53 -12
  16. data/assets/css/ledger/chrome.css +23 -0
  17. data/assets/css/ledger/cv.css +13 -0
  18. data/assets/css/ledger/home.css +27 -20
  19. data/assets/css/ledger/writing.css +27 -3
  20. data/assets/css/tailwind.css +3 -0
  21. data/assets/tailwind-play.js +2 -1
  22. data/lib/pandorga/commands/doctor.rb +3 -1
  23. data/lib/pandorga/commands/new.rb +1 -1
  24. data/lib/pandorga/discovery.rb +78 -0
  25. data/lib/pandorga/jekyll/plugins/discovery.rb +32 -0
  26. data/lib/pandorga/version.rb +1 -1
  27. data/scripts/cdn-allowlist.json +2 -5
  28. data/scripts/tailwind/build.sh +19 -0
  29. data/scripts/tailwind/input.css +12 -0
  30. data/scripts/tailwind/tailwind.config.js +102 -0
  31. data/scripts/test/render-parity/blocks/project-inventory-row.json +3 -2
  32. data/scripts/test/test-css-delivery.rb +32 -0
  33. data/scripts/test/test-css-structure.rb +8 -0
  34. data/scripts/test/test-discovery.rb +46 -0
  35. data/scripts/test/test-doctor.rb +38 -0
  36. data/scripts/test/test-examples-build.rb +15 -0
  37. data/scripts/test/test-hero-portraits.mjs +1 -1
  38. data/scripts/test/test-hydrate-reserve.rb +64 -0
  39. data/scripts/test/test-new.rb +11 -0
  40. data/scripts/test/test-shell-a11y.rb +30 -0
  41. data/scripts/test/test-template-packages.rb +8 -0
  42. data/scripts/test/test-validate-allowlist.rb +29 -0
  43. data/scripts/validate.sh +7 -1
  44. metadata +14 -2
  45. data/assets/css/cv.css +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eab1ea9696bdf99c28026f135762061c5a627ea398df5ddd8080f2d10dd9abc7
4
- data.tar.gz: 89b13dcd397f666cf5660845d7ffece69478d6275b1d821d9500a4bf08046e5b
3
+ metadata.gz: 2bb5b2e689bf56f24930270b0f6dff89d91674594b67267a7e971a96252f1ac1
4
+ data.tar.gz: f5ded06e9a148e5095437afae98852a63aaf994737e38a91c5b5c9d4f6a9ada9
5
5
  SHA512:
6
- metadata.gz: e83aceb1f79cdd9f3ab201315b19532f3182cad64ed34ac4ed285be49f4257e4038a672aada1e6b7ce6b2ce7fb837749fc3eeb4347b99687f07fc2a33a8a6367
7
- data.tar.gz: 7d987a05926fa2d9653faf04fc66585219430e57cd12d5383421aa9a32fcb6b1a52831fa4576f321e5392e482d6128bf4755cb36e237eb6ae8a1788f086848e3
6
+ metadata.gz: 0e18926a06e09cab67eea5ff3df47893e70abda71e2dad6ef7686f64a69f2b0d7527a65efe4069ec55342c51f6d7aa86cbb47f5005ded2650a1abe7c9ef55724
7
+ data.tar.gz: fda5ff032a0dcf24995bdfa280b80e3b2470c2e8e5bf455cfbbcac149df30d02637fd69e947562c0c7fe1f09a0834ae15dd6976c15411e92dea5a9a95dca2d00
data/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.0] — 2026-10-10
4
+
5
+ ### Added
6
+
7
+ - `/sitemap.xml` lists the generated pages. `/robots.txt` allows the public
8
+ site, skips `/studio/`, and names the sitemap. Both use
9
+ `pandorga.identity.url` when it is set, otherwise `site.url`.
10
+ - JSON-LD `WebSite` and `Person` in the shell, from `identity.name` and the
11
+ optional `identity.url`. `site.description` is included when it is set.
12
+ - Hero portrait entries may include optional `srcset` and `sizes`. A portrait
13
+ that only has `src` is unchanged.
14
+
15
+ ### Changed
16
+
17
+ - The home subtitle and summary are requested before the stylesheets and
18
+ painted without a "Loading..." swap. Article rows, CV sections, and empty
19
+ home bands keep a reserved box until the real content replaces it.
20
+ - The hero subtitle stays at `--type-body`, the same size as the summary.
21
+ - `.writing-revised` uses the solid variant ink. The transparent mix was
22
+ 3.69 and 3.90 on the light paper.
23
+ - A selectable project inventory row is a `button`.
24
+ - Utilities ship as `assets/css/tailwind.css`. The shell no longer loads the
25
+ Tailwind Play CDN. Each layout links the shared chrome and its own ledger
26
+ sheet, not every sheet.
27
+
28
+ No `pandorga:` config keys were added or renamed. `srcset` and `sizes` are
29
+ optional fields on objects in `pages/home/portraits.json`.
30
+
31
+ ## [1.5.1] — 2026-10-10
32
+
33
+ ### Fixed
34
+
35
+ - The shell has a "Skip to content" link. When `site.author` is blank, the
36
+ home link's accessible name uses `pandorga.identity.name`, then the top-bar
37
+ wordmark, instead of a bare dash. The footer wordmark uses the same
38
+ fallback. A site that sets `site.author` is unchanged.
39
+ - `pandorga doctor` says when `pandorga.pages` is missing, and says when the
40
+ list is empty.
41
+ - `./scripts/validate.sh` fails if a listed gate file is missing.
42
+
43
+ ### Changed
44
+
45
+ - Getting started and the README name the scaffold pin `~> 1.3`.
46
+ - `pandorga new` comments the `v1.5.0` tag.
47
+ - Home CSS drops an empty `@media (min-width: 768px)` block and the unused
48
+ `.home-profile-summary` rules.
49
+ - Chord chart and transcript include comments are English. The visible
50
+ transcript labels are unchanged.
51
+
52
+ No `pandorga:` config keys were added or renamed.
53
+
3
54
  ## [1.5.0] — 2026-10-10
4
55
 
5
56
  ### Added
data/README.md CHANGED
@@ -17,7 +17,7 @@ An aesthetic Jekyll publishing platform for creative and technical writing. The
17
17
  Scaffold a site (Ada Example defaults, `content.backend: static`):
18
18
 
19
19
  ```bash
20
- gem install pandorga # once 1.1 is on RubyGems; or use a local checkout
20
+ gem install pandorga # or use a local checkout
21
21
  pandorga new my-site
22
22
  cd my-site
23
23
  bundle install
@@ -1,14 +1,14 @@
1
1
  {% comment %}
2
2
  chord-tab.html
3
- Exibe uma cifra estruturada em N colunas monoespaçadas, usando toda a largura disponível.
4
- Cada coluna respeita o limite de 79 caracteres por linha.
5
- Uso: {% include chord-tab.html tab=tab_data %}
6
- Onde tab_data tem:
7
- intro: "linha de acordes do intro (opcional)"
3
+ Renders a chord chart in N monospaced columns, using the full available width.
4
+ Each column stays within 79 characters per line.
5
+ Use: {% include chord-tab.html tab=tab_data %}
6
+ tab_data has:
7
+ intro: "intro chord line (optional)"
8
8
  sections:
9
- - label: "rótulo da seção (opcional)"
9
+ - label: "section label (optional)"
10
10
  content: |
11
- linhas de cifra/letra
11
+ chord and lyric lines
12
12
  {% endcomment %}
13
13
 
14
14
  <div class="chord-tab-container mt-16 mb-8"
@@ -60,6 +60,13 @@
60
60
  },
61
61
  showLoadingState: function (element, message) {
62
62
  if (!element) return;
63
+ // Reserved skeletons already occupy the box. Replacing them with a
64
+ // one-line message is the shift the listing then repeats when the
65
+ // cards arrive.
66
+ if (element.hasAttribute('data-content-reserve')) {
67
+ element.setAttribute('aria-busy', 'true');
68
+ return;
69
+ }
63
70
  element.innerHTML = '<p class="font-body text-on-surface-variant italic col-span-full">' + escapeHtml(message) + '</p>';
64
71
  },
65
72
  showErrorState: function (element, message) {
@@ -344,14 +344,15 @@
344
344
  var slug = item.slug || f.slug || '';
345
345
  var index = options.index;
346
346
 
347
- var row = document.createElement('article');
347
+ // A selectable row is a real button. role=button on an article is
348
+ // what the audit flags, and the button already handles Enter and Space.
349
+ var row = document.createElement(selectable ? 'button' : 'article');
348
350
  row.className = 'home-project-inventory-row group' +
349
351
  (selected ? ' is-selected' : '');
352
+ if (selectable) row.type = 'button';
350
353
  if (slug) row.setAttribute('data-slug', slug);
351
354
 
352
355
  if (selectable) {
353
- row.setAttribute('role', 'button');
354
- row.setAttribute('tabindex', '0');
355
356
  row.setAttribute('aria-pressed', selected ? 'true' : 'false');
356
357
  row.setAttribute('aria-label', name);
357
358
  }
@@ -13,6 +13,19 @@
13
13
  }
14
14
 
15
15
  async function listPaths(collection, manifestKey) {
16
+ var pre = window.__pandorgaPreload && window.__pandorgaPreload['manifest.json'];
17
+ if (pre && typeof pre.then === 'function') {
18
+ try {
19
+ var preloaded = await pre;
20
+ assertManifestSchema(preloaded);
21
+ var preloadedFiles = preloaded.files && preloaded.files[manifestKey];
22
+ if (Array.isArray(preloadedFiles)) return preloadedFiles;
23
+ } catch (error) {
24
+ if (error && String(error.message || '').indexOf('Unsupported content schema') === 0) throw error;
25
+ delete window.__pandorgaPreload['manifest.json'];
26
+ }
27
+ }
28
+
16
29
  var manifestResponse = await fetch(buildUrl('manifest.json'));
17
30
  if (manifestResponse.ok) {
18
31
  var manifest = await manifestResponse.json();
@@ -63,16 +76,31 @@
63
76
  return collectionCache[cacheKey];
64
77
  }
65
78
 
66
- async function loadJson(path) {
67
- var key = String(path || '');
68
- if (jsonCache[key]) return jsonCache[key];
69
- jsonCache[key] = fetch(buildUrl(path)).then(function (response) {
79
+ function fetchJson(path) {
80
+ return fetch(buildUrl(path)).then(function (response) {
70
81
  if (!response.ok) throw new Error('Content JSON fetch failed: ' + path);
71
82
  return response.json();
72
- }).catch(function (error) {
73
- delete jsonCache[key];
74
- throw error;
75
83
  });
84
+ }
85
+
86
+ async function loadJson(path) {
87
+ var key = String(path || '');
88
+ if (jsonCache[key]) return jsonCache[key];
89
+ var pre = window.__pandorgaPreload && window.__pandorgaPreload[key];
90
+ jsonCache[key] = Promise.resolve()
91
+ .then(function () {
92
+ if (pre && typeof pre.then === 'function') return pre;
93
+ return fetchJson(path);
94
+ })
95
+ .catch(function () {
96
+ if (!(pre && typeof pre.then === 'function')) throw new Error('Content JSON fetch failed: ' + path);
97
+ delete window.__pandorgaPreload[key];
98
+ return fetchJson(path);
99
+ })
100
+ .catch(function (error) {
101
+ delete jsonCache[key];
102
+ throw error;
103
+ });
76
104
  return jsonCache[key];
77
105
  }
78
106
 
@@ -192,10 +220,21 @@
192
220
  options = options || {};
193
221
  var el = typeof elementOrId === 'string' ? document.getElementById(elementOrId) : elementOrId;
194
222
  if (!el) return Promise.resolve();
195
- var loadingText = options.loadingText || 'Loading...';
196
- el.innerHTML = '<p class="font-body text-on-surface-variant italic">' + escapeHtml(loadingText) + '</p>';
223
+ // A quiet slot keeps its reserved box. Writing "Loading..." into the
224
+ // hero summary makes that placeholder the LCP, then swaps it away.
225
+ var quiet = el.hasAttribute('data-content-quiet');
226
+ var loadingText = quiet ? '' : (options.loadingText || 'Loading...');
227
+ if (loadingText) {
228
+ el.innerHTML = '<p class="font-body text-on-surface-variant italic">' + escapeHtml(loadingText) + '</p>';
229
+ }
197
230
  return loadJson(pageFragmentPath(relativePath)).then(function (fragment) {
198
- el.innerHTML = renderMarkdown(fragment.body_markdown || '');
231
+ var html = renderMarkdown(fragment.body_markdown || '');
232
+ if (el.getAttribute('data-content-painted') === '1' && el.innerHTML === html) {
233
+ el.removeAttribute('data-content-painted');
234
+ return;
235
+ }
236
+ el.innerHTML = html;
237
+ el.removeAttribute('data-content-painted');
199
238
  }).catch(function (error) {
200
239
  console.error(error);
201
240
  el.innerHTML = '<p class="font-body text-on-surface-variant italic">' + escapeHtml(options.errorText || 'Content could not be loaded right now.') + '</p>';
@@ -473,11 +512,36 @@
473
512
  var src = typeof entry.src === 'string' ? entry.src.trim() : '';
474
513
  if (!src) continue;
475
514
  var alt = typeof entry.alt === 'string' ? entry.alt.trim() : '';
476
- entries.push({ src: src, alt: alt });
515
+ var record = { src: src, alt: alt };
516
+ // Optional responsive fields. Absent keys stay off the record so a
517
+ // pool that only has src keeps the previous shape.
518
+ var srcset = typeof entry.srcset === 'string' ? entry.srcset.trim() : '';
519
+ var sizes = typeof entry.sizes === 'string' ? entry.sizes.trim() : '';
520
+ if (srcset) record.srcset = srcset;
521
+ if (sizes) record.sizes = sizes;
522
+ entries.push(record);
477
523
  }
478
524
  return entries;
479
525
  }
480
526
 
527
+ function resolveSrcset(srcset) {
528
+ return String(srcset || '').split(',').map(function (part) {
529
+ var bits = part.trim().split(/\s+/);
530
+ if (!bits[0]) return '';
531
+ bits[0] = resolveAssetUrl(bits[0]);
532
+ return bits.filter(Boolean).join(' ');
533
+ }).filter(Boolean).join(', ');
534
+ }
535
+
536
+ function applyHeroPortraitSources(img, chosen) {
537
+ var srcset = resolveSrcset(chosen && chosen.srcset);
538
+ var sizes = chosen && typeof chosen.sizes === 'string' ? chosen.sizes.trim() : '';
539
+ if (srcset) img.setAttribute('srcset', srcset);
540
+ else img.removeAttribute('srcset');
541
+ if (sizes) img.setAttribute('sizes', sizes);
542
+ else img.removeAttribute('sizes');
543
+ }
544
+
481
545
  function pickHeroPortrait(entries, random) {
482
546
  if (!entries || !entries.length) return null;
483
547
  var roll = typeof random === 'function' ? random() : Math.random();
@@ -491,6 +555,7 @@
491
555
  if (!nextSrc) return Promise.resolve(false);
492
556
  if (img.getAttribute('src') === nextSrc) {
493
557
  img.alt = nextAlt;
558
+ applyHeroPortraitSources(img, chosen);
494
559
  img.classList.add('is-shown');
495
560
  return Promise.resolve(true);
496
561
  }
@@ -499,6 +564,7 @@
499
564
  probe.onload = function () {
500
565
  img.src = nextSrc;
501
566
  img.alt = nextAlt;
567
+ applyHeroPortraitSources(img, chosen);
502
568
  img.classList.add('is-shown');
503
569
  resolve(true);
504
570
  };
@@ -29,6 +29,7 @@
29
29
  {%- endcomment -%}
30
30
 
31
31
  {%- assign footer_active = include.nav_active -%}
32
+ {%- assign footer_name = site.author | default: site.pandorga.identity.name -%}
32
33
  {%- assign footer_license = site.content_license -%}
33
34
 
34
35
  <footer class="site-footer">
@@ -39,9 +40,9 @@
39
40
  <div class="site-footer-grid">
40
41
 
41
42
  <div class="site-footer-block site-footer-masthead">
42
- <a href="/" class="site-footer-wordmark-link" aria-label="{{ site.author }} – Home">
43
+ <a href="/" class="site-footer-wordmark-link" aria-label="{% if footer_name != blank %}{{ footer_name }} – Home{% else %}Home{% endif %}">
43
44
  {% include theme/site-logo.html %}
44
- <span class="site-footer-wordmark">{{ site.author }}</span>
45
+ <span class="site-footer-wordmark">{{ footer_name }}</span>
45
46
  </a>
46
47
  {%- comment -%}
47
48
  The same fragment the home hero mounts, so the line the site opens
@@ -0,0 +1,35 @@
1
+ {%- comment -%}
2
+ Start the JSON fetches the first paint depends on, before the stylesheets
3
+ and the content runtime. loadJson and listPaths reuse window.__pandorgaPreload.
4
+ {%- endcomment -%}
5
+ {%- case page.layout -%}
6
+ {%- when "home" -%}
7
+ {%- assign _preload_paths = "pages/cv/summary.json|pages/home/subtitle.json|pages/home/portraits.json|pages/headers.json|data/writing_index.json" -%}
8
+ {%- when "articles" -%}
9
+ {%- assign _preload_paths = "data/writing_index.json|pages/headers.json|manifest.json" -%}
10
+ {%- when "blog" -%}
11
+ {%- assign _preload_paths = "data/writing_index.json|pages/headers.json|manifest.json" -%}
12
+ {%- when "cv" -%}
13
+ {%- assign _preload_paths = "pages/cv/summary.json|pages/cv/disclaimer.json|data/education.json|data/languages.json|pages/headers.json|manifest.json" -%}
14
+ {%- when "projects" -%}
15
+ {%- assign _preload_paths = "data/writing_index.json|pages/headers.json|manifest.json" -%}
16
+ {%- else -%}
17
+ {%- assign _preload_paths = "" -%}
18
+ {%- endcase -%}
19
+ {%- if _preload_paths != blank -%}
20
+ <script>
21
+ (function () {
22
+ var base = {{ site.content_api_base_url | default: "" | jsonify }};
23
+ base = String(base || "").replace(/\/$/, "");
24
+ var paths = {{ _preload_paths | split: "|" | jsonify }};
25
+ window.__pandorgaPreload = window.__pandorgaPreload || {};
26
+ paths.forEach(function (path) {
27
+ var url = base ? base + "/" + path : "/" + path;
28
+ window.__pandorgaPreload[path] = fetch(url, { credentials: "same-origin" }).then(function (response) {
29
+ if (!response.ok) throw new Error(path);
30
+ return response.json();
31
+ });
32
+ });
33
+ })();
34
+ </script>
35
+ {%- endif -%}
@@ -0,0 +1,35 @@
1
+ {%- comment -%}
2
+ WebSite and Person from identity. No new required keys: name is already
3
+ required, and url is the existing optional canonical origin.
4
+ {%- endcomment -%}
5
+ {%- assign ld_name = site.pandorga.identity.name | default: site.author | default: site.title -%}
6
+ {%- assign ld_url = site.pandorga.identity.url | default: site.url -%}
7
+ {%- if ld_url != blank -%}
8
+ {%- assign ld_website = ld_url -%}
9
+ {%- else -%}
10
+ {%- assign ld_website = "/" | absolute_url -%}
11
+ {%- endif -%}
12
+ {%- if ld_name != blank -%}
13
+ <script type="application/ld+json">
14
+ {
15
+ "@context": "https://schema.org",
16
+ "@graph": [
17
+ {
18
+ "@type": "WebSite",
19
+ "name": {{ ld_name | jsonify }},
20
+ "url": {{ ld_website | jsonify }}
21
+ {%- if site.description != blank -%},
22
+ "description": {{ site.description | jsonify }}
23
+ {%- endif -%}
24
+ },
25
+ {
26
+ "@type": "Person",
27
+ "name": {{ ld_name | jsonify }}
28
+ {%- if ld_url != blank -%},
29
+ "url": {{ ld_url | jsonify }}
30
+ {%- endif -%}
31
+ }
32
+ ]
33
+ }
34
+ </script>
35
+ {%- endif -%}
@@ -1,11 +1,11 @@
1
1
  {% comment %}
2
2
  transcription.html
3
- Exibe uma transcrição estruturada, com até 4 personagens (cores definidas por ordem de aparição).
4
- Uso: {% include transcription.html transcription=page.transcription youtube_id='fpmZiY24U0k' %}
5
- Ou para usar um arquivo em _data:
3
+ Renders a structured transcript, with up to 4 speakers (colors follow order of appearance).
4
+ Use: {% include transcription.html transcription=page.transcription youtube_id='fpmZiY24U0k' %}
5
+ Or to load a file from _data:
6
6
  {% assign transcription_data = site.data.transcriptions[page.transcription].transcription %}
7
7
  {% include transcription.html transcription=transcription_data youtube_id='fpmZiY24U0k' %}
8
- Também suporta url e image para thumbnail:
8
+ Also accepts url and image for a thumbnail:
9
9
  {% include transcription.html transcription=transcription_data url='https://...' image='https://...' %}
10
10
 
11
11
  Speaker labels are title-cased at render time (Patrícia, not PATRÍCIA) so
@@ -26,7 +26,7 @@
26
26
  <hr class="border-t border-gentle-divider mb-6">
27
27
  {% if include.youtube_id %}
28
28
  <div class="mb-2">
29
- {% include youtube.html id=include.youtube_id %} <!-- Exibe o vídeo do YouTube -->
29
+ {% include youtube.html id=include.youtube_id %} <!-- YouTube video -->
30
30
  <span class="font-label text-xs text-on-surface-variant">Transcrição do vídeo</span>
31
31
  </div>
32
32
  {% elsif include.url and include.image %}
@@ -90,8 +90,14 @@ layout: shell
90
90
  </div>
91
91
 
92
92
  <div class="writing-archive" id="articles-list">
93
- <section id="empty-state" class="writing-archive-state">
94
- <p class="font-body text-on-surface-variant italic">Loading articles...</p>
93
+ <section id="empty-state" class="writing-archive-reserve" aria-busy="true">
94
+ <p class="sr-only">Loading articles...</p>
95
+ <span class="writing-reserve-row" aria-hidden="true"></span>
96
+ <span class="writing-reserve-row" aria-hidden="true"></span>
97
+ <span class="writing-reserve-row" aria-hidden="true"></span>
98
+ <span class="writing-reserve-row" aria-hidden="true"></span>
99
+ <span class="writing-reserve-row" aria-hidden="true"></span>
100
+ <span class="writing-reserve-row" aria-hidden="true"></span>
95
101
  </section>
96
102
  <section id="no-results" class="writing-archive-state hidden">
97
103
  <p class="font-body text-on-surface-variant italic">Nothing matches these filters yet.</p>
data/_layouts/cv.html CHANGED
@@ -25,43 +25,45 @@ layout: shell
25
25
 
26
26
  <div id="cv-disclaimer" class="cv-disclaimer-box">
27
27
  <span class="material-symbols-outlined cv-dossier-note-icon" aria-hidden="true">info</span>
28
- <div class="cv-dossier-note font-body" data-content-fragment="cv/disclaimer">
29
- <p class="font-body text-on-surface-variant italic">Loading...</p>
30
- </div>
28
+ <div class="cv-dossier-note font-body" data-content-fragment="cv/disclaimer" data-content-quiet></div>
31
29
  </div>
32
30
 
33
31
  <div id="cv-listing">
34
32
  {% capture cv_summary %}
35
- <div class="cv-summary font-body" data-content-fragment="cv/summary">
36
- <p class="font-body text-on-surface-variant italic">Loading...</p>
37
- </div>
33
+ <div class="cv-summary font-body" data-content-fragment="cv/summary" data-content-quiet></div>
38
34
  {% endcapture %}
39
35
  {% include cv/cv-box.html id="summary" title="Summary" content=cv_summary %}
40
36
 
41
37
  {% capture cv_experience %}
42
- <div id="cv-experience-list" class="cv-entry-stack">
43
- <p class="font-body text-on-surface-variant italic">Loading experience...</p>
38
+ <div id="cv-experience-list" class="cv-entry-stack" data-content-reserve aria-busy="true">
39
+ <span class="cv-reserve-card" aria-hidden="true"></span>
40
+ <span class="cv-reserve-card" aria-hidden="true"></span>
41
+ <span class="cv-reserve-card" aria-hidden="true"></span>
44
42
  </div>
45
43
  {% endcapture %}
46
44
  {% include cv/cv-box.html id="experience" title="Experience" content=cv_experience %}
47
45
 
48
46
  {% capture cv_products %}
49
- <div id="cv-products-grid" class="cv-product-grid">
50
- <p class="font-body text-on-surface-variant italic">Loading products...</p>
47
+ <div id="cv-products-grid" class="cv-product-grid" data-content-reserve aria-busy="true">
48
+ <span class="cv-reserve-card" aria-hidden="true"></span>
49
+ <span class="cv-reserve-card" aria-hidden="true"></span>
50
+ <span class="cv-reserve-card" aria-hidden="true"></span>
51
51
  </div>
52
52
  {% endcapture %}
53
53
  {% include cv/cv-box.html id="products" title="Products" content=cv_products %}
54
54
 
55
55
  {% capture cv_education %}
56
- <div id="cv-education-list" class="cv-entry-stack">
57
- <p class="font-body text-on-surface-variant italic">Loading education...</p>
56
+ <div id="cv-education-list" class="cv-entry-stack" data-content-reserve aria-busy="true">
57
+ <span class="cv-reserve-card" aria-hidden="true"></span>
58
+ <span class="cv-reserve-card" aria-hidden="true"></span>
58
59
  </div>
59
60
  {% endcapture %}
60
61
  {% include cv/cv-box.html id="education" title="Education" content=cv_education %}
61
62
 
62
63
  {% capture cv_languages %}
63
- <div id="cv-languages-list" class="cv-language-list">
64
- <p class="font-body text-on-surface-variant italic">Loading languages...</p>
64
+ <div id="cv-languages-list" class="cv-language-list" data-content-reserve aria-busy="true">
65
+ <span class="cv-reserve-card" aria-hidden="true"></span>
66
+ <span class="cv-reserve-card" aria-hidden="true"></span>
65
67
  </div>
66
68
  {% endcapture %}
67
69
  {% include cv/cv-box.html id="languages" title="Languages" content=cv_languages %}
@@ -327,6 +329,7 @@ layout: shell
327
329
  productMap[product.slug] = product;
328
330
  });
329
331
  experienceList.innerHTML = '';
332
+ experienceList.removeAttribute('aria-busy');
330
333
  jobs.sort(function (a, b) {
331
334
  var as = (a.front_matter && a.front_matter.start) || '';
332
335
  var bs = (b.front_matter && b.front_matter.start) || '';
@@ -344,6 +347,7 @@ layout: shell
344
347
  runtime.Listing.showLoadingState(educationList, 'Loading education...');
345
348
  runtime.loadJson('data/education.json').then(function (entries) {
346
349
  educationList.innerHTML = '';
350
+ educationList.removeAttribute('aria-busy');
347
351
  entries.forEach(function (entry) { educationList.appendChild(renderEducation(entry)); });
348
352
  }).catch(function (error) {
349
353
  console.error(error);
@@ -355,6 +359,7 @@ layout: shell
355
359
  runtime.Listing.showLoadingState(languagesList, 'Loading languages...');
356
360
  runtime.loadJson('data/languages.json').then(function (entries) {
357
361
  languagesList.innerHTML = '';
362
+ languagesList.removeAttribute('aria-busy');
358
363
  entries.forEach(function (entry) { languagesList.appendChild(renderLanguage(entry)); });
359
364
  }).catch(function (error) {
360
365
  console.error(error);
@@ -366,6 +371,7 @@ layout: shell
366
371
  runtime.Listing.showLoadingState(productsGrid, 'Loading products...');
367
372
  runtime.loadWritingIndex('products').then(function (products) {
368
373
  productsGrid.innerHTML = '';
374
+ productsGrid.removeAttribute('aria-busy');
369
375
  var ordered = products.slice().sort(compareProductsByEndDate);
370
376
  ordered.forEach(function (product) {
371
377
  productsGrid.appendChild(renderProduct(product));
data/_layouts/home.html CHANGED
@@ -29,12 +29,12 @@ layout: shell
29
29
  <div class="home-hero">
30
30
  <div class="home-hero-copy">
31
31
  <div id="home-subtitle-content" class="home-hero-tagline font-label"
32
- data-content-fragment="home/subtitle"></div>
32
+ data-content-fragment="home/subtitle" data-content-quiet></div>
33
33
  <h1 class="home-hero-title">
34
34
  <span class="home-hero-given">{{ _hero_name.given }}</span>{%- if _hero_name.family != "" %} <span class="home-hero-family">{{ _hero_name.family }}</span>{%- endif %}
35
35
  </h1>
36
36
  <div id="home-cv-summary" class="home-hero-summary font-body"
37
- data-content-fragment="cv/summary"></div>
37
+ data-content-fragment="cv/summary" data-content-quiet></div>
38
38
  <div class="home-hero-actions">
39
39
  {%- if _cv_page -%}
40
40
  <a class="ledger-cta ledger-cta--emphasis home-hero-cta" href="{{ _cv_page.url }}">
@@ -50,7 +50,7 @@ layout: shell
50
50
  <div class="home-hero-plate-wrap">
51
51
  <span class="ledger-frame-crosshairs" aria-hidden="true"></span>
52
52
  <figure class="home-hero-plate">
53
- <img alt="" width="1200" height="800" data-content-hero-portrait />
53
+ <img alt="" width="1200" height="800" fetchpriority="high" data-content-hero-portrait />
54
54
  <span class="ledger-frame-corners" aria-hidden="true"></span>
55
55
  </figure>
56
56
  </div>
@@ -58,6 +58,26 @@ layout: shell
58
58
  </div>
59
59
  </div>
60
60
  </section>
61
+ <script>
62
+ (function () {
63
+ var runtime = window.ContentRuntime;
64
+ var pre = window.__pandorgaPreload || {};
65
+ function paint(id, path) {
66
+ var el = document.getElementById(id);
67
+ var pending = pre[path];
68
+ if (!el || !pending || !runtime || !runtime.renderMarkdown) return;
69
+ pending.then(function (fragment) {
70
+ if (!fragment || el.getAttribute('data-content-painted') === '1') return;
71
+ var html = runtime.renderMarkdown(fragment.body_markdown || '');
72
+ if (!html) return;
73
+ el.innerHTML = html;
74
+ el.setAttribute('data-content-painted', '1');
75
+ }).catch(function () {});
76
+ }
77
+ paint('home-subtitle-content', 'pages/home/subtitle.json');
78
+ paint('home-cv-summary', 'pages/cv/summary.json');
79
+ })();
80
+ </script>
61
81
 
62
82
  {%- for band in _home_bands -%}
63
83
  {%- if band.kind == "index" -%}