diff --git a/news/changelog-1.9.md b/news/changelog-1.9.md index 7c00fa71624..901a034fc88 100644 --- a/news/changelog-1.9.md +++ b/news/changelog-1.9.md @@ -28,6 +28,12 @@ All changes included in 1.9: - ([#13452](https://github.com/quarto-dev/quarto-cli/issues/13452)): Wraps subfigure captions generated by `quarto_super()` in `block` function to avoid emitting `par` elements. (author: @christopherkenny) - ([#13474](https://github.com/quarto-dev/quarto-cli/issues/13474)): Heading font for title should default to `mainfont`. +## Projects + +### `website` + +- Algolia Insights now uses privacy-friendly defaults: `useCookie: false` with random session tokens when cookie consent is not configured. When `cookie-consent: true` is enabled, Algolia scripts are deferred and only use cookies after user grants "tracking" consent, ensuring GDPR compliance. + ## `publish` ### Confluence diff --git a/src/project/types/website/website-search.ts b/src/project/types/website/website-search.ts index 89b21c61108..c2073928042 100644 --- a/src/project/types/website/website-search.ts +++ b/src/project/types/website/website-search.ts @@ -117,6 +117,7 @@ const kTitle = "title"; const kText = "text"; const kAnalyticsEvents = "analytics-events"; const kShowLogo = "show-logo"; +const kCookieConsentEnabled = "cookie-consent-enabled"; interface SearchOptionsAlgolia { [kSearchOnlyApiKey]?: string; @@ -131,6 +132,7 @@ interface SearchOptionsAlgolia { [kSearchParams]?: Record; [kAnalyticsEvents]?: boolean; [kShowLogo]?: boolean; + [kCookieConsentEnabled]?: boolean; } export type SearchInputLocation = "navbar" | "sidebar"; @@ -615,20 +617,28 @@ export async function websiteSearchIncludeInHeader( } }); - const searchOptionsJson = JSON.stringify(options, null, 2); - const searchOptionsScript = - ``; - const includes = [searchOptionsScript]; + const includes: string[] = []; + // Process all Algolia configuration and scripts if (options[kAlgolia]) { includes.push(kAlogioSearchApiScript); - if (options[kAlgolia]?.[kAnalyticsEvents]) { + if (options[kAlgolia][kAnalyticsEvents]) { const cookieConsent = cookieConsentEnabled(project); + // Set cookie consent flag for JavaScript configuration + options[kAlgolia][kCookieConsentEnabled] = cookieConsent; + // Add Algolia Insights scripts with proper consent handling includes.push(algoliaSearchInsightsScript(cookieConsent)); includes.push(autocompleteInsightsPluginScript(cookieConsent)); } } + // Serialize search options to JSON after all modifications + const searchOptionsJson = JSON.stringify(options, null, 2); + const searchOptionsScript = + ``; + // Prepend search options script to beginning of includes + includes.unshift(searchOptionsScript); + Deno.writeTextFileSync(websiteSearchScript, includes.join("\n")); return websiteSearchScript; } diff --git a/src/resources/projects/website/search/quarto-search.js b/src/resources/projects/website/search/quarto-search.js index d788a958127..68a7c8a2e7b 100644 --- a/src/resources/projects/website/search/quarto-search.js +++ b/src/resources/projects/website/search/quarto-search.js @@ -466,10 +466,19 @@ function configurePlugins(quartoSearchOptions) { window.aa && window["@algolia/autocomplete-plugin-algolia-insights"] ) { + // Check if cookie consent is enabled from search options + const cookieConsentEnabled = algoliaOptions["cookie-consent-enabled"] || false; + + // Generate random session token only when cookies are disabled + const userToken = cookieConsentEnabled ? undefined : Array.from(Array(20), () => + Math.floor(Math.random() * 36).toString(36) + ).join(""); + window.aa("init", { appId, apiKey, - useCookie: true, + useCookie: cookieConsentEnabled, + userToken: userToken, }); const { createAlgoliaInsightsPlugin } = diff --git a/tests/docs/smoke-all/website-search/algolia-no-consent/.gitignore b/tests/docs/smoke-all/website-search/algolia-no-consent/.gitignore new file mode 100644 index 00000000000..ad293093b07 --- /dev/null +++ b/tests/docs/smoke-all/website-search/algolia-no-consent/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/website-search/algolia-no-consent/_quarto.yml b/tests/docs/smoke-all/website-search/algolia-no-consent/_quarto.yml new file mode 100644 index 00000000000..bc1ff035729 --- /dev/null +++ b/tests/docs/smoke-all/website-search/algolia-no-consent/_quarto.yml @@ -0,0 +1,17 @@ +project: + type: website + +website: + title: "Algolia Without Cookie Consent Test" + search: + algolia: + application-id: "TEST_APP_ID" + search-only-api-key: "TEST_API_KEY" + index-name: "test-index" + analytics-events: true + # This is the default and should act as such + # cookie-consent: false + +format: + html: + theme: cosmo diff --git a/tests/docs/smoke-all/website-search/algolia-no-consent/index.qmd b/tests/docs/smoke-all/website-search/algolia-no-consent/index.qmd new file mode 100644 index 00000000000..618d5466037 --- /dev/null +++ b/tests/docs/smoke-all/website-search/algolia-no-consent/index.qmd @@ -0,0 +1,25 @@ +--- +title: "Algolia Search Without Cookie Consent" +_quarto: + tests: + html: + ensureHtmlElements: + # Ensure search options script exists + - ['script#quarto-search-options'] + ensureFileRegexMatches: + - + # Cookie consent should be false + - '"cookie-consent-enabled":\s*false' + # Scripts should load immediately with type="text/javascript" + - ']*type="text/javascript">[\s\S]+search-insights' + - + # Verify scripts do NOT have cookie-consent attribute + - ']*cookie-consent[^>]+>[\s\S]*search-insights' +--- + +This test verifies that when `cookie-consent` is NOT configured in the website settings: + +1. The Algolia search options JSON contains `"cookie-consent-enabled": false` (or the key is absent) +2. Algolia Insights scripts are loaded immediately with `type="text/javascript"` +3. Scripts are not deferred behind cookie consent (no `cookie-consent="tracking"` attribute) + diff --git a/tests/docs/smoke-all/website-search/algolia-with-consent/.gitignore b/tests/docs/smoke-all/website-search/algolia-with-consent/.gitignore new file mode 100644 index 00000000000..ad293093b07 --- /dev/null +++ b/tests/docs/smoke-all/website-search/algolia-with-consent/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/website-search/algolia-with-consent/_quarto.yml b/tests/docs/smoke-all/website-search/algolia-with-consent/_quarto.yml new file mode 100644 index 00000000000..5a2effde7ea --- /dev/null +++ b/tests/docs/smoke-all/website-search/algolia-with-consent/_quarto.yml @@ -0,0 +1,16 @@ +project: + type: website + +website: + title: "Algolia With Cookie Consent Test" + search: + algolia: + application-id: "TEST_APP_ID" + search-only-api-key: "TEST_API_KEY" + index-name: "test-index" + analytics-events: true + cookie-consent: true + +format: + html: + theme: cosmo diff --git a/tests/docs/smoke-all/website-search/algolia-with-consent/index.qmd b/tests/docs/smoke-all/website-search/algolia-with-consent/index.qmd new file mode 100644 index 00000000000..9ac8cb5c8f1 --- /dev/null +++ b/tests/docs/smoke-all/website-search/algolia-with-consent/index.qmd @@ -0,0 +1,25 @@ +--- +title: "Algolia Search With Cookie Consent" +_quarto: + tests: + html: + ensureHtmlElements: + - + # Ensure search options script exists + - 'script#quarto-search-options' + # Cookie consent element are loaded + - script[src$='cookie-consent.js'] + ensureFileRegexMatches: + - + # Cookie consent should be enabled + - '"cookie-consent-enabled":\s*true' + # Scripts should be deferred with cookie-consent attribute + - 'type="text/plain"[^>]*cookie-consent="tracking"[^>]*>[\s\S]*search-insights' +--- + +This test verifies that when `cookie-consent` is configured in the website settings: + +1. The Algolia search options JSON contains `"cookie-consent-enabled": true` +2. Algolia Insights scripts are deferred with `type="text/plain" cookie-consent="tracking"` +3. Scripts only execute after user grants "tracking" consent +4. Cookie consent UI elements are present on the page \ No newline at end of file