pandorga 1.7.2 → 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 +14 -0
- data/_includes/content-runtime/50-detail.html +16 -1
- data/_includes/theme/json-ld.html +3 -35
- data/_plugins/local_detail_rewrites.rb +1 -10
- data/lib/pandorga/detail_routes.rb +24 -17
- data/lib/pandorga/jekyll/plugins/local_detail_rewrites.rb +1 -10
- data/lib/pandorga/jekyll/plugins/shell_redirects.rb +26 -0
- data/lib/pandorga/version.rb +1 -1
- data/scripts/test/test-discovery.rb +24 -21
- data/scripts/test/test-examples-build.rb +32 -62
- metadata +2 -2
- data/lib/pandorga/jekyll/plugins/detail_documents.rb +0 -154
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,19 @@
|
|
|
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
|
+
|
|
3
17
|
## [1.7.2] — 2026-10-11
|
|
4
18
|
|
|
5
19
|
### Fixed
|
|
@@ -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;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{%- comment -%}
|
|
2
|
-
WebSite and Person from identity.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
origin.
|
|
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.
|
|
6
5
|
{%- endcomment -%}
|
|
7
6
|
{%- assign ld_name = site.pandorga.identity.name | default: site.author | default: site.title -%}
|
|
8
7
|
{%- assign ld_url = site.pandorga.identity.url | default: site.url -%}
|
|
@@ -11,37 +10,6 @@
|
|
|
11
10
|
{%- else -%}
|
|
12
11
|
{%- assign ld_website = "/" | absolute_url -%}
|
|
13
12
|
{%- endif -%}
|
|
14
|
-
{%- assign ld_canonical = canonical_path | default: page.canonical_url | default: page.url -%}
|
|
15
|
-
{%- if page.schema_type == "Article" or page.schema_type == "BlogPosting" -%}
|
|
16
|
-
{%- if page.title != blank -%}
|
|
17
|
-
<script type="application/ld+json">
|
|
18
|
-
{
|
|
19
|
-
"@context": "https://schema.org",
|
|
20
|
-
"@type": {{ page.schema_type | jsonify }},
|
|
21
|
-
"headline": {{ page.title | jsonify }},
|
|
22
|
-
"url": {{ ld_canonical | absolute_url | jsonify }}
|
|
23
|
-
{%- if page.description != blank -%},
|
|
24
|
-
"description": {{ page.description | jsonify }}
|
|
25
|
-
{%- endif -%}
|
|
26
|
-
{%- if page.date -%},
|
|
27
|
-
"datePublished": {{ page.date | date_to_xmlschema | jsonify }}
|
|
28
|
-
{%- endif -%}
|
|
29
|
-
{%- if page.last_updated -%},
|
|
30
|
-
"dateModified": {{ page.last_updated | date_to_xmlschema | jsonify }}
|
|
31
|
-
{%- endif -%}
|
|
32
|
-
{%- if page.lang -%},
|
|
33
|
-
"inLanguage": {{ page.lang | jsonify }}
|
|
34
|
-
{%- endif -%}
|
|
35
|
-
{%- if ld_name != blank -%},
|
|
36
|
-
"author": {
|
|
37
|
-
"@type": "Person",
|
|
38
|
-
"name": {{ ld_name | jsonify }}
|
|
39
|
-
}
|
|
40
|
-
{%- endif -%}
|
|
41
|
-
}
|
|
42
|
-
</script>
|
|
43
|
-
{%- endif -%}
|
|
44
|
-
{%- endif -%}
|
|
45
13
|
{%- if ld_name != blank -%}
|
|
46
14
|
<script type="application/ld+json">
|
|
47
15
|
{
|
|
@@ -6,14 +6,6 @@ require "net/http"
|
|
|
6
6
|
require "uri"
|
|
7
7
|
require_relative "content_detail_paths"
|
|
8
8
|
|
|
9
|
-
# The gem copy and the _plugins copy stay the same file. Either path can
|
|
10
|
-
# resolve the shared helper.
|
|
11
|
-
detail_routes = [
|
|
12
|
-
File.expand_path("../../../detail_routes", __dir__),
|
|
13
|
-
File.expand_path("../lib/pandorga/detail_routes", __dir__)
|
|
14
|
-
].find { |path| File.file?("#{path}.rb") }
|
|
15
|
-
require detail_routes if detail_routes
|
|
16
|
-
|
|
17
9
|
# Mirrors Cloudflare _redirects detail rewrites during `jekyll serve` so
|
|
18
10
|
# /posts/:slug/ and /articles/:slug/ work locally without per-slug HTML files.
|
|
19
11
|
# Also handles the local dev re-export endpoint and proxies /api/studio/* to
|
|
@@ -123,8 +115,7 @@ module Jekyll
|
|
|
123
115
|
end
|
|
124
116
|
|
|
125
117
|
target = ContentDetailPaths.rewrite_path(req.path)
|
|
126
|
-
|
|
127
|
-
if target && !Pandorga::DetailRoutes.static_index?(root, req.path)
|
|
118
|
+
if target
|
|
128
119
|
req.instance_variable_set(:@path, target)
|
|
129
120
|
req.instance_variable_set(:@path_info, target)
|
|
130
121
|
end
|
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Pandorga
|
|
4
|
-
#
|
|
5
|
-
# Cloudflare follows `_redirects`
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
8
|
module DetailRoutes
|
|
9
|
-
|
|
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
|
|
10
16
|
|
|
11
17
|
module_function
|
|
12
18
|
|
|
13
|
-
def
|
|
14
|
-
text.to_s
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def static_index?(root, path)
|
|
18
|
-
root = root.to_s
|
|
19
|
-
return false if root.empty?
|
|
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?
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
27
34
|
end
|
|
28
35
|
end
|
|
29
36
|
end
|
|
@@ -6,14 +6,6 @@ require "net/http"
|
|
|
6
6
|
require "uri"
|
|
7
7
|
require_relative "content_detail_paths"
|
|
8
8
|
|
|
9
|
-
# The gem copy and the _plugins copy stay the same file. Either path can
|
|
10
|
-
# resolve the shared helper.
|
|
11
|
-
detail_routes = [
|
|
12
|
-
File.expand_path("../../../detail_routes", __dir__),
|
|
13
|
-
File.expand_path("../lib/pandorga/detail_routes", __dir__)
|
|
14
|
-
].find { |path| File.file?("#{path}.rb") }
|
|
15
|
-
require detail_routes if detail_routes
|
|
16
|
-
|
|
17
9
|
# Mirrors Cloudflare _redirects detail rewrites during `jekyll serve` so
|
|
18
10
|
# /posts/:slug/ and /articles/:slug/ work locally without per-slug HTML files.
|
|
19
11
|
# Also handles the local dev re-export endpoint and proxies /api/studio/* to
|
|
@@ -123,8 +115,7 @@ module Jekyll
|
|
|
123
115
|
end
|
|
124
116
|
|
|
125
117
|
target = ContentDetailPaths.rewrite_path(req.path)
|
|
126
|
-
|
|
127
|
-
if target && !Pandorga::DetailRoutes.static_index?(root, req.path)
|
|
118
|
+
if target
|
|
128
119
|
req.instance_variable_set(:@path, target)
|
|
129
120
|
req.instance_variable_set(:@path_info, target)
|
|
130
121
|
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
|
@@ -3,11 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
# Portrait sources and discovery documents (PLT-AC-20, PLT-AC-21).
|
|
5
5
|
|
|
6
|
-
require "fileutils"
|
|
7
6
|
require "pathname"
|
|
8
7
|
|
|
9
|
-
require "tmpdir"
|
|
10
|
-
|
|
11
8
|
require_relative "../../lib/pandorga/discovery"
|
|
12
9
|
require_relative "../../lib/pandorga/detail_routes"
|
|
13
10
|
|
|
@@ -51,32 +48,38 @@ image_meta = shell[/<meta name="image"[^>]*>/]
|
|
|
51
48
|
fail!("shell missing name=image") unless image_meta
|
|
52
49
|
fail!("name=image also sets property=og:image") if image_meta.include?("property=")
|
|
53
50
|
fail!("discovery generator is not loaded from the plugin dir") unless ROOT.join("lib/pandorga/jekyll/plugins/discovery.rb").file?
|
|
54
|
-
fail!("detail pages are
|
|
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?
|
|
55
53
|
|
|
56
54
|
redirects = <<~TXT
|
|
57
55
|
/legacy /articles/2026-01-01-welcome/ 301
|
|
58
|
-
/articles/:slug /pages/articles/ 200
|
|
59
56
|
/posts/:slug/ /pages/blog/ 200
|
|
60
|
-
/blog/:slug /pages/blog/ 200
|
|
61
|
-
/projects/:slug /pages/projects/ 200
|
|
62
|
-
/products/* /pages/cv/ 200
|
|
63
57
|
/keep /pages/articles/ 301
|
|
64
58
|
TXT
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
fail!("
|
|
69
|
-
|
|
70
|
-
Dir.mktmpdir("detail-static") do |dir|
|
|
71
|
-
FileUtils.mkdir_p(File.join(dir, "articles", "welcome"))
|
|
72
|
-
File.write(File.join(dir, "articles", "welcome", "index.html"), "<p>welcome</p>")
|
|
73
|
-
fail!("a built detail page must not be rewritten") unless Pandorga::DetailRoutes.static_index?(dir, "/articles/welcome/")
|
|
74
|
-
fail!("a missing detail page must still be rewritten") if Pandorga::DetailRoutes.static_index?(dir, "/articles/missing/")
|
|
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
|
|
75
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'")
|
|
76
80
|
|
|
77
81
|
json_ld = ROOT.join("_includes/theme/json-ld.html").read
|
|
78
|
-
fail!("
|
|
79
|
-
fail!("
|
|
80
|
-
fail!("detail schema omits datePublished") unless json_ld.include?("datePublished")
|
|
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"')
|
|
81
84
|
|
|
82
85
|
puts "PASS test-discovery"
|
|
@@ -39,17 +39,6 @@ end
|
|
|
39
39
|
begin
|
|
40
40
|
run!("bundle", "install", "--quiet", chdir: example) if example.join("Gemfile").file?
|
|
41
41
|
run!("ruby", EXE.to_s, "export", "content", "_content_json", chdir: example)
|
|
42
|
-
if name == "full" && redirects_path.file?
|
|
43
|
-
splat = <<~TXT
|
|
44
|
-
|
|
45
|
-
/articles/:slug /pages/articles/ 200
|
|
46
|
-
/posts/:slug /pages/blog/ 200
|
|
47
|
-
/blog/:slug /pages/blog/ 200
|
|
48
|
-
/projects/:slug /pages/projects/ 200
|
|
49
|
-
/products/:slug /pages/cv/ 200
|
|
50
|
-
TXT
|
|
51
|
-
redirects_path.write("#{redirects_path.read.rstrip}\n#{splat}")
|
|
52
|
-
end
|
|
53
42
|
run!("bundle", "exec", "jekyll", "build", "--quiet", chdir: example)
|
|
54
43
|
|
|
55
44
|
index = example.join("_site/index.html")
|
|
@@ -113,65 +102,46 @@ end
|
|
|
113
102
|
warn "FAIL test-examples-build: #{name} robots missing the sitemap line\n#{robots}"
|
|
114
103
|
exit 1
|
|
115
104
|
end
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
canonical = body[/rel="canonical" href="([^"]+)"/, 1].to_s
|
|
133
|
-
og_url = body[/property="og:url" content="([^"]+)"/, 1].to_s
|
|
134
|
-
unless canonical.include?(path) && !canonical.include?("/pages/")
|
|
135
|
-
warn "FAIL test-examples-build: canonical for #{path} is #{canonical}"
|
|
136
|
-
exit 1
|
|
137
|
-
end
|
|
138
|
-
unless og_url.include?(path) && !og_url.include?("/pages/")
|
|
139
|
-
warn "FAIL test-examples-build: og:url for #{path} is #{og_url}"
|
|
140
|
-
exit 1
|
|
141
|
-
end
|
|
142
|
-
unless body.include?(%("@type": "#{schema}"))
|
|
143
|
-
warn "FAIL test-examples-build: #{path} missing #{schema} JSON-LD"
|
|
144
|
-
exit 1
|
|
145
|
-
end
|
|
146
|
-
listing_attr = path.include?("/posts/") ? "/pages/blog/" : "/pages/articles/"
|
|
147
|
-
unless body.include?(%(data-listing-path="#{listing_attr}"))
|
|
148
|
-
warn "FAIL test-examples-build: #{path} lost the listing path"
|
|
149
|
-
exit 1
|
|
150
|
-
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
|
|
151
121
|
end
|
|
152
|
-
|
|
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|
|
|
153
134
|
loc = "#{origin}#{path}"
|
|
154
|
-
|
|
155
|
-
warn "FAIL test-examples-build: sitemap
|
|
135
|
+
if sitemap.include?("<loc>#{loc}</loc>")
|
|
136
|
+
warn "FAIL test-examples-build: sitemap lists detail #{loc}"
|
|
156
137
|
exit 1
|
|
157
138
|
end
|
|
158
139
|
page = example.join("_site#{path}index.html")
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
unless canonical.include?(path)
|
|
162
|
-
warn "FAIL test-examples-build: canonical for #{path} is #{canonical}"
|
|
140
|
+
if page.file?
|
|
141
|
+
warn "FAIL test-examples-build: static detail page #{page}"
|
|
163
142
|
exit 1
|
|
164
143
|
end
|
|
165
144
|
end
|
|
166
|
-
built_redirects = example.join("_site/_redirects").read
|
|
167
|
-
if built_redirects.match?(%r{/(?:articles|posts|blog|projects|products)/:slug})
|
|
168
|
-
warn "FAIL test-examples-build: built _redirects still rewrites detail URLs\n#{built_redirects}"
|
|
169
|
-
exit 1
|
|
170
|
-
end
|
|
171
|
-
unless redirects_path.read.match?(%r{/articles/:slug\s+/pages/articles/\s+200})
|
|
172
|
-
warn "FAIL test-examples-build: the build edited the source _redirects"
|
|
173
|
-
exit 1
|
|
174
|
-
end
|
|
175
145
|
end
|
|
176
146
|
unless html.include?('application/ld+json') && html.include?('"@type": "Person"')
|
|
177
147
|
warn "FAIL test-examples-build: #{name} home is missing JSON-LD"
|
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
|
|
@@ -304,7 +304,6 @@ files:
|
|
|
304
304
|
- lib/pandorga/jekyll/plugins/content_validators_hook.rb
|
|
305
305
|
- lib/pandorga/jekyll/plugins/content_writing_index.rb
|
|
306
306
|
- lib/pandorga/jekyll/plugins/content_writing_paths.rb
|
|
307
|
-
- lib/pandorga/jekyll/plugins/detail_documents.rb
|
|
308
307
|
- lib/pandorga/jekyll/plugins/discovery.rb
|
|
309
308
|
- lib/pandorga/jekyll/plugins/fonts.rb
|
|
310
309
|
- lib/pandorga/jekyll/plugins/force_utf8_encoding.rb
|
|
@@ -319,6 +318,7 @@ files:
|
|
|
319
318
|
- lib/pandorga/jekyll/plugins/lib/text_excerpt.rb
|
|
320
319
|
- lib/pandorga/jekyll/plugins/lib/writing_entry.rb
|
|
321
320
|
- lib/pandorga/jekyll/plugins/local_detail_rewrites.rb
|
|
321
|
+
- lib/pandorga/jekyll/plugins/shell_redirects.rb
|
|
322
322
|
- lib/pandorga/jekyll/plugins/timeline.rb
|
|
323
323
|
- lib/pandorga/jekyll/plugins/web_manifest.rb
|
|
324
324
|
- lib/pandorga/jekyll/plugins/writing_slug_limits.rb
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "jekyll"
|
|
4
|
-
require "pathname"
|
|
5
|
-
|
|
6
|
-
require_relative "../../detail_routes"
|
|
7
|
-
require_relative "lib/writing_entry"
|
|
8
|
-
|
|
9
|
-
module Pandorga
|
|
10
|
-
module Jekyll
|
|
11
|
-
# One static page per published article, post, project, and product.
|
|
12
|
-
# The listing layout stays, so the client still opens the entry from
|
|
13
|
-
# the URL. `listing_path` stays the listing URL. The permalink is the
|
|
14
|
-
# detail URL, which is what the sitemap and the static canonical use.
|
|
15
|
-
class DetailDocumentsGenerator < ::Jekyll::Generator
|
|
16
|
-
safe true
|
|
17
|
-
priority :low
|
|
18
|
-
|
|
19
|
-
COLLECTION_FOR_TYPE = {
|
|
20
|
-
"article" => "articles",
|
|
21
|
-
"post" => "posts",
|
|
22
|
-
"project" => "projects",
|
|
23
|
-
"product" => "products"
|
|
24
|
-
}.freeze
|
|
25
|
-
|
|
26
|
-
DETAIL_PATTERN = {
|
|
27
|
-
"article" => "/articles/:slug/",
|
|
28
|
-
"post" => "/posts/:slug/",
|
|
29
|
-
"project" => "/projects/:slug/",
|
|
30
|
-
"product" => "/products/:slug/"
|
|
31
|
-
}.freeze
|
|
32
|
-
|
|
33
|
-
SCHEMA = {
|
|
34
|
-
"article" => "Article",
|
|
35
|
-
"post" => "BlogPosting"
|
|
36
|
-
}.freeze
|
|
37
|
-
|
|
38
|
-
def generate(site)
|
|
39
|
-
return if site.config["shell_content_agnostic"]
|
|
40
|
-
|
|
41
|
-
root = Pathname.new(site.source)
|
|
42
|
-
COLLECTION_FOR_TYPE.each do |type, collection|
|
|
43
|
-
listing = listing_page(site, type)
|
|
44
|
-
next unless listing
|
|
45
|
-
next if listing.data["listing_path"].to_s.empty?
|
|
46
|
-
|
|
47
|
-
directory = root.join(ContentKeys::CONTENT_PATHS.fetch(type))
|
|
48
|
-
next unless directory.directory?
|
|
49
|
-
|
|
50
|
-
Dir.glob(directory.join("*.{md,markdown}")).sort.each do |absolute|
|
|
51
|
-
emit(site, listing, type, collection, absolute, root)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
def emit(site, listing, type, collection, absolute, root)
|
|
59
|
-
relative = Pathname.new(absolute).relative_path_from(root.join("content"))
|
|
60
|
-
repo_relative = Pathname.new("content").join(relative).to_s
|
|
61
|
-
record = WritingEntry.build(absolute, collection, repo_relative)
|
|
62
|
-
return if record.nil?
|
|
63
|
-
|
|
64
|
-
detail_path = record["url"].to_s
|
|
65
|
-
return if detail_path.empty?
|
|
66
|
-
return if taken?(site, detail_path)
|
|
67
|
-
|
|
68
|
-
front_matter, = ContentKeys.split_front_matter(File.read(absolute))
|
|
69
|
-
dir = detail_path.sub(%r{\A/}, "").sub(%r{/\z}, "")
|
|
70
|
-
page = ::Jekyll::PageWithoutAFile.new(site, site.source, dir, "index.html")
|
|
71
|
-
page.data["layout"] = listing.data["layout"]
|
|
72
|
-
page.data["permalink"] = detail_path
|
|
73
|
-
page.data["canonical_url"] = detail_path
|
|
74
|
-
page.data["listing_path"] = listing.data["listing_path"]
|
|
75
|
-
page.data["collection"] = collection
|
|
76
|
-
page.data["detail"] = listing.data["detail"].to_s
|
|
77
|
-
page.data["detail"] = DETAIL_PATTERN.fetch(type) if page.data["detail"].empty?
|
|
78
|
-
page.data["detail_document"] = true
|
|
79
|
-
page.data["accent"] = listing.data["accent"] if listing.data["accent"]
|
|
80
|
-
title = record["title"].to_s.strip
|
|
81
|
-
title = record["key"].to_s if title.empty?
|
|
82
|
-
page.data["title"] = title unless title.empty?
|
|
83
|
-
description = record["description"].to_s.strip
|
|
84
|
-
description = record["excerpt"].to_s.strip if description.empty?
|
|
85
|
-
page.data["description"] = description unless description.empty?
|
|
86
|
-
page.data["date"] = record["date"] unless record["date"].to_s.empty?
|
|
87
|
-
page.data["last_updated"] = record["last_updated"] unless record["last_updated"].to_s.empty?
|
|
88
|
-
thumbnail = record["thumbnail"].to_s.strip
|
|
89
|
-
page.data["thumbnail"] = thumbnail unless thumbnail.empty?
|
|
90
|
-
lang = front_matter["language"] || front_matter["lang"] || listing.data["lang"]
|
|
91
|
-
page.data["lang"] = lang if lang
|
|
92
|
-
if SCHEMA.key?(type)
|
|
93
|
-
page.data["og_type"] = "article"
|
|
94
|
-
page.data["schema_type"] = SCHEMA.fetch(type)
|
|
95
|
-
end
|
|
96
|
-
page.content = ""
|
|
97
|
-
site.pages << page
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def taken?(site, detail_path)
|
|
101
|
-
site.pages.any? do |page|
|
|
102
|
-
page.data["permalink"].to_s == detail_path || page.url.to_s == detail_path
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def listing_page(site, type)
|
|
107
|
-
entry = registry_entry(site, type)
|
|
108
|
-
return nil unless entry
|
|
109
|
-
|
|
110
|
-
site.pages.find do |page|
|
|
111
|
-
page.data["detail_document"] != true && page.data["key"].to_s == entry["key"].to_s
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def registry_entry(site, type)
|
|
116
|
-
pages = site.data["pandorga_pages"]
|
|
117
|
-
return nil unless pages.is_a?(Array)
|
|
118
|
-
|
|
119
|
-
pages.find { |entry| entry.is_a?(Hash) && registry_match?(entry, type) }
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def registry_match?(entry, type)
|
|
123
|
-
case type
|
|
124
|
-
when "article"
|
|
125
|
-
entry["template"].to_s == "articles" || entry["collection"].to_s == "articles"
|
|
126
|
-
when "post"
|
|
127
|
-
entry["template"].to_s == "blog" || entry["collection"].to_s == "posts"
|
|
128
|
-
when "project"
|
|
129
|
-
entry["template"].to_s == "portfolio" || entry["collection"].to_s == "projects"
|
|
130
|
-
when "product"
|
|
131
|
-
cols = entry["collections"]
|
|
132
|
-
entry["template"].to_s == "cv" ||
|
|
133
|
-
(cols.is_a?(Hash) && !cols["products"].to_s.empty?)
|
|
134
|
-
else
|
|
135
|
-
false
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
Jekyll::Hooks.register :site, :post_write do |site|
|
|
143
|
-
# Jekyll skips underscore files, so a source `_redirects` never reaches
|
|
144
|
-
# the output unless the site lists it under `include`. The published
|
|
145
|
-
# file has to be the one Cloudflare reads.
|
|
146
|
-
dest = File.join(site.dest, "_redirects")
|
|
147
|
-
source = File.join(site.source, "_redirects")
|
|
148
|
-
File.write(dest, File.read(source)) if !File.file?(dest) && File.file?(source)
|
|
149
|
-
next unless File.file?(dest)
|
|
150
|
-
|
|
151
|
-
original = File.read(dest)
|
|
152
|
-
stripped = Pandorga::DetailRoutes.strip_splat_rewrites(original)
|
|
153
|
-
File.write(dest, stripped) if stripped != original
|
|
154
|
-
end
|