jekyll-theme-zer0 0.5.0 → 0.7.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 +4 -4
- data/CHANGELOG.md +186 -60
- data/README.md +225 -12
- data/_data/authors.yml +52 -0
- data/_data/navigation/posts.yml +13 -14
- data/_includes/analytics/posthog.html +281 -0
- data/_includes/components/author-card.html +177 -0
- data/_includes/components/cookie-consent.html +382 -0
- data/_includes/components/info-section.html +5 -0
- data/_includes/components/js-cdn.html +27 -22
- data/_includes/components/post-card.html +176 -0
- data/_includes/components/theme-info.html +312 -0
- data/_includes/core/branding.html +24 -12
- data/_includes/core/footer.html +16 -0
- data/_includes/core/head.html +8 -8
- data/_includes/navigation/sidebar-folders.html +63 -103
- data/_layouts/blog.html +424 -232
- data/_layouts/category.html +247 -0
- data/_layouts/journals.html +272 -23
- data/_layouts/root.html +6 -0
- data/_layouts/tag.html +111 -0
- data/assets/css/main.scss +5 -5
- metadata +15 -7
- /data/_sass/{it-journey → core}/_docs.scss +0 -0
- /data/_sass/{it-journey → core}/_syntax.scss +0 -0
- /data/_sass/{it-journey → core}/_theme.scss +0 -0
- /data/_sass/{it-journey → core}/_variables.scss +0 -0
- /data/_sass/{it-journey → core}/code-copy.scss +0 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
===================================================================
|
|
3
|
+
COOKIE CONSENT - GDPR/CCPA Compliant Privacy Management
|
|
4
|
+
===================================================================
|
|
5
|
+
|
|
6
|
+
File: cookie-consent.html
|
|
7
|
+
Path: _includes/components/cookie-consent.html
|
|
8
|
+
Purpose: Privacy-compliant cookie consent banner and management modal
|
|
9
|
+
with granular user permission controls
|
|
10
|
+
|
|
11
|
+
Template Logic:
|
|
12
|
+
- Displays consent banner for new visitors after 1-second delay
|
|
13
|
+
- Provides granular consent options (Essential, Analytics, Marketing)
|
|
14
|
+
- Stores user preferences in localStorage with 365-day expiry
|
|
15
|
+
- Integrates with PostHog analytics for consent-based tracking
|
|
16
|
+
|
|
17
|
+
Dependencies:
|
|
18
|
+
- PostHog analytics integration
|
|
19
|
+
- Bootstrap 5 modal and form components
|
|
20
|
+
- Bootstrap Icons for UI elements
|
|
21
|
+
- site.posthog configuration from _config.yml
|
|
22
|
+
|
|
23
|
+
Privacy Features:
|
|
24
|
+
- GDPR/CCPA compliance with explicit user consent
|
|
25
|
+
- Granular permission controls with persistent storage
|
|
26
|
+
- Automatic consent expiry and re-consent after 365 days
|
|
27
|
+
- Integration with PostHog opt-in/opt-out mechanisms
|
|
28
|
+
|
|
29
|
+
Accessibility:
|
|
30
|
+
- ARIA labels for screen readers
|
|
31
|
+
- Keyboard navigation support
|
|
32
|
+
- High contrast design for visibility
|
|
33
|
+
- Semantic HTML structure
|
|
34
|
+
===================================================================
|
|
35
|
+
-->
|
|
36
|
+
|
|
37
|
+
{% comment %}
|
|
38
|
+
Cookie Consent Banner for Zer0-Mistakes Jekyll Theme
|
|
39
|
+
|
|
40
|
+
Features:
|
|
41
|
+
- GDPR/CCPA compliance
|
|
42
|
+
- Granular consent options (essential, analytics, marketing)
|
|
43
|
+
- PostHog analytics integration
|
|
44
|
+
- Local storage for preferences
|
|
45
|
+
- Bootstrap 5 styling
|
|
46
|
+
- Accessibility compliant
|
|
47
|
+
|
|
48
|
+
Usage: Include in root.html layout
|
|
49
|
+
Configuration: Uses site.posthog settings from _config.yml
|
|
50
|
+
{% endcomment %}
|
|
51
|
+
|
|
52
|
+
<div id="cookieConsent" class="cookie-consent-banner position-fixed bottom-0 start-0 end-0 bg-dark text-light p-3 shadow-lg" style="z-index: 9999; transform: translateY(100%); transition: transform 0.3s ease-in-out;">
|
|
53
|
+
<div class="container-fluid">
|
|
54
|
+
<div class="row align-items-center">
|
|
55
|
+
<div class="col-12 col-lg-8">
|
|
56
|
+
<h6 class="mb-2 mb-lg-0">
|
|
57
|
+
<i class="bi bi-shield-check me-2"></i>
|
|
58
|
+
We value your privacy
|
|
59
|
+
</h6>
|
|
60
|
+
<p class="mb-2 mb-lg-0 small">
|
|
61
|
+
This website uses cookies and similar technologies to enhance your browsing experience, analyze traffic, and provide personalized content.
|
|
62
|
+
<a href="/privacy-policy/" class="text-light text-decoration-underline">Learn more in our Privacy Policy</a>.
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="col-12 col-lg-4">
|
|
66
|
+
<div class="d-flex flex-wrap gap-2 justify-content-lg-end">
|
|
67
|
+
<button type="button" class="btn btn-sm btn-outline-light" data-bs-toggle="modal" data-bs-target="#cookieSettingsModal">
|
|
68
|
+
<i class="bi bi-gear me-1"></i>
|
|
69
|
+
Manage Cookies
|
|
70
|
+
</button>
|
|
71
|
+
<button type="button" class="btn btn-sm btn-secondary" id="rejectAllCookies">
|
|
72
|
+
Reject All
|
|
73
|
+
</button>
|
|
74
|
+
<button type="button" class="btn btn-sm btn-primary" id="acceptAllCookies">
|
|
75
|
+
Accept All
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Cookie Settings Modal -->
|
|
84
|
+
<div class="modal fade" id="cookieSettingsModal" tabindex="-1" aria-labelledby="cookieSettingsModalLabel" aria-hidden="true">
|
|
85
|
+
<div class="modal-dialog modal-lg">
|
|
86
|
+
<div class="modal-content">
|
|
87
|
+
<div class="modal-header">
|
|
88
|
+
<h5 class="modal-title" id="cookieSettingsModalLabel">
|
|
89
|
+
<i class="bi bi-shield-check me-2"></i>
|
|
90
|
+
Cookie Preferences
|
|
91
|
+
</h5>
|
|
92
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="modal-body">
|
|
95
|
+
<p class="text-muted">
|
|
96
|
+
Customize your cookie preferences below. You can change these settings at any time by clicking the cookie preferences link in our footer.
|
|
97
|
+
</p>
|
|
98
|
+
|
|
99
|
+
<div class="cookie-category mb-4">
|
|
100
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
101
|
+
<h6 class="mb-0">
|
|
102
|
+
<i class="bi bi-shield-fill-check text-success me-2"></i>
|
|
103
|
+
Essential Cookies
|
|
104
|
+
</h6>
|
|
105
|
+
<span class="badge bg-success">Always Active</span>
|
|
106
|
+
</div>
|
|
107
|
+
<p class="small text-muted mb-0">
|
|
108
|
+
These cookies are necessary for the website to function properly. They enable core functionality such as navigation, security, and accessibility features.
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div class="cookie-category mb-4">
|
|
113
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
114
|
+
<h6 class="mb-0">
|
|
115
|
+
<i class="bi bi-graph-up me-2"></i>
|
|
116
|
+
Analytics Cookies
|
|
117
|
+
</h6>
|
|
118
|
+
<div class="form-check form-switch">
|
|
119
|
+
<input class="form-check-input" type="checkbox" id="analyticsCookies" checked>
|
|
120
|
+
<label class="form-check-label" for="analyticsCookies">Enable</label>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<p class="small text-muted mb-0">
|
|
124
|
+
These cookies help us understand how visitors interact with our website by collecting anonymous information about page visits, time spent, and user behavior patterns.
|
|
125
|
+
</p>
|
|
126
|
+
<details class="mt-2">
|
|
127
|
+
<summary class="small text-primary cursor-pointer">View analytics providers</summary>
|
|
128
|
+
<ul class="small text-muted mt-2 ps-3">
|
|
129
|
+
<li>PostHog Analytics - Website usage analytics and session recording (when enabled)</li>
|
|
130
|
+
<li>Google Analytics - Traffic analysis and user demographics</li>
|
|
131
|
+
</ul>
|
|
132
|
+
</details>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div class="cookie-category mb-4">
|
|
136
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
137
|
+
<h6 class="mb-0">
|
|
138
|
+
<i class="bi bi-megaphone me-2"></i>
|
|
139
|
+
Marketing Cookies
|
|
140
|
+
</h6>
|
|
141
|
+
<div class="form-check form-switch">
|
|
142
|
+
<input class="form-check-input" type="checkbox" id="marketingCookies">
|
|
143
|
+
<label class="form-check-label" for="marketingCookies">Enable</label>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<p class="small text-muted mb-0">
|
|
147
|
+
These cookies are used to deliver personalized advertisements and marketing content based on your interests and browsing behavior.
|
|
148
|
+
</p>
|
|
149
|
+
<p class="small text-muted mt-2">
|
|
150
|
+
<em>Note: We currently do not use marketing cookies, but this option is provided for future enhancements.</em>
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="bg-light p-3 rounded">
|
|
155
|
+
<h6 class="text-dark mb-2">
|
|
156
|
+
<i class="bi bi-info-circle me-2"></i>
|
|
157
|
+
Your Privacy Rights
|
|
158
|
+
</h6>
|
|
159
|
+
<ul class="small text-dark mb-0 ps-3">
|
|
160
|
+
<li>You can withdraw consent at any time by updating your cookie preferences</li>
|
|
161
|
+
<li>Essential cookies cannot be disabled as they are necessary for site functionality</li>
|
|
162
|
+
<li>Disabling analytics cookies will prevent us from improving your experience</li>
|
|
163
|
+
<li>Your preferences are stored locally and will be remembered for future visits</li>
|
|
164
|
+
</ul>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="modal-footer">
|
|
168
|
+
<button type="button" class="btn btn-outline-secondary" id="rejectAllModal">
|
|
169
|
+
Reject All
|
|
170
|
+
</button>
|
|
171
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
172
|
+
Cancel
|
|
173
|
+
</button>
|
|
174
|
+
<button type="button" class="btn btn-primary" id="saveCookiePreferences">
|
|
175
|
+
Save Preferences
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<script>
|
|
183
|
+
// Cookie Consent Management
|
|
184
|
+
(function() {
|
|
185
|
+
'use strict';
|
|
186
|
+
|
|
187
|
+
const STORAGE_KEY = 'zer0-cookie-consent';
|
|
188
|
+
const CONSENT_EXPIRY_DAYS = 365;
|
|
189
|
+
|
|
190
|
+
// Default consent state
|
|
191
|
+
const defaultConsent = {
|
|
192
|
+
essential: true,
|
|
193
|
+
analytics: false,
|
|
194
|
+
marketing: false,
|
|
195
|
+
timestamp: Date.now(),
|
|
196
|
+
version: '1.0'
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Get current consent state
|
|
200
|
+
function getConsentState() {
|
|
201
|
+
try {
|
|
202
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
203
|
+
if (stored) {
|
|
204
|
+
const consent = JSON.parse(stored);
|
|
205
|
+
// Check if consent is expired (365 days)
|
|
206
|
+
const isExpired = (Date.now() - consent.timestamp) > (CONSENT_EXPIRY_DAYS * 24 * 60 * 60 * 1000);
|
|
207
|
+
if (!isExpired) {
|
|
208
|
+
return consent;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch (e) {
|
|
212
|
+
console.warn('Error reading cookie consent:', e);
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Save consent state
|
|
218
|
+
function saveConsentState(consent) {
|
|
219
|
+
try {
|
|
220
|
+
consent.timestamp = Date.now();
|
|
221
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(consent));
|
|
222
|
+
applyConsent(consent);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.warn('Error saving cookie consent:', e);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Apply consent decisions
|
|
229
|
+
function applyConsent(consent) {
|
|
230
|
+
// Analytics consent
|
|
231
|
+
if (consent.analytics && window.posthog) {
|
|
232
|
+
window.posthog.opt_in_capturing();
|
|
233
|
+
console.log('PostHog analytics enabled via consent');
|
|
234
|
+
} else if (window.posthog) {
|
|
235
|
+
window.posthog.opt_out_capturing();
|
|
236
|
+
console.log('PostHog analytics disabled via consent');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Set global consent flags for other scripts
|
|
240
|
+
window.cookieConsent = consent;
|
|
241
|
+
|
|
242
|
+
// Dispatch consent event for other scripts to listen to
|
|
243
|
+
document.dispatchEvent(new CustomEvent('cookieConsentChanged', {
|
|
244
|
+
detail: consent
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Show consent banner
|
|
249
|
+
function showConsentBanner() {
|
|
250
|
+
const banner = document.getElementById('cookieConsent');
|
|
251
|
+
if (banner) {
|
|
252
|
+
setTimeout(() => {
|
|
253
|
+
banner.style.transform = 'translateY(0)';
|
|
254
|
+
}, 1000); // Show after 1 second
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Hide consent banner
|
|
259
|
+
function hideConsentBanner() {
|
|
260
|
+
const banner = document.getElementById('cookieConsent');
|
|
261
|
+
if (banner) {
|
|
262
|
+
banner.style.transform = 'translateY(100%)';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Update modal UI with current preferences
|
|
267
|
+
function updateModalUI(consent) {
|
|
268
|
+
document.getElementById('analyticsCookies').checked = consent.analytics;
|
|
269
|
+
document.getElementById('marketingCookies').checked = consent.marketing;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Get consent from modal UI
|
|
273
|
+
function getModalConsent() {
|
|
274
|
+
return {
|
|
275
|
+
essential: true, // Always true
|
|
276
|
+
analytics: document.getElementById('analyticsCookies').checked,
|
|
277
|
+
marketing: document.getElementById('marketingCookies').checked,
|
|
278
|
+
version: '1.0'
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Initialize on DOM content loaded
|
|
283
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
284
|
+
const existingConsent = getConsentState();
|
|
285
|
+
|
|
286
|
+
if (existingConsent) {
|
|
287
|
+
// Apply existing consent
|
|
288
|
+
applyConsent(existingConsent);
|
|
289
|
+
updateModalUI(existingConsent);
|
|
290
|
+
} else {
|
|
291
|
+
// Show consent banner for new visitors
|
|
292
|
+
showConsentBanner();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Accept all cookies
|
|
296
|
+
document.getElementById('acceptAllCookies')?.addEventListener('click', function() {
|
|
297
|
+
const consent = {
|
|
298
|
+
essential: true,
|
|
299
|
+
analytics: true,
|
|
300
|
+
marketing: false, // Keep false until we implement marketing
|
|
301
|
+
version: '1.0'
|
|
302
|
+
};
|
|
303
|
+
saveConsentState(consent);
|
|
304
|
+
hideConsentBanner();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Reject all cookies
|
|
308
|
+
document.getElementById('rejectAllCookies')?.addEventListener('click', function() {
|
|
309
|
+
saveConsentState(defaultConsent);
|
|
310
|
+
hideConsentBanner();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Reject all from modal
|
|
314
|
+
document.getElementById('rejectAllModal')?.addEventListener('click', function() {
|
|
315
|
+
saveConsentState(defaultConsent);
|
|
316
|
+
updateModalUI(defaultConsent);
|
|
317
|
+
hideConsentBanner();
|
|
318
|
+
bootstrap.Modal.getInstance(document.getElementById('cookieSettingsModal')).hide();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Save preferences from modal
|
|
322
|
+
document.getElementById('saveCookiePreferences')?.addEventListener('click', function() {
|
|
323
|
+
const consent = getModalConsent();
|
|
324
|
+
saveConsentState(consent);
|
|
325
|
+
hideConsentBanner();
|
|
326
|
+
bootstrap.Modal.getInstance(document.getElementById('cookieSettingsModal')).hide();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Update modal when opened
|
|
330
|
+
document.getElementById('cookieSettingsModal')?.addEventListener('show.bs.modal', function() {
|
|
331
|
+
const currentConsent = getConsentState() || defaultConsent;
|
|
332
|
+
updateModalUI(currentConsent);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Expose functions globally for external use
|
|
337
|
+
window.cookieManager = {
|
|
338
|
+
getConsent: getConsentState,
|
|
339
|
+
setConsent: saveConsentState,
|
|
340
|
+
showBanner: showConsentBanner,
|
|
341
|
+
hideBanner: hideConsentBanner,
|
|
342
|
+
hasConsent: function(type) {
|
|
343
|
+
const consent = getConsentState();
|
|
344
|
+
return consent ? consent[type] : false;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
})();
|
|
348
|
+
</script>
|
|
349
|
+
|
|
350
|
+
<style>
|
|
351
|
+
.cookie-consent-banner {
|
|
352
|
+
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
|
353
|
+
backdrop-filter: blur(10px);
|
|
354
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.cursor-pointer {
|
|
358
|
+
cursor: pointer;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.cookie-category {
|
|
362
|
+
border: 1px solid #dee2e6;
|
|
363
|
+
border-radius: 8px;
|
|
364
|
+
padding: 1rem;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.form-check-input:checked {
|
|
368
|
+
background-color: var(--bs-success);
|
|
369
|
+
border-color: var(--bs-success);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
@media (max-width: 768px) {
|
|
373
|
+
.cookie-consent-banner .btn {
|
|
374
|
+
width: 100%;
|
|
375
|
+
margin-bottom: 0.5rem;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.cookie-consent-banner .btn:last-child {
|
|
379
|
+
margin-bottom: 0;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
</style>
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
{% include navigation/breadcrumbs.html %}
|
|
22
22
|
{% include components/searchbar.html %}
|
|
23
23
|
|
|
24
|
+
<!-- Theme and System Information -->
|
|
25
|
+
<div class="my-4">
|
|
26
|
+
{% include components/theme-info.html %}
|
|
27
|
+
</div>
|
|
28
|
+
|
|
24
29
|
<!-- Shortcuts to source bode -->
|
|
25
30
|
{% include components/dev-shortcuts.html %}
|
|
26
31
|
<!-- Dark Mode Switch -->
|
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
<!--
|
|
2
|
+
===================================================================
|
|
3
|
+
JAVASCRIPT CDN - External JavaScript Library Loading
|
|
4
|
+
===================================================================
|
|
5
|
+
|
|
6
|
+
File: js-cdn.html
|
|
7
|
+
Path: _includes/components/js-cdn.html
|
|
8
|
+
Purpose: Load JavaScript libraries from CDN for site functionality
|
|
9
|
+
|
|
10
|
+
Libraries:
|
|
11
|
+
- jQuery 3.7.1 - DOM manipulation and AJAX
|
|
12
|
+
- Bootstrap 5.3.3 Bundle - UI components (includes Popper.js)
|
|
13
|
+
|
|
14
|
+
Note: Bootstrap 5 bundle includes Popper.js, so separate Popper
|
|
15
|
+
loading is not needed and can cause conflicts.
|
|
16
|
+
|
|
17
|
+
Source: https://getbootstrap.com/docs/5.3/getting-started/contents/
|
|
18
|
+
===================================================================
|
|
19
|
+
-->
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
<!-- jQuery 3.7.1 -->
|
|
22
|
+
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
|
|
23
|
+
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
|
|
24
|
+
crossorigin="anonymous"></script>
|
|
13
25
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<!-- Popper -->
|
|
20
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
|
21
|
-
<!-- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script> -->
|
|
22
|
-
|
|
23
|
-
<!-- TODO: Install local bootstrap -->
|
|
24
|
-
<!-- <script src="/assets/js/bootstrap.js"></script> -->
|
|
26
|
+
<!-- Bootstrap 5.3.3 Bundle (includes Popper.js) -->
|
|
27
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
|
28
|
+
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
|
29
|
+
crossorigin="anonymous"></script>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
===================================================================
|
|
3
|
+
POST CARD COMPONENT - Reusable blog post card
|
|
4
|
+
===================================================================
|
|
5
|
+
|
|
6
|
+
File: post-card.html
|
|
7
|
+
Path: _includes/components/post-card.html
|
|
8
|
+
Purpose: Consistent post card display across blog, category, and archive pages
|
|
9
|
+
|
|
10
|
+
Parameters:
|
|
11
|
+
- post (required): The post object to display
|
|
12
|
+
- show_category (optional): Show category badge (default: true)
|
|
13
|
+
- show_excerpt (optional): Show post excerpt (default: true)
|
|
14
|
+
- show_author (optional): Show author name (default: true)
|
|
15
|
+
- show_reading_time (optional): Show reading time (default: true)
|
|
16
|
+
- card_class (optional): Additional CSS classes for the card
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
{% include components/post-card.html post=post %}
|
|
20
|
+
{% include components/post-card.html post=post show_category=false %}
|
|
21
|
+
{% include components/post-card.html post=post card_class="shadow-lg" %}
|
|
22
|
+
|
|
23
|
+
Features:
|
|
24
|
+
- Breaking news badge (red, top-left)
|
|
25
|
+
- Featured badge (gold star, top-right)
|
|
26
|
+
- Preview image with fallback
|
|
27
|
+
- Category badge
|
|
28
|
+
- Reading time calculation
|
|
29
|
+
- Author attribution
|
|
30
|
+
- Publication date
|
|
31
|
+
- Responsive design
|
|
32
|
+
|
|
33
|
+
Dependencies:
|
|
34
|
+
- Bootstrap 5 card components
|
|
35
|
+
- Bootstrap Icons
|
|
36
|
+
- site.public_folder for image paths
|
|
37
|
+
- site.teaser for fallback image
|
|
38
|
+
===================================================================
|
|
39
|
+
-->
|
|
40
|
+
|
|
41
|
+
{% comment %} Parameter defaults {% endcomment %}
|
|
42
|
+
{% assign show_category = include.show_category | default: true %}
|
|
43
|
+
{% assign show_excerpt = include.show_excerpt | default: true %}
|
|
44
|
+
{% assign show_author = include.show_author | default: true %}
|
|
45
|
+
{% assign show_reading_time = include.show_reading_time | default: true %}
|
|
46
|
+
{% assign card_class = include.card_class | default: "" %}
|
|
47
|
+
|
|
48
|
+
{% comment %}
|
|
49
|
+
Reading time: Use estimated_reading_time from front matter if available,
|
|
50
|
+
otherwise skip to avoid accessing post.content which causes nesting issues
|
|
51
|
+
{% endcomment %}
|
|
52
|
+
{% if include.post.estimated_reading_time %}
|
|
53
|
+
{% assign reading_time = include.post.estimated_reading_time %}
|
|
54
|
+
{% else %}
|
|
55
|
+
{% assign reading_time = "2 min" %}
|
|
56
|
+
{% endif %}
|
|
57
|
+
|
|
58
|
+
<div class="col">
|
|
59
|
+
<div class="card h-100 post-card border-0 shadow-sm {{ card_class }}">
|
|
60
|
+
|
|
61
|
+
<!-- ====================== -->
|
|
62
|
+
<!-- CARD IMAGE & BADGES -->
|
|
63
|
+
<!-- ====================== -->
|
|
64
|
+
<div class="position-relative">
|
|
65
|
+
<!-- Breaking News Badge -->
|
|
66
|
+
{% if include.post.breaking %}
|
|
67
|
+
<span class="badge bg-danger position-absolute top-0 start-0 m-2 z-1">
|
|
68
|
+
<i class="bi bi-lightning-fill me-1"></i>Breaking
|
|
69
|
+
</span>
|
|
70
|
+
{% endif %}
|
|
71
|
+
|
|
72
|
+
<!-- Featured Badge -->
|
|
73
|
+
{% if include.post.featured %}
|
|
74
|
+
<span class="badge bg-warning text-dark position-absolute top-0 end-0 m-2 z-1">
|
|
75
|
+
<i class="bi bi-star-fill me-1"></i>Featured
|
|
76
|
+
</span>
|
|
77
|
+
{% endif %}
|
|
78
|
+
|
|
79
|
+
<!-- Preview Image -->
|
|
80
|
+
<a href="{{ include.post.url | relative_url }}" class="text-decoration-none">
|
|
81
|
+
{% if include.post.preview %}
|
|
82
|
+
<img src="{{ site.baseurl }}/{{ site.public_folder }}/{{ include.post.preview }}"
|
|
83
|
+
class="card-img-top"
|
|
84
|
+
alt="Preview image for {{ include.post.title }}"
|
|
85
|
+
loading="lazy">
|
|
86
|
+
{% else %}
|
|
87
|
+
<img src="{{ site.baseurl }}/{{ site.public_folder }}/{{ site.teaser }}"
|
|
88
|
+
class="card-img-top"
|
|
89
|
+
alt="Default preview image"
|
|
90
|
+
loading="lazy">
|
|
91
|
+
{% endif %}
|
|
92
|
+
</a>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- ====================== -->
|
|
96
|
+
<!-- CARD BODY -->
|
|
97
|
+
<!-- ====================== -->
|
|
98
|
+
<div class="card-body d-flex flex-column">
|
|
99
|
+
|
|
100
|
+
<!-- Category Badge -->
|
|
101
|
+
{% if show_category and include.post.categories.size > 0 %}
|
|
102
|
+
<div class="mb-2">
|
|
103
|
+
{% assign primary_category = include.post.categories | first %}
|
|
104
|
+
<a href="{{ site.baseurl }}/posts/{{ primary_category | slugify }}/"
|
|
105
|
+
class="badge bg-primary text-decoration-none">
|
|
106
|
+
{{ primary_category }}
|
|
107
|
+
</a>
|
|
108
|
+
</div>
|
|
109
|
+
{% endif %}
|
|
110
|
+
|
|
111
|
+
<!-- Post Title -->
|
|
112
|
+
<h5 class="card-title mb-2">
|
|
113
|
+
<a href="{{ include.post.url | relative_url }}"
|
|
114
|
+
class="text-decoration-none text-body-emphasis stretched-link">
|
|
115
|
+
{{ include.post.title }}
|
|
116
|
+
</a>
|
|
117
|
+
</h5>
|
|
118
|
+
|
|
119
|
+
<!-- Subtitle if available -->
|
|
120
|
+
{% if include.post.sub-title %}
|
|
121
|
+
<p class="card-subtitle text-muted small mb-2">
|
|
122
|
+
{{ include.post.sub-title }}
|
|
123
|
+
</p>
|
|
124
|
+
{% endif %}
|
|
125
|
+
|
|
126
|
+
<!-- Post Excerpt -->
|
|
127
|
+
{% if show_excerpt %}
|
|
128
|
+
<p class="card-text text-muted small flex-grow-1">
|
|
129
|
+
{{ include.post.excerpt | strip_html | truncate: 120 }}
|
|
130
|
+
</p>
|
|
131
|
+
{% endif %}
|
|
132
|
+
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<!-- ====================== -->
|
|
136
|
+
<!-- CARD FOOTER -->
|
|
137
|
+
<!-- ====================== -->
|
|
138
|
+
<div class="card-footer bg-transparent border-top-0">
|
|
139
|
+
<div class="d-flex justify-content-between align-items-center small text-muted">
|
|
140
|
+
|
|
141
|
+
<!-- Left: Author & Date -->
|
|
142
|
+
<div>
|
|
143
|
+
{% if show_author and include.post.author %}
|
|
144
|
+
<span class="me-2">
|
|
145
|
+
<i class="bi bi-person me-1"></i>{{ include.post.author }}
|
|
146
|
+
</span>
|
|
147
|
+
{% endif %}
|
|
148
|
+
<span>
|
|
149
|
+
<i class="bi bi-calendar me-1"></i>{{ include.post.date | date: "%b %d, %Y" }}
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<!-- Right: Reading Time -->
|
|
154
|
+
{% if show_reading_time %}
|
|
155
|
+
<span>
|
|
156
|
+
<i class="bi bi-clock me-1"></i>{{ reading_time }}
|
|
157
|
+
</span>
|
|
158
|
+
{% endif %}
|
|
159
|
+
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- Tags (compact) -->
|
|
163
|
+
{% if include.post.tags.size > 0 %}
|
|
164
|
+
<div class="mt-2">
|
|
165
|
+
{% for tag in include.post.tags limit: 3 %}
|
|
166
|
+
<span class="badge bg-light text-dark border me-1">{{ tag }}</span>
|
|
167
|
+
{% endfor %}
|
|
168
|
+
{% if include.post.tags.size > 3 %}
|
|
169
|
+
<span class="badge bg-light text-muted border">+{{ include.post.tags.size | minus: 3 }}</span>
|
|
170
|
+
{% endif %}
|
|
171
|
+
</div>
|
|
172
|
+
{% endif %}
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|