datalog-theme 0.6.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.
Files changed (180) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +162 -0
  3. data/CITATION.cff +29 -0
  4. data/LICENSE +21 -0
  5. data/README.md +257 -0
  6. data/_data/academic.yml +217 -0
  7. data/_data/cdn-integrity.yml +51 -0
  8. data/_data/config/author.yml +121 -0
  9. data/_data/config/features.yml +262 -0
  10. data/_data/config/site.yml +42 -0
  11. data/_data/config/theme.yml +181 -0
  12. data/_data/datasets.yml +28 -0
  13. data/_data/i18n/en.yml +438 -0
  14. data/_data/i18n/es.yml +438 -0
  15. data/_data/i18n/pt.yml +438 -0
  16. data/_data/js_manifest.json +22 -0
  17. data/_data/js_meta.json +371 -0
  18. data/_data/navigation.yml +145 -0
  19. data/_data/projects.yml +41 -0
  20. data/_data/publications.yml +28 -0
  21. data/_data/social.yml +73 -0
  22. data/_data/visualizations.yml +51 -0
  23. data/_includes/analytics/dashboard.html +149 -0
  24. data/_includes/analytics.html +9 -0
  25. data/_includes/components/academic-dashboard.html +347 -0
  26. data/_includes/components/advanced-search.html +682 -0
  27. data/_includes/components/api-function.html +94 -0
  28. data/_includes/components/author-bio.html +166 -0
  29. data/_includes/components/bookmark-system.html +96 -0
  30. data/_includes/components/breadcrumbs.html +89 -0
  31. data/_includes/components/citation-tools.html +94 -0
  32. data/_includes/components/comments.html +244 -0
  33. data/_includes/components/content-provenance.html +43 -0
  34. data/_includes/components/content-recommendations.html +228 -0
  35. data/_includes/components/difficulty-badge.html +84 -0
  36. data/_includes/components/email-preferences.html +200 -0
  37. data/_includes/components/enhanced-code-block.html +212 -0
  38. data/_includes/components/enhanced-metadata.html +228 -0
  39. data/_includes/components/enhanced-toc.html +209 -0
  40. data/_includes/components/hero.html +15 -0
  41. data/_includes/components/language-switcher.html +396 -0
  42. data/_includes/components/math-fallback.html +19 -0
  43. data/_includes/components/navigation-enhancements.html +454 -0
  44. data/_includes/components/newsletter-signup.html +178 -0
  45. data/_includes/components/open-science-badges.html +16 -0
  46. data/_includes/components/package-install.html +194 -0
  47. data/_includes/components/performance-monitor.html +170 -0
  48. data/_includes/components/popular-posts.html +233 -0
  49. data/_includes/components/post-hero.html +62 -0
  50. data/_includes/components/reading-progress.html +133 -0
  51. data/_includes/components/reading-time.html +121 -0
  52. data/_includes/components/responsive-image.html +63 -0
  53. data/_includes/components/search-facets.html +307 -0
  54. data/_includes/components/series-navigation.html +124 -0
  55. data/_includes/components/social-proof.html +34 -0
  56. data/_includes/components/social-share.html +119 -0
  57. data/_includes/components/user-preferences.html +566 -0
  58. data/_includes/components/visualization-card.html +24 -0
  59. data/_includes/components/viz-table-fallback.html +19 -0
  60. data/_includes/critical-css/default.html +0 -0
  61. data/_includes/critical-css/home.html +0 -0
  62. data/_includes/critical-css/post.html +0 -0
  63. data/_includes/csp-meta.html +17 -0
  64. data/_includes/footer/nav-column.html +31 -0
  65. data/_includes/footer.html +76 -0
  66. data/_includes/head.html +337 -0
  67. data/_includes/header/navigation.html +48 -0
  68. data/_includes/header.html +70 -0
  69. data/_includes/helpers/array-to-sentence.html +21 -0
  70. data/_includes/helpers/date-format.html +16 -0
  71. data/_includes/helpers/link-with-icon.html +26 -0
  72. data/_includes/layouts/default/article.html +67 -0
  73. data/_includes/meta/language-attributes.html +25 -0
  74. data/_includes/meta/math-config.html +21 -0
  75. data/_includes/meta/schema.html +153 -0
  76. data/_includes/meta/scripts-loader.html +52 -0
  77. data/_includes/post/related-posts.html +63 -0
  78. data/_includes/scripts.html +72 -0
  79. data/_includes/skip-link.html +1 -0
  80. data/_includes/toc.html +2 -0
  81. data/_layouts/archive.html +282 -0
  82. data/_layouts/dataset.html +52 -0
  83. data/_layouts/default.html +33 -0
  84. data/_layouts/home.html +48 -0
  85. data/_layouts/notebook.html +173 -0
  86. data/_layouts/package.html +238 -0
  87. data/_layouts/page.html +16 -0
  88. data/_layouts/portfolio.html +41 -0
  89. data/_layouts/post-sidebar.html +183 -0
  90. data/_layouts/post.html +338 -0
  91. data/_layouts/project.html +252 -0
  92. data/_layouts/research.html +504 -0
  93. data/_plugins/analytics_dashboard.rb +315 -0
  94. data/_plugins/config_validator.rb +397 -0
  95. data/_plugins/csp_generator.rb +115 -0
  96. data/_plugins/datalog_bibliography.rb +19 -0
  97. data/_plugins/datalog_comments.rb +18 -0
  98. data/_plugins/datalog_slides.rb +20 -0
  99. data/_plugins/front_matter_compat.rb +109 -0
  100. data/_plugins/i18n.rb +160 -0
  101. data/_plugins/image_optimizer.rb +445 -0
  102. data/_plugins/math_preprocessor.rb +185 -0
  103. data/_plugins/notebook_converter.rb +1107 -0
  104. data/_plugins/plugin_loader.rb +185 -0
  105. data/_plugins/publications_generator.rb +234 -0
  106. data/_plugins/reading_time.rb +15 -0
  107. data/_plugins/search_normalizer.rb +74 -0
  108. data/_plugins/sri_filter.rb +18 -0
  109. data/_plugins/toc_filter.rb +68 -0
  110. data/_plugins/warning_filter.rb +3 -0
  111. data/_sass/_base.scss +142 -0
  112. data/_sass/_components.scss +2380 -0
  113. data/_sass/_header.scss +470 -0
  114. data/_sass/_interactive.scss +344 -0
  115. data/_sass/_layout.scss +1760 -0
  116. data/_sass/_mathematical.scss +368 -0
  117. data/_sass/_mixins.scss +99 -0
  118. data/_sass/_package-docs.scss +715 -0
  119. data/_sass/_phase1-enhancements.scss +721 -0
  120. data/_sass/_phase3-enhancements.scss +874 -0
  121. data/_sass/_phase4-enhancements.scss +1214 -0
  122. data/_sass/_phase5-enhancements.scss +414 -0
  123. data/_sass/_search.scss +654 -0
  124. data/_sass/_syntax-highlighting.scss +128 -0
  125. data/_sass/_theme.scss +79 -0
  126. data/_sass/_typography.scss +148 -0
  127. data/_sass/_utilities.scss +334 -0
  128. data/_sass/_variables.scss +153 -0
  129. data/_sass/_visualizations.scss +242 -0
  130. data/assets/css/main.scss +4 -0
  131. data/assets/img/20220607123041_detail.001.png +0 -0
  132. data/assets/img/favicons/android-chrome-192x192.png +0 -0
  133. data/assets/img/favicons/android-chrome-512x512.png +0 -0
  134. data/assets/img/favicons/apple-touch-icon.png +0 -0
  135. data/assets/img/favicons/favicon-16x16.png +0 -0
  136. data/assets/img/favicons/favicon-32x32.png +0 -0
  137. data/assets/img/favicons/favicon.ico +0 -0
  138. data/assets/img/favicons/site.webmanifest +21 -0
  139. data/assets/img/portfolio-placeholder.svg +20 -0
  140. data/assets/js/academic.js +262 -0
  141. data/assets/js/analytics-dashboard.js +382 -0
  142. data/assets/js/core/dark-mode.js +79 -0
  143. data/assets/js/core/github-cards.js +123 -0
  144. data/assets/js/core/language-filter.js +69 -0
  145. data/assets/js/core/navigation.js +184 -0
  146. data/assets/js/core/scroll-progress.js +45 -0
  147. data/assets/js/core/search-hotkeys.js +62 -0
  148. data/assets/js/core/skip-links.js +62 -0
  149. data/assets/js/loader.js +163 -0
  150. data/assets/js/main.js +23 -0
  151. data/assets/js/math.js +818 -0
  152. data/assets/js/notebook.js +158 -0
  153. data/assets/js/search/analytics.js +91 -0
  154. data/assets/js/search/app.js +271 -0
  155. data/assets/js/search/autocomplete.js +120 -0
  156. data/assets/js/search/engine.js +260 -0
  157. data/assets/js/search/filters.js +38 -0
  158. data/assets/js/search/render.js +217 -0
  159. data/assets/js/search/utils.js +99 -0
  160. data/assets/js/search.js +354 -0
  161. data/assets/js/visualizations.js +816 -0
  162. data/assets/publications/datalog-publications.bib +8 -0
  163. data/assets/publications/datalog-publications.ris +9 -0
  164. data/assets/publications/publications.bib +30 -0
  165. data/assets/templates/diogo-ribeiro-cv.md +31 -0
  166. data/assets/templates/diogo-ribeiro-cv.tex +32 -0
  167. data/bin/datalog +26 -0
  168. data/datalog-theme.gemspec +72 -0
  169. data/lib/datalog/cli.rb +563 -0
  170. data/lib/datalog/plugin_system/dependency_resolver.rb +223 -0
  171. data/lib/datalog/plugin_system.rb +237 -0
  172. data/lib/datalog/plugins/citations.rb +182 -0
  173. data/lib/datalog/plugins/comments.rb +183 -0
  174. data/lib/datalog/plugins/search.rb +45 -0
  175. data/lib/datalog/plugins/slides.rb +121 -0
  176. data/lib/datalog/theme/theme.rb +18 -0
  177. data/lib/datalog/theme/version.rb +7 -0
  178. data/lib/datalog/warning_filter.rb +28 -0
  179. data/lib/datalog-theme.rb +17 -0
  180. metadata +555 -0
@@ -0,0 +1,194 @@
1
+ {%- comment -%}
2
+ Package Installation Component
3
+ Displays installation instructions for different package managers
4
+
5
+ Parameters:
6
+ - language: python, r, julia, etc.
7
+ - package_name: Name of the package
8
+ - pip_command: Custom pip install command
9
+ - conda_command: Custom conda install command
10
+ - cran_command: Custom R install command
11
+ - git_url: GitHub clone URL
12
+ {%- endcomment -%}
13
+
14
+ {%- assign language = include.language | default: "python" -%}
15
+ {%- assign package_name = include.package_name | default: page.title -%}
16
+ {%- assign pip_command = include.pip_command -%}
17
+ {%- assign conda_command = include.conda_command -%}
18
+ {%- assign cran_command = include.cran_command -%}
19
+ {%- assign git_url = include.git_url | default: page.github_url -%}
20
+
21
+ <div class="package-install" id="installation">
22
+ <h2 class="package-install__title">Installation</h2>
23
+
24
+ {%- if language == "python" -%}
25
+ <div class="package-install__tabs" data-install-tabs>
26
+ <div class="package-install__tab-buttons">
27
+ <button class="package-install__tab-btn package-install__tab-btn--active"
28
+ data-tab="pip">
29
+ pip
30
+ </button>
31
+ <button class="package-install__tab-btn" data-tab="conda">
32
+ conda
33
+ </button>
34
+ <button class="package-install__tab-btn" data-tab="git">
35
+ Git
36
+ </button>
37
+ </div>
38
+
39
+ <div class="package-install__tab-content" data-tab-content="pip">
40
+ <div class="package-install__code-block">
41
+ <div class="package-install__code-header">
42
+ <span class="package-install__code-label">Terminal</span>
43
+ <button class="package-install__copy-btn" data-copy-btn>
44
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
45
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
46
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
47
+ </svg>
48
+ Copy
49
+ </button>
50
+ </div>
51
+ <pre><code data-copy-text>{{ pip_command | default: "pip install " | append: package_name }}</code></pre>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="package-install__tab-content" data-tab-content="conda" hidden>
56
+ <div class="package-install__code-block">
57
+ <div class="package-install__code-header">
58
+ <span class="package-install__code-label">Terminal</span>
59
+ <button class="package-install__copy-btn" data-copy-btn>
60
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
61
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
62
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
63
+ </svg>
64
+ Copy
65
+ </button>
66
+ </div>
67
+ <pre><code data-copy-text>{{ conda_command | default: "conda install " | append: package_name }}</code></pre>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="package-install__tab-content" data-tab-content="git" hidden>
72
+ <div class="package-install__code-block">
73
+ <div class="package-install__code-header">
74
+ <span class="package-install__code-label">Terminal</span>
75
+ <button class="package-install__copy-btn" data-copy-btn>
76
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
77
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
78
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
79
+ </svg>
80
+ Copy
81
+ </button>
82
+ </div>
83
+ <pre><code data-copy-text>git clone {{ git_url }}
84
+ cd {{ package_name }}
85
+ pip install -e .</code></pre>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ {%- elsif language == "r" -%}
91
+ <div class="package-install__tabs" data-install-tabs>
92
+ <div class="package-install__tab-buttons">
93
+ <button class="package-install__tab-btn package-install__tab-btn--active" data-tab="cran">
94
+ CRAN
95
+ </button>
96
+ <button class="package-install__tab-btn" data-tab="github">
97
+ GitHub
98
+ </button>
99
+ </div>
100
+
101
+ <div class="package-install__tab-content" data-tab-content="cran">
102
+ <div class="package-install__code-block">
103
+ <div class="package-install__code-header">
104
+ <span class="package-install__code-label">R Console</span>
105
+ <button class="package-install__copy-btn" data-copy-btn>
106
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
107
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
108
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
109
+ </svg>
110
+ Copy
111
+ </button>
112
+ </div>
113
+ <pre><code data-copy-text>{{ cran_command | default: "install.packages('" | append: package_name | append: "')" }}</code></pre>
114
+ </div>
115
+ </div>
116
+
117
+ <div class="package-install__tab-content" data-tab-content="github" hidden>
118
+ <div class="package-install__code-block">
119
+ <div class="package-install__code-header">
120
+ <span class="package-install__code-label">R Console</span>
121
+ <button class="package-install__copy-btn" data-copy-btn>
122
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
123
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
124
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
125
+ </svg>
126
+ Copy
127
+ </button>
128
+ </div>
129
+ <pre><code data-copy-text>devtools::install_github("{{ git_url | remove: "https://github.com/" }}")</code></pre>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ {%- endif -%}
134
+ </div>
135
+
136
+ <script>
137
+ (function() {
138
+ 'use strict';
139
+
140
+ const initInstallTabs = () => {
141
+ const containers = document.querySelectorAll('[data-install-tabs]');
142
+
143
+ containers.forEach(container => {
144
+ const tabBtns = container.querySelectorAll('[data-tab]');
145
+ const tabContents = container.querySelectorAll('[data-tab-content]');
146
+ const copyBtns = container.querySelectorAll('[data-copy-btn]');
147
+
148
+ // Tab switching
149
+ tabBtns.forEach(btn => {
150
+ btn.addEventListener('click', () => {
151
+ const tabName = btn.dataset.tab;
152
+
153
+ // Update buttons
154
+ tabBtns.forEach(b => b.classList.remove('package-install__tab-btn--active'));
155
+ btn.classList.add('package-install__tab-btn--active');
156
+
157
+ // Update content
158
+ tabContents.forEach(content => {
159
+ content.hidden = content.dataset.tabContent !== tabName;
160
+ });
161
+ });
162
+ });
163
+
164
+ // Copy functionality
165
+ copyBtns.forEach(btn => {
166
+ btn.addEventListener('click', async () => {
167
+ const codeEl = btn.closest('.package-install__code-block').querySelector('[data-copy-text]');
168
+ const text = codeEl.textContent.trim();
169
+
170
+ try {
171
+ await navigator.clipboard.writeText(text);
172
+ const originalHTML = btn.innerHTML;
173
+ btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg> Copied!';
174
+ btn.classList.add('package-install__copy-btn--success');
175
+
176
+ setTimeout(() => {
177
+ btn.innerHTML = originalHTML;
178
+ btn.classList.remove('package-install__copy-btn--success');
179
+ }, 2000);
180
+ } catch (err) {
181
+ console.error('Failed to copy:', err);
182
+ }
183
+ });
184
+ });
185
+ });
186
+ };
187
+
188
+ if (document.readyState === 'loading') {
189
+ document.addEventListener('DOMContentLoaded', initInstallTabs);
190
+ } else {
191
+ initInstallTabs();
192
+ }
193
+ })();
194
+ </script>
@@ -0,0 +1,170 @@
1
+ {%- comment -%}
2
+ Performance Monitoring - Track Core Web Vitals and RUM
3
+ {%- endcomment -%}
4
+
5
+ <script>
6
+ (function() {
7
+ 'use strict';
8
+
9
+ class PerformanceMonitor {
10
+ constructor() {
11
+ this.metrics = {};
12
+ this.init();
13
+ }
14
+
15
+ init() {
16
+ // Core Web Vitals
17
+ this.measureCLS();
18
+ this.measureFID();
19
+ this.measureLCP();
20
+ this.measureTTFB();
21
+ this.measureFCP();
22
+
23
+ // Custom metrics
24
+ this.measurePageLoad();
25
+ this.measureResourceTiming();
26
+
27
+ // Report after page load
28
+ window.addEventListener('load', () => {
29
+ setTimeout(() => this.reportMetrics(), 2000);
30
+ });
31
+ }
32
+
33
+ measureCLS() {
34
+ let clsValue = 0;
35
+ let clsEntries = [];
36
+
37
+ const observer = new PerformanceObserver((list) => {
38
+ for (const entry of list.getEntries()) {
39
+ if (!entry.hadRecentInput) {
40
+ clsValue += entry.value;
41
+ clsEntries.push(entry);
42
+ }
43
+ }
44
+ });
45
+
46
+ observer.observe({type: 'layout-shift', buffered: true});
47
+ this.metrics.cls = () => clsValue;
48
+ }
49
+
50
+ measureFID() {
51
+ const observer = new PerformanceObserver((list) => {
52
+ for (const entry of list.getEntries()) {
53
+ this.metrics.fid = entry.processingStart - entry.startTime;
54
+ }
55
+ });
56
+
57
+ observer.observe({type: 'first-input', buffered: true});
58
+ }
59
+
60
+ measureLCP() {
61
+ const observer = new PerformanceObserver((list) => {
62
+ const entries = list.getEntries();
63
+ const lastEntry = entries[entries.length - 1];
64
+ this.metrics.lcp = lastEntry.renderTime || lastEntry.loadTime;
65
+ });
66
+
67
+ observer.observe({type: 'largest-contentful-paint', buffered: true});
68
+ }
69
+
70
+ measureTTFB() {
71
+ const navTiming = performance.getEntriesByType('navigation')[0];
72
+ if (navTiming) {
73
+ this.metrics.ttfb = navTiming.responseStart - navTiming.requestStart;
74
+ }
75
+ }
76
+
77
+ measureFCP() {
78
+ const observer = new PerformanceObserver((list) => {
79
+ for (const entry of list.getEntries()) {
80
+ if (entry.name === 'first-contentful-paint') {
81
+ this.metrics.fcp = entry.startTime;
82
+ }
83
+ }
84
+ });
85
+
86
+ observer.observe({type: 'paint', buffered: true});
87
+ }
88
+
89
+ measurePageLoad() {
90
+ window.addEventListener('load', () => {
91
+ const navTiming = performance.getEntriesByType('navigation')[0];
92
+ if (navTiming) {
93
+ this.metrics.domContentLoaded = navTiming.domContentLoadedEventEnd - navTiming.domContentLoadedEventStart;
94
+ this.metrics.loadComplete = navTiming.loadEventEnd - navTiming.loadEventStart;
95
+ this.metrics.totalTime = navTiming.loadEventEnd - navTiming.fetchStart;
96
+ }
97
+ });
98
+ }
99
+
100
+ measureResourceTiming() {
101
+ window.addEventListener('load', () => {
102
+ const resources = performance.getEntriesByType('resource');
103
+ const byType = {};
104
+
105
+ resources.forEach(resource => {
106
+ const type = resource.initiatorType;
107
+ if (!byType[type]) byType[type] = {count: 0, totalSize: 0, totalDuration: 0};
108
+
109
+ byType[type].count++;
110
+ byType[type].totalSize += resource.transferSize || 0;
111
+ byType[type].totalDuration += resource.duration;
112
+ });
113
+
114
+ this.metrics.resources = byType;
115
+ });
116
+ }
117
+
118
+ reportMetrics() {
119
+ const report = {
120
+ url: window.location.href,
121
+ userAgent: navigator.userAgent,
122
+ connection: navigator.connection ? {
123
+ effectiveType: navigator.connection.effectiveType,
124
+ downlink: navigator.connection.downlink,
125
+ rtt: navigator.connection.rtt
126
+ } : null,
127
+ metrics: {
128
+ cls: typeof this.metrics.cls === 'function' ? this.metrics.cls() : this.metrics.cls,
129
+ fid: this.metrics.fid,
130
+ lcp: this.metrics.lcp,
131
+ ttfb: this.metrics.ttfb,
132
+ fcp: this.metrics.fcp,
133
+ domContentLoaded: this.metrics.domContentLoaded,
134
+ loadComplete: this.metrics.loadComplete,
135
+ totalTime: this.metrics.totalTime
136
+ },
137
+ resources: this.metrics.resources,
138
+ timestamp: new Date().toISOString()
139
+ };
140
+
141
+ // Log to console (in production, send to analytics endpoint)
142
+ console.log('Performance Metrics:', report);
143
+
144
+ // Store in localStorage for debugging
145
+ try {
146
+ const history = JSON.parse(localStorage.getItem('performanceHistory') || '[]');
147
+ history.push(report);
148
+ localStorage.setItem('performanceHistory', JSON.stringify(history.slice(-10))); // Keep last 10
149
+ } catch (e) {
150
+ console.error('Failed to store performance metrics:', e);
151
+ }
152
+
153
+ // Send to analytics (if configured)
154
+ if (window.gtag) {
155
+ window.gtag('event', 'web_vitals', {
156
+ event_category: 'Performance',
157
+ cls: report.metrics.cls,
158
+ fid: report.metrics.fid,
159
+ lcp: report.metrics.lcp
160
+ });
161
+ }
162
+ }
163
+ }
164
+
165
+ // Initialize
166
+ if ('PerformanceObserver' in window) {
167
+ new PerformanceMonitor();
168
+ }
169
+ })();
170
+ </script>
@@ -0,0 +1,233 @@
1
+ {%- comment -%}
2
+ Popular Posts Widget Component
3
+ Displays trending and most popular content
4
+
5
+ Parameters:
6
+ - max_posts: Maximum posts to display (default: 5)
7
+ - sort_by: Sorting method (recent, comments, trending) (default: recent)
8
+ - show_stats: Show reading stats (default: true)
9
+ - show_thumbnail: Show post thumbnail (default: true)
10
+ - layout: Display layout (list, grid, compact) (default: list)
11
+ - heading: Custom heading (default: "Popular Posts")
12
+ {%- endcomment -%}
13
+
14
+ {%- assign max_posts = include.max_posts | default: 5 -%}
15
+ {%- assign sort_by = include.sort_by | default: "recent" -%}
16
+ {%- assign show_stats = include.show_stats | default: true -%}
17
+ {%- assign show_thumbnail = include.show_thumbnail | default: true -%}
18
+ {%- assign layout = include.layout | default: "list" -%}
19
+ {%- assign heading = include.heading | default: "Popular Posts" -%}
20
+
21
+ {%- comment -%} Get posts and apply basic filtering {%- endcomment -%}
22
+ {%- assign all_posts = site.posts -%}
23
+
24
+ {%- comment -%} Simulate popularity scoring (in real app, use analytics data) {%- endcomment -%}
25
+ {%- assign scored_posts = "" | split: "" -%}
26
+
27
+ {%- for post in all_posts limit: 20 -%}
28
+ {%- assign score = 0 -%}
29
+
30
+ {%- comment -%} Recency score (prefer recent content) {%- endcomment -%}
31
+ {%- assign days_old = "now" | date: "%s" | minus: post.date | date: "%s" | divided_by: 86400 -%}
32
+ {%- if days_old < 7 -%}
33
+ {%- assign score = score | plus: 50 -%}
34
+ {%- elsif days_old < 30 -%}
35
+ {%- assign score = score | plus: 30 -%}
36
+ {%- elsif days_old < 90 -%}
37
+ {%- assign score = score | plus: 10 -%}
38
+ {%- endif -%}
39
+
40
+ {%- comment -%} Tag count (more tags = more discoverable) {%- endcomment -%}
41
+ {%- assign tag_score = post.tags.size | times: 5 -%}
42
+ {%- assign score = score | plus: tag_score -%}
43
+
44
+ {%- comment -%} Featured posts bonus {%- endcomment -%}
45
+ {%- if post.featured -%}
46
+ {%- assign score = score | plus: 25 -%}
47
+ {%- endif -%}
48
+
49
+ {%- comment -%} Difficulty-based scoring (beginner posts might be more popular) {%- endcomment -%}
50
+ {%- if post.difficulty == "beginner" -%}
51
+ {%- assign score = score | plus: 15 -%}
52
+ {%- endif -%}
53
+
54
+ {%- comment -%} Add randomness for variety (simulates view counts) {%- endcomment -%}
55
+ {%- assign random_bonus = post.title | size | modulo: 30 -%}
56
+ {%- assign score = score | plus: random_bonus -%}
57
+
58
+ {%- comment -%} Store post with score {%- endcomment -%}
59
+ {%- assign post_with_score = post.url | append: "||" | append: score -%}
60
+ {%- assign scored_posts = scored_posts | push: post_with_score -%}
61
+ {%- endfor -%}
62
+
63
+ {%- comment -%} Simple bubble sort by score {%- endcomment -%}
64
+ {%- assign sorted_posts = scored_posts -%}
65
+ {%- for i in (1..scored_posts.size) -%}
66
+ {%- for j in (1..scored_posts.size) -%}
67
+ {%- assign current = sorted_posts[j] | split: "||" -%}
68
+ {%- assign prev = sorted_posts[j-1] | split: "||" -%}
69
+ {%- if current[1] and prev[1] -%}
70
+ {%- assign current_score = current[1] | plus: 0 -%}
71
+ {%- assign prev_score = prev[1] | plus: 0 -%}
72
+ {%- if current_score > prev_score -%}
73
+ {%- assign temp = sorted_posts[j] -%}
74
+ {%- assign sorted_posts[j] = sorted_posts[j-1] -%}
75
+ {%- assign sorted_posts[j-1] = temp -%}
76
+ {%- endif -%}
77
+ {%- endif -%}
78
+ {%- endfor -%}
79
+ {%- endfor -%}
80
+
81
+ {%- comment -%} Build popular posts array {%- endcomment -%}
82
+ {%- assign popular_posts = "" | split: "" -%}
83
+ {%- for item in sorted_posts limit: max_posts -%}
84
+ {%- assign item_parts = item | split: "||" -%}
85
+ {%- assign post_url = item_parts[0] -%}
86
+
87
+ {%- for post in site.posts -%}
88
+ {%- if post.url == post_url -%}
89
+ {%- assign popular_posts = popular_posts | push: post -%}
90
+ {%- break -%}
91
+ {%- endif -%}
92
+ {%- endfor -%}
93
+ {%- endfor -%}
94
+
95
+ <aside class="popular-posts popular-posts--{{ layout }}"
96
+ data-popular-posts
97
+ aria-label="{{ heading }}">
98
+
99
+ <div class="popular-posts__header">
100
+ <h3 class="popular-posts__title">
101
+ <svg xmlns="http://www.w3.org/2000/svg"
102
+ width="20"
103
+ height="20"
104
+ viewBox="0 0 24 24"
105
+ fill="none"
106
+ stroke="currentColor"
107
+ stroke-width="2"
108
+ stroke-linecap="round"
109
+ stroke-linejoin="round"
110
+ aria-hidden="true">
111
+ <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
112
+ <polyline points="17 6 23 6 23 12"></polyline>
113
+ </svg>
114
+ {{ heading }}
115
+ </h3>
116
+
117
+ {%- if show_stats -%}
118
+ <span class="popular-posts__subtitle">
119
+ Trending this month
120
+ </span>
121
+ {%- endif -%}
122
+ </div>
123
+
124
+ <div class="popular-posts__list">
125
+ {%- for post in popular_posts -%}
126
+ {%- assign loop_index = forloop.index -%}
127
+
128
+ <article class="popular-post"
129
+ itemscope
130
+ itemtype="https://schema.org/BlogPosting">
131
+
132
+ <div class="popular-post__rank" aria-label="Rank {{ loop_index }}">
133
+ {{ loop_index }}
134
+ </div>
135
+
136
+ {%- if show_thumbnail and post.featured_image or post.image -%}
137
+ <div class="popular-post__thumbnail">
138
+ <a href="{{ post.url | relative_url }}"
139
+ tabindex="-1"
140
+ aria-hidden="true">
141
+ <img src="{{ post.featured_image | default: post.image | relative_url }}"
142
+ alt=""
143
+ loading="lazy"
144
+ itemprop="image">
145
+ </a>
146
+ </div>
147
+ {%- endif -%}
148
+
149
+ <div class="popular-post__content">
150
+ <h4 class="popular-post__title" itemprop="headline">
151
+ <a href="{{ post.url | relative_url }}"
152
+ class="popular-post__link">
153
+ {{ post.title }}
154
+ </a>
155
+ </h4>
156
+
157
+ {%- if show_stats -%}
158
+ <div class="popular-post__meta">
159
+ {%- if post.date -%}
160
+ <time class="popular-post__date"
161
+ datetime="{{ post.date | date_to_xmlschema }}"
162
+ itemprop="datePublished">
163
+ {{ post.date | date: "%b %d, %Y" }}
164
+ </time>
165
+ {%- endif -%}
166
+
167
+ <span class="popular-post__separator" aria-hidden="true">•</span>
168
+
169
+ {%- comment -%} Reading time {%- endcomment -%}
170
+ {% include components/reading-time.html
171
+ content=post.content
172
+ variant="inline"
173
+ show_icon=false %}
174
+
175
+ {%- comment -%} View count removed — was simulated. Re-enable with real analytics data. {%- endcomment -%}
176
+ </div>
177
+ {%- endif -%}
178
+
179
+ {%- if post.excerpt and layout != "compact" -%}
180
+ <p class="popular-post__excerpt" itemprop="description">
181
+ {{ post.excerpt | strip_html | truncatewords: 15 }}
182
+ </p>
183
+ {%- endif -%}
184
+
185
+ {%- if post.tags.size > 0 and layout == "grid" -%}
186
+ <div class="popular-post__tags">
187
+ {%- for tag in post.tags limit: 2 -%}
188
+ <span class="popular-post__tag">{{ tag }}</span>
189
+ {%- endfor -%}
190
+ </div>
191
+ {%- endif -%}
192
+ </div>
193
+
194
+ {%- comment -%} Trending indicator for top 3 {%- endcomment -%}
195
+ {%- if loop_index <= 3 -%}
196
+ <div class="popular-post__badge" aria-label="Trending">
197
+ <svg xmlns="http://www.w3.org/2000/svg"
198
+ width="16"
199
+ height="16"
200
+ viewBox="0 0 24 24"
201
+ fill="none"
202
+ stroke="currentColor"
203
+ stroke-width="2"
204
+ aria-hidden="true">
205
+ <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
206
+ <polyline points="17 6 23 6 23 12"></polyline>
207
+ </svg>
208
+ </div>
209
+ {%- endif -%}
210
+ </article>
211
+ {%- endfor -%}
212
+ </div>
213
+
214
+ {%- if site.phase4_features.popular_posts.show_view_all -%}
215
+ <div class="popular-posts__footer">
216
+ <a href="{{ '/archive/' | relative_url }}"
217
+ class="popular-posts__view-all">
218
+ View all posts
219
+ <svg xmlns="http://www.w3.org/2000/svg"
220
+ width="16"
221
+ height="16"
222
+ viewBox="0 0 24 24"
223
+ fill="none"
224
+ stroke="currentColor"
225
+ stroke-width="2"
226
+ aria-hidden="true">
227
+ <line x1="5" y1="12" x2="19" y2="12"></line>
228
+ <polyline points="12 5 19 12 12 19"></polyline>
229
+ </svg>
230
+ </a>
231
+ </div>
232
+ {%- endif -%}
233
+ </aside>
@@ -0,0 +1,62 @@
1
+ {%- comment -%}
2
+ ---
3
+ required:
4
+ - page
5
+ ---
6
+ Renders a post hero from the `post_hero` hash, which the Minimal Mistakes
7
+ compatibility hook (_plugins/front_matter_compat.rb) builds from `header.*`
8
+ fields and which can also be written directly:
9
+
10
+ post_hero:
11
+ image: /assets/images/hero.jpg
12
+ overlay: true # title over the image, as Minimal Mistakes does
13
+ overlay_filter: 0.35 # number, rgba(...) or a gradient
14
+ overlay_color: "#123"
15
+ alt: Description of the image (figure variant)
16
+ caption: Caption under the image (figure variant)
17
+
18
+ The overlay variant sets its background through a nonce-carrying <style>
19
+ element rather than a style attribute, because the theme's Content Security
20
+ Policy does not allow inline style attributes.
21
+ {%- endcomment -%}
22
+ {%- assign hero = include.page.post_hero -%}
23
+ {%- assign hero_src = hero.image -%}
24
+ {%- unless hero_src contains '://' -%}
25
+ {%- assign hero_src = hero_src | relative_url -%}
26
+ {%- endunless -%}
27
+ {%- assign hero_alt = hero.alt | default: include.page.title | default: '' | strip_html | strip_newlines -%}
28
+ {%- assign overlay_filter = '' -%}
29
+ {%- if hero.overlay_filter -%}
30
+ {%- assign filter_value = hero.overlay_filter | append: '' -%}
31
+ {%- if filter_value contains 'gradient' -%}
32
+ {%- assign overlay_filter = filter_value -%}
33
+ {%- elsif filter_value contains 'rgba' -%}
34
+ {%- assign overlay_filter = 'linear-gradient(' | append: filter_value | append: ', ' | append: filter_value | append: ')' -%}
35
+ {%- else -%}
36
+ {%- assign rgba = 'rgba(0, 0, 0, ' | append: filter_value | append: ')' -%}
37
+ {%- assign overlay_filter = 'linear-gradient(' | append: rgba | append: ', ' | append: rgba | append: ')' -%}
38
+ {%- endif -%}
39
+ {%- endif -%}
40
+ {%- if hero.overlay -%}
41
+ {%- assign hero_id = include.page.url | default: 'page' | slugify | prepend: 'post-hero-' -%}
42
+ <style nonce="{{ page.csp_nonce }}">
43
+ #{{ hero_id }} { {% if hero.overlay_color %}background-color: {{ hero.overlay_color }}; {% endif %}background-image: {% if overlay_filter != '' %}{{ overlay_filter }}, {% endif %}url('{{ hero_src }}'); }
44
+ </style>
45
+ <div id="{{ hero_id }}" class="post-hero post-hero--overlay">
46
+ <div class="post-hero__inner">
47
+ {%- if include.page.title -%}
48
+ <h1 class="article-title post-hero__title" itemprop="headline">{{ include.page.title }}</h1>
49
+ {%- endif -%}
50
+ {%- if include.page.subtitle -%}
51
+ <p class="post-hero__subtitle">{{ include.page.subtitle }}</p>
52
+ {%- endif -%}
53
+ </div>
54
+ </div>
55
+ {%- else -%}
56
+ <figure class="post-hero">
57
+ <img class="post-hero__image" src="{{ hero_src }}" alt="{{ hero_alt | escape_once }}" loading="eager" decoding="async" />
58
+ {%- if hero.caption -%}
59
+ <figcaption class="post-hero__caption">{{ hero.caption | markdownify | strip_html | strip_newlines }}</figcaption>
60
+ {%- endif -%}
61
+ </figure>
62
+ {%- endif -%}