jekyll-theme-zer0 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 300fd839d62b5d716dc62bde6853834fecca0b431871d818511f83b3b118586e
4
- data.tar.gz: 7faf9a32a6709bb8bd4cc5c9681910b9595c81761c2582224f71f6623e5573d7
3
+ metadata.gz: 4cfcaf3d689c47878154509eba7578f52610d2d2b93ac70b1c0f1cc0ec2ce2b2
4
+ data.tar.gz: 6020f74226767a325b0591a7aebeb8140b9c6fe43dc7d597e0dadd483e26c7d6
5
5
  SHA512:
6
- metadata.gz: 5c41e56c171902d054122a1288a10e2a45998cfabd06231d28e78e027ffaa9fc79b7511a3c34a50d7b81ab4f4911797fed2be78891bb45bd0e977aa28d298634
7
- data.tar.gz: 143441cff23b9bec1bbf3222d4fcf21fb4949d4a9bf6394aff67c4cefb56e8da1bd71871c76ccdb900a78cc8f8623ce28c4bbcb7004357171e4f032bc02c8242
6
+ metadata.gz: e7e010b9a63a86c6dccc37662ac33ab0b908bc2a9e08cce8d54155750088f0cf5491fbc2f62114d312402a6add238009ab651ff986343a1c837f0433811e6e88
7
+ data.tar.gz: 50ff58b6a81b804e1cc906fa91df6b855b72115da6ffaf8a9dc3aa8545f086fa8ca1c343181ad6ab4c5f89e9557afc53225eaba9a98f5ecc05fd714fbb779cb3
data/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.14.0] - 2026-06-11
9
+
10
+ ### Changed
11
+ - Version bump: minor release
12
+
13
+ ### Commits in this release
14
+ - 5de341aa feat(security): sanitize sensitive config lines in admin config-page DOM (T-009) (#140)
15
+ - 42468785 chore(deps): update Ruby gem dependencies (#129)
16
+ - 89e21988 Improve LinkedIn share flow with cleaned article summary (#99)
17
+
18
+ ### Security
19
+ - **Admin config page (T-009 hardening)**: added a pure-Liquid line-redaction layer for the hidden `<pre id="cfg-full-yaml">` element — the `sanitize_config_yaml` plugin filter shipped in 1.13.1 does not run on GitHub Pages builds (safe mode ignores custom plugins, and the unknown filter is a silent no-op), so Pages-built sites were still injecting raw config; the Liquid layer protects every build path, with the plugin filter kept as defense-in-depth
20
+
21
+ ### Fixed
22
+ - **Workflow lint (T-017)**: `version-bump.yml` now passes the repo yamllint config (trailing spaces, bracket spacing, sequence indentation) — these pre-existing violations failed the `auto-version` integration suite on every code PR once the T-012 gate went live; YAML verified semantically identical before/after
23
+ - **Changelog tooling**: `update_changelog_file` normalizes trailing newlines on the entry, guaranteeing exactly one blank line before the next release block even when callers pass entries via command substitution (review feedback on the T-012 PR)
24
+
25
+ ## [1.13.1] - 2026-06-11
26
+
27
+ ### Changed
28
+ - Version bump: patch release
29
+
30
+ ### Commits in this release
31
+ - 583fa997 fix(infra): sanitize sensitive config keys before DOM injection (T-009) (#141)
32
+
33
+ ### Security
34
+ - **Admin config page sanitization (T-009)**: the hidden `<pre id="cfg-full-yaml">` element on the admin config page now has values masked for keys matching `api_key`, `secret`, `password`, `token`, and `phc_` (PostHog) prefixes via a new `sanitize_config_yaml` Liquid filter (`_plugins/sanitize_config_filter.rb`); the corresponding Playwright regression guard (`test/visual/security.spec.js`) is promoted from `test.fixme` to a live test
35
+
8
36
  ## [1.13.0] - 2026-06-11
9
37
 
10
38
  ### Changed
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  title: zer0-mistakes
3
3
  sub-title: AI-Native Jekyll Theme
4
4
  description: AI-native Jekyll theme for GitHub Pages — Docker-first development, AI-powered installation, multi-agent integration (Copilot, Codex, Cursor, Claude), AI preview-image generation, and AIEO content optimization with Bootstrap 5.3.
5
- version: 1.13.0
5
+ version: 1.14.0
6
6
  layout: landing
7
7
  tags:
8
8
  - jekyll
@@ -20,7 +20,7 @@ categories:
20
20
  - bootstrap
21
21
  - ai-tooling
22
22
  created: 2024-02-10T23:51:11.480Z
23
- lastmod: 2026-06-11T04:16:44.000Z
23
+ lastmod: 2026-06-11T22:23:48.000Z
24
24
  draft: false
25
25
  permalink: /
26
26
  slug: zer0
@@ -830,10 +830,10 @@ gantt
830
830
  v1.9 Installer v2 & Site Scraper :done, 2026-05, 2026-05
831
831
  v1.10 Roadmap Validation :done, 2026-06, 2026-06
832
832
  v1.11 Continuous-Evolution Loop :done, 2026-06, 2026-06
833
+ v1.12 Headless Endpoints :done, 2026-06, 2026-06
833
834
  section Current
834
- v1.12 Headless Endpoints :active, 2026-06, 2026-06
835
+ v1.13 Zer0-Mistake Quality Framework :active, 2026-06, 2026-08
835
836
  section Future
836
- v1.13 Zer0-Mistake Quality Framework :2026-06, 2026-08
837
837
  v2.0 CMS Integration :2026-06, 2026-08
838
838
  v2.1 i18n Support :2026-08, 2026-10
839
839
  v2.2 Advanced Analytics :2026-10, 2026-12
@@ -864,8 +864,8 @@ gantt
864
864
  | **v1.9** | ✅ Completed | May 2026 | Modular installer v2 with deploy plugins, AI wizard pipeline, and a site scraper. |
865
865
  | **v1.10** | ✅ Completed | Jun 2026 | Roadmap integrity validation and catch-up milestones so the roadmap tracks the shipped gem. |
866
866
  | **v1.11** | ✅ Completed | Jun 2026 | Self-sustaining backlog loop so AI agents keep improving the repo between human sessions. |
867
- | **v1.12** | 🚧 In Progress | Current (1.12.x) | Machine-readable site endpoints for downstream sites and AI agents. |
868
- | **v1.13** | 🗓 Planned | Q3 2026 | Close the gap between the repo's quality gates and what CI actually enforces — no mistake lands green. |
867
+ | **v1.12** | Completed | Jun 2026 | Machine-readable site endpoints for downstream sites and AI agents. |
868
+ | **v1.13** | 🚧 In Progress | Current (1.13.x) | Close the gap between the repo's quality gates and what CI actually enforces — no mistake lands green. |
869
869
  | **v2.0** | 🗓 Planned | Q3 2026 | Headless CMS integration with a content API and admin dashboard. |
870
870
  | **v2.1** | 🗓 Planned | Q4 2026 | Multi-language content support with locale-aware routing. |
871
871
  | **v2.2** | 🗓 Planned | Q4 2026 | Visual theme customizer, A/B testing, and conversion funnels. |
@@ -909,7 +909,7 @@ git push origin feature/awesome-feature
909
909
 
910
910
  | Metric | Value |
911
911
  |--------|-------|
912
- | **Current Version** | 1.13.0 ([RubyGems](https://rubygems.org/gems/jekyll-theme-zer0), [CHANGELOG](/CHANGELOG)) |
912
+ | **Current Version** | 1.14.0 ([RubyGems](https://rubygems.org/gems/jekyll-theme-zer0), [CHANGELOG](/CHANGELOG)) |
913
913
  | **Documented Features** | 43 ([Feature Registry](https://github.com/bamr87/zer0-mistakes/blob/main/_data/features.yml)) |
914
914
  | **Setup Time** | 2-5 minutes ([install.sh benchmarks](https://github.com/bamr87/zer0-mistakes/blob/main/install.sh)) |
915
915
  | **Documentation Pages** | 70+ ([browse docs](https://zer0-mistakes.com/pages/)) |
@@ -964,6 +964,6 @@ And these AI partners that make zer0-mistakes truly AI-native:
964
964
 
965
965
  **Built with ❤️ — and a little help from our AI partners — for the Jekyll community**
966
966
 
967
- **v1.13.0** • [Changelog](CHANGELOG.md) • [License](LICENSE) • [Contributing](CONTRIBUTING.md) • [AI Agent Guide](AGENTS.md)
967
+ **v1.14.0** • [Changelog](CHANGELOG.md) • [License](LICENSE) • [Contributing](CONTRIBUTING.md) • [AI Agent Guide](AGENTS.md)
968
968
 
969
969
 
data/_data/backlog.yml CHANGED
@@ -55,8 +55,8 @@
55
55
 
56
56
  meta:
57
57
  title: "zer0-mistakes Backlog"
58
- updated: 2026-06-10
59
- next_id: 17
58
+ updated: 2026-06-11
59
+ next_id: 19
60
60
 
61
61
  tasks:
62
62
  # --- Housekeeping (seeded so the loop has work on day one) ------------------
@@ -213,7 +213,7 @@ tasks:
213
213
 
214
214
  - id: T-009
215
215
  title: "Sanitize sensitive config keys from admin config-page DOM injection"
216
- status: open
216
+ status: done
217
217
  priority: P1
218
218
  area: infra
219
219
  risk: standard
@@ -225,13 +225,16 @@ tasks:
225
225
  (`api_key`, `token`, PostHog `phc_*` keys, etc.) they are exposed in the page
226
226
  DOM. A regression test in `test/visual/security.spec.js` is frozen as
227
227
  `test.fixme` until a sanitisation pass is in place.
228
+ Done 2026-06-11: pure-Liquid line filter redacts matching lines before
229
+ DOM injection (GitHub Pages safe — no custom plugin); regression test
230
+ promoted to a live `test()`; visible Raw-YAML tab untouched.
228
231
  acceptance:
229
232
  - "The Liquid/Ruby that populates `<pre id=\"cfg-full-yaml\">` strips or masks keys matching `api_key`, `secret`, `password`, `token`, and `phc_` prefix before DOM injection."
230
233
  - "The `test.fixme` in `test/visual/security.spec.js` is promoted to a live `test()` and passes in CI."
231
234
  - "The visible config display in the admin UI is unaffected (only the raw hidden element is sanitised)."
232
235
  links: { issue: null, pr: null, roadmap: null }
233
236
  created: 2026-06-01
234
- updated: 2026-06-01
237
+ updated: 2026-06-11
235
238
 
236
239
  - id: T-010
237
240
  title: "Complete v1.9 quickstart docs rewrite with getting-started guide and screenshots"
@@ -403,3 +406,51 @@ tasks:
403
406
  created: 2026-06-10
404
407
  updated: 2026-06-10
405
408
 
409
+ - id: T-017
410
+ title: "Fix yamllint violations in .github/workflows/version-bump.yml"
411
+ status: done
412
+ priority: P2
413
+ area: lint
414
+ risk: low
415
+ effort: S
416
+ source: audit
417
+ summary: >-
418
+ `.github/workflows/version-bump.yml` has ~30 trailing-space lines, two
419
+ indentation errors, and one brackets error that cause the `auto-version`
420
+ integration test (which runs yamllint) to fail in CI on every PR. Discovered
421
+ while babysitting PR #141 — the file was unchanged by that PR, confirming
422
+ the failures are pre-existing.
423
+ Done 2026-06-11: full cleanup (trailing spaces, brackets, sequence
424
+ indentation) landed with the T-009 PR; YAML verified semantically
425
+ identical and yamllint exits 0 with the repo config.
426
+ acceptance:
427
+ - "`yamllint -c .github/config/.yamllint.yml .github/workflows/version-bump.yml` exits 0."
428
+ - "`./scripts/test/integration/auto-version` passes the 'version-bump workflow syntax' check."
429
+ - "No functional change to the workflow logic."
430
+ links: { issue: null, pr: null, roadmap: null }
431
+ created: 2026-06-11
432
+ updated: 2026-06-11
433
+
434
+ - id: T-018
435
+ title: "Admin config page displays a stale copy of _config.yml — keep it in sync safely"
436
+ status: open
437
+ priority: P2
438
+ area: feat
439
+ risk: standard
440
+ effort: M
441
+ source: audit
442
+ summary: >-
443
+ `pages/_about/settings/config.md` renders `pages/_about/settings/_config.yml`
444
+ via `include_relative` (Liquid cannot reach the repo root), and that copy
445
+ has drifted from the live `_config.yml` (it predates, e.g., the PostHog
446
+ analytics block). Found while implementing T-009. Caution: a naive
447
+ refresh would inject the live `phc_` api_key into the *visible* Raw-YAML
448
+ tab — any sync mechanism must sanitize the visible tab the same way
449
+ T-009 sanitizes the hidden copy element, or redact at sync time.
450
+ acceptance:
451
+ - "The config shown at /about/config/ matches the live `_config.yml` (automated sync step or build-time check that fails on drift)."
452
+ - "The visible Raw-YAML tab applies the same sensitive-line redaction as the hidden `cfg-full-yaml` element."
453
+ - "`test/visual/security.spec.js` raw-tab test passes without its silent skip path (locator matches the actual `code#cfg-raw-yaml` element)."
454
+ links: { issue: null, pr: null, roadmap: "1.13" }
455
+ created: 2026-06-11
456
+ updated: 2026-06-11
data/_data/roadmap.yml CHANGED
@@ -60,7 +60,7 @@
60
60
  meta:
61
61
  title: "zer0-mistakes Roadmap"
62
62
  tagline: "Past releases, current focus, and future plans for the zer0-mistakes Jekyll theme."
63
- updated: 2026-06-10
63
+ updated: 2026-06-11
64
64
 
65
65
  milestones:
66
66
  # --- Completed -------------------------------------------------------------
@@ -313,30 +313,28 @@ milestones:
313
313
  - "`/repo-audit` and `/backlog-implement` agent routines"
314
314
  - "Documentation maintenance system and consolidation"
315
315
 
316
- # --- Current ---------------------------------------------------------------
317
-
318
316
  - version: "1.12"
319
317
  title: "Headless Endpoints"
320
- status: active
321
- section: Current
318
+ status: completed
319
+ section: Completed
322
320
  start: 2026-06
323
321
  end: 2026-06
324
- target: "Current (1.12.x)"
322
+ released: 2026-06-03
325
323
  summary: "Machine-readable site endpoints for downstream sites and AI agents."
326
324
  features:
327
325
  - "Auto-generated `/search.json` endpoint for downstream sites"
328
326
  - "Auto-generated `/sitemap/` endpoint"
329
327
  - "Ruby gem and Mermaid dependency updates"
330
328
 
331
- # --- Future ----------------------------------------------------------------
329
+ # --- Current ---------------------------------------------------------------
332
330
 
333
331
  - version: "1.13"
334
332
  title: "Zer0-Mistake Quality Framework"
335
- status: planned
336
- section: Future
333
+ status: active
334
+ section: Current
337
335
  start: 2026-06
338
336
  end: 2026-08
339
- target: "Q3 2026"
337
+ target: "Current (1.13.x)"
340
338
  summary: "Close the gap between the repo's quality gates and what CI actually enforces — no mistake lands green."
341
339
  features:
342
340
  - "CI gate parity — PRs run the full canonical test entrypoint (lib unit, theme, integration, installer e2e), not a subset"
@@ -32,6 +32,9 @@
32
32
  <!-- Search modal controller -->
33
33
  <script src="{{ '/assets/js/search-modal.js' | relative_url }}"></script>
34
34
 
35
+ <!-- Share helpers -->
36
+ <script src="{{ '/assets/js/share-actions.js' | relative_url }}"></script>
37
+
35
38
  <!-- fffuel-style background customizer (skin switching, toggle, opacity) -->
36
39
  <script src="{{ '/assets/js/background-customizer.js' | relative_url }}"></script>
37
40
 
@@ -46,6 +46,9 @@
46
46
  {% assign file_path = page_dir | append: "/" | append: page.path %}
47
47
  {% assign repo_parts = site.repository | split: "/" %}
48
48
  {% assign repo_owner = repo_parts[0] %}
49
+ {% assign page_absolute_url = page.url | absolute_url %}
50
+ {% assign linkedin_share_title = page.title | default: site.title | strip_html | strip_newlines | strip %}
51
+ {% assign linkedin_share_description = page.description | default: page.excerpt | default: site.description | strip_html | strip_newlines | strip %}
49
52
 
50
53
  {% comment %}
51
54
  Resolve author display name and optional profile URL from page front matter,
@@ -233,12 +236,19 @@
233
236
  </button>
234
237
  <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="shareDropdownBottom">
235
238
  <li>
236
- <a class="dropdown-item" href="https://reddit.com/submit?url={{ site.url | append: page.url | url_encode }}&amp;title={{ page.title | url_encode }}" target="_blank">
239
+ <a class="dropdown-item" href="https://reddit.com/submit?url={{ page_absolute_url | uri_escape }}&amp;title={{ page.title | uri_escape }}" target="_blank" rel="noopener noreferrer">
237
240
  <i class="bi bi-reddit me-2"></i>Share on Reddit
238
241
  </a>
239
242
  </li>
240
243
  <li>
241
- <a class="dropdown-item" href="https://www.linkedin.com/sharing/share-offsite/?url={{ site.url | append: page.url | url_encode }}" target="_blank">
244
+ <a class="dropdown-item js-linkedin-share"
245
+ href="https://www.linkedin.com/sharing/share-offsite/?url={{ page_absolute_url | uri_escape }}"
246
+ target="_blank"
247
+ rel="noopener noreferrer"
248
+ title="Copies a cleaned article summary to your clipboard, then opens LinkedIn sharing"
249
+ data-share-url="{{ page_absolute_url | escape }}"
250
+ data-share-title="{{ linkedin_share_title | escape }}"
251
+ data-share-description="{{ linkedin_share_description | escape }}">
242
252
  <i class="bi bi-linkedin me-2"></i>Share on LinkedIn
243
253
  </a>
244
254
  </li>
@@ -332,4 +342,4 @@
332
342
  </div>
333
343
  </div>
334
344
  </div>
335
- </div>
345
+ </div>
data/_layouts/note.html CHANGED
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  layout: default
3
3
  ---
4
+ {% assign page_absolute_url = page.url | absolute_url %}
5
+ {% assign linkedin_share_title = page.title | default: site.title | strip_html | strip_newlines | strip %}
6
+ {% assign linkedin_share_description = page.description | default: page.excerpt | default: site.description | strip_html | strip_newlines | strip %}
4
7
  <!--
5
8
  ===================================================================
6
9
  NOTE LAYOUT - Quick Notes and Reference Display
@@ -153,11 +156,17 @@ layout: default
153
156
  class="btn btn-outline-primary btn-sm" target="_blank" rel="noopener noreferrer" aria-label="Share on X">
154
157
  <i class="bi bi-twitter-x"></i> X
155
158
  </a>
156
- <a href="https://www.linkedin.com/sharing/share-offsite/?url={{ site.url | append: page.url | uri_escape }}"
157
- class="btn btn-outline-primary btn-sm" target="_blank" rel="noopener noreferrer">
159
+ <a href="https://www.linkedin.com/sharing/share-offsite/?url={{ page_absolute_url | uri_escape }}"
160
+ class="btn btn-outline-primary btn-sm js-linkedin-share"
161
+ data-share-url="{{ page_absolute_url | escape }}"
162
+ data-share-title="{{ linkedin_share_title | escape }}"
163
+ data-share-description="{{ linkedin_share_description | escape }}"
164
+ title="Copies a cleaned article summary to your clipboard, then opens LinkedIn sharing"
165
+ target="_blank" rel="noopener noreferrer">
158
166
  <i class="bi bi-linkedin"></i> LinkedIn
159
167
  </a>
160
- <a href="mailto:?subject={{ page.title | uri_escape }}&body={{ site.url | append: page.url | uri_escape }}"
168
+ <a href="mailto:?subject={{ page.title | uri_escape }}&body={{ page_absolute_url | uri_escape }}"
169
+ title="Share this note by email"
161
170
  class="btn btn-outline-primary btn-sm">
162
171
  <i class="bi bi-envelope"></i> Email
163
172
  </a>
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # File: sanitize_config_filter.rb
4
+ # Path: _plugins/sanitize_config_filter.rb
5
+ # Purpose: Liquid filter that masks sensitive key-value pairs in raw YAML
6
+ # before the content is injected into the DOM. Used by the admin
7
+ # config page to sanitize <pre id="cfg-full-yaml">.
8
+ #
9
+ # Masked patterns:
10
+ # Key names: api_key, apikey, secret, password, token (case-insensitive)
11
+ # Value prefix: phc_ (PostHog project API keys)
12
+
13
+ module Jekyll
14
+ module SanitizeConfigFilter
15
+ # Matches YAML lines whose key name is a common secret identifier.
16
+ SENSITIVE_KEY_RE = /\A(\s*(?:api[_-]?key|secret|password|token)\s*:)/i.freeze
17
+ # Matches PostHog project API key values anywhere on a line.
18
+ PHC_VALUE_RE = /phc_[A-Za-z0-9]+/.freeze
19
+
20
+ def sanitize_config_yaml(input)
21
+ return input unless input.is_a?(String)
22
+
23
+ input.each_line.map do |line|
24
+ if SENSITIVE_KEY_RE.match?(line)
25
+ # Keep the key name and colon; replace everything after with [REDACTED]
26
+ line.sub(/(:\s*).*$/, '\1[REDACTED]')
27
+ elsif PHC_VALUE_RE.match?(line)
28
+ line.gsub(PHC_VALUE_RE, '[REDACTED]')
29
+ else
30
+ line
31
+ end
32
+ end.join
33
+ end
34
+ end
35
+ end
36
+
37
+ Liquid::Template.register_filter(Jekyll::SanitizeConfigFilter)
@@ -0,0 +1,156 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ function normalizeWhitespace(text) {
5
+ return (text || '').replace(/\s+/g, ' ').trim();
6
+ }
7
+
8
+ function dedupeSections(sections) {
9
+ return sections.filter((section, index) => {
10
+ if (!section) return false;
11
+ const normalized = section.toLowerCase();
12
+ return !sections.slice(0, index).some((previous) => previous && previous.toLowerCase() === normalized);
13
+ });
14
+ }
15
+
16
+ function truncateToSentence(text, maxLength) {
17
+ if (text.length <= maxLength) return text;
18
+
19
+ const trimmed = text.slice(0, maxLength);
20
+ const sentenceBreak = Math.max(
21
+ trimmed.lastIndexOf('. '),
22
+ trimmed.lastIndexOf('! '),
23
+ trimmed.lastIndexOf('? ')
24
+ );
25
+
26
+ if (sentenceBreak > Math.floor(maxLength * 0.6)) {
27
+ return trimmed.slice(0, sentenceBreak + 1).trim();
28
+ }
29
+
30
+ const lastSpace = trimmed.lastIndexOf(' ');
31
+ const endIndex = lastSpace > 0 ? lastSpace : maxLength;
32
+ return `${trimmed.slice(0, endIndex).trim()}…`;
33
+ }
34
+
35
+ function getShareContentRoot() {
36
+ return document.querySelector('[itemprop="articleBody"]')
37
+ || document.querySelector('.bd-content')
38
+ || document.querySelector('main')
39
+ || document.body;
40
+ }
41
+
42
+ function extractCleanExcerpt(description) {
43
+ const contentRoot = getShareContentRoot();
44
+ const normalizedDescription = normalizeWhitespace(description).toLowerCase();
45
+ const paragraphTexts = Array.from(contentRoot.querySelectorAll('p'))
46
+ .map((paragraph) => normalizeWhitespace(paragraph.textContent))
47
+ .filter((paragraph) => paragraph.length > 40)
48
+ .filter((paragraph) => {
49
+ const normalizedParagraph = paragraph.toLowerCase();
50
+ return !normalizedDescription
51
+ || (normalizedParagraph !== normalizedDescription && !normalizedParagraph.includes(normalizedDescription));
52
+ });
53
+
54
+ const combinedParagraphs = paragraphTexts.slice(0, 4).join(' ');
55
+ const fallbackText = normalizeWhitespace(contentRoot.textContent);
56
+ const candidateText = combinedParagraphs || fallbackText;
57
+
58
+ return truncateToSentence(candidateText, 420);
59
+ }
60
+
61
+ function buildLinkedInShareText(anchor) {
62
+ const title = normalizeWhitespace(anchor.dataset.shareTitle || document.title);
63
+ const description = normalizeWhitespace(
64
+ anchor.dataset.shareDescription
65
+ || document.querySelector('meta[name="description"]')?.getAttribute('content')
66
+ || ''
67
+ );
68
+ const excerpt = extractCleanExcerpt(description);
69
+ const url = normalizeWhitespace(anchor.dataset.shareUrl || window.location.href);
70
+
71
+ return dedupeSections([title, description, excerpt, url]).join('\n\n');
72
+ }
73
+
74
+ async function copyShareText(text) {
75
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
76
+ return false;
77
+ }
78
+
79
+ try {
80
+ await navigator.clipboard.writeText(text);
81
+ return true;
82
+ } catch (error) {
83
+ console.warn('LinkedIn share copy failed:', error);
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function openShareWindow(href) {
89
+ return window.open(href || 'about:blank', '_blank', 'noopener,noreferrer');
90
+ }
91
+
92
+ function notify(message, type) {
93
+ const notification = document.createElement('div');
94
+ notification.className = `alert alert-${type || 'info'} shadow position-fixed top-0 end-0 m-3`;
95
+ notification.style.zIndex = '1085';
96
+ notification.setAttribute('role', 'status');
97
+ notification.textContent = message;
98
+ document.body.appendChild(notification);
99
+
100
+ window.setTimeout(() => {
101
+ notification.remove();
102
+ }, 4000);
103
+ }
104
+
105
+ function bindLinkedInShare(anchor) {
106
+ if (anchor.dataset.linkedinShareBound === 'true') return;
107
+
108
+ anchor.dataset.linkedinShareBound = 'true';
109
+ anchor.addEventListener('click', async function (event) {
110
+ event.preventDefault();
111
+
112
+ const shareWindow = openShareWindow('', '_blank');
113
+
114
+ const shareText = buildLinkedInShareText(anchor);
115
+ const copied = await copyShareText(shareText);
116
+
117
+ if (shareWindow) {
118
+ shareWindow.location = anchor.href;
119
+ } else {
120
+ window.location.assign(anchor.href);
121
+ }
122
+
123
+ if (copied) {
124
+ notify('A cleaned LinkedIn-ready summary was copied to your clipboard. Paste it on LinkedIn after the share page opens.', 'info');
125
+ } else {
126
+ notify('LinkedIn opened, but clipboard access was unavailable. Copy the summary manually if needed.', 'warning');
127
+ }
128
+ });
129
+ }
130
+
131
+ function bindCopyButton(button) {
132
+ if (button.dataset.copyBound === 'true') return;
133
+
134
+ button.dataset.copyBound = 'true';
135
+ button.addEventListener('click', async function () {
136
+ const copied = await copyShareText(button.dataset.copyText || '');
137
+
138
+ if (copied) {
139
+ notify(button.dataset.copySuccess || 'Copied to clipboard.', 'success');
140
+ } else {
141
+ notify('Clipboard access was unavailable.', 'warning');
142
+ }
143
+ });
144
+ }
145
+
146
+ function initLinkedInShareButtons() {
147
+ document.querySelectorAll('.js-linkedin-share').forEach(bindLinkedInShare);
148
+ document.querySelectorAll('.js-copy-share-link').forEach(bindCopyButton);
149
+ }
150
+
151
+ if (document.readyState === 'loading') {
152
+ document.addEventListener('DOMContentLoaded', initLinkedInShareButtons);
153
+ } else {
154
+ initLinkedInShareButtons();
155
+ }
156
+ })();
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-theme-zer0
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amr Abdel
@@ -220,6 +220,7 @@ files:
220
220
  - _plugins/content_statistics_generator.rb
221
221
  - _plugins/obsidian_links.rb
222
222
  - _plugins/preview_image_generator.rb
223
+ - _plugins/sanitize_config_filter.rb
223
224
  - _plugins/search_and_sitemap_generator.rb
224
225
  - _plugins/theme_version.rb
225
226
  - _sass/components/_back-to-top.scss
@@ -305,6 +306,7 @@ files:
305
306
  - assets/js/posts-pagination.js
306
307
  - assets/js/search-modal.js
307
308
  - assets/js/setup-wizard.js
309
+ - assets/js/share-actions.js
308
310
  - assets/js/side-bar-folders.js
309
311
  - assets/js/skin-editor.js
310
312
  - assets/js/stats.js