Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,25 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install webp encoder
# _plugins/webp_generator.rb shells out to cwebp to write .webp
# siblings of every PNG/JPEG into the deployed _site. The plugin
# no-ops gracefully if cwebp is missing, but production should ship
# the WebP siblings so future <picture> markup can use them.
run: sudo apt-get update -qq && sudo apt-get install -y -qq webp
- name: Setup Ruby
uses: ruby/setup-ruby@086ffb1a2090c870a3f881cc91ea83aa4243d408 # v1.195.0
with:
ruby-version: '3.1' # Not needed with a .ruby-version file
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
cache-version: 0 # Increment this number if you need to re-download cached gems
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install npm deps
run: npm ci
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
Expand All @@ -54,6 +67,10 @@ jobs:
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
env:
JEKYLL_ENV: production
- name: Generate Pagefind search index
# Pagefind crawls the built _site/ and writes /_site/pagefind/* — the
# static UI bundle + WASM index that the header search input loads.
run: npx pagefind --site _site
- name: Upload Pages artifact
# Automatically uploads an artifact from the './_site' directory by default
uses: actions/upload-pages-artifact@v3
Expand Down Expand Up @@ -159,6 +176,10 @@ jobs:
JEKYLL_ENV: production
- name: Install npm deps
run: npm ci
- name: Generate Pagefind search index
# lhci serves _site/, so the search bundle must exist there or the
# header search input would 404 on every URL it audits.
run: npx pagefind --site _site
- name: Run Lighthouse CI
id: lhci
# The Playwright image ships chromium at /ms-playwright/chromium-*/...
Expand Down
11 changes: 11 additions & 0 deletions _includes/_consent-banner.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{%- if site.google_analytics -%}
<div id="consent-banner" class="consent-banner" hidden role="region" aria-label="Cookie consent">
<p class="consent-banner__copy">
This site uses Google Analytics to understand which posts are useful. No ads, no tracking across other sites. You can decline and the rest of the page works exactly the same.
</p>
<div class="consent-banner__actions">
<button type="button" id="consent-decline" class="consent-banner__button consent-banner__button--decline">Decline</button>
<button type="button" id="consent-accept" class="consent-banner__button consent-banner__button--accept">Accept analytics</button>
</div>
</div>
{%- endif -%}
32 changes: 29 additions & 3 deletions _includes/head-custom-google-analytics.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
<script async src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics }}"></script>
{%- comment -%}
Consent Mode v2: every signal starts denied. assets/js/consent.js flips them
to 'granted' when the user accepts the banner, and persists the choice in
localStorage so we don't re-prompt on subsequent visits. If the user already
accepted in a previous visit, we restore their consent here BEFORE gtag.js
fetches, so the very first pageview after page load is recorded.
{%- endcomment -%}
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied',
'wait_for_update': 500
});

try {
if (localStorage.getItem('analytics_consent') === 'granted') {
gtag('consent', 'update', {
'ad_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted',
'analytics_storage': 'granted'
});
}
} catch (e) { /* localStorage unavailable — stay denied */ }

gtag('js', new Date());
gtag('config', '{{ site.google_analytics }}');
</script>
</script>
<script async src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics }}"></script>
10 changes: 9 additions & 1 deletion _includes/head-custom.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,15 @@
"name": "Szołkowski's Blog",
"description": {{ site.description | jsonify }},
"publisher": { "@id": "{{ person_id }}" },
"inLanguage": "en-US"
"inLanguage": "en-US",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "{{ site.url }}/?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
},
{
"@type": "Person",
Expand Down
21 changes: 21 additions & 0 deletions _layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@
<!-- Font Awesome CDN -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha384-/o6I2CkkWC//PSjvWC/eYN7l3xM3tJm8ZzVkCOfp//W05QcE3mlGskpoHB6XqI+B" crossorigin="anonymous">

<!-- Pagefind search (header input mounts itself in #search via assets/js/search.js) -->
<link rel="stylesheet" href="{{ '/pagefind/pagefind-ui.css' | relative_url }}">
<script defer src="{{ '/pagefind/pagefind-ui.js' | relative_url }}"></script>
<script defer src="{{ '/assets/js/search.js' | relative_url }}"></script>

<!-- custom-styles.css MUST load after pagefind-ui.css so our `.sidebar-search`
overrides (icon centring, placeholder color, border reset) win on
equal specificity. -->
<link rel="stylesheet" href="{{ '/assets/css/custom-styles.css' | relative_url }}">

{%- comment -%}
Expand All @@ -48,11 +56,19 @@
{% include _header.html %}
<div id="content-wrapper">
<div class="inner clearfix">
<!-- Mobile-only search slot — hidden via CSS on desktop. The sidebar
instance below stacks under main content on small viewports, so
we render the search a second time at the top so it's reachable
without scrolling past every post. assets/js/search.js mounts a
separate Pagefind UI per `.pagefind-search-mount`. -->
<div class="pagefind-search-mount mobile-search" role="search" aria-label="Site search"></div>

<main id="main-content">
{{ content }}
</main>

<aside id="sidebar">
<div class="pagefind-search-mount sidebar-search" role="search" aria-label="Site search"></div>
<p class="repo-owner">Tags</p>
{% include _all-tags.html %}
{% assign tags = all_tag_names | sort %}
Expand All @@ -63,9 +79,14 @@
</div>
{% include _footer.html %}

{% include _consent-banner.html %}

{%- if page.layout == "post" -%}
<script defer src="{{ '/assets/js/code-modal.js' | relative_url }}"></script>
{%- endif -%}
{%- if site.google_analytics -%}
<script defer src="{{ '/assets/js/consent.js' | relative_url }}"></script>
{%- endif -%}

</body>
</html>
35 changes: 35 additions & 0 deletions _plugins/pagefind_runner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require 'open3'

module Jekyll
# Runs `pagefind --site <dest>` after every build so the header search
# works without a separate `npm run build` step. Mirrors the
# WebpGenerator pattern: hooks :site, :post_write, shells out to a
# native binary, no-ops gracefully if the binary isn't present.
#
# We invoke the platform-specific native binary directly (e.g.
# `node_modules/@pagefind/darwin-arm64/bin/pagefind_extended`) instead of
# the npm wrapper at `node_modules/.bin/pagefind`. The wrapper is a Node
# script — running it via Jekyll's shellout picks up whatever `node` is
# first on PATH, which on this machine resolves to the macOS system Node
# (darwin-x64) and fails to find a darwin-x64 prebuild. Native binary
# has no such ambiguity.
Jekyll::Hooks.register :site, :post_write do |site|
next unless File.directory?(site.dest)

bin = Dir.glob(File.join(site.source, 'node_modules', '@pagefind', '*', 'bin', '*'))
.reject { |p| p.end_with?('.sha256') }
.find { |p| File.executable?(p) && !File.directory?(p) }

unless bin
Jekyll.logger.warn 'PagefindRunner:', 'no pagefind binary in node_modules/@pagefind/*/bin — run `npm install`. Header search will be empty.'
next
end

_out, status = Open3.capture2e(bin, '--site', site.dest, '--quiet')
if status.success?
Jekyll.logger.info 'PagefindRunner:', "indexed #{site.dest}/pagefind/"
else
Jekyll.logger.warn 'PagefindRunner:', "#{File.basename(bin)} exited non-zero (header search will be empty)"
end
end
end
45 changes: 45 additions & 0 deletions _plugins/webp_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require 'open3'

module Jekyll
# Generates .webp siblings for every .png / .jpg / .jpeg in the rendered
# site under assets/img/. Runs on :site, :post_write so it sees the
# final destination paths Jekyll just copied — no source-tree pollution,
# no clash with the existing pngquant'd PNGs.
#
# Requires the `cwebp` binary (Google's libwebp encoder):
# - macOS: brew install webp
# - Ubuntu: apt-get install -y webp
#
# If cwebp isn't on PATH the plugin warns once and skips. WebP siblings are
# still useful to have on disk — `<picture>` markup that prefers them is a
# follow-up, kept out of this PR per the SEO/OG validator caveat.
Jekyll::Hooks.register :site, :post_write do |site|
img_dir = File.join(site.dest, 'assets', 'img')
next unless File.directory?(img_dir)

unless system('which cwebp > /dev/null 2>&1')
Jekyll.logger.warn 'WebpGenerator:', 'cwebp not found on PATH — skipping WebP generation'
next
end

converted = 0
skipped = 0
# FNM_CASEFOLD so we match .PNG/.JPG too without double-counting on
# case-insensitive filesystems (macOS default APFS).
Dir.glob(File.join(img_dir, '*.{png,jpg,jpeg}'), File::FNM_CASEFOLD).each do |src|
webp = src.sub(/\.(png|jpe?g)\z/i, '.webp')
if File.exist?(webp) && File.mtime(webp) >= File.mtime(src)
skipped += 1
next
end
_, status = Open3.capture2e('cwebp', '-quiet', '-q', '80', src, '-o', webp)
if status.success?
converted += 1
else
Jekyll.logger.warn 'WebpGenerator:', "cwebp failed on #{File.basename(src)}"
end
end

Jekyll.logger.info 'WebpGenerator:', "converted #{converted}, skipped #{skipped} (already up to date)"
end
end
Loading
Loading