jekyll-theme-zer0 1.31.1 → 1.31.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -0
- data/_data/ai.yml +1 -1
- data/_data/backlog.yml +54 -2
- data/_data/features.yml +9 -2
- data/_data/site_builder.yml +1 -1
- data/_data/theme-manifest.yml +19 -19
- data/_includes/analytics/google-analytics.html +46 -9
- data/_includes/analytics/posthog.html +101 -77
- data/_includes/components/ai-chat.html +1 -1
- data/_includes/components/cookie-consent.html +29 -0
- data/_includes/components/page-feedback.html +1 -1
- data/_includes/content/intro.html +22 -2
- data/_includes/content/sitemap.html +173 -17
- data/_includes/core/favicon.html +42 -6
- data/_includes/core/footer.html +2 -3
- data/_includes/core/header.html +11 -2
- data/_includes/setup/wizard.html +2 -2
- data/_layouts/default.html +8 -1
- data/_sass/components/_back-to-top.scss +9 -0
- data/_sass/components/_cookie-banner.scss +31 -0
- data/_sass/utilities/_focus.scss +12 -1
- data/assets/js/modules/navigation/index.js +94 -40
- data/assets/js/modules/navigation/scroll-spy.js +70 -7
- data/scripts/ai/api_call.rb +1 -1
- data/scripts/ci/test_visual_evidence_autogen.py +53 -1
- data/scripts/ci/visual_evidence_autogen.py +26 -7
- data/scripts/install/ai/client.sh +1 -1
- data/scripts/lib/preview_generator.py +1 -1
- data/scripts/translate.rb +1 -1
- metadata +2 -2
|
@@ -79,9 +79,13 @@ Privacy: Respects Do Not Track and provides opt-out mechanisms
|
|
|
79
79
|
|
|
80
80
|
{% comment %} Consent gate (privacy.require_consent): until the visitor
|
|
81
81
|
accepts the "analytics" category in the theme's cookie-consent banner
|
|
82
|
-
(localStorage "zer0-cookie-consent"), PostHog
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
(localStorage "zer0-cookie-consent"), PostHog is not loaded at all: no
|
|
83
|
+
request to PostHog and no ph_* cookie or localStorage entry. It used to
|
|
84
|
+
load opted-out with memory persistence, which still fetched array.js,
|
|
85
|
+
config.js and surveys.js before any choice was made. Accepting dispatches
|
|
86
|
+
`cookieConsentChanged`; the listener below then boots PostHog on the spot,
|
|
87
|
+
so no reload is needed. Reject keeps it off. Same record and 365-day
|
|
88
|
+
expiry as components/cookie-consent.html. {% endcomment %}
|
|
85
89
|
window.__phHasAnalyticsConsent = true;
|
|
86
90
|
if (window.posthogConfig.requireConsent) {
|
|
87
91
|
window.__phHasAnalyticsConsent = false;
|
|
@@ -89,9 +93,10 @@ Privacy: Respects Do Not Track and provides opt-out mechanisms
|
|
|
89
93
|
var __phStored = window.localStorage.getItem('zer0-cookie-consent');
|
|
90
94
|
if (__phStored) {
|
|
91
95
|
var __phConsent = JSON.parse(__phStored);
|
|
92
|
-
window.__phHasAnalyticsConsent = __phConsent && __phConsent.analytics === true
|
|
96
|
+
window.__phHasAnalyticsConsent = !!(__phConsent && __phConsent.analytics === true &&
|
|
97
|
+
(Date.now() - __phConsent.timestamp) <= 365 * 864e5);
|
|
93
98
|
}
|
|
94
|
-
} catch (e) { /* localStorage unavailable — stay
|
|
99
|
+
} catch (e) { /* localStorage unavailable — stay off */ }
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
{% comment %} Honor browser privacy signals before loading: Do Not Track
|
|
@@ -100,83 +105,102 @@ Privacy: Respects Do Not Track and provides opt-out mechanisms
|
|
|
100
105
|
(window.posthogConfig.respectGpc && window.navigator.globalPrivacyControl === true)) {
|
|
101
106
|
console.log('PostHog: respecting browser privacy signal (DNT/GPC)');
|
|
102
107
|
} else {
|
|
103
|
-
{% comment %}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
{% comment %} Everything that loads or configures PostHog runs in __phBoot,
|
|
109
|
+
once: at parse time when consent is not required or was already
|
|
110
|
+
given, otherwise when the visitor accepts analytics. {% endcomment %}
|
|
111
|
+
var __phBoot = function () {
|
|
112
|
+
if (window.__phBooted) return;
|
|
113
|
+
window.__phBooted = true;
|
|
114
|
+
{% comment %} Deferred loading: the stub below is installed immediately, so
|
|
115
|
+
posthog.capture()/opt_in_capturing() calls (e.g. from the cookie
|
|
116
|
+
banner) queue as before, but the ~130 KB library (array.js, then
|
|
117
|
+
config.js/surveys.js) is only fetched after the window `load` event
|
|
118
|
+
and the next idle period. It used to be inserted while the page was
|
|
119
|
+
still parsing and competed with first paint on mobile. The snippet is
|
|
120
|
+
PostHog's official one; the only change is that its insertBefore()
|
|
121
|
+
runs through __phInject. {% endcomment %}
|
|
122
|
+
window.__phInject = function (insert) {
|
|
123
|
+
var run = function () {
|
|
124
|
+
if (window.requestIdleCallback) window.requestIdleCallback(insert, { timeout: 3000 });
|
|
125
|
+
else setTimeout(insert, 1);
|
|
126
|
+
};
|
|
127
|
+
if (document.readyState === 'complete') run();
|
|
128
|
+
else window.addEventListener('load', run, { once: true });
|
|
115
129
|
};
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
};
|
|
119
|
-
{% comment %} PostHog Loading Script {% endcomment %}
|
|
120
|
-
!function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",window.__phInject(function(){(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r)});var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init Rr Mr fi Or Ar ci Tr Cr capture Mi calculateEventProperties Lr register register_once register_for_session unregister unregister_for_session Hr getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey displaySurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Ur jr createPersonProfile zr kr Br opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing get_explicit_consent_status is_capturing clear_opt_in_out_capturing Dr debug M Nr getPageViewId captureTraceFeedback captureTraceMetric $r".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
|
130
|
+
{% comment %} PostHog Loading Script {% endcomment %}
|
|
131
|
+
!function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",window.__phInject(function(){(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r)});var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init Rr Mr fi Or Ar ci Tr Cr capture Mi calculateEventProperties Lr register register_once register_for_session unregister unregister_for_session Hr getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey displaySurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Ur jr createPersonProfile zr kr Br opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing get_explicit_consent_status is_capturing clear_opt_in_out_capturing Dr debug M Nr getPageViewId captureTraceFeedback captureTraceMetric $r".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
|
121
132
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
{% comment %} Initialize PostHog with configuration {% endcomment %}
|
|
134
|
+
posthog.init(window.posthogConfig.apiKey, {
|
|
135
|
+
api_host: window.posthogConfig.apiHost,
|
|
136
|
+
person_profiles: window.posthogConfig.personProfiles,
|
|
137
|
+
autocapture: window.posthogConfig.autocapture,
|
|
138
|
+
capture_pageview: window.posthogConfig.capturePageview,
|
|
139
|
+
capture_pageleave: window.posthogConfig.capturePageleave,
|
|
140
|
+
disable_cookie: window.posthogConfig.disableCookie,
|
|
141
|
+
cross_subdomain_cookie: window.posthogConfig.crossSubdomainCookie,
|
|
142
|
+
secure_cookie: window.posthogConfig.secureCookie,
|
|
143
|
+
persistence: window.__phHasAnalyticsConsent ? window.posthogConfig.persistence : 'memory',
|
|
144
|
+
opt_out_capturing_by_default: !window.__phHasAnalyticsConsent,
|
|
145
|
+
session_recording: window.posthogConfig.sessionRecording,
|
|
146
|
+
{% if site.posthog.privacy.ip_anonymization %}
|
|
147
|
+
ip: false,
|
|
148
|
+
{% endif %}
|
|
149
|
+
loaded: function(posthog) {
|
|
150
|
+
{% comment %} PostHog loaded successfully {% endcomment %}
|
|
151
|
+
console.log('PostHog analytics loaded successfully');
|
|
141
152
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
153
|
+
{% comment %} privacy.geoip_disable: stop PostHog from resolving
|
|
154
|
+
IP to location on every event {% endcomment %}
|
|
155
|
+
if (window.posthogConfig.geoipDisable) {
|
|
156
|
+
posthog.register({ '$geoip_disable': true });
|
|
157
|
+
}
|
|
147
158
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
159
|
+
{% comment %} Track Jekyll-specific page properties. Free-text values go
|
|
160
|
+
through `jsonify`: they used to be pasted between single quotes, so
|
|
161
|
+
an apostrophe in a title or description ("you're") was a
|
|
162
|
+
SyntaxError that stopped PostHog loading at all. {% endcomment %}
|
|
163
|
+
posthog.register({
|
|
164
|
+
'site_title': {{ site.title | jsonify }},
|
|
165
|
+
'site_description': {{ site.description | strip_newlines | strip | jsonify }},
|
|
166
|
+
'jekyll_version': '{{ jekyll.version }}',
|
|
167
|
+
'theme': {{ site.remote_theme | default: site.theme | jsonify }},
|
|
168
|
+
'page_layout': {{ page.layout | jsonify }},
|
|
169
|
+
'page_collection': {{ page.collection | jsonify }},
|
|
170
|
+
{% if page.categories %}
|
|
171
|
+
'page_categories': {{ page.categories | jsonify }},
|
|
172
|
+
{% endif %}
|
|
173
|
+
{% if page.tags %}
|
|
174
|
+
'page_tags': {{ page.tags | jsonify }},
|
|
175
|
+
{% endif %}
|
|
176
|
+
'page_url': window.location.pathname,
|
|
177
|
+
'page_title': {{ page.title | strip_newlines | strip | jsonify }},
|
|
178
|
+
'page_author': {{ page.author | default: site.author.name | jsonify }},
|
|
179
|
+
{% if page.date %}
|
|
180
|
+
'page_date': '{{ page.date | date: "%Y-%m-%d" }}',
|
|
181
|
+
{% endif %}
|
|
182
|
+
'user_agent': navigator.userAgent,
|
|
183
|
+
'screen_resolution': screen.width + 'x' + screen.height,
|
|
184
|
+
'viewport_size': window.innerWidth + 'x' + window.innerHeight
|
|
185
|
+
});
|
|
175
186
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
187
|
+
{% comment %} Initialize custom event tracking {% endcomment %}
|
|
188
|
+
initializeCustomTracking();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (window.__phHasAnalyticsConsent) {
|
|
194
|
+
__phBoot();
|
|
195
|
+
} else {
|
|
196
|
+
document.addEventListener('cookieConsentChanged', function (e) {
|
|
197
|
+
if (!(e.detail && e.detail.analytics === true) || window.__phBooted) return;
|
|
198
|
+
window.__phHasAnalyticsConsent = true;
|
|
199
|
+
__phBoot();
|
|
200
|
+
{% comment %} Clears an opt-out a previous Reject may have stored. {% endcomment %}
|
|
201
|
+
window.posthog.opt_in_capturing();
|
|
202
|
+
});
|
|
203
|
+
}
|
|
180
204
|
}
|
|
181
205
|
|
|
182
206
|
{% comment %} Custom Event Tracking Functions {% endcomment %}
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"authMode": {{ ai_auth_mode | jsonify }},
|
|
186
186
|
"apiKey": {{ site.ai_chat.api_key | jsonify }},
|
|
187
187
|
"anthropicVersion": {{ site.ai_chat.anthropic_version | default: "2023-06-01" | jsonify }},
|
|
188
|
-
"model": {{ site.ai_chat.model | default: "claude-opus-
|
|
188
|
+
"model": {{ site.ai_chat.model | default: "claude-opus-5-5" | jsonify }},
|
|
189
189
|
"maxTokens": {{ site.ai_chat.max_tokens | default: 1024 }},
|
|
190
190
|
"strictContext": {{ site.ai_chat.strict_context | default: true | jsonify }},
|
|
191
191
|
"outOfScopeMessage": {{ site.ai_chat.out_of_scope_message | default: "I can only answer from the content on this page." | jsonify }},
|
|
@@ -249,6 +249,33 @@ Configuration: Uses site.posthog settings from _config.yml
|
|
|
249
249
|
}));
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// Reserve the banner's height while it is up. Publishes it as
|
|
253
|
+
// --zer0-cookie-banner-inset on <html> plus the zer0-cookie-banner-open
|
|
254
|
+
// class; _sass/components/_cookie-banner.scss turns that into body
|
|
255
|
+
// padding, scroll-padding-bottom (keyboard focus scrolls clear of the
|
|
256
|
+
// banner) and a lift for the floating buttons, which used to sit under
|
|
257
|
+
// it. A ResizeObserver keeps it right when the banner reflows (94px on
|
|
258
|
+
// phones, 48px from md up). Set as soon as the banner is unhidden, before
|
|
259
|
+
// the 1s slide-in, so the page does not shift when it appears.
|
|
260
|
+
let insetObserver = null;
|
|
261
|
+
function setBannerInset(banner) {
|
|
262
|
+
const root = document.documentElement;
|
|
263
|
+
const apply = () => {
|
|
264
|
+
root.style.setProperty('--zer0-cookie-banner-inset', Math.ceil(banner.offsetHeight) + 'px');
|
|
265
|
+
root.classList.add('zer0-cookie-banner-open');
|
|
266
|
+
};
|
|
267
|
+
apply();
|
|
268
|
+
if (!insetObserver && 'ResizeObserver' in window) {
|
|
269
|
+
insetObserver = new ResizeObserver(apply);
|
|
270
|
+
insetObserver.observe(banner);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function clearBannerInset() {
|
|
274
|
+
if (insetObserver) { insetObserver.disconnect(); insetObserver = null; }
|
|
275
|
+
document.documentElement.classList.remove('zer0-cookie-banner-open');
|
|
276
|
+
document.documentElement.style.removeProperty('--zer0-cookie-banner-inset');
|
|
277
|
+
}
|
|
278
|
+
|
|
252
279
|
// Show consent banner
|
|
253
280
|
function showConsentBanner() {
|
|
254
281
|
const banner = document.getElementById('cookieConsent');
|
|
@@ -257,6 +284,7 @@ Configuration: Uses site.posthog settings from _config.yml
|
|
|
257
284
|
// Reveal the element (CSS keeps it translated off-screen + opacity:0).
|
|
258
285
|
banner.hidden = false;
|
|
259
286
|
banner.classList.remove('cookie-banner-visible');
|
|
287
|
+
setBannerInset(banner);
|
|
260
288
|
|
|
261
289
|
setTimeout(() => {
|
|
262
290
|
// Force reflow so the next class change actually animates.
|
|
@@ -277,6 +305,7 @@ Configuration: Uses site.posthog settings from _config.yml
|
|
|
277
305
|
if (done) return;
|
|
278
306
|
done = true;
|
|
279
307
|
banner.hidden = true;
|
|
308
|
+
clearBannerInset();
|
|
280
309
|
banner.removeEventListener('transitionend', onEnd);
|
|
281
310
|
};
|
|
282
311
|
const onEnd = (e) => {
|
|
@@ -280,7 +280,7 @@
|
|
|
280
280
|
"ai": {
|
|
281
281
|
"available": {{ pf_ai_available | jsonify }},
|
|
282
282
|
"auto": {{ site.page_feedback.ai.auto | default: false | jsonify }},
|
|
283
|
-
"model": {{ site.page_feedback.ai.model | default: "claude-opus-
|
|
283
|
+
"model": {{ site.page_feedback.ai.model | default: "claude-opus-5-5" | jsonify }},
|
|
284
284
|
"maxTokens": {{ site.page_feedback.ai.max_tokens | default: 1024 }}
|
|
285
285
|
}
|
|
286
286
|
}
|
|
@@ -111,6 +111,20 @@
|
|
|
111
111
|
{% endif %}
|
|
112
112
|
{% endif %}
|
|
113
113
|
|
|
114
|
+
{% comment %} Tags index existence gate (issue #537) — the same policy as
|
|
115
|
+
news.html / section.html: an explicit site.tags_page is trusted; otherwise
|
|
116
|
+
link only when a page at /tags/ is actually in the build. A remote-theme
|
|
117
|
+
consumer without the tags index gets plain badges instead of 404 links on
|
|
118
|
+
every tagged page. {% endcomment %}
|
|
119
|
+
{% assign intro_tags_base = site.tags_page | default: "/tags/" %}
|
|
120
|
+
{% assign intro_tags_linkable = false %}
|
|
121
|
+
{% if site.tags_page %}
|
|
122
|
+
{% assign intro_tags_linkable = true %}
|
|
123
|
+
{% else %}
|
|
124
|
+
{% assign intro_tags_index = site.html_pages | where: "url", intro_tags_base | first %}
|
|
125
|
+
{% if intro_tags_index %}{% assign intro_tags_linkable = true %}{% endif %}
|
|
126
|
+
{% endif %}
|
|
127
|
+
|
|
114
128
|
{% comment %} Category or collection badge (most specific category wins) {% endcomment %}
|
|
115
129
|
{% assign intro_category = nil %}
|
|
116
130
|
{% assign intro_category_url = nil %}
|
|
@@ -129,7 +143,9 @@
|
|
|
129
143
|
{% capture intro_category_url %}{{ site.baseurl }}/docs/{{ intro_category_slug }}/{% endcapture %}
|
|
130
144
|
{% else %}
|
|
131
145
|
{% assign intro_category_slug = intro_category | append: '' | slugify %}
|
|
132
|
-
{%
|
|
146
|
+
{% if intro_tags_linkable %}
|
|
147
|
+
{% capture intro_category_url %}{{ intro_tags_base | relative_url }}#{{ intro_category_slug }}{% endcapture %}
|
|
148
|
+
{% endif %}
|
|
133
149
|
{% endif %}
|
|
134
150
|
{% elsif page.collection and page.collection != "pages" %}
|
|
135
151
|
{% assign intro_category = page.collection %}
|
|
@@ -217,7 +233,11 @@
|
|
|
217
233
|
<span class="bd-intro-tag-list">
|
|
218
234
|
{% for tag in page.tags limit: 5 %}
|
|
219
235
|
{% assign intro_tag_label = tag | append: '' %}
|
|
220
|
-
|
|
236
|
+
{% if intro_tags_linkable %}
|
|
237
|
+
<a class="bd-intro-badge bd-intro-badge--tag" href="{{ intro_tags_base | relative_url }}#{{ intro_tag_label | slugify }}">{{ intro_tag_label }}</a>
|
|
238
|
+
{% else %}
|
|
239
|
+
<span class="bd-intro-badge bd-intro-badge--tag">{{ intro_tag_label }}</span>
|
|
240
|
+
{% endif %}
|
|
221
241
|
{% endfor %}
|
|
222
242
|
{% if page.tags.size > 5 %}
|
|
223
243
|
<span class="bd-intro-badge bd-intro-badge--tag bd-intro-badge--more">+{{ page.tags.size | minus: 5 }}</span>
|
|
@@ -211,11 +211,20 @@
|
|
|
211
211
|
<tbody id="sitemapTableBody">
|
|
212
212
|
{% assign total_pages = 0 %}
|
|
213
213
|
{% assign collections_data = '' %}
|
|
214
|
+
{% comment %}
|
|
215
|
+
Only the first page of rows is rendered as HTML, which is enough
|
|
216
|
+
for no-JS visitors and first paint. Every row (these included) is
|
|
217
|
+
also in the compact #sitemapData JSON below, and the script builds
|
|
218
|
+
the other rows from it when they are needed. With 431 rows on
|
|
219
|
+
it-journey.dev, rendering them all made /search/ 2.2 MB of HTML.
|
|
220
|
+
{% endcomment %}
|
|
221
|
+
{% assign sitemap_server_rows = 50 %}
|
|
214
222
|
|
|
215
223
|
{% comment %}Loose Jekyll pages (site.pages in Liquid is the "pages" collection, not these files){% endcomment %}
|
|
216
224
|
{% for page in site.html_pages %}
|
|
217
225
|
{% unless page.sitemap == false %}
|
|
218
226
|
{% assign total_pages = total_pages | plus: 1 %}
|
|
227
|
+
{% if total_pages <= sitemap_server_rows %}
|
|
219
228
|
<tr data-collection="pages"
|
|
220
229
|
data-title="{{ page.title | default: page.name | escape }}"
|
|
221
230
|
data-description="{{ page.description | escape }}"
|
|
@@ -281,6 +290,7 @@
|
|
|
281
290
|
</div>
|
|
282
291
|
</td>
|
|
283
292
|
</tr>
|
|
293
|
+
{% endif %}
|
|
284
294
|
{% endunless %}
|
|
285
295
|
{% endfor %}
|
|
286
296
|
|
|
@@ -289,6 +299,7 @@
|
|
|
289
299
|
{% for item in collection.docs %}
|
|
290
300
|
{% unless item.sitemap == false %}
|
|
291
301
|
{% assign total_pages = total_pages | plus: 1 %}
|
|
302
|
+
{% if total_pages <= sitemap_server_rows %}
|
|
292
303
|
<tr data-collection="{{ collection.label }}"
|
|
293
304
|
data-title="{{ item.title | escape }}"
|
|
294
305
|
data-description="{{ item.description | escape }}"
|
|
@@ -354,6 +365,7 @@
|
|
|
354
365
|
</div>
|
|
355
366
|
</td>
|
|
356
367
|
</tr>
|
|
368
|
+
{% endif %}
|
|
357
369
|
{% endunless %}
|
|
358
370
|
{% endfor %}
|
|
359
371
|
{% endfor %}
|
|
@@ -365,6 +377,43 @@
|
|
|
365
377
|
</div>
|
|
366
378
|
</div>
|
|
367
379
|
|
|
380
|
+
{% comment %}
|
|
381
|
+
Row data for the whole index, one compact entry per row in the same order
|
|
382
|
+
as the loops above. Keys: c collection, cl collection label, t title,
|
|
383
|
+
d description, x description cell text (description or excerpt),
|
|
384
|
+
cat/tag arrays, dt ISO date, dl display date, u url. "</" is escaped so
|
|
385
|
+
a title can't close the script element early.
|
|
386
|
+
{% endcomment %}
|
|
387
|
+
{%- capture sitemap_json -%}
|
|
388
|
+
[
|
|
389
|
+
{%- assign sm_first = true -%}
|
|
390
|
+
{%- for page in site.html_pages -%}
|
|
391
|
+
{%- unless page.sitemap == false -%}
|
|
392
|
+
{%- unless sm_first %},{% endunless -%}{%- assign sm_first = false -%}
|
|
393
|
+
{"c":"pages","cl":"Pages","t":{{ page.title | default: page.name | jsonify }},"d":{{ page.description | default: "" | jsonify }},"x":{{ page.description | default: "No description available" | jsonify }},"cat":{{ page.categories | default: empty | jsonify }},"tag":{{ page.tags | default: empty | jsonify }},"dt":{{ page.lastmod | default: page.date | date: '%Y-%m-%d' | jsonify }},"dl":{{ page.lastmod | default: page.date | date: "%b %d, %Y" | default: "Unknown" | jsonify }},"u":{{ page.url | relative_url | jsonify }}}
|
|
394
|
+
{%- endunless -%}
|
|
395
|
+
{%- endfor -%}
|
|
396
|
+
{%- for collection in site.collections -%}
|
|
397
|
+
{%- assign sm_label = collection.label | capitalize -%}
|
|
398
|
+
{%- for item in collection.docs -%}
|
|
399
|
+
{%- unless item.sitemap == false -%}
|
|
400
|
+
{%- unless sm_first %},{% endunless -%}{%- assign sm_first = false -%}
|
|
401
|
+
{"c":{{ collection.label | jsonify }},"cl":{{ sm_label | jsonify }},"t":{{ item.title | default: "" | jsonify }},"d":{{ item.description | default: "" | jsonify }},"x":{{ item.description | default: item.excerpt | strip_html | default: "No description available" | jsonify }},"cat":{{ item.categories | default: empty | jsonify }},"tag":{{ item.tags | default: empty | jsonify }},"dt":{{ item.lastmod | default: item.date | date: '%Y-%m-%d' | jsonify }},"dl":{{ item.lastmod | default: item.date | date: "%b %d, %Y" | default: "Unknown" | jsonify }},"u":{{ item.url | relative_url | jsonify }}}
|
|
402
|
+
{%- endunless -%}
|
|
403
|
+
{%- endfor -%}
|
|
404
|
+
{%- endfor -%}
|
|
405
|
+
]
|
|
406
|
+
{%- endcapture -%}
|
|
407
|
+
<script type="application/json" id="sitemapData" data-site-url="{{ site.url }}" data-server-rows="{{ sitemap_server_rows }}">{{ sitemap_json | replace: "</", "<\/" }}</script>
|
|
408
|
+
{%- if total_pages > sitemap_server_rows %}
|
|
409
|
+
<noscript>
|
|
410
|
+
<p class="text-center text-muted mt-3">
|
|
411
|
+
Showing the first {{ sitemap_server_rows }} of {{ total_pages }} pages. Turn on JavaScript to search and page through all of them
|
|
412
|
+
{%- if site.plugins contains 'jekyll-sitemap' or site.gems contains 'jekyll-sitemap' %}, or browse the full list in the <a href="{{ '/sitemap.xml' | relative_url }}">XML sitemap</a>{% endif %}.
|
|
413
|
+
</p>
|
|
414
|
+
</noscript>
|
|
415
|
+
{%- endif %}
|
|
416
|
+
|
|
368
417
|
{% comment %} ================================ {% endcomment %}
|
|
369
418
|
{% comment %} CARDS VIEW CONTAINER {% endcomment %}
|
|
370
419
|
{% comment %} ================================ {% endcomment %}
|
|
@@ -474,13 +523,17 @@ th:hover .sort-icon {
|
|
|
474
523
|
* Enhanced Sitemap Functionality
|
|
475
524
|
* Features: Advanced search, sorting, filtering, statistics, and card/table views
|
|
476
525
|
*
|
|
477
|
-
* Paginated:
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
* the first
|
|
482
|
-
*
|
|
483
|
-
*
|
|
526
|
+
* Paginated: only PAGE_SIZE matching rows are in the document at a time,
|
|
527
|
+
* and a "Show more" button appends the next page. Cards are built on the
|
|
528
|
+
* first switch to card view, one page at a time.
|
|
529
|
+
*
|
|
530
|
+
* Only the first page of rows is rendered server-side, which is enough for
|
|
531
|
+
* no-JS visitors and first paint. Every row is described by a compact entry
|
|
532
|
+
* in #sitemapData, and this script builds the other <tr> elements from it
|
|
533
|
+
* the first time they are shown or sorted. Search and filters run on the
|
|
534
|
+
* entries, so ?q= from the search modal's "View all results" still covers
|
|
535
|
+
* the whole index. On it-journey.dev the full server-rendered table (431
|
|
536
|
+
* rows) made /search/ about 2.2 MB of HTML.
|
|
484
537
|
*/
|
|
485
538
|
class SitemapManager {
|
|
486
539
|
static PAGE_SIZE = 50;
|
|
@@ -503,7 +556,22 @@ class SitemapManager {
|
|
|
503
556
|
this.showMoreWrap = document.getElementById('sitemapShowMore');
|
|
504
557
|
this.showMoreBtn = document.getElementById('sitemapShowMoreBtn');
|
|
505
558
|
|
|
506
|
-
|
|
559
|
+
// One entry per index row. `dataset` mirrors the <tr data-*> attributes
|
|
560
|
+
// so search, filters, stats and cards read entries and rows alike.
|
|
561
|
+
// The first rows reuse the server-rendered <tr>; the rest are built
|
|
562
|
+
// on demand by rowFor().
|
|
563
|
+
const serverRows = Array.from(this.tbody.querySelectorAll('tr'));
|
|
564
|
+
const dataEl = document.getElementById('sitemapData');
|
|
565
|
+
let data = [];
|
|
566
|
+
try {
|
|
567
|
+
data = dataEl ? JSON.parse(dataEl.textContent) : [];
|
|
568
|
+
} catch (error) {
|
|
569
|
+
console.warn('Sitemap: could not read #sitemapData', error);
|
|
570
|
+
}
|
|
571
|
+
this.siteUrl = dataEl ? (dataEl.dataset.siteUrl || '') : '';
|
|
572
|
+
this.rows = data.length
|
|
573
|
+
? data.map((e, i) => this.entryFrom(e, serverRows[i] || null))
|
|
574
|
+
: serverRows.map(tr => ({ dataset: tr.dataset, row: tr }));
|
|
507
575
|
this.visibleCount = document.getElementById('visibleCount');
|
|
508
576
|
this.totalCount = document.getElementById('totalCount');
|
|
509
577
|
|
|
@@ -517,6 +585,91 @@ class SitemapManager {
|
|
|
517
585
|
this.init();
|
|
518
586
|
}
|
|
519
587
|
|
|
588
|
+
// Normalise one #sitemapData entry; `row` is its server-rendered <tr>, if any.
|
|
589
|
+
entryFrom(e, row) {
|
|
590
|
+
const list = v => Array.isArray(v) ? v.map(String) : (v ? [String(v)] : []);
|
|
591
|
+
const categories = list(e.cat);
|
|
592
|
+
const tags = list(e.tag);
|
|
593
|
+
return {
|
|
594
|
+
row,
|
|
595
|
+
raw: e,
|
|
596
|
+
categoriesList: categories,
|
|
597
|
+
tagsList: tags,
|
|
598
|
+
dataset: {
|
|
599
|
+
collection: e.c || '',
|
|
600
|
+
title: e.t || '',
|
|
601
|
+
description: e.d || '',
|
|
602
|
+
categories: categories.join(', '),
|
|
603
|
+
tags: tags.join(', '),
|
|
604
|
+
date: e.dt || '',
|
|
605
|
+
url: e.u || ''
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Liquid's `truncate`: at most n characters including the "..." suffix.
|
|
611
|
+
static truncate(text, n) {
|
|
612
|
+
const chars = Array.from(text || '');
|
|
613
|
+
return chars.length > n ? chars.slice(0, n - 3).join('') + '...' : chars.join('');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// The <tr> for an entry: the server-rendered one, or built once with
|
|
617
|
+
// the same markup the include renders (text set via textContent).
|
|
618
|
+
rowFor(entry) {
|
|
619
|
+
if (entry.row) return entry.row;
|
|
620
|
+
const e = entry.raw;
|
|
621
|
+
const d = entry.dataset;
|
|
622
|
+
const tr = document.createElement('tr');
|
|
623
|
+
Object.entries(d).forEach(([k, v]) => { tr.dataset[k] = v; });
|
|
624
|
+
const isPage = d.collection === 'pages';
|
|
625
|
+
tr.innerHTML = `
|
|
626
|
+
<td><span class="badge ${isPage ? 'text-bg-primary' : 'text-bg-info'} rounded-pill"></span></td>
|
|
627
|
+
<td><a class="text-decoration-none fw-medium"></a></td>
|
|
628
|
+
<td class="d-none d-md-table-cell"><small class="text-muted"></small></td>
|
|
629
|
+
<td><small class="text-muted"></small></td>
|
|
630
|
+
<td class="d-none d-lg-table-cell"></td>
|
|
631
|
+
<td class="d-none d-xl-table-cell"></td>
|
|
632
|
+
<td class="text-center">
|
|
633
|
+
<div class="btn-group btn-group-sm" role="group">
|
|
634
|
+
<a class="btn btn-outline-primary btn-sm" title="View page"><i class="bi bi-eye"></i></a>
|
|
635
|
+
<button class="btn btn-outline-secondary btn-sm copy-url" title="Copy URL"><i class="bi bi-clipboard"></i></button>
|
|
636
|
+
</div>
|
|
637
|
+
</td>`;
|
|
638
|
+
const cells = tr.children;
|
|
639
|
+
cells[0].firstElementChild.textContent = e.cl || d.collection;
|
|
640
|
+
const link = cells[1].firstElementChild;
|
|
641
|
+
link.href = d.url;
|
|
642
|
+
link.textContent = SitemapManager.truncate(d.title, 50);
|
|
643
|
+
if (d.description) {
|
|
644
|
+
const small = document.createElement('small');
|
|
645
|
+
small.className = 'text-muted d-md-none';
|
|
646
|
+
small.textContent = SitemapManager.truncate(d.description, 80);
|
|
647
|
+
cells[1].append(document.createElement('br'), small);
|
|
648
|
+
}
|
|
649
|
+
cells[2].firstElementChild.textContent = SitemapManager.truncate(e.x || 'No description available', 120);
|
|
650
|
+
cells[3].firstElementChild.textContent = e.dl || 'Unknown';
|
|
651
|
+
const chips = (cell, items, max, cls, prefix) => {
|
|
652
|
+
items.slice(0, max).forEach(item => {
|
|
653
|
+
const b = document.createElement('span');
|
|
654
|
+
b.className = `badge ${cls} me-1`;
|
|
655
|
+
b.textContent = prefix + item;
|
|
656
|
+
cell.append(b, ' ');
|
|
657
|
+
});
|
|
658
|
+
if (items.length > max) {
|
|
659
|
+
const more = document.createElement('span');
|
|
660
|
+
more.className = 'badge text-body-secondary border';
|
|
661
|
+
more.textContent = `+${items.length - max}`;
|
|
662
|
+
cell.append(more);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
chips(cells[4], entry.categoriesList, 2, 'bg-secondary', '');
|
|
666
|
+
chips(cells[5], entry.tagsList, 3, 'bg-outline-primary', '#');
|
|
667
|
+
cells[6].querySelector('a').href = d.url;
|
|
668
|
+
cells[6].querySelector('.copy-url').dataset.url = this.siteUrl + d.url;
|
|
669
|
+
entry.row = tr;
|
|
670
|
+
return tr;
|
|
671
|
+
}
|
|
672
|
+
|
|
520
673
|
init() {
|
|
521
674
|
this.setupCollectionFilter();
|
|
522
675
|
this.updateStatistics();
|
|
@@ -651,11 +804,11 @@ class SitemapManager {
|
|
|
651
804
|
header.addEventListener('click', () => this.sortTable(header));
|
|
652
805
|
});
|
|
653
806
|
|
|
654
|
-
// Copy URL functionality (table rows
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
});
|
|
807
|
+
// Copy URL functionality (table rows, delegated so rows built later
|
|
808
|
+
// work too; cards bind their own)
|
|
809
|
+
this.tbody.addEventListener('click', (e) => {
|
|
810
|
+
const btn = e.target.closest('.copy-url');
|
|
811
|
+
if (btn && this.tbody.contains(btn)) this.copyUrl({ currentTarget: btn });
|
|
659
812
|
});
|
|
660
813
|
|
|
661
814
|
// Keyboard shortcuts
|
|
@@ -729,7 +882,7 @@ class SitemapManager {
|
|
|
729
882
|
render() {
|
|
730
883
|
const shown = this.matches.slice(0, this.limit);
|
|
731
884
|
if (this.currentView === 'table') {
|
|
732
|
-
this.tbody.replaceChildren(...shown);
|
|
885
|
+
this.tbody.replaceChildren(...shown.map(entry => this.rowFor(entry)));
|
|
733
886
|
} else {
|
|
734
887
|
this.cardsContainer.replaceChildren(...shown.map(row => this.cardFor(row)));
|
|
735
888
|
}
|
|
@@ -760,10 +913,13 @@ class SitemapManager {
|
|
|
760
913
|
const icon = header.querySelector('.sort-icon');
|
|
761
914
|
icon.className = `sort-icon bi bi-arrow-${newOrder === 'asc' ? 'up' : 'down'} ms-1 active`;
|
|
762
915
|
|
|
763
|
-
// Sort every row (not just the rendered page), then re-render
|
|
916
|
+
// Sort every row (not just the rendered page), then re-render. Keys
|
|
917
|
+
// come from each row's cell text, so rows not built yet are built
|
|
918
|
+
// here (once, on the visitor's click).
|
|
919
|
+
const keys = new Map(this.rows.map(entry => [entry, this.rowFor(entry).children[headerIndex].textContent.trim()]));
|
|
764
920
|
this.rows.sort((a, b) => {
|
|
765
|
-
let aValue =
|
|
766
|
-
let bValue =
|
|
921
|
+
let aValue = keys.get(a);
|
|
922
|
+
let bValue = keys.get(b);
|
|
767
923
|
|
|
768
924
|
if (type === 'date') {
|
|
769
925
|
aValue = aValue === "Unknown" ? new Date(0) : new Date(aValue);
|