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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13bf0047d336a1cfa896a02c88b8bcb172175c564d922474bf65136b113db106
4
- data.tar.gz: e146ecadc0250a2315ab17b887a8b2425d9c7ded47cfc0f45c6a3ee1bc0edc6d
3
+ metadata.gz: c5c04a8266084060b4aa4810ef57d5d9c25a29302ebd8d946183ba2b95a2daeb
4
+ data.tar.gz: b0aceffe59d1febefc9a308db429f7da42856463b1b7143bdb23f125729de192
5
5
  SHA512:
6
- metadata.gz: e5ccb2966deff68f8c3292f997cd39980024e47eeaab2e756f74d193e03ff6b40fd5f235c51fc30a69277057c31350246451004b25f1086df24a163efd45c777
7
- data.tar.gz: df67d8c8be5fd84ffca3abde105d4b05e6c76030e9e1d45f9e26c6dfdb1722a2ecd82ba55a4b9d699a73f1dacd5bc5999402d1ae7f60f727bee5a8058f7f1c16
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) setMetaTag('og:url', absoluteUrl(meta.url), 'property');
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. A detail page with schema_type
3
- Article or BlogPosting also emits that type. No new required keys:
4
- name is already required, and url is the existing optional canonical
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
- root = instance_variable_get(:@root)
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
- # Detail URLs that used to be 200-rewritten onto a listing shell.
5
- # Cloudflare follows `_redirects` even when a static file exists, so the
6
- # built file must drop those splat lines or the generated page is never
7
- # served. The source file is left alone.
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
- SPLAT_LINE = %r{\A/(?:articles|posts|blog|projects|products)/(?::slug|\*)/?\s+\S+\s+200\s*\z}
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 strip_splat_rewrites(text)
14
- text.to_s.lines.reject { |line| SPLAT_LINE.match?(line.strip) }.join
15
- end
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
- clean = path.to_s.split("?").first.to_s
22
- relative = clean.sub(%r{\A/}, "").sub(%r{/\z}, "")
23
- return false if relative.empty? || relative.include?("..")
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
- File.file?(File.join(root, relative, "index.html")) ||
26
- File.file?(File.join(root, "#{relative}.html"))
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
- root = instance_variable_get(:@root)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pandorga
4
- VERSION = "1.7.2"
4
+ VERSION = "1.7.3"
5
5
  end
@@ -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 not generated from the plugin dir") unless ROOT.join("lib/pandorga/jekyll/plugins/detail_documents.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?
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
- stripped = Pandorga::DetailRoutes.strip_splat_rewrites(redirects)
66
- fail!("splat 200 rewrite survived") if stripped.match?(%r{/(?:articles|posts|blog|projects|products)/(?::slug|\*)})
67
- fail!("a 301 was stripped with the splats") unless stripped.include?("/legacy /articles/2026-01-01-welcome/ 301")
68
- fail!("a listing 301 was stripped") unless stripped.include?("/keep /pages/articles/ 301")
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!("detail pages have no Article schema") unless json_ld.include?('page.schema_type == "Article"')
79
- fail!("detail pages have no BlogPosting schema") unless json_ld.include?('page.schema_type == "BlogPosting"')
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
- if name == "full"
117
- {
118
- "/articles/2026-01-01-welcome/" => "Article",
119
- "/posts/2026-01-02-note/" => "BlogPosting"
120
- }.each do |path, schema|
121
- loc = "#{origin}#{path}"
122
- unless sitemap.include?("<loc>#{loc}</loc>")
123
- warn "FAIL test-examples-build: sitemap missing #{loc}\n#{sitemap}"
124
- exit 1
125
- end
126
- page = example.join("_site#{path}index.html")
127
- unless page.file?
128
- warn "FAIL test-examples-build: missing static detail #{page}"
129
- exit 1
130
- end
131
- body = page.read
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
- ["/projects/demo-kit/", "/products/ledger-tool/"].each do |path|
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
- unless sitemap.include?("<loc>#{loc}</loc>")
155
- warn "FAIL test-examples-build: sitemap missing #{loc}\n#{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
- body = page.file? ? page.read : ""
160
- canonical = body[/rel="canonical" href="([^"]+)"/, 1].to_s
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.2
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