pandorga 1.7.1 → 1.7.3
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 +37 -0
- data/_data/themes/dark.yml +2 -1
- data/_includes/content-runtime/50-detail.html +16 -1
- data/_includes/content-runtime/60-fragments.html +2 -2
- data/_includes/page/page-header.html +3 -1
- data/_includes/theme/json-ld.html +3 -2
- data/_layouts/shell.html +3 -3
- data/lib/pandorga/detail_routes.rb +36 -0
- data/lib/pandorga/jekyll/plugins/shell_redirects.rb +26 -0
- data/lib/pandorga/version.rb +1 -1
- data/scripts/test/test-discovery.rb +34 -0
- data/scripts/test/test-examples-build.rb +60 -0
- data/scripts/test/test-hero-portraits.mjs +12 -2
- data/scripts/test/test-hero-portraits.rb +12 -4
- data/scripts/test/test-lighthouse-policy.rb +4 -0
- data/scripts/test/test-shell-a11y.rb +6 -0
- data/scripts/test/test-theme-contrast.rb +43 -0
- data/scripts/validate.sh +6 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c5c04a8266084060b4aa4810ef57d5d9c25a29302ebd8d946183ba2b95a2daeb
|
|
4
|
+
data.tar.gz: b0aceffe59d1febefc9a308db429f7da42856463b1b7143bdb23f125729de192
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22939efa0329adbf9cc11e0ba771a9fd11d0e154777df7918a1a7bbf821295f3592f4ca1233e43448578deb8a9c69955696f3df282c9f435389e371c69815c1d
|
|
7
|
+
data.tar.gz: 70590f4574d43b101ce9e4148d53df33fc1f6f6e2225df5368d0b11a636d425b1300a10d150038c8d2494e004aaa78f7efd495d56a5cadadee1f671f6ae58158
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.3] — 2026-10-12
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- A detail URL is the listing shell again. The build does not write a
|
|
8
|
+
static page per article, post, project, or product, and those URLs
|
|
9
|
+
are not in the sitemap. The published `_redirects` keeps the splat
|
|
10
|
+
200 rewrites onto the listing shells, and adds any line the source
|
|
11
|
+
file is missing. The source file is unchanged. The client sets the
|
|
12
|
+
canonical link and `og:url` when it renders the entry.
|
|
13
|
+
|
|
14
|
+
No `pandorga:` config keys were added or renamed. Font roles and the
|
|
15
|
+
ledger fallbacks are unchanged.
|
|
16
|
+
|
|
17
|
+
## [1.7.2] — 2026-10-11
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- A published article, post, project, or product is a static page at its
|
|
22
|
+
public URL. The sitemap lists that URL. The static canonical and
|
|
23
|
+
`og:url` are the detail path, and `data-listing-path` stays the
|
|
24
|
+
listing. Articles emit `Article` JSON-LD. Posts emit `BlogPosting`.
|
|
25
|
+
- The build writes `_redirects` into the output and drops the splat 200
|
|
26
|
+
lines that sent those URLs back to the listing shell. The source file
|
|
27
|
+
is unchanged. Publish the built file.
|
|
28
|
+
- A listing page's static `<h1>` is the title from
|
|
29
|
+
`content/pages/headers.yml`, or the page title. It is not the word
|
|
30
|
+
Loading.
|
|
31
|
+
- The top bar is a `<header>`. The section links are one
|
|
32
|
+
`<nav aria-label="Sections">` inside it.
|
|
33
|
+
- Dark theme text on the tertiary container is `#ebe7c3` on `#6a684f`
|
|
34
|
+
(4.52:1). The container colour is unchanged.
|
|
35
|
+
- The hero portrait gate crops the runtime through `pickHeroPortrait`.
|
|
36
|
+
|
|
37
|
+
No `pandorga:` config keys were added or renamed. Font roles and the
|
|
38
|
+
ledger fallbacks are unchanged.
|
|
39
|
+
|
|
3
40
|
## [1.7.1] — 2026-10-11
|
|
4
41
|
|
|
5
42
|
### Fixed
|
data/_data/themes/dark.yml
CHANGED
|
@@ -13,7 +13,8 @@ colors:
|
|
|
13
13
|
tertiary_container: "#6a684f"
|
|
14
14
|
on_primary_container: "#c0d8e6"
|
|
15
15
|
on_secondary_container: "#b5d9c2"
|
|
16
|
-
|
|
16
|
+
# #e8e4c0 on #6a684f was 4.40:1. Three steps lighter clears 4.5:1.
|
|
17
|
+
on_tertiary_container: "#ebe7c3"
|
|
17
18
|
chart_project: "#98c8ac"
|
|
18
19
|
|
|
19
20
|
neutrals:
|
|
@@ -515,6 +515,17 @@
|
|
|
515
515
|
el.setAttribute('content', content);
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
+
function setCanonicalLink(url) {
|
|
519
|
+
if (!url) return;
|
|
520
|
+
var el = document.head.querySelector('link[rel="canonical"]');
|
|
521
|
+
if (!el) {
|
|
522
|
+
el = document.createElement('link');
|
|
523
|
+
el.setAttribute('rel', 'canonical');
|
|
524
|
+
document.head.appendChild(el);
|
|
525
|
+
}
|
|
526
|
+
el.setAttribute('href', url);
|
|
527
|
+
}
|
|
528
|
+
|
|
518
529
|
function applyDocumentMeta(meta) {
|
|
519
530
|
meta = meta || {};
|
|
520
531
|
var siteSuffix = (window.ContentRuntimeConfig && window.ContentRuntimeConfig.siteSuffix) || '';
|
|
@@ -523,7 +534,11 @@
|
|
|
523
534
|
if (meta.description) setMetaTag('description', meta.description);
|
|
524
535
|
setMetaTag('og:title', title, 'property');
|
|
525
536
|
if (meta.description) setMetaTag('og:description', meta.description, 'property');
|
|
526
|
-
if (meta.url)
|
|
537
|
+
if (meta.url) {
|
|
538
|
+
var pageUrl = absoluteUrl(meta.url);
|
|
539
|
+
setMetaTag('og:url', pageUrl, 'property');
|
|
540
|
+
setCanonicalLink(pageUrl);
|
|
541
|
+
}
|
|
527
542
|
|
|
528
543
|
var defaultOgImage = '/assets/images/og-default-dark.png';
|
|
529
544
|
var imagePath = meta.image || defaultOgImage;
|
|
@@ -264,7 +264,7 @@
|
|
|
264
264
|
var standfirstEl = root.querySelector('[data-content-header-standfirst]');
|
|
265
265
|
return loadPageHeaders().then(function (headers) {
|
|
266
266
|
var data = headers[pageKey];
|
|
267
|
-
if (titleEl) {
|
|
267
|
+
if (titleEl && !titleEl.hasAttribute('data-content-static')) {
|
|
268
268
|
var title = data && data.title;
|
|
269
269
|
if (title) {
|
|
270
270
|
titleEl.innerHTML = stripSingleParagraph(parseInlineMarkdown(String(title)));
|
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
}
|
|
300
300
|
}).catch(function (error) {
|
|
301
301
|
console.error(error);
|
|
302
|
-
if (titleEl) {
|
|
302
|
+
if (titleEl && !titleEl.hasAttribute('data-content-static')) {
|
|
303
303
|
titleEl.textContent = 'Page header could not be loaded right now.';
|
|
304
304
|
}
|
|
305
305
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
{% assign _header_key = include.key | default: page.key %}
|
|
2
|
+
{% assign _header_record = site.data.page_headers[_header_key] %}
|
|
3
|
+
{% assign _header_title = _header_record.title | default: page.title | default: _header_key %}
|
|
2
4
|
|
|
3
5
|
<header class="{{ include.header_class | default: 'mb-10 md:mb-12' }}" data-content-page-header="{{ _header_key }}">
|
|
4
6
|
<div class="page-header-row{% if include.listing_filter %} page-header-row--with-filter{% endif %}">
|
|
@@ -6,7 +8,7 @@
|
|
|
6
8
|
<span data-content-header-eyebrow class="{{ include.eyebrow_class | default: 'font-label text-sm text-secondary block mb-4 hidden' }}" hidden></span>
|
|
7
9
|
|
|
8
10
|
<h1 class="page-header-title font-title{% if include.title_class %} {{ include.title_class }}{% endif %}">
|
|
9
|
-
<span data-content-header-title class="italic">
|
|
11
|
+
<span data-content-header-title class="italic"{% if _header_title != blank %} data-content-static{% endif %}>{% if _header_title contains "*" %}{% assign _header_bits = _header_title | split: "*" %}{% for _header_bit in _header_bits %}{% assign _header_em = forloop.index0 | modulo: 2 %}{% if _header_em == 1 %}<em>{{ _header_bit | escape }}</em>{% else %}{{ _header_bit | escape }}{% endif %}{% endfor %}{% else %}{{ _header_title | escape }}{% endif %}</span>
|
|
10
12
|
</h1>
|
|
11
13
|
|
|
12
14
|
<span data-content-header-tags class="{{ include.tags_class | default: 'page-header-tags font-label text-tertiary mt-4 block hidden' }}" hidden></span>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
WebSite and Person from identity.
|
|
3
|
-
|
|
2
|
+
WebSite and Person from identity. Detail Article and BlogPosting
|
|
3
|
+
JSON-LD is not written into the shell. No new required keys: name is
|
|
4
|
+
already required, and url is the existing optional canonical origin.
|
|
4
5
|
{%- endcomment -%}
|
|
5
6
|
{%- assign ld_name = site.pandorga.identity.name | default: site.author | default: site.title -%}
|
|
6
7
|
{%- assign ld_url = site.pandorga.identity.url | default: site.url -%}
|
data/_layouts/shell.html
CHANGED
|
@@ -211,8 +211,8 @@
|
|
|
211
211
|
|
|
212
212
|
{% assign _all_pages = site.pages %}
|
|
213
213
|
|
|
214
|
-
<!-- TopNavBar -->
|
|
215
|
-
<
|
|
214
|
+
<!-- TopNavBar. The bar is the banner landmark. The section links are the only nav inside it. -->
|
|
215
|
+
<header class="sticky top-0 z-50 bg-surface">
|
|
216
216
|
<div class="flex justify-between items-center w-full px-6 sm:px-8 md:px-10 pt-5 pb-3.5 leading-none max-w-7xl mx-auto">
|
|
217
217
|
<div class="flex items-center gap-3 sm:gap-4 min-w-0">
|
|
218
218
|
<a href="/" aria-label="{% if _home_label != blank %}{{ _home_label }} – Home{% else %}Home{% endif %}" class="shrink-0 inline-flex items-center gap-3 site-wordmark-link">
|
|
@@ -279,7 +279,7 @@
|
|
|
279
279
|
</div>
|
|
280
280
|
<!-- Tonal separator — "No-Line" design rule: background-color shift instead of a CSS border -->
|
|
281
281
|
<div class="bg-surface-container-low h-[1px]"></div>
|
|
282
|
-
</
|
|
282
|
+
</header>
|
|
283
283
|
<script>
|
|
284
284
|
(function () {
|
|
285
285
|
var wrap = document.getElementById('site-menu');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pandorga
|
|
4
|
+
# Splat 200 lines that serve every detail URL from one listing shell.
|
|
5
|
+
# Cloudflare follows `_redirects` for the request, and the shell loads
|
|
6
|
+
# the entry from the content runtime. The source file is left alone;
|
|
7
|
+
# a missing line is added only to the published copy.
|
|
8
|
+
module DetailRoutes
|
|
9
|
+
SHELL_REWRITES = [
|
|
10
|
+
["/articles/:slug", "/pages/articles/"],
|
|
11
|
+
["/posts/:slug", "/pages/blog/"],
|
|
12
|
+
["/blog/:slug", "/pages/blog/"],
|
|
13
|
+
["/projects/:slug", "/pages/projects/"],
|
|
14
|
+
["/products/:slug", "/pages/cv/"]
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def ensure_shell_rewrites(text)
|
|
20
|
+
body = text.to_s
|
|
21
|
+
missing = SHELL_REWRITES.reject { |prefix, _target| rewrite_present?(body, prefix) }
|
|
22
|
+
return body if missing.empty?
|
|
23
|
+
|
|
24
|
+
addition = missing.map { |prefix, target| "#{prefix} #{target} 200" }.join("\n")
|
|
25
|
+
stripped = body.rstrip
|
|
26
|
+
stripped.empty? ? "#{addition}\n" : "#{stripped}\n#{addition}\n"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def rewrite_present?(text, prefix)
|
|
30
|
+
text.to_s.lines.any? do |line|
|
|
31
|
+
stripped = line.strip
|
|
32
|
+
stripped.match?(%r{\A#{Regexp.escape(prefix)}/?\s+\S+\s+200\z})
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jekyll"
|
|
4
|
+
|
|
5
|
+
require_relative "../../detail_routes"
|
|
6
|
+
|
|
7
|
+
# Jekyll skips underscore files, so a source `_redirects` never reaches
|
|
8
|
+
# the output unless the site lists it under `include`. The published
|
|
9
|
+
# file is the one Cloudflare reads. Detail URLs stay on the listing
|
|
10
|
+
# shell, so the splat 200 lines have to be in that file. The source
|
|
11
|
+
# file is not edited.
|
|
12
|
+
Jekyll::Hooks.register :site, :post_write do |site|
|
|
13
|
+
dest = File.join(site.dest, "_redirects")
|
|
14
|
+
source = File.join(site.source, "_redirects")
|
|
15
|
+
text = if File.file?(source)
|
|
16
|
+
File.read(source)
|
|
17
|
+
elsif File.file?(dest)
|
|
18
|
+
File.read(dest)
|
|
19
|
+
else
|
|
20
|
+
""
|
|
21
|
+
end
|
|
22
|
+
updated = Pandorga::DetailRoutes.ensure_shell_rewrites(text)
|
|
23
|
+
next if File.file?(dest) && File.read(dest) == updated
|
|
24
|
+
|
|
25
|
+
File.write(dest, updated)
|
|
26
|
+
end
|
data/lib/pandorga/version.rb
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
require "pathname"
|
|
7
7
|
|
|
8
8
|
require_relative "../../lib/pandorga/discovery"
|
|
9
|
+
require_relative "../../lib/pandorga/detail_routes"
|
|
9
10
|
|
|
10
11
|
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
11
12
|
|
|
@@ -47,5 +48,38 @@ image_meta = shell[/<meta name="image"[^>]*>/]
|
|
|
47
48
|
fail!("shell missing name=image") unless image_meta
|
|
48
49
|
fail!("name=image also sets property=og:image") if image_meta.include?("property=")
|
|
49
50
|
fail!("discovery generator is not loaded from the plugin dir") unless ROOT.join("lib/pandorga/jekyll/plugins/discovery.rb").file?
|
|
51
|
+
fail!("detail pages are still generated") if ROOT.join("lib/pandorga/jekyll/plugins/detail_documents.rb").file?
|
|
52
|
+
fail!("shell redirect hook is missing") unless ROOT.join("lib/pandorga/jekyll/plugins/shell_redirects.rb").file?
|
|
53
|
+
|
|
54
|
+
redirects = <<~TXT
|
|
55
|
+
/legacy /articles/2026-01-01-welcome/ 301
|
|
56
|
+
/posts/:slug/ /pages/blog/ 200
|
|
57
|
+
/keep /pages/articles/ 301
|
|
58
|
+
TXT
|
|
59
|
+
ensured = Pandorga::DetailRoutes.ensure_shell_rewrites(redirects)
|
|
60
|
+
%w[/articles/:slug /posts/:slug /blog/:slug /projects/:slug /products/:slug].each do |prefix|
|
|
61
|
+
present = ensured.lines.any? { |line| line.strip.start_with?("#{prefix} ") || line.strip.start_with?("#{prefix}/ ") }
|
|
62
|
+
fail!("published _redirects missing #{prefix}") unless present
|
|
63
|
+
end
|
|
64
|
+
fail!("a 301 was dropped") unless ensured.include?("/legacy /articles/2026-01-01-welcome/ 301")
|
|
65
|
+
fail!("a listing 301 was dropped") unless ensured.include?("/keep /pages/articles/ 301")
|
|
66
|
+
posts = ensured.lines.select { |line| line.include?("/posts/:slug") }
|
|
67
|
+
fail!("an existing splat was duplicated") unless posts.size == 1
|
|
68
|
+
fail!("an existing splat target was rewritten") unless posts.first.include?("/pages/blog/")
|
|
69
|
+
fail!("ensure is not idempotent") unless Pandorga::DetailRoutes.ensure_shell_rewrites(ensured) == ensured
|
|
70
|
+
|
|
71
|
+
serve = ROOT.join("lib/pandorga/jekyll/plugins/local_detail_rewrites.rb").read
|
|
72
|
+
plugins_serve = ROOT.join("_plugins/local_detail_rewrites.rb").read
|
|
73
|
+
fail!("the two local serve copies diverged") unless serve == plugins_serve
|
|
74
|
+
fail!("local serve skips a detail URL") if serve.include?("static_index?")
|
|
75
|
+
fail!("local serve does not rewrite detail URLs") unless serve.include?("ContentDetailPaths.rewrite_path(req.path)")
|
|
76
|
+
|
|
77
|
+
detail = ROOT.join("_includes/content-runtime/50-detail.html").read
|
|
78
|
+
fail!("the client does not set the canonical link") unless detail.include?("setCanonicalLink")
|
|
79
|
+
fail!("the client does not set og:url") unless detail.include?("setMetaTag('og:url'")
|
|
80
|
+
|
|
81
|
+
json_ld = ROOT.join("_includes/theme/json-ld.html").read
|
|
82
|
+
fail!("the build still emits detail JSON-LD") if json_ld.include?("schema_type")
|
|
83
|
+
fail!("shell JSON-LD dropped the person") unless json_ld.include?('"@type": "Person"')
|
|
50
84
|
|
|
51
85
|
puts "PASS test-discovery"
|
|
@@ -33,6 +33,10 @@ end
|
|
|
33
33
|
FileUtils.rm_rf(example.join("_site"))
|
|
34
34
|
FileUtils.rm_rf(example.join("_content_json"))
|
|
35
35
|
|
|
36
|
+
redirects_path = example.join("_redirects")
|
|
37
|
+
original_redirects = redirects_path.file? ? redirects_path.read : nil
|
|
38
|
+
|
|
39
|
+
begin
|
|
36
40
|
run!("bundle", "install", "--quiet", chdir: example) if example.join("Gemfile").file?
|
|
37
41
|
run!("ruby", EXE.to_s, "export", "content", "_content_json", chdir: example)
|
|
38
42
|
run!("bundle", "exec", "jekyll", "build", "--quiet", chdir: example)
|
|
@@ -65,6 +69,18 @@ end
|
|
|
65
69
|
warn "FAIL test-examples-build: full home still hydrates the summary"
|
|
66
70
|
exit 1
|
|
67
71
|
end
|
|
72
|
+
{
|
|
73
|
+
"pages/articles/index.html" => "Articles",
|
|
74
|
+
"pages/blog/index.html" => "Rascunhos",
|
|
75
|
+
"pages/projects/index.html" => "Projects"
|
|
76
|
+
}.each do |rel, title|
|
|
77
|
+
listing = example.join("_site", rel).read
|
|
78
|
+
heading = listing[/<h1 class="page-header-title.*?<\/h1>/m].to_s
|
|
79
|
+
unless heading.include?(title) && heading.include?("data-content-static") && !heading.include?("Loading...")
|
|
80
|
+
warn "FAIL test-examples-build: #{rel} static h1 is #{heading.inspect}"
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
end
|
|
68
84
|
else
|
|
69
85
|
unless html.include?('id="home-cv-summary" class="home-hero-summary font-body" data-content-fragment="cv/summary" data-content-quiet')
|
|
70
86
|
warn "FAIL test-examples-build: minimal home dropped the summary fallback"
|
|
@@ -86,6 +102,47 @@ end
|
|
|
86
102
|
warn "FAIL test-examples-build: #{name} robots missing the sitemap line\n#{robots}"
|
|
87
103
|
exit 1
|
|
88
104
|
end
|
|
105
|
+
built_redirects_path = example.join("_site/_redirects")
|
|
106
|
+
unless built_redirects_path.file?
|
|
107
|
+
warn "FAIL test-examples-build: #{name} missing built _redirects"
|
|
108
|
+
exit 1
|
|
109
|
+
end
|
|
110
|
+
built_redirects = built_redirects_path.read
|
|
111
|
+
[
|
|
112
|
+
"/articles/:slug /pages/articles/ 200",
|
|
113
|
+
"/posts/:slug /pages/blog/ 200",
|
|
114
|
+
"/blog/:slug /pages/blog/ 200",
|
|
115
|
+
"/projects/:slug /pages/projects/ 200",
|
|
116
|
+
"/products/:slug /pages/cv/ 200"
|
|
117
|
+
].each do |line|
|
|
118
|
+
unless built_redirects.include?(line)
|
|
119
|
+
warn "FAIL test-examples-build: #{name} built _redirects missing #{line}\n#{built_redirects}"
|
|
120
|
+
exit 1
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
if original_redirects && redirects_path.read != original_redirects
|
|
124
|
+
warn "FAIL test-examples-build: #{name} build edited the source _redirects"
|
|
125
|
+
exit 1
|
|
126
|
+
end
|
|
127
|
+
if name == "full"
|
|
128
|
+
%w[
|
|
129
|
+
/articles/2026-01-01-welcome/
|
|
130
|
+
/posts/2026-01-02-note/
|
|
131
|
+
/projects/demo-kit/
|
|
132
|
+
/products/ledger-tool/
|
|
133
|
+
].each do |path|
|
|
134
|
+
loc = "#{origin}#{path}"
|
|
135
|
+
if sitemap.include?("<loc>#{loc}</loc>")
|
|
136
|
+
warn "FAIL test-examples-build: sitemap lists detail #{loc}"
|
|
137
|
+
exit 1
|
|
138
|
+
end
|
|
139
|
+
page = example.join("_site#{path}index.html")
|
|
140
|
+
if page.file?
|
|
141
|
+
warn "FAIL test-examples-build: static detail page #{page}"
|
|
142
|
+
exit 1
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
89
146
|
unless html.include?('application/ld+json') && html.include?('"@type": "Person"')
|
|
90
147
|
warn "FAIL test-examples-build: #{name} home is missing JSON-LD"
|
|
91
148
|
exit 1
|
|
@@ -99,6 +156,9 @@ end
|
|
|
99
156
|
warn "FAIL test-examples-build: #{name} home dropped the title face"
|
|
100
157
|
exit 1
|
|
101
158
|
end
|
|
159
|
+
ensure
|
|
160
|
+
redirects_path.write(original_redirects) if original_redirects
|
|
161
|
+
end
|
|
102
162
|
end
|
|
103
163
|
|
|
104
164
|
puts "PASS test-examples-build"
|
|
@@ -13,14 +13,24 @@ const source = fs.readFileSync(
|
|
|
13
13
|
);
|
|
14
14
|
|
|
15
15
|
const start = source.indexOf('function heroPortraitEntries');
|
|
16
|
-
|
|
16
|
+
// applyHeroPortraitSources is defined before pickHeroPortrait. Ending at
|
|
17
|
+
// applyHeroPortrait( still works today, and it is one character away from
|
|
18
|
+
// stopping on the Sources function. showHeroPortrait is the next function
|
|
19
|
+
// after the picker, so the slice always includes pickHeroPortrait.
|
|
20
|
+
const end = source.indexOf('function showHeroPortrait');
|
|
17
21
|
if (start < 0 || end < start) {
|
|
18
22
|
console.error('hero portrait functions missing from 60-fragments.html');
|
|
19
23
|
process.exit(1);
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
const slice = source.slice(start, end);
|
|
27
|
+
if (!slice.includes('function pickHeroPortrait')) {
|
|
28
|
+
console.error('hero portrait crop stopped before pickHeroPortrait');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
const api = new Function(
|
|
23
|
-
`${
|
|
33
|
+
`${slice}\nreturn { heroPortraitEntries, pickHeroPortrait };`
|
|
24
34
|
)();
|
|
25
35
|
|
|
26
36
|
const { heroPortraitEntries, pickHeroPortrait } = api;
|
|
@@ -37,13 +37,19 @@ puts "Testing home hero portraits..."
|
|
|
37
37
|
|
|
38
38
|
# ── AC-HERO-01 ──────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
check!(POOL.directory?, "missing #{POOL}")
|
|
41
40
|
check!(!ROOT.join("content/pages/home/portraits.yml").exist?,
|
|
42
41
|
"the author does not keep a portrait list")
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
# The four photographs live with the site, not in the gem. When they are
|
|
44
|
+
# here, the folder must be exactly that set. When they are not, the picker
|
|
45
|
+
# extracted from the runtime still has to run.
|
|
46
|
+
personal_pool = %w[DSC00571.JPG DSC00632.JPG DSC00721.JPG DSC00727.JPG]
|
|
47
|
+
pool_names = POOL.directory? ? POOL.children.select(&:file?).map { |path| path.basename.to_s }.sort : []
|
|
48
|
+
if pool_names.empty?
|
|
49
|
+
puts "AC-HERO-01: no hero photographs in this checkout"
|
|
50
|
+
elsif pool_names != personal_pool
|
|
51
|
+
check!(false, "the hero folder must hold the four uploaded photographs, got #{pool_names.inspect}")
|
|
52
|
+
end
|
|
47
53
|
|
|
48
54
|
Dir.mktmpdir("hero-pool") do |dir|
|
|
49
55
|
root = Pathname.new(dir)
|
|
@@ -135,6 +141,7 @@ check!(status.success?, "picker cases failed")
|
|
|
135
141
|
|
|
136
142
|
# ── AC-HERO-02 ──────────────────────────────────────────────────────────────
|
|
137
143
|
|
|
144
|
+
if pool_names == personal_pool
|
|
138
145
|
Dir.mktmpdir("hero-portraits") do |dir|
|
|
139
146
|
out = Pathname.new(dir).join("export")
|
|
140
147
|
env = { "CONTENT_API_BASE_URL" => "", "CONTENT_EXPORT_SKIP_VALIDATE" => "1" }
|
|
@@ -158,5 +165,6 @@ Dir.mktmpdir("hero-portraits") do |dir|
|
|
|
158
165
|
check!(listed.include?("pages/home/portraits.json"),
|
|
159
166
|
"manifest page_headers must include the portrait list, got #{listed.inspect}")
|
|
160
167
|
end
|
|
168
|
+
end
|
|
161
169
|
|
|
162
170
|
puts "All home hero portrait checks passed."
|
|
@@ -101,6 +101,10 @@ fail!("band note is still painted by script") unless band.include?("page_headers
|
|
|
101
101
|
fail!("project inventory has a name and no role") unless band.include?('role="region"') && band.include?('aria-label="Project inventory"')
|
|
102
102
|
headers = File.read(File.expand_path("../../_plugins/content_headers.rb", __dir__))
|
|
103
103
|
fail!("page headers are not loaded at build time") unless headers.include?('site.data["page_headers"]')
|
|
104
|
+
page_header = File.read(File.expand_path("../../_includes/page/page-header.html", __dir__))
|
|
105
|
+
fail!("listing h1 is still Loading...") if page_header.include?("Loading...")
|
|
106
|
+
fail!("listing h1 does not read page headers") unless page_header.include?("page_headers")
|
|
107
|
+
fail!("listing h1 is not marked static") unless page_header.include?("data-content-static")
|
|
104
108
|
home = File.read(File.expand_path("../../_layouts/home.html", __dir__))
|
|
105
109
|
fail!("hero subtitle is still painted by script") unless home.include?("page_headers.home") && home.include?("data-content-static")
|
|
106
110
|
|
|
@@ -27,6 +27,12 @@ fail!("footer home link still uses site.author alone") if FOOTER.include?(bare)
|
|
|
27
27
|
fail!("shell home link needs an identity.name fallback") unless SHELL.include?("site.pandorga.identity.name")
|
|
28
28
|
fail!("footer home link needs an identity.name fallback") unless FOOTER.include?("site.pandorga.identity.name")
|
|
29
29
|
|
|
30
|
+
fail!("top bar is still a nav") if SHELL.include?('<nav class="sticky')
|
|
31
|
+
fail!("top bar is not a header") unless SHELL.include?('<header class="sticky top-0 z-50 bg-surface">')
|
|
32
|
+
fail!("section links lost their name") unless SHELL.include?('aria-label="Sections"')
|
|
33
|
+
navs = SHELL.scan(/<nav\b/).size
|
|
34
|
+
fail!("shell should have one nav landmark, found #{navs}") unless navs == 1
|
|
35
|
+
|
|
30
36
|
# Decorative Material Symbols sit beside a text label. Without aria-hidden
|
|
31
37
|
# the ligature ("arrow_left_alt", "shuffle", an icon name) is read aloud.
|
|
32
38
|
ICON_FILES = [
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Dark tertiary container text must clear WCAG AA for normal text.
|
|
5
|
+
# PLT-AC-33
|
|
6
|
+
|
|
7
|
+
require "pathname"
|
|
8
|
+
require "yaml"
|
|
9
|
+
|
|
10
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
11
|
+
|
|
12
|
+
def fail!(message)
|
|
13
|
+
warn "FAIL test-theme-contrast: #{message}"
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def channel(value)
|
|
18
|
+
value = value / 255.0
|
|
19
|
+
value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055)**2.4
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def luminance(hex)
|
|
23
|
+
rgb = hex.delete("#").scan(/../).map { |part| part.to_i(16) }
|
|
24
|
+
fail!("not a hex colour: #{hex}") unless rgb.length == 3
|
|
25
|
+
|
|
26
|
+
(0.2126 * channel(rgb[0])) + (0.7152 * channel(rgb[1])) + (0.0722 * channel(rgb[2]))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def contrast(foreground, background)
|
|
30
|
+
lighter, darker = [luminance(foreground), luminance(background)].sort.reverse
|
|
31
|
+
(lighter + 0.05) / (darker + 0.05)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
dark = YAML.safe_load(ROOT.join("_data/themes/dark.yml").read) || {}
|
|
35
|
+
colors = dark["colors"] || {}
|
|
36
|
+
background = colors["tertiary_container"].to_s
|
|
37
|
+
foreground = colors["on_tertiary_container"].to_s
|
|
38
|
+
fail!("dark tertiary container moved") unless background.casecmp("#6a684f").zero?
|
|
39
|
+
|
|
40
|
+
ratio = contrast(foreground, background)
|
|
41
|
+
fail!("#{foreground} on #{background} is #{ratio.round(2)}:1") unless ratio >= 4.5
|
|
42
|
+
|
|
43
|
+
puts "PASS test-theme-contrast (#{foreground} on #{background} = #{ratio.round(2)}:1)"
|
data/scripts/validate.sh
CHANGED
|
@@ -57,6 +57,7 @@ TESTS=(
|
|
|
57
57
|
scripts/test/test-examples-build.rb
|
|
58
58
|
scripts/test/test-validate-allowlist.rb
|
|
59
59
|
scripts/test/test-shell-a11y.rb
|
|
60
|
+
scripts/test/test-theme-contrast.rb
|
|
60
61
|
scripts/test/test-hydrate-reserve.rb
|
|
61
62
|
scripts/test/test-hero-summary.rb
|
|
62
63
|
scripts/test/test-lighthouse-policy.rb
|
|
@@ -82,6 +83,11 @@ for test in "${TESTS[@]}"; do
|
|
|
82
83
|
fi
|
|
83
84
|
done
|
|
84
85
|
|
|
86
|
+
echo "-- scripts/test/test-hero-portraits.mjs"
|
|
87
|
+
if ! node scripts/test/test-hero-portraits.mjs; then
|
|
88
|
+
failed=1
|
|
89
|
+
fi
|
|
90
|
+
|
|
85
91
|
echo "-- scripts/test/test-studio-pipeline-config.mjs"
|
|
86
92
|
if ! node scripts/test/test-studio-pipeline-config.mjs; then
|
|
87
93
|
failed=1
|
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.7.
|
|
4
|
+
version: 1.7.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Pandorga contributors
|
|
@@ -287,6 +287,7 @@ files:
|
|
|
287
287
|
- lib/pandorga/commands/publish.rb
|
|
288
288
|
- lib/pandorga/commands/serve.rb
|
|
289
289
|
- lib/pandorga/commands/studio_schema.rb
|
|
290
|
+
- lib/pandorga/detail_routes.rb
|
|
290
291
|
- lib/pandorga/discovery.rb
|
|
291
292
|
- lib/pandorga/fonts.rb
|
|
292
293
|
- lib/pandorga/hero_summary.rb
|
|
@@ -317,6 +318,7 @@ files:
|
|
|
317
318
|
- lib/pandorga/jekyll/plugins/lib/text_excerpt.rb
|
|
318
319
|
- lib/pandorga/jekyll/plugins/lib/writing_entry.rb
|
|
319
320
|
- lib/pandorga/jekyll/plugins/local_detail_rewrites.rb
|
|
321
|
+
- lib/pandorga/jekyll/plugins/shell_redirects.rb
|
|
320
322
|
- lib/pandorga/jekyll/plugins/timeline.rb
|
|
321
323
|
- lib/pandorga/jekyll/plugins/web_manifest.rb
|
|
322
324
|
- lib/pandorga/jekyll/plugins/writing_slug_limits.rb
|
|
@@ -466,6 +468,7 @@ files:
|
|
|
466
468
|
- scripts/test/test-studio-slug-config.rb
|
|
467
469
|
- scripts/test/test-template-packages.rb
|
|
468
470
|
- scripts/test/test-text-excerpt.rb
|
|
471
|
+
- scripts/test/test-theme-contrast.rb
|
|
469
472
|
- scripts/test/test-unwrap-media-src.mjs
|
|
470
473
|
- scripts/test/test-unwrap-media-src.rb
|
|
471
474
|
- scripts/test/test-validate-allowlist.rb
|