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.
@@ -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
- Javascript CDN
3
- sources:
4
- - Bootstrap 4.0.0
5
- - jQuery 3.2.1
6
- - Popper.js 1.12.9
7
- sources: https://getbootstrap.com/docs/5.3/getting-started/contents/#css-files
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
- <!-- Jquery -->
11
- <!-- <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> -->
12
- <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
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
- <!-- bootstrap js -->
16
- <!-- <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script> -->
17
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
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>