Add GDPR-compliant cookie consent with analytics blocking (#3492)

Implements cookie consent using vanilla-cookieconsent v3 to comply with
GDPR requirements. Analytics scripts are blocked until user consent is
obtained.

## Changes

**Library Integration**
- Added `vanilla-cookieconsent@3.1.0` to `_config.yml` third-party
libraries with SRI hashes
- Created `_scripts/cookie-consent-setup.js` with consent modal and
settings configuration
- Added CSS/JS includes in `_includes/head.liquid` and
`_includes/scripts.liquid`

**Analytics Blocking**
- Modified analytics scripts in `scripts.liquid` and
`distill_scripts.liquid` to use conditional `type="text/plain"
data-category="analytics"` when consent is disabled
- Blocks Google Analytics, Cronitor, Pirsch, and OpenPanel until consent
granted
- Library automatically converts blocked scripts to executable on user
acceptance

**Configuration**
- Added `enable_cookie_consent` flag (default: `false`)
- Cookie categories: `necessary` (always on), `analytics` (optional)
- 182-day cookie expiration, auto-clear on rejection

## Implementation

```liquid
{% if site.enable_cookie_consent %}
  <script type="text/plain" data-category="analytics" async src="...gtag.js"></script>
{% else %}
  <script async src="...gtag.js"></script>
{% endif %}
```

Enable in `_config.yml`:
```yaml
enable_cookie_consent: true
```

## Screenshots

**Consent Modal**

![Cookie consent
modal](https://github.com/user-attachments/assets/9edcddad-6a54-49ca-a164-083157d98370)

**Settings Modal**

![Cookie
preferences](https://github.com/user-attachments/assets/cf2a58ea-68c0-4699-b401-38377b98b718)

> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `cdn.jsdelivr.net`
>   - Triggering command: `/usr/bin/curl curl -s REDACTED` (dns block)
> - `cookieconsent.orestbida.com`
> - Triggering command:
`/home/REDACTED/work/_temp/ghcca-node/node/bin/node
/home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps
/home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js`
(dns block)
> - `medium.com`
> - Triggering command: `/usr/local/bin/ruby ruby
/usr/local/bundle/bin/bundle exec jekyll serve --watch --port=8080
--host=0.0.0.0 --livereload --verbose --trace --force_polling s
6_64-linux /etc/locale.gen by/backward -binutils_2.44-3grep .deb eb
/systemd-sysctl` (dns block)
> - `publish.twitter.com`
> - Triggering command: `/usr/local/bin/ruby ruby
/usr/local/bundle/bin/bundle exec jekyll serve --watch --port=8080
--host=0.0.0.0 --livereload --verbose --trace --force_polling lude
lude/ruby-4.0.0//run/containerd/io.containerd.runtime.v2.task/moby/a4760212ce4eda97e32b6dfa4d3ea/usr/sbin/iptables
r/runc-log.json f8_range lude r-strong E r/wc7in709a54d7hshow` (dns
block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/alshedivat/al-folio/settings/copilot/coding_agent)
(admins only)
>
> </details>

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>GDPR compliant cookie consent dialog</issue_title>
> <issue_description>Hi,
> 
> since I am from Europe and want my site to be accessed by EU citizens
I need to ask users if they are ok with the use of cookies. Adding a
dialogue by default that that asks the user before any cookies /
localStorage are used would be nice.
> 
> I have integrated the dialog from
[https://www.cookieconsent.com/](https://www.cookieconsent.com/) on [my
website](http://hschwane.github.io) and am happy to file a PR if you are
interested. However It is not shown directly at the top, but below the
menu bar for some reason. Not sure how to fix that.
> </issue_description>
> 
> <agent_instructions>Implement this using cookie consent dialog from
https://github.com/orestbida/cookieconsent. The documentation is
available at
https://cookieconsent.orestbida.com/essential/getting-started.html.</agent_instructions>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> <comment_new><author>@alshedivat</author><body>
> @hschwane, thanks for pointing this out. It's would be great if you
could send a PR.
> 
> To answer your question about positioning, the menu bar has the
absolute position at the top of the page and keeps moving with
scrolling. You can change that behavior in `_config.yml` by setting
`navbar_fixed: false` (see [line
27](https://github.com/alshedivat/al-folio/blob/master/_config.yml#L27)).
If you send a PR with basic functionality, I can help with adjusting the
styles a bit. Thanks!</body></comment_new>
> <comment_new><author>@alshedivat</author><body>
> Just catching up on this discussion.
> 
> @hschwane, thanks for providing an implementation based on
`ihavecookies`! I looked through alshedivat/al-folio#223. It seems like
to be able to correctly (un)block different types of cookies (all of
which are third-party cookies) requires adding quite a bit of extra
JavaScript (including adding it directly to posts or pages in some
cases, e.g., when posts embed youtube videos or other external content).
This approach will put some extra burden on contributors and maintainers
of al-folio down the line.
> 
> By the way, the dialog from https://www.cookieconsent.com/ is similar
in that sense as it requires to manually add ALL scripts that use
cookies. So, if someone contributes a new feature to al-folio that uses
cookies, the contributor would have to go and update the cookie consent
dialog as well (or edit quite a bit of JS if `ihavecookies` is used).
> 
> Thinking about this more, I currently see only three options:
> 1. If we want to have cookie consent as a core part of al-folio (which
can be easily enabled or disabled), it needs to be a service that
automatically identifies and blocks cookies, without putting an extra
burden on the contributors or maintainers. The hosted version of [Cookie
Consent by Osano](https://www.osano.com/features/consent-management) is
an example.
> 2. Another option is to provide a very minimalistic cookie consent
dialog which simply says that the website uses cookies, without an
option to opt-in or opt-out (e.g., see the open-source edition of
[Osano](https://www.osano.com/cookieconsent/download/)).
> 3. Finally, not have cookie consent as part of al-folio, and make the
user responsible for figuring out whether they need a consent dialog and
which service to use.
> 
> Let me know what you think.</body></comment_new>
> <comment_new><author>@alshedivat</author><body>
> the best way to add GDPR compliant cookie consent is using
https://github.com/orestbida/cookieconsent</body></comment_new>
> </comments>
> 


</details>


> **Custom agent used: customization_agent**
> Expert customization assistant for the al-folio Jekyll academic
website template



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes alshedivat/al-folio#199

<!-- START COPILOT CODING AGENT TIPS -->
---

 Let Copilot coding agent [set things up for
you](https://github.com/alshedivat/al-folio/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.

---------

Signed-off-by: George Araújo <george.gcac@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: george-gca <31376482+george-gca@users.noreply.github.com>
Co-authored-by: George Araújo <george.gcac@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot 2026-02-04 10:16:34 -03:00 committed by GitHub
parent dae018f6d2
commit 14136f6592
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 582 additions and 59 deletions

View File

@ -54,6 +54,15 @@ Here we will give you some tips on how to customize the website. One important t
- [Adding social media information](#adding-social-media-information) - [Adding social media information](#adding-social-media-information)
- [Adding a newsletter](#adding-a-newsletter) - [Adding a newsletter](#adding-a-newsletter)
- [Configuring search features](#configuring-search-features) - [Configuring search features](#configuring-search-features)
- [Social media previews](#social-media-previews)
- [How to enable](#how-to-enable)
- [Configuring preview images](#configuring-preview-images)
- [Preview image best practices](#preview-image-best-practices)
- [Related posts](#related-posts)
- [How it works](#how-it-works)
- [Configuration](#configuration-1)
- [Disable related posts for a specific post](#disable-related-posts-for-a-specific-post)
- [Additional configuration in \_config.yml](#additional-configuration-in-_configyml)
- [Managing publication display](#managing-publication-display) - [Managing publication display](#managing-publication-display)
- [Adding a Google Calendar](#adding-a-google-calendar) - [Adding a Google Calendar](#adding-a-google-calendar)
- [Basic usage](#basic-usage) - [Basic usage](#basic-usage)
@ -73,6 +82,14 @@ Here we will give you some tips on how to customize the website. One important t
- [Scheduled Posts](#scheduled-posts) - [Scheduled Posts](#scheduled-posts)
- [Name Format](#name-format) - [Name Format](#name-format)
- [Important Notes](#important-notes) - [Important Notes](#important-notes)
- [GDPR Cookie Consent Dialog](#gdpr-cookie-consent-dialog)
- [How it works](#how-it-works-1)
- [When to use](#when-to-use)
- [How to enable](#how-to-enable-1)
- [Customizing the consent dialog](#customizing-the-consent-dialog)
- [Supported analytics providers](#supported-analytics-providers)
- [How it integrates with analytics](#how-it-integrates-with-analytics)
- [For developers](#for-developers)
- [Setting up a Personal Access Token (PAT) for Google Scholar Citation Updates](#setting-up-a-personal-access-token-pat-for-google-scholar-citation-updates) - [Setting up a Personal Access Token (PAT) for Google Scholar Citation Updates](#setting-up-a-personal-access-token-pat-for-google-scholar-citation-updates)
- [Why is a PAT required?](#why-is-a-pat-required) - [Why is a PAT required?](#why-is-a-pat-required)
- [How to set up the PAT](#how-to-set-up-the-pat) - [How to set up the PAT](#how-to-set-up-the-pat)
@ -845,6 +862,129 @@ socials_in_search: true
All these search features work in real-time and do not require a page reload. All these search features work in real-time and do not require a page reload.
## Social media previews
**al-folio** supports Open Graph (OG) meta tags, which create rich preview objects when your pages are shared on social media platforms like Twitter, Facebook, LinkedIn, and others. These previews include your site's image, title, and description.
### How to enable
To enable social media previews:
1. Open `_config.yml` and set:
```yaml
serve_og_meta: true
```
2. Rebuild your site:
```bash
docker compose down && docker compose up
# or
bundle exec jekyll serve
```
Once enabled, all your site's pages will automatically include Open Graph meta tags in the HTML head element.
### Configuring preview images
You can configure what image displays in social media previews on a per-page or site-wide basis.
**Site-wide default image:**
Add the following to `_config.yml`:
```yaml
og_image: /assets/img/your-default-preview-image.png
```
Replace the path with your actual image location in `assets/img/`.
**Per-page custom image:**
To override the site-wide default for a specific page, add `og_image` to the page's frontmatter:
```yaml
---
layout: page
title: My Page
og_image: /assets/img/custom-preview-image.png
---
```
### Preview image best practices
- **Dimensions:** Use 1200×630 pixels for optimal display on most social media platforms
- **Format:** PNG or JPG formats work best
- **Size:** Keep file size under 5MB
- **Content:** Ensure the image clearly represents your page content
When a page is shared on social media, the platform will display your configured image along with the page title, description (from your site title or page description), and URL.
---
## Related posts
The theme can automatically display related posts at the bottom of each blog post. These are selected by finding the most recent posts that share common tags with the current post.
### How it works
- By default, the most recent posts that share at least one tag with the current post are displayed
- You can customize how many posts are shown and how many tags must match
- You can disable related posts for individual posts or across your entire site
### Configuration
To customize related posts behavior, edit the `related_blog_posts` section in `_config.yml`:
```yaml
related_blog_posts:
enabled: true
max_related: 5
```
- `enabled`: Set to `true` (default) to show related posts, or `false` to disable them site-wide
- `max_related`: Maximum number of related posts to display (default: 5)
The theme also uses tags to find related content. Make sure your blog posts include relevant tags in their frontmatter:
```yaml
---
layout: post
title: My Blog Post
tags: machine-learning python
---
```
### Disable related posts for a specific post
To hide related posts on an individual blog post, add this to the post's frontmatter:
```yaml
---
layout: post
title: My Blog Post
related_posts: false
---
```
### Additional configuration in \_config.yml
You can also customize related posts behavior with these settings:
```yaml
related_blog_posts:
enabled: true
max_related: 5
```
These settings control:
- Which posts are considered "related" (based on shared tags)
- How many related posts to display
- The algorithm used to calculate post similarity (uses the `classifier-reborn` gem)
---
## Managing publication display ## Managing publication display
The theme offers several options for customizing how publications are displayed: The theme offers several options for customizing how publications are displayed:
@ -1114,6 +1254,111 @@ In this folder you need to store your file in the same format as you would in `_
- `File3.md` will not be posted at all - `File3.md` will not be posted at all
- `2026-02-31-file4.md` is supposed to be posted on 31-February-2026, but there is no 31st in February hence this file will never be posted either - `2026-02-31-file4.md` is supposed to be posted on 31-February-2026, but there is no 31st in February hence this file will never be posted either
## GDPR Cookie Consent Dialog
**al-folio** includes a built-in GDPR-compliant cookie consent dialog to help you respect visitor privacy and comply with privacy regulations (GDPR, CCPA, etc.). The feature is powered by [Vanilla Cookie Consent](https://cookieconsent.orestbida.com/) and integrates with all analytics providers.
### How it works
- A consent dialog appears on the visitor's first visit to your site
- Visitors can **accept all**, **reject all**, or **customize preferences** for analytics cookies
- Analytics scripts (Google Analytics, Cronitor, Pirsch, Openpanel) are **blocked by default** and only run after explicit consent
- Google Consent Mode ensures Google services operate in privacy mode before consent is granted
- User preferences are saved in their browser and respected on subsequent visits
- The dialog is mobile-responsive and supports multiple languages
### When to use
- ✅ **Required** if your site serves EU visitors and uses any analytics
- ✅ Recommended for any website using analytics, tracking, or marketing tools
- ❌ Not needed if your site doesn't use any analytics providers
### How to enable
1. Open `_config.yml` and locate the following line:
```yaml
enable_cookie_consent: false
```
2. Change it to:
```yaml
enable_cookie_consent: true
```
3. Rebuild your site:
```bash
docker compose down && docker compose up
# or
bundle exec jekyll serve
```
4. The consent dialog will automatically appear on your site's homepage on first visit
### Customizing the consent dialog
The consent dialog configuration and messages are defined in [`_scripts/cookie-consent-setup.js`](_scripts/cookie-consent-setup.js). You can customize:
- Dialog titles and button labels
- Cookie categories and descriptions
- Contact information links (points to `#contact` by default)
- Language translations
To modify the dialog, edit the `language.translations.en` section in `_scripts/cookie-consent-setup.js`. For example, to change the consent dialog title:
```javascript
consentModal: {
title: 'Your custom title here',
description: 'Your custom description...',
// ... other options
}
```
### Supported analytics providers
When cookie consent is enabled, these analytics providers are automatically blocked until the user consents:
- **Google Analytics (GA4)** Uses Google Consent Mode for privacy-first operation before consent
- **Cronitor RUM** Real User Monitoring for performance tracking
- **Pirsch Analytics** GDPR-compliant analytics alternative
- **Openpanel Analytics** Privacy-focused analytics platform
Each provider only collects data if:
1. It's enabled in `_config.yml` (e.g., `enable_google_analytics: true`)
2. The user has granted consent to the "analytics" category in the consent dialog
### How it integrates with analytics
When `enable_cookie_consent: true`, the template automatically:
1. Adds `type="text/plain" data-category="analytics"` to all analytics script tags
2. This tells the cookie consent library to block these scripts until consent is granted
3. Loads the consent library and initializes Google Consent Mode
4. Updates consent preferences when the user changes them in the dialog
You don't need to modify any analytics configuration—it works automatically.
### For developers
If you want to programmatically check consent status or react to consent changes, the library exposes the following:
```javascript
// Check if user has granted analytics consent
window.CookieConsent.getCategories().analytics; // returns true or false
// Listen for consent changes
window.CookieConsent.onChange(function (consentData) {
// Handle consent change
});
```
For more API details, see [Vanilla Cookie Consent documentation](https://cookieconsent.orestbida.com/).
---
## Setting up a Personal Access Token (PAT) for Google Scholar Citation Updates ## Setting up a Personal Access Token (PAT) for Google Scholar Citation Updates
> [!TIP] > [!TIP]

View File

@ -266,7 +266,7 @@ Run the test yourself: [Google Lighthouse PageSpeed Insights](https://pagespeed.
- [Collections](#collections) - [Collections](#collections)
- [Layouts](#layouts) - [Layouts](#layouts)
- [The iconic style of Distill](#the-iconic-style-of-distill) - [The iconic style of Distill](#the-iconic-style-of-distill)
- [Full support for math &amp; code](#full-support-for-math--code) - [Full support for math \& code](#full-support-for-math--code)
- [Photos, Audio, Video and more](#photos-audio-video-and-more) - [Photos, Audio, Video and more](#photos-audio-video-and-more)
- [Other features](#other-features) - [Other features](#other-features)
- [GitHub's repositories and user stats](#githubs-repositories-and-user-stats) - [GitHub's repositories and user stats](#githubs-repositories-and-user-stats)
@ -275,6 +275,7 @@ Run the test yourself: [Google Lighthouse PageSpeed Insights](https://pagespeed.
- [Atom (RSS-like) Feed](#atom-rss-like-feed) - [Atom (RSS-like) Feed](#atom-rss-like-feed)
- [Related posts](#related-posts) - [Related posts](#related-posts)
- [Code quality checks](#code-quality-checks) - [Code quality checks](#code-quality-checks)
- [GDPR Cookie Consent Dialog](#gdpr-cookie-consent-dialog)
- [FAQ](#faq) - [FAQ](#faq)
- [Contributing](#contributing) - [Contributing](#contributing)
- [Maintainers](#maintainers) - [Maintainers](#maintainers)
@ -351,15 +352,12 @@ This template has a built-in light/dark mode. It detects the user preferred colo
### CV ### CV
Your CV can be generated in one of two modern formats: Your CV can be generated in one of two modern formats: **RenderCV** (recommended, with automatic PDF generation) or **JSONResume** (standardized JSON format). You can use both simultaneously and switch between them, or maintain just the one you prefer.
- **[RenderCV](https://rendercv.com/) Format** (recommended): Edit [`_data/cv.yml`](_data/cv.yml) using the human-readable RenderCV YAML format. This format enables automatic PDF generation via GitHub Actions and provides professional styling options.
- **[JSONResume](https://jsonresume.org/) Format**: Edit [`assets/json/resume.json`](assets/json/resume.json) using the standardized JSON format. This is compatible with other resume tools and services.
You can use both formats simultaneously and switch which one is rendered on your CV page using the `cv_format` frontmatter variable, or maintain just the one you prefer. The two files are independent data sources: if you choose to keep both, you must update each file separately—there is no automatic synchronization between them.
[![CV Preview](readme_preview/cv.png)](https://alshedivat.github.io/al-folio/cv/) [![CV Preview](readme_preview/cv.png)](https://alshedivat.github.io/al-folio/cv/)
For setup and customization details, see [Modifying the CV information](CUSTOMIZE.md#modifying-the-cv-information) in [CUSTOMIZE.md](CUSTOMIZE.md).
--- ---
### People ### People
@ -372,21 +370,21 @@ You can create a people page if you want to feature more than one person. Each p
### Publications ### Publications
Your publications' page is generated automatically from your BibTex bibliography. Simply edit [\_bibliography/papers.bib](_bibliography/papers.bib). You can also add new `*.bib` files and customize the look of your publications however you like by editing [\_pages/publications.md](_pages/publications.md). By default, the publications will be sorted by year and the most recent will be displayed first. You can change this behavior and more in the `Jekyll Scholar` section in [\_config.yml](_config.yml) file. Your publications page is generated automatically from your BibTeX bibliography. You can customize publication display, add extra information like PDFs, and control sorting behavior.
You can add extra information to a publication, like a PDF file in the [assets/pdf/](assets/pdf/) directory and add the path to the PDF file in the BibTeX entry with the `pdf` field. Some of the supported fields are: `abstract`, `altmetric`, `arxiv`, `bibtex_show`, `blog`, `code`, `dimensions`, `doi`, `eprint`, `html`, `isbn`, `pdf`, `pmid`, `poster`, `slides`, `supp`, `video`, and `website`.
[![Publications Preview](readme_preview/publications.png)](https://alshedivat.github.io/al-folio/publications/) [![Publications Preview](readme_preview/publications.png)](https://alshedivat.github.io/al-folio/publications/)
For setup, BibTeX field documentation, and customization options, see [Adding a new publication](CUSTOMIZE.md#adding-a-new-publication) and [Managing publication display](CUSTOMIZE.md#managing-publication-display) in [CUSTOMIZE.md](CUSTOMIZE.md).
--- ---
### Collections ### Collections
This Jekyll theme implements `collections` to let you break up your work into categories. The theme comes with two default collections: `news` and `projects`. Items from the `news` collection are automatically displayed on the home page. Items from the `projects` collection are displayed on a responsive grid on projects page. This Jekyll theme implements `collections` to organize content into categories. The theme comes with default collections for `news`, `projects`, `books`, and `teachings`. You can easily create your own collections for apps, stories, courses, or any other creative work.
[![Projects Preview](readme_preview/projects.png)](https://alshedivat.github.io/al-folio/projects/) [![Projects Preview](readme_preview/projects.png)](https://alshedivat.github.io/al-folio/projects/)
You can easily create your own collections, apps, short stories, courses, or whatever your creative work is. To do this, edit the collections in the [\_config.yml](_config.yml) file, create a corresponding folder, and create a landing page for your collection, similar to `_pages/projects.md`. For detailed instructions on creating and customizing collections, see [Adding Collections](CUSTOMIZE.md#adding-collections) in [CUSTOMIZE.md](CUSTOMIZE.md).
--- ---
@ -427,52 +425,27 @@ Photo formatting is made simple using [Bootstrap's grid system](https://getboots
#### GitHub's repositories and user stats #### GitHub's repositories and user stats
**al-folio** uses [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) and [github-profile-trophy](https://github.com/ryo-ma/github-profile-trophy) to display GitHub repositories and user stats on the `/repositories/` page. **al-folio** displays GitHub repositories and user stats on the `/repositories/` page using [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) and [github-profile-trophy](https://github.com/ryo-ma/github-profile-trophy).
[![Repositories Preview](readme_preview/repositories.png)](https://alshedivat.github.io/al-folio/repositories/) [![Repositories Preview](readme_preview/repositories.png)](https://alshedivat.github.io/al-folio/repositories/)
Edit the `_data/repositories.yml` and change the `github_users` and `github_repos` lists to include your own GitHub profile and repositories to the `/repositories/` page. To configure which repositories and GitHub profiles to display, see [Modifying the user and repository information](CUSTOMIZE.md#modifying-the-user-and-repository-information) in [CUSTOMIZE.md](CUSTOMIZE.md).
You may also use the following codes for displaying this in any other pages.
```html
<!-- code for GitHub users -->
{% if site.data.repositories.github_users %}
<div class="repositories d-flex flex-wrap flex-md-row flex-column justify-content-between align-items-center">
{% for user in site.data.repositories.github_users %} {% include repository/repo_user.liquid username=user %} {% endfor %}
</div>
{% endif %}
<!-- code for GitHub trophies -->
{% if site.repo_trophies.enabled %} {% for user in site.data.repositories.github_users %} {% if site.data.repositories.github_users.size > 1 %}
<h4>{{ user }}</h4>
{% endif %}
<div class="repositories d-flex flex-wrap flex-md-row flex-column justify-content-between align-items-center">
{% include repository/repo_trophies.liquid username=user %}
</div>
{% endfor %} {% endif %}
<!-- code for GitHub repositories -->
{% if site.data.repositories.github_repos %}
<div class="repositories d-flex flex-wrap flex-md-row flex-column justify-content-between align-items-center">
{% for repo in site.data.repositories.github_repos %} {% include repository/repo.liquid repository=repo %} {% endfor %}
</div>
{% endif %}
```
--- ---
#### Theming #### Theming
A variety of beautiful theme colors have been selected for you to choose from. The default is purple, but you can quickly change it by editing the `--global-theme-color` variable in the `_sass/_themes.scss` file. Other color variables are listed there as well. The stock theme color options available can be found at [\_sass/\_variables.scss](_sass/_variables.scss). You can also add your own colors to this file assigning each a name for ease of use across the template. **al-folio** offers a variety of beautiful theme colors to choose from. The default is purple, but you can customize colors, fonts, spacing, and more to match your style.
For detailed customization instructions, see [Changing theme color](CUSTOMIZE.md#changing-theme-color) and [Customizing fonts, spacing, and more](CUSTOMIZE.md#customizing-fonts-spacing-and-more) in [CUSTOMIZE.md](CUSTOMIZE.md).
--- ---
#### Social media previews #### Social media previews
**al-folio** supports preview images on social media. To enable this functionality you will need to set `serve_og_meta` to `true` in your [\_config.yml](_config.yml). Once you have done so, all your site's pages will include Open Graph data in the HTML head element. **al-folio** supports Open Graph preview images on social media. When enabled, your site's pages display rich preview objects with images, titles, and descriptions when shared.
You will then need to configure what image to display in your site's social media previews. This can be configured on a per-page basis, by setting the `og_image` page variable. If for an individual page this variable is not set, then the theme will fall back to a site-wide `og_image` variable, configurable in your [\_config.yml](_config.yml). In both the page-specific and site-wide cases, the `og_image` variable needs to hold the URL for the image you wish to display in social media previews. For setup and customization, see [Social media previews](CUSTOMIZE.md#social-media-previews) in [CUSTOMIZE.md](CUSTOMIZE.md).
--- ---
@ -484,7 +457,9 @@ It generates an Atom (RSS-like) feed of your posts, useful for Atom and RSS read
#### Related posts #### Related posts
By default, there will be a related posts section on the bottom of the blog posts. These are generated by selecting the `max_related` most recent posts that share at least `min_common_tags` tags with the current post. If you do not want to display related posts on a specific post, simply add `related_posts: false` to the front matter of the post. If you want to disable it for all posts, simply set `enabled` to false in the `related_blog_posts` section in [\_config.yml](_config.yml). By default, blog posts display related posts at the bottom. These are selected by finding the most recent posts that share tags with the current post. You can customize this behavior on a per-post or site-wide basis.
For configuration details, see [Related posts](CUSTOMIZE.md#related-posts) in [CUSTOMIZE.md](CUSTOMIZE.md).
--- ---
@ -498,6 +473,16 @@ Currently, we run some checks to ensure that the code quality and generated site
We decided to keep `Axe` runs manual because fixing the issues are not straightforward and might be hard for people without web development knowledge. We decided to keep `Axe` runs manual because fixing the issues are not straightforward and might be hard for people without web development knowledge.
---
#### GDPR Cookie Consent Dialog
**al-folio** includes a built-in, GDPR-compliant cookie consent dialog to ensure your website respects visitor privacy. The dialog is powered by [Vanilla Cookie Consent](https://cookieconsent.orestbida.com/) and integrates seamlessly with all supported analytics providers.
When enabled, analytics scripts are blocked until the user explicitly consents, and user preferences are saved across visits. This is essential for websites serving visitors in the European Union and other regions with strict privacy regulations.
For complete setup and customization details, see [GDPR Cookie Consent Dialog](#gdpr-cookie-consent-dialog) in [CUSTOMIZE.md](CUSTOMIZE.md).
## FAQ ## FAQ
For frequently asked questions, please refer to [FAQ.md](FAQ.md). For frequently asked questions, please refer to [FAQ.md](FAQ.md).

View File

@ -63,8 +63,6 @@ bib_search: true
# Dimensions # Dimensions
max_width: 930px max_width: 930px
# TODO: add layout settings (single page vs. multi-page)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Open Graph & Schema.org # Open Graph & Schema.org
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -403,6 +401,7 @@ enable_pirsch_analytics: false # enables Pirsch analytics (https://pirsch.io/)
enable_openpanel_analytics: false # enables Openpanel analytics (https://openpanel.dev/) enable_openpanel_analytics: false # enables Openpanel analytics (https://openpanel.dev/)
enable_google_verification: false # enables google site verification enable_google_verification: false # enables google site verification
enable_bing_verification: false # enables bing site verification enable_bing_verification: false # enables bing site verification
enable_cookie_consent: false # enables GDPR-compliant cookie consent dialog (https://github.com/orestbida/cookieconsent)
enable_masonry: true # enables automatic project cards arrangement enable_masonry: true # enables automatic project cards arrangement
enable_math: true # enables math typesetting (uses MathJax) enable_math: true # enables math typesetting (uses MathJax)
enable_tooltips: false # enables automatic tooltip links generated for each section titles on pages and posts enable_tooltips: false # enables automatic tooltip links generated for each section titles on pages and posts
@ -609,6 +608,14 @@ third_party_libraries:
url: url:
js: "https://cdn.jsdelivr.net/npm/swiper@11.1.0/swiper-element-bundle.min.js.map" js: "https://cdn.jsdelivr.net/npm/swiper@11.1.0/swiper-element-bundle.min.js.map"
version: "11.0.5" version: "11.0.5"
vanilla-cookieconsent:
integrity:
css: "sha256-ygRrixsQlBByBZiOcJamh7JByO9fP+/l5UPtKNJmRsE="
js: "sha256-vG4vLmOB/AJbJ6awr7Wg4fxonG+fxAp4cIrbIFTvRXU="
url:
css: "https://cdn.jsdelivr.net/npm/vanilla-cookieconsent@{{version}}/dist/cookieconsent.css"
js: "https://cdn.jsdelivr.net/npm/vanilla-cookieconsent@{{version}}/dist/cookieconsent.umd.js"
version: "3.1.0"
vega: vega:
integrity: integrity:
js: "sha256-Yot/cfgMMMpFwkp/5azR20Tfkt24PFqQ6IQS+80HIZs=" js: "sha256-Yot/cfgMMMpFwkp/5azR20Tfkt24PFqQ6IQS+80HIZs="

View File

@ -189,20 +189,58 @@
<!-- Removed Pseudocode --> <!-- Removed Pseudocode -->
{% endif %} {% endif %}
{% if site.enable_cookie_consent %}
<!-- Cookie Consent -->
<script
defer
src="{{ site.third_party_libraries.vanilla-cookieconsent.url.js }}"
integrity="{{ site.third_party_libraries.vanilla-cookieconsent.integrity.js }}"
crossorigin="anonymous"
></script>
<script defer src="{{ '/assets/js/cookie-consent-setup.js' | relative_url }}"></script>
{% endif %}
{% if site.enable_google_analytics %} {% if site.enable_google_analytics %}
<!-- Analytics --> <!-- Analytics -->
<!-- Global site tag (gtag.js) - Google Analytics --> <!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics }}"></script> <script
<script defer src="{{ '/assets/js/google-analytics-setup.js' | relative_url }}"></script> {% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
async
src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics }}"
></script>
<script
{% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
defer
src="{{ '/assets/js/google-analytics-setup.js' | relative_url }}"
></script>
{% endif %} {% endif %}
{% if site.enable_cronitor_analytics %} {% if site.enable_cronitor_analytics %}
<!-- Cronitor RUM --> <!-- Cronitor RUM -->
<script async src="https://rum.cronitor.io/script.js"></script> <script
<script defer src="{{ '/assets/js/cronitor-analytics-setup.js' | relative_url }}"></script> {% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
async
src="https://rum.cronitor.io/script.js"
></script>
<script
{% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
defer
src="{{ '/assets/js/cronitor-analytics-setup.js' | relative_url }}"
></script>
{% endif %} {% endif %}
{% if site.enable_pirsch_analytics %} {% if site.enable_pirsch_analytics %}
<script <script
{% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
defer defer
src="https://api.pirsch.io/pa.js" src="https://api.pirsch.io/pa.js"
id="pianjs" id="pianjs"
@ -210,8 +248,21 @@
></script> ></script>
{% endif %} {% endif %}
{% if site.enable_openpanel_analytics %} {% if site.enable_openpanel_analytics %}
<script defer src="{{ '/assets/js/open-panel-analytics-setup.js' | relative_url }}"></script> <script
<script async defer src="https://openpanel.dev/op1.js"></script> {% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
defer
src="{{ '/assets/js/open-panel-analytics-setup.js' | relative_url }}"
></script>
<script
{% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
async
defer
src="https://openpanel.dev/op1.js"
></script>
{% endif %} {% endif %}
{% if site.enable_progressbar %} {% if site.enable_progressbar %}

View File

@ -195,3 +195,14 @@
{% if page.tikzjax %} {% if page.tikzjax %}
<link defer rel="stylesheet" type="text/css" href="{{ '/assets/css/tikzjax.min.css' | relative_url | bust_file_cache }}"> <link defer rel="stylesheet" type="text/css" href="{{ '/assets/css/tikzjax.min.css' | relative_url | bust_file_cache }}">
{% endif %} {% endif %}
{% if site.enable_cookie_consent %}
<!-- Cookie Consent -->
<link
defer
rel="stylesheet"
href="{{ site.third_party_libraries.vanilla-cookieconsent.url.css }}"
integrity="{{ site.third_party_libraries.vanilla-cookieconsent.integrity.css }}"
crossorigin="anonymous"
>
{% endif %}

View File

@ -223,20 +223,58 @@
{% endunless %} {% endunless %}
{% endif %} {% endif %}
{% if site.enable_cookie_consent %}
<!-- Cookie Consent -->
<script
defer
src="{{ site.third_party_libraries.vanilla-cookieconsent.url.js }}"
integrity="{{ site.third_party_libraries.vanilla-cookieconsent.integrity.js }}"
crossorigin="anonymous"
></script>
<script defer src="{{ '/assets/js/cookie-consent-setup.js' | relative_url }}"></script>
{% endif %}
{% if site.enable_google_analytics %} {% if site.enable_google_analytics %}
<!-- Analytics --> <!-- Analytics -->
<!-- Global site tag (gtag.js) - Google Analytics --> <!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics }}"></script> <script
<script defer src="{{ '/assets/js/google-analytics-setup.js' | relative_url }}"></script> {% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
async
src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics }}"
></script>
<script
{% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
defer
src="{{ '/assets/js/google-analytics-setup.js' | relative_url }}"
></script>
{% endif %} {% endif %}
{% if site.enable_cronitor_analytics %} {% if site.enable_cronitor_analytics %}
<!-- Cronitor RUM --> <!-- Cronitor RUM -->
<script async src="https://rum.cronitor.io/script.js"></script> <script
<script defer src="{{ '/assets/js/cronitor-analytics-setup.js' | relative_url }}"></script> {% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
async
src="https://rum.cronitor.io/script.js"
></script>
<script
{% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
defer
src="{{ '/assets/js/cronitor-analytics-setup.js' | relative_url }}"
></script>
{% endif %} {% endif %}
{% if site.enable_pirsch_analytics %} {% if site.enable_pirsch_analytics %}
<script <script
{% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
defer defer
src="https://api.pirsch.io/pa.js" src="https://api.pirsch.io/pa.js"
id="pianjs" id="pianjs"
@ -244,8 +282,21 @@
></script> ></script>
{% endif %} {% endif %}
{% if site.enable_openpanel_analytics %} {% if site.enable_openpanel_analytics %}
<script defer src="{{ '/assets/js/open-panel-analytics-setup.js' | relative_url }}"></script> <script
<script async defer src="https://openpanel.dev/op1.js"></script> {% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
defer
src="{{ '/assets/js/open-panel-analytics-setup.js' | relative_url }}"
></script>
<script
{% if site.enable_cookie_consent %}
type="text/plain" data-category="analytics"
{% endif %}
async
defer
src="https://openpanel.dev/op1.js"
></script>
{% endif %} {% endif %}
{% if site.enable_progressbar %} {% if site.enable_progressbar %}

View File

@ -0,0 +1,160 @@
---
permalink: /assets/js/cookie-consent-setup.js
---
/**
* Cookie Consent Configuration
* Documentation: https://cookieconsent.orestbida.com/
*
* GDPR-Compliant Approach:
* - Analytics scripts use type="text/plain" data-category="analytics"
* - The library blocks all marked scripts until user consents
* - Scripts NEVER run until explicit consent is given
* - Google Consent Mode is used for Google Analytics privacy mode before consent
* - Other analytics (Cronitor, Pirsch, OpenPanel) are blocked until consent given
*
* Supported Analytics Providers:
* - Cronitor RUM
* - Google Analytics (GA4)
* - OpenPanel Analytics
* - Pirsch Analytics
*/
// Initialize Google Consent Mode BEFORE any tracking
// This tells Google services to operate in privacy mode until user consents
window.dataLayer = window.dataLayer || [];
// Reuse existing global gtag if it was already defined (e.g. by other GA scripts)
// to avoid redefining it multiple times when consent is granted.
if (typeof window.gtag !== 'function') {
window.gtag = function() {
window.dataLayer.push(arguments);
};
}
// Local alias for convenience in this file
var gtag = window.gtag;
gtag('consent', 'default', {
'ad_storage': 'denied',
'analytics_storage': 'denied',
'functionality_storage': 'denied',
'personalization_storage': 'denied'
});
// Wait for the library to be available
var cookieConsentRetryCount = 0;
var COOKIE_CONSENT_MAX_RETRIES = 50; // 5 seconds max wait time
function initializeCookieConsent() {
// Check if CookieConsent is available
if (!window.CookieConsent) {
if (cookieConsentRetryCount++ < COOKIE_CONSENT_MAX_RETRIES) {
// Library not yet loaded, try again after a short delay
setTimeout(initializeCookieConsent, 100);
} else {
console.error('CookieConsent library failed to load');
}
return;
}
window.CookieConsent.run({
categories: {
necessary: {
enabled: true,
readOnly: true
},
analytics: {}
},
language: {
default: 'en',
translations: {
en: {
consentModal: {
title: 'We use cookies',
description: 'This website uses cookies to improve your experience and analyze site traffic. By clicking "Accept all", you consent to our use of cookies.',
acceptAllBtn: 'Accept all',
acceptNecessaryBtn: 'Reject all',
showPreferencesBtn: 'Manage Individual preferences'
},
preferencesModal: {
title: 'Manage cookie preferences',
acceptAllBtn: 'Accept all',
acceptNecessaryBtn: 'Reject all',
savePreferencesBtn: 'Accept current selection',
closeIconLabel: 'Close modal',
sections: [
{
title: 'Cookie usage',
description: 'We use cookies to ensure the basic functionalities of the website and to enhance your online experience. You can choose for each category to opt-in/out whenever you want.'
},
{
title: 'Strictly Necessary cookies',
description: 'These cookies are essential for the proper functioning of the website. Without these cookies, the website would not work properly.',
linkedCategory: 'necessary'
},
{
title: 'Analytics cookies',
description: 'These cookies allow us to measure traffic and analyze your behavior to improve our service.',
linkedCategory: 'analytics'
},
{
title: 'More information',
description: 'For any queries in relation to our policy on cookies and your choices, please <a class="cc-link" href="{{ site.url }}{{ site.baseurl }}/#contact">contact us</a>.'
}
]
}
}
}
},
// Callback when user accepts/rejects consent
onFirstConsent: function(consentData) {
updateConsentMode(consentData);
},
// Callback when user changes preferences
onChange: function(consentData) {
updateConsentMode(consentData);
}
});
/**
* Update Google Consent Mode based on user preferences
* This ensures Google services respect user choices
*/
function updateConsentMode(consentData) {
// Handle both callback data structures
var categories = consentData.categories || consentData;
// Ensure categories is an object
if (!categories || typeof categories !== 'object') {
console.warn('Invalid consent data structure:', consentData);
return;
}
gtag('consent', 'update', {
'analytics_storage': categories.analytics ? 'granted' : 'denied',
'ad_storage': 'denied',
'functionality_storage': 'denied',
'personalization_storage': 'denied'
});
if (categories.analytics) {
console.debug('✓ Analytics consent granted - tracking enabled for all providers');
// Analytics scripts with data-category="analytics" will automatically run
// when the library re-evaluates them after this consent update
} else {
console.debug('✗ Analytics consent denied - no tracking data collected');
// Analytics scripts are already blocked by the library (type="text/plain")
// No tracking will occur for:
// - Cronitor RUM
// - Google Analytics (GA4)
// - OpenPanel Analytics
// - Pirsch Analytics
}
}
}
// Initialize when the library is available
initializeCookieConsent();

View File

@ -29,6 +29,7 @@ let applyTheme = () => {
setHighlight(theme); setHighlight(theme);
setGiscusTheme(theme); setGiscusTheme(theme);
setSearchTheme(theme); setSearchTheme(theme);
setCookieConsentTheme(theme);
updateCalendarUrl(); updateCalendarUrl();
// if mermaid is not defined, do nothing // if mermaid is not defined, do nothing
@ -245,6 +246,18 @@ let setSearchTheme = (theme) => {
} }
}; };
let setCookieConsentTheme = (theme) => {
// Sync cookie consent modal with site's theme
// The cookie consent library supports dark mode via the cc--darkmode class
var htmlElement = document.documentElement;
if (theme === "dark") {
htmlElement.classList.add("cc--darkmode");
} else {
htmlElement.classList.remove("cc--darkmode");
}
};
let transTheme = () => { let transTheme = () => {
document.documentElement.classList.add("transition"); document.documentElement.classList.add("transition");
window.setTimeout(() => { window.setTimeout(() => {