pandorga 1.6.3 → 1.6.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 107838b82bee49181ddb10435ed0ab5726579fda06fe0bc0e5399ce0f17f86ad
4
- data.tar.gz: f8e893a43d87ed1b846a522cef98b2314c56efdfd304804354458d6c5ae9e0be
3
+ metadata.gz: 0b772584dba676ef335aa1f56a912948ce50d9a142334711a24e9ebf6629f35e
4
+ data.tar.gz: 046a693ae7e9cc93e4c300e998aeea7f72c0b31cfd9fa98847ed4a05c6761964
5
5
  SHA512:
6
- metadata.gz: df29223d7d858b285e1b1e9cf991cd3362e192895fdbcbded93c3f2cc7991f7594779b45ee5a04d7c840ac7279cf26eb23bb704b25b55f17ab7b95d4a483743d
7
- data.tar.gz: 1e1ac9acc5c296025106b85da0bc4535ca67a3bf72061932dbffc1273f027f66d69666dc17f265eb40d9298b37b99cdf23cef2476c510a9cd1e4862a5b965bd0
6
+ metadata.gz: 705f9a05d328407ea488e5493642debaaad9ee4775a7ac1fc0587adf494065960fb5f2f6b202c3ff14c04d21e7812cb02abef1f39b8a2c85da7bfe2a96e941c3
7
+ data.tar.gz: ebd39b4db3858646577c254a590b6a5925d6b90e62b04d41bd29c17d13d9b3dfeeb2a8ced0189c227520d532ddaa3dc1419888dfd02f37c53802d99c7565c1a2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.5] — 2026-10-10
4
+
5
+ ### Fixed
6
+
7
+ - The example home measured like the public site: compressed responses,
8
+ served concurrently. The stock one-thread HTTP/1.0 server was scoring
9
+ the same templates in the 70s while the live site on 1.6.3 scored 97.
10
+ - The Material Symbols face is about a megabyte. It is no longer linked
11
+ from the head. The shell adds that sheet after the load event, so the
12
+ simulator does not wait on it for the largest paint. The text font
13
+ sheet and the flag stylesheet use print media and apply on load.
14
+ - The icon box stays 1em.
15
+
16
+ ### Changed
17
+
18
+ - Lighthouse mobile thresholds apply to releases. Pull requests and merges
19
+ to main only warn. The release build checks out the tag and fails below 75.
20
+
21
+ No `pandorga:` config keys were added or renamed.
22
+
23
+ ## [1.6.4] — 2026-10-10
24
+
25
+ ### Fixed
26
+
27
+ - The Material Symbols stylesheet no longer blocks first paint. It loads
28
+ as print media and is applied on load. The icon box stays 1em, and its
29
+ size stays inherited, so the Google sheet cannot restyle it after paint.
30
+ - The content runtime, including marked, comes after the page so the
31
+ hero is in the document before that script. A home band's kicker, title,
32
+ and note from `content/pages/headers.yml` are in the HTML.
33
+ - The hero subtitle from `content/pages/headers.yml` is in the HTML.
34
+ Leaving that line empty until script runs moved the portrait.
35
+ - On a phone the hero band is shorter, so the first section title stays in
36
+ the first viewport. That title was the late LCP element.
37
+
38
+ ### Added
39
+
40
+ - Lighthouse mobile is a release rule. A stable release scores 90 or above.
41
+ A score from 75 to 89 is acceptable only to ship a bug fix, and an urgent
42
+ performance campaign must then bring the score back to 90 or above.
43
+ Below 75 is never acceptable. CI runs one mobile pass on the example home.
44
+
45
+ No `pandorga:` config keys were added or renamed.
46
+
3
47
  ## [1.6.3] — 2026-10-10
4
48
 
5
49
  ### Fixed
data/README.md CHANGED
@@ -75,6 +75,17 @@ bundle exec pandorga serve
75
75
  | [docs/deploy/cloudflare.md](docs/deploy/cloudflare.md) | Pages + object store |
76
76
  | [docs/spec.md](docs/spec.md) | Migration plan and acceptance criteria |
77
77
  | [docs/decisions.md](docs/decisions.md) | Frozen decisions |
78
+ | [CONTRIBUTING.md](CONTRIBUTING.md) | Gates, and the Lighthouse mobile release rule |
79
+
80
+ ## Releases
81
+
82
+ Lighthouse mobile thresholds apply to releases. A stable release scores
83
+ 90 or above. A score from 75 to 89 is acceptable only to ship a bug fix,
84
+ and an urgent performance campaign must then bring it back to 90 or above.
85
+ Below 75 is never acceptable for a release. A pull request or a merge may
86
+ score lower when that is unavoidable to keep the work modular. The release
87
+ build fails below 75. Pull requests and merges only warn. The checklist
88
+ is in [CONTRIBUTING.md](CONTRIBUTING.md).
78
89
 
79
90
  ## License
80
91
 
@@ -329,8 +329,9 @@
329
329
  return;
330
330
  }
331
331
  if (inline) {
332
- el.innerHTML = stripSingleParagraph(parseInlineMarkdown(text));
333
- } else {
332
+ var next = stripSingleParagraph(parseInlineMarkdown(text));
333
+ if (el.innerHTML.trim() !== next.trim()) el.innerHTML = next;
334
+ } else if (el.textContent.trim() !== text) {
334
335
  el.textContent = text;
335
336
  }
336
337
  el.hidden = false;
@@ -23,11 +23,18 @@
23
23
  data-home-language="{{ _lang }}"
24
24
  >
25
25
  <div class="home-band-inner">
26
+ {%- assign _section = site.data.page_headers[band.section_key] -%}
27
+ {%- assign _title = band.title -%}
28
+ {%- if _section.title != blank -%}{%- assign _title = _section.title -%}{%- endif -%}
26
29
  <div class="ledger-section-banner" data-content-home-section="{{ band.section_key }}">
27
30
  <div class="ledger-section-lead">
28
- <span class="ledger-kicker" data-content-section-kicker></span>
29
- <h2 class="section-title" data-content-section-title></h2>
30
- <p class="home-section-note font-body" data-content-section-note></p>
31
+ {%- if _section.kicker != blank -%}
32
+ <span class="ledger-kicker" data-content-section-kicker>{{ _section.kicker }}</span>
33
+ {%- endif -%}
34
+ <h2 class="section-title" data-content-section-title>{{ _title }}</h2>
35
+ {%- if _section.note != blank -%}
36
+ <p class="home-section-note font-body" data-content-section-note>{{ _section.note }}</p>
37
+ {%- endif -%}
31
38
  </div>
32
39
  <div class="home-section-aside">
33
40
  <a class="ledger-cta" href="{{ _path }}">
@@ -52,4 +52,5 @@
52
52
  {% endif %}
53
53
  {% assign fonts_href = fonts_href | append: "&display=swap" %}
54
54
  <link rel="preload" as="style" href="{{ fonts_href }}" />
55
- <link href="{{ fonts_href }}" rel="stylesheet" />
55
+ <link rel="stylesheet" href="{{ fonts_href }}" media="print" onload="this.media='all'" />
56
+ <noscript><link rel="stylesheet" href="{{ fonts_href }}" /></noscript>
data/_layouts/home.html CHANGED
@@ -9,9 +9,9 @@ layout: shell
9
9
  plus content fragments. Decorative art is a slot
10
10
  (`pandorga.home.hero_art`, default page/hero-default.html). Listing and
11
11
  index bands come from site.data.pandorga_home_bands in registry order.
12
- Band copy (kicker, title, note) arrives from content/pages/headers.yml
13
- via hydrateHomeSections(). No Portuguese or owner-specific strings live
14
- in this layout.
12
+ Band copy (kicker, title, note) and the hero subtitle come from
13
+ content/pages/headers.yml at build time. No Portuguese or owner-specific
14
+ strings live in this layout.
15
15
  {%- endcomment -%}
16
16
 
17
17
  {%- assign _hero_name = site.pandorga.identity | pandorga_hero_name: site.author, site.title -%}
@@ -28,8 +28,13 @@ layout: shell
28
28
  <div class="home-band-inner">
29
29
  <div class="home-hero">
30
30
  <div class="home-hero-copy">
31
+ {%- assign _home_header = site.data.page_headers.home -%}
32
+ {%- if _home_header.subtitle != blank -%}
33
+ <div id="home-subtitle-content" class="home-hero-tagline font-label" data-content-static>{{ _home_header.subtitle }}</div>
34
+ {%- else -%}
31
35
  <div id="home-subtitle-content" class="home-hero-tagline font-label"
32
36
  data-content-fragment="home/subtitle" data-content-quiet></div>
37
+ {%- endif -%}
33
38
  <h1 class="home-hero-title">
34
39
  <span class="home-hero-given">{{ _hero_name.given }}</span>{%- if _hero_name.family != "" %} <span class="home-hero-family">{{ _hero_name.family }}</span>{%- endif %}
35
40
  </h1>
data/_layouts/shell.html CHANGED
@@ -116,9 +116,23 @@
116
116
  <link rel="stylesheet" href="/assets/css/ledger/{{ _sheet }}.css" />
117
117
  {%- endfor -%}
118
118
  {% include theme/font-loader.html %}
119
- <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
120
- rel="stylesheet" />
121
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" />
119
+ {%- comment -%}
120
+ Icons are not first paint. The face is about a megabyte. A link in the
121
+ head, even with print media, is discovered immediately and the simulator
122
+ then waits on that face for the largest paint. Add the sheet after load.
123
+ {%- endcomment -%}
124
+ {%- assign _symbols_href = "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" -%}
125
+ <script>
126
+ window.addEventListener('load', function () {
127
+ var link = document.createElement('link');
128
+ link.rel = 'stylesheet';
129
+ link.href = {{ _symbols_href | jsonify }};
130
+ document.head.appendChild(link);
131
+ });
132
+ </script>
133
+ <noscript><link rel="stylesheet" href="{{ _symbols_href }}" /></noscript>
134
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" media="print" onload="this.media='all'" />
135
+ <noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" /></noscript>
122
136
  <style>
123
137
  :root { --drop-cap-lines: {{ site.drop_cap_lines | default: 2 }}; }
124
138
  </style>
@@ -233,8 +247,6 @@
233
247
  })();
234
248
  </script>
235
249
 
236
- {% include page/content-runtime.html %}
237
-
238
250
  <div id="page-content" tabindex="-1">
239
251
  {{ content }}
240
252
  </div>
@@ -244,6 +256,8 @@
244
256
  <!-- Footer -->
245
257
  {% include page/site-footer.html nav_active=nav_active %}
246
258
 
259
+ {% include page/content-runtime.html %}
260
+
247
261
  </body>
248
262
 
249
263
  </html>
@@ -25,6 +25,7 @@ module Jekyll
25
25
  site.data["content_fragments"] ||= {}
26
26
 
27
27
  load_page_content(site, content_path)
28
+ load_page_headers(site, content_path)
28
29
  load_structured_data(site, content_path)
29
30
  end
30
31
 
@@ -40,6 +41,17 @@ module Jekyll
40
41
  end
41
42
  end
42
43
 
44
+ # content/pages/headers.yml is also exported as pages/headers.json.
45
+ # The home bands read it here so a section title and note are in the
46
+ # HTML. A heading painted later by the runtime becomes the LCP element.
47
+ def load_page_headers(site, content_path)
48
+ path = File.join(content_path, "pages", "headers.yml")
49
+ return unless File.file?(path)
50
+
51
+ loaded = load_yaml(path)
52
+ site.data["page_headers"] = loaded.is_a?(Hash) ? loaded : {}
53
+ end
54
+
43
55
  def load_structured_data(site, content_path)
44
56
  data_path = File.join(content_path, "collections", "data")
45
57
  return unless Dir.exist?(data_path)
data/assets/css/base.css CHANGED
@@ -1,12 +1,40 @@
1
1
  /* ── Material Symbols ── */
2
- .material-symbols-outlined {
2
+ /* Doubled class so this beats the Google stylesheet, which arrives later
3
+ and sets font-size: 24px. That late size change moves the hero. */
4
+ .material-symbols-outlined.material-symbols-outlined {
3
5
  font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
6
+ font-weight: normal;
7
+ font-style: normal;
8
+ font-size: inherit;
9
+ letter-spacing: normal;
4
10
  display: inline-flex;
5
11
  align-items: center;
6
12
  justify-content: center;
7
13
  vertical-align: middle;
8
14
  line-height: 1;
9
15
  text-transform: none;
16
+ white-space: nowrap;
17
+ /* One em square. Until the icon face arrives the ligature is a word,
18
+ and an unsized word widens the control. */
19
+ width: 1em;
20
+ height: 1em;
21
+ min-width: 1em;
22
+ overflow: hidden;
23
+ flex-shrink: 0;
24
+ }
25
+
26
+ .material-symbols-outlined.text-sm { font-size: 0.875rem; }
27
+ .material-symbols-outlined.text-lg { font-size: 1.125rem; }
28
+ .material-symbols-outlined.text-xl { font-size: 1.25rem; }
29
+ .material-symbols-outlined.text-5xl { font-size: 3rem; }
30
+
31
+ @media (min-width: 768px) {
32
+ .material-symbols-outlined.text-6xl { font-size: 3.75rem; }
33
+ }
34
+
35
+ .ledger-cta-arrow.material-symbols-outlined,
36
+ .ledger-cta-icon.material-symbols-outlined {
37
+ font-size: 1.35em;
10
38
  }
11
39
 
12
40
  /* Cubist triad — ~+25% over the 1.25rem theme-toggle box (rounded to 1.5rem). */
@@ -22,6 +50,8 @@
22
50
  font-size: var(--type-card);
23
51
  width: 1.25rem;
24
52
  height: 1.25rem;
53
+ min-width: 1.25rem;
54
+ max-width: 1.25rem;
25
55
  line-height: 1;
26
56
  }
27
57
 
@@ -74,6 +74,19 @@
74
74
  overflow: hidden;
75
75
  }
76
76
 
77
+ /* The first band title is the LCP text. On a phone the hero padding and the
78
+ gap under the copy were pushing that title just past the first viewport,
79
+ so the paint waited on a later frame. */
80
+ @media (max-width: 767px) {
81
+ .home-band--hero {
82
+ padding-block: 1.75rem 1.25rem;
83
+ }
84
+
85
+ .home-hero {
86
+ gap: var(--space-md);
87
+ }
88
+ }
89
+
77
90
  .home-band--hero .home-band-inner {
78
91
  position: relative;
79
92
  z-index: 1;
@@ -25,6 +25,7 @@ module Jekyll
25
25
  site.data["content_fragments"] ||= {}
26
26
 
27
27
  load_page_content(site, content_path)
28
+ load_page_headers(site, content_path)
28
29
  load_structured_data(site, content_path)
29
30
  end
30
31
 
@@ -40,6 +41,17 @@ module Jekyll
40
41
  end
41
42
  end
42
43
 
44
+ # content/pages/headers.yml is also exported as pages/headers.json.
45
+ # The home bands read it here so a section title and note are in the
46
+ # HTML. A heading painted later by the runtime becomes the LCP element.
47
+ def load_page_headers(site, content_path)
48
+ path = File.join(content_path, "pages", "headers.yml")
49
+ return unless File.file?(path)
50
+
51
+ loaded = load_yaml(path)
52
+ site.data["page_headers"] = loaded.is_a?(Hash) ? loaded : {}
53
+ end
54
+
43
55
  def load_structured_data(site, content_path)
44
56
  data_path = File.join(content_path, "collections", "data")
45
57
  return unless Dir.exist?(data_path)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pandorga
4
- VERSION = "1.6.3"
4
+ VERSION = "1.6.5"
5
5
  end
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # One Lighthouse mobile run against the built example home.
3
+ # Thresholds apply to releases. LIGHTHOUSE_MODE=enforce fails below 75
4
+ # and warns below 90. LIGHTHOUSE_MODE=warn never fails the run. PLT-AC-24.
5
+ set -euo pipefail
6
+
7
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
8
+ SITE="${ROOT}/examples/minimal/_site"
9
+ PORT="${LIGHTHOUSE_PORT:-4173}"
10
+ OUT="${LIGHTHOUSE_REPORT:-/tmp/lighthouse-home.json}"
11
+ FAIL_BELOW=75
12
+ WARN_BELOW=90
13
+ MODE="${LIGHTHOUSE_MODE:-enforce}"
14
+
15
+ fail() {
16
+ echo "ERROR: $1" >&2
17
+ exit 1
18
+ }
19
+
20
+ step() { echo "==> $1"; }
21
+
22
+ if [[ "${MODE}" != "enforce" && "${MODE}" != "warn" ]]; then
23
+ fail "LIGHTHOUSE_MODE must be enforce or warn (got ${MODE})"
24
+ fi
25
+
26
+ [[ -f "${SITE}/index.html" ]] || fail "missing ${SITE}/index.html (run scripts/validate.sh first)"
27
+
28
+ if [[ -n "${CHROME_PATH:-}" && -x "${CHROME_PATH}" ]]; then
29
+ CHROME="${CHROME_PATH}"
30
+ elif command -v google-chrome >/dev/null 2>&1; then
31
+ CHROME="$(command -v google-chrome)"
32
+ elif command -v google-chrome-stable >/dev/null 2>&1; then
33
+ CHROME="$(command -v google-chrome-stable)"
34
+ else
35
+ fail "Chrome is not installed"
36
+ fi
37
+
38
+ step "Serving ${SITE} (gzip, concurrent)"
39
+ python3 "${ROOT}/scripts/lighthouse_server.py" "${PORT}" "${SITE}" >/tmp/lighthouse-home-server.log 2>&1 &
40
+ SERVER_PID=$!
41
+ cleanup() {
42
+ kill "${SERVER_PID}" >/dev/null 2>&1 || true
43
+ }
44
+ trap cleanup EXIT
45
+
46
+ ready=0
47
+ for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
48
+ if curl -sf "http://127.0.0.1:${PORT}/" >/dev/null; then
49
+ ready=1
50
+ break
51
+ fi
52
+ sleep 0.25
53
+ done
54
+ [[ "${ready}" == "1" ]] || fail "example server did not answer on port ${PORT}"
55
+
56
+ step "Lighthouse mobile (one run)"
57
+ rm -f "${OUT}"
58
+ npx --yes lighthouse@13.5.0 "http://127.0.0.1:${PORT}/" \
59
+ --only-categories=performance \
60
+ --form-factor=mobile \
61
+ --output=json \
62
+ --output-path="${OUT}" \
63
+ --quiet \
64
+ --no-enable-error-reporting \
65
+ --chrome-path="${CHROME}" \
66
+ --chrome-flags="--headless=new --no-sandbox --disable-gpu"
67
+
68
+ python3 - "${OUT}" "${FAIL_BELOW}" "${WARN_BELOW}" "${MODE}" <<'PY'
69
+ import json
70
+ import sys
71
+
72
+ path, fail_below, warn_below, mode = sys.argv[1:]
73
+ fail_below = int(fail_below)
74
+ warn_below = int(warn_below)
75
+ report = json.load(open(path, encoding="utf-8"))
76
+ raw = report.get("categories", {}).get("performance", {}).get("score")
77
+ if raw is None:
78
+ print("ERROR: Lighthouse returned no performance score", file=sys.stderr)
79
+ sys.exit(1)
80
+ score = int(round(float(raw) * 100))
81
+ print(f"Lighthouse mobile performance: {score} ({mode})")
82
+ campaign = (
83
+ "A score from 75 to 89 is acceptable only to ship a bug fix. "
84
+ "An urgent performance campaign must bring it back to 90 or above."
85
+ )
86
+ if score < fail_below and mode == "enforce":
87
+ print(f"::error::Lighthouse mobile performance {score} is below {fail_below}")
88
+ sys.exit(1)
89
+ if score < fail_below:
90
+ print(
91
+ f"::warning::Lighthouse mobile performance {score} is below {fail_below}. "
92
+ "Thresholds apply to releases. This run only warns."
93
+ )
94
+ if score < warn_below:
95
+ print(
96
+ f"::warning::Lighthouse mobile performance {score} is below {warn_below}. "
97
+ + campaign
98
+ )
99
+ PY
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ """Serve a built site the way the public host does.
3
+
4
+ Python's stock http.server speaks HTTP/1.0 on one thread and sends HTML
5
+ and CSS uncompressed. Lighthouse then scores the same templates far below
6
+ the live site, which compresses responses and serves them concurrently.
7
+ """
8
+
9
+ import gzip
10
+ import io
11
+ import os
12
+ import sys
13
+ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
14
+
15
+ COMPRESSIBLE_EXACT = {
16
+ "application/javascript",
17
+ "application/json",
18
+ "application/xml",
19
+ "image/svg+xml",
20
+ }
21
+
22
+
23
+ def compressible(ctype):
24
+ return ctype.startswith("text/") or ctype in COMPRESSIBLE_EXACT
25
+
26
+
27
+ class Handler(SimpleHTTPRequestHandler):
28
+ protocol_version = "HTTP/1.1"
29
+
30
+ def send_head(self):
31
+ path = self.translate_path(self.path)
32
+ if os.path.isdir(path):
33
+ index = os.path.join(path, "index.html")
34
+ if os.path.isfile(index):
35
+ path = index
36
+ else:
37
+ return super().send_head()
38
+
39
+ ctype = self.guess_type(path)
40
+ accept = self.headers.get("Accept-Encoding", "")
41
+ if "gzip" not in accept or not compressible(ctype):
42
+ return super().send_head()
43
+
44
+ try:
45
+ with open(path, "rb") as raw:
46
+ data = raw.read()
47
+ except OSError:
48
+ self.send_error(404, "File not found")
49
+ return None
50
+
51
+ body = gzip.compress(data, compresslevel=6)
52
+ self.send_response(200)
53
+ self.send_header("Content-Type", ctype)
54
+ self.send_header("Content-Encoding", "gzip")
55
+ self.send_header("Vary", "Accept-Encoding")
56
+ self.send_header("Content-Length", str(len(body)))
57
+ self.end_headers()
58
+ return io.BytesIO(body)
59
+
60
+ def log_message(self, fmt, *args):
61
+ return
62
+
63
+
64
+ def main():
65
+ if len(sys.argv) != 3:
66
+ print("usage: lighthouse_server.py PORT DIRECTORY", file=sys.stderr)
67
+ sys.exit(2)
68
+ port = int(sys.argv[1])
69
+ os.chdir(sys.argv[2])
70
+ ThreadingHTTPServer(("127.0.0.1", port), Handler).serve_forever()
71
+
72
+
73
+ if __name__ == "__main__":
74
+ main()
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Lighthouse mobile release rule and the non-blocking icon sheet.
5
+ # PLT-AC-24, PLT-AC-25.
6
+
7
+ require "pathname"
8
+
9
+ ROOT = Pathname.new(__dir__).join("..", "..").expand_path
10
+
11
+ def fail!(message)
12
+ warn "FAIL test-lighthouse-policy: #{message}"
13
+ exit 1
14
+ end
15
+
16
+ docs = {
17
+ "spec" => ROOT.join("docs/spec.md").read,
18
+ "contributing" => ROOT.join("CONTRIBUTING.md").read,
19
+ "readme" => ROOT.join("README.md").read
20
+ }
21
+ shell = ROOT.join("_layouts/shell.html").read
22
+ base = ROOT.join("assets/css/base.css").read
23
+ script = ROOT.join("scripts/lighthouse-home.sh").read
24
+ workflow = ROOT.join(".github/workflows/validate.yml").read
25
+ release = ROOT.join(".github/workflows/publish-gem.yml").read
26
+
27
+ docs.each do |name, text|
28
+ flat = text.gsub(/\s+/, " ")
29
+ fail!("#{name} does not require 90 or above") unless flat.include?("90 or above")
30
+ fail!("#{name} does not limit 75 to 89 to a bug fix") unless flat.include?("only to ship a bug fix")
31
+ fail!("#{name} does not require the performance campaign") unless flat.include?("urgent performance campaign")
32
+ fail!("#{name} does not forbid a score below 75") unless flat.include?("Below 75 is never acceptable")
33
+ fail!("#{name} does not limit the floors to releases") unless flat.include?("apply to releases")
34
+ fail!("#{name} does not allow a lower pull-request score") unless flat.include?("may score lower")
35
+ fail!("#{name} does not allow modular work") unless flat.include?("modular")
36
+ end
37
+
38
+ spec = docs.fetch("spec")
39
+ contributing = docs.fetch("contributing")
40
+
41
+ fail!("spec dropped the slower-release reason") unless spec.include?("slower release is better than a broken one")
42
+ fail!("contributing dropped the slower-release reason") unless contributing.include?("slower release is better than a broken one")
43
+
44
+ fail!("lighthouse script is not the CI check") unless workflow.include?("./scripts/lighthouse-home.sh")
45
+ fail!("pull requests still fail the lighthouse gate") unless workflow.include?("LIGHTHOUSE_MODE: warn")
46
+ fail!("release build does not enforce the score") unless release.include?("LIGHTHOUSE_MODE: enforce")
47
+ fail!("tag builds do not run the release gate") unless release.include?('tags: ["v*"]')
48
+ fail!("lighthouse script lost the fail floor") unless script.include?("FAIL_BELOW=75")
49
+ fail!("lighthouse script lost the warn floor") unless script.include?("WARN_BELOW=90")
50
+ fail!("lighthouse script lost enforce mode") unless script.include?('LIGHTHOUSE_MODE:-enforce')
51
+ fail!("warn mode still fails the run") unless script.include?('mode == "enforce"')
52
+ fail!("lighthouse script is not a single mobile run") unless script.include?("--form-factor=mobile")
53
+ fail!("lighthouse script repeats runs") if script.include?("--repeat")
54
+ fail!("example server is still one-thread HTTP/1.0") if script.include?("python3 -m http.server")
55
+ server = ROOT.join("scripts/lighthouse_server.py").read
56
+ fail!("example server does not gzip") unless server.include?('Content-Encoding", "gzip"')
57
+ fail!("example server is not concurrent") unless server.include?("ThreadingHTTPServer")
58
+ fail!("icon face is preloaded") if shell.include?("rel=\"preload\" as=\"style\" href=\"{{ _symbols_href }}\"")
59
+
60
+ screen = shell.sub(%r{<noscript>.*?</noscript>}m, "")
61
+ fail!("Material Symbols sheet is missing") unless shell.include?("Material+Symbols+Outlined")
62
+ fail!("icon sheet is not deferred until load") unless screen.include?("addEventListener('load'")
63
+ fail!("screen still has a Material Symbols stylesheet link") if screen.match?(/<link\b[^>]*Material\+Symbols/)
64
+
65
+ icon = base[/\.material-symbols-outlined\.material-symbols-outlined \{.*?\n\}/m]
66
+ fail!("icon rule missing") unless icon
67
+ fail!("icon box is not 1em") unless icon.include?("width: 1em;") && icon.include?("height: 1em;")
68
+ fail!("icon size can still jump when the Google sheet arrives") unless icon.include?("font-size: inherit;")
69
+ fail!("icon box does not clip the fallback word") unless icon.include?("overflow: hidden;")
70
+ fail!("content runtime still precedes the page") unless shell.index("{{ content }}") && shell.index("page/content-runtime.html") > shell.index("{{ content }}")
71
+ band = File.read(File.expand_path("../../_includes/home/band-listing.html", __dir__))
72
+ fail!("band title is still painted by script") unless band.include?('data-content-section-title>{{ _title }}')
73
+ fail!("band note is still painted by script") unless band.include?("page_headers")
74
+ headers = File.read(File.expand_path("../../_plugins/content_headers.rb", __dir__))
75
+ fail!("page headers are not loaded at build time") unless headers.include?('site.data["page_headers"]')
76
+ home = File.read(File.expand_path("../../_layouts/home.html", __dir__))
77
+ fail!("hero subtitle is still painted by script") unless home.include?("page_headers.home") && home.include?("data-content-static")
78
+
79
+ puts "PASS test-lighthouse-policy"
data/scripts/validate.sh CHANGED
@@ -59,6 +59,7 @@ TESTS=(
59
59
  scripts/test/test-shell-a11y.rb
60
60
  scripts/test/test-hydrate-reserve.rb
61
61
  scripts/test/test-hero-summary.rb
62
+ scripts/test/test-lighthouse-policy.rb
62
63
  scripts/test/test-css-delivery.rb
63
64
  scripts/test/test-discovery.rb
64
65
  )
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pandorga
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.3
4
+ version: 1.6.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors
@@ -322,6 +322,8 @@ files:
322
322
  - scripts/lib/r2_json_sync.rb
323
323
  - scripts/lib/withdrawn_public_objects.rb
324
324
  - scripts/lib/writing_index_contract.rb
325
+ - scripts/lighthouse-home.sh
326
+ - scripts/lighthouse_server.py
325
327
  - scripts/pdf/md-to-pdf.mjs
326
328
  - scripts/pdf/md-to-pdf.sh
327
329
  - scripts/tailwind/build.sh
@@ -412,6 +414,7 @@ files:
412
414
  - scripts/test/test-install-functions.rb
413
415
  - scripts/test/test-language-export.rb
414
416
  - scripts/test/test-ledger-blocks.rb
417
+ - scripts/test/test-lighthouse-policy.rb
415
418
  - scripts/test/test-listing-filter-contract.rb
416
419
  - scripts/test/test-math-row-breaks.mjs
417
420
  - scripts/test/test-math-row-breaks.rb