From 19d120019b6f8c7041965895dad2764dc2aa4f5d Mon Sep 17 00:00:00 2001 From: welpo Date: Wed, 12 Jul 2023 18:44:28 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20support=20for=20giscu?= =?UTF-8?q?s=20&=20utterances=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.toml | 47 ++++------- content/blog/security.md | 4 +- sass/parts/_comments.scss | 6 +- static/js/giscus.js | 81 +++++++++++++++++++ static/js/giscus_min.js | 1 + ...initialize_theme.js => initializeTheme.js} | 0 ...ze_theme_min.js => initializeTheme_min.js} | 0 static/js/main.js | 37 --------- static/js/main_min.js | 1 - static/js/themeSwitcher.js | 33 ++++++++ static/js/themeSwitcher_min.js | 37 +++++++++ static/js/utterances.js | 64 +++++++++++++++ static/js/utterances_min.js | 1 + templates/base.html | 1 + templates/macros/add_comments.html | 64 +++++++++++++++ templates/macros/content.html | 21 +---- templates/partials/header.html | 28 +++++-- 17 files changed, 330 insertions(+), 96 deletions(-) create mode 100644 static/js/giscus.js create mode 100644 static/js/giscus_min.js rename static/js/{initialize_theme.js => initializeTheme.js} (100%) rename static/js/{initialize_theme_min.js => initializeTheme_min.js} (100%) delete mode 100644 static/js/main.js delete mode 100644 static/js/main_min.js create mode 100644 static/js/themeSwitcher.js create mode 100644 static/js/themeSwitcher_min.js create mode 100644 static/js/utterances.js create mode 100644 static/js/utterances_min.js create mode 100644 templates/macros/add_comments.html diff --git a/config.toml b/config.toml index da0f719..f571cbe 100644 --- a/config.toml +++ b/config.toml @@ -179,41 +179,28 @@ allowed_domains = [ custom_subset = true # giscus support for comments. https://giscus.app -# TODO: make sure giscus / utterances are added to the Custom security headers when enabled -# TODO: Mention this fact in the docs [extra.giscus] -enabled_for_all_posts = false # Enables utterances on all posts. You can also do it on a post-per-post basis by setting `utterances = true` in the [extra] section of the post's front matter. -repo = "yourname/yourrepository" - - -enable_reactions = true +enabled_for_all_posts = false # Enables giscus on all posts. It can be enabled on individual posts by setting `giscus = true` in the [extra] section of the post's front matter. +repo = "welpo/test-utterances" +repo_id = "R_kgDOJ59Urw" +category = "Announcements" +category_id = "DIC_kwDOJ59Ur84CX0QG" +mapping = "slug" # Available: pathname; url; title; slug. "slug" will use the post's filename (slug); this is the only way to share comments between languages. +strict_title_matching = 1 # 1 to enable, 0 to disable. https://github.com/giscus/giscus/blob/main/ADVANCED-USAGE.md#data-strict +enable_reactions = 1 # 1 to enable, 0 to disable. comment_box_above_comments = true -lazy_loading = true light_theme = "noborder_light" dark_theme = "noborder_dark" -# TODO: Set data-lang to page's language - -# - - - - - +lang = "" # Leave blank to set it based on the page's language. +lazy_loading = true +automatic_loading = true # If set to false, a "Show comments" button will be shown. -# Utterances support for comments. https://utteranc.es +# utterances support for comments. https://utteranc.es [extra.utterances] -enabled_for_all_posts = false # Enables utterances on all posts. You can also do it on a post-per-post basis by setting `utterances = true` in the [extra] section of the post's front matter. -repo = "welpo/test-utterances" # https://utteranc.es/#heading-repository -issue_term = "pathname" # Available: pathname; url; title. More info: https://utteranc.es/#heading-mapping +enabled_for_all_posts = false # Enables utterances on all posts. It can be enabled on individual posts by setting `utterances = true` in the [extra] section of the post's front matter. +repo = "yourgithubuser/yourrepo" # https://utteranc.es/#heading-repository +issue_term = "slug" # Available: pathname; url; title; slug. "slug" will use the post's filename (slug); this is the only way to share comments between languages. https://utteranc.es/#heading-mapping label = "💬" # https://utteranc.es/#heading-issue-label -theme = "preferred-color-scheme" # https://utteranc.es/#heading-theme. If you use the theme switcher, it's probably best to use "preferred-color-scheme" +light_theme = "github-light" # https://utteranc.es/#heading-theme +dark_theme = "github-dark" # https://utteranc.es/#heading-theme lazy_loading = true diff --git a/content/blog/security.md b/content/blog/security.md index d8170d4..48f60b9 100644 --- a/content/blog/security.md +++ b/content/blog/security.md @@ -1,7 +1,7 @@ +++ title = "Secure by default" date = 2023-02-22 -updated = 2023-05-22 +updated = 2023-07-12 description = "tabi has an easily customizable Content Security Policy (CSP) with safe defaults. Get peace of mind and an A+ on Mozilla Observatory." [taxonomies] @@ -27,4 +27,6 @@ The `allowed_domains` list specifies the URLs that the website should be able to This feature allows you to easily customize the website's security headers to allow for specific use cases, such as embedding YouTube videos, loading scripts or remote fonts ([not recommended](https://www.albertovarela.net/blog/2022/11/stop-using-google-fonts/)). +**Note**: enabling comments automatically allows scripts and frames from either utterances/giscus, as well as unsafe-inline styles when using utterances (required so that the widget can adjust its size based on the number of comments). + [^1]: Requires proper webserver configuration (e.g. redirecting HTTP traffic to HTTPS). diff --git a/sass/parts/_comments.scss b/sass/parts/_comments.scss index 5188b40..36917dd 100644 --- a/sass/parts/_comments.scss +++ b/sass/parts/_comments.scss @@ -1,10 +1,12 @@ .utterances-frame { - min-height: 18rem; width: 100%; } .comments { - padding-top: 5vmin; + border-top: var(--divider-color) solid 0.5px; + margin-top: 2rem; + padding-top: 2rem; + padding-bottom: 2rem; iframe { border: none; diff --git a/static/js/giscus.js b/static/js/giscus.js new file mode 100644 index 0000000..004fe45 --- /dev/null +++ b/static/js/giscus.js @@ -0,0 +1,81 @@ +function setGiscusTheme(newTheme) { + // Get the giscus iframe. + const frame = document.querySelector('iframe.giscus-frame'); + + if (frame) { + // If the iframe exists, send a message to set the theme. + frame.contentWindow.postMessage( + { giscus: { setConfig: { theme: newTheme } } }, + 'https://giscus.app' + ); + } +} + +// Function to initialize Giscus. This function is run when the window loads. +function initGiscus() { + // Get the div that will contain the comments. + const commentsDiv = document.querySelector('.comments'); + if (commentsDiv) { + // Get the various settings from data attributes on the div. + const repo = commentsDiv.getAttribute('data-repo'); + const repoId = commentsDiv.getAttribute('data-repo-id'); + const category = commentsDiv.getAttribute('data-category'); + const categoryId = commentsDiv.getAttribute('data-category-id'); + const strictTitleMatching = commentsDiv.getAttribute('data-strict'); + const term = commentsDiv.getAttribute('data-term'); + const reactionsEnabled = commentsDiv.getAttribute('data-reactions-enabled'); + const inputPosition = commentsDiv.getAttribute('data-input-position'); + const lightTheme = commentsDiv.getAttribute('data-light-theme'); + const darkTheme = commentsDiv.getAttribute('data-dark-theme'); + const lang = commentsDiv.getAttribute('data-lang'); + const lazyLoading = commentsDiv.getAttribute('data-lazy-loading'); + + // Create a new script tag that will load the Giscus script. + const script = document.createElement('script'); + script.src = 'https://giscus.app/client.js'; + script.async = true; + + // Set the various settings as data attributes on the script tag. + script.setAttribute('data-repo', repo); + script.setAttribute('data-repo-id', repoId); + script.setAttribute('data-category', category); + script.setAttribute('data-category-id', categoryId); + script.setAttribute('data-term', term); + script.setAttribute('data-strict', strictTitleMatching); + script.setAttribute('data-reactions-enabled', reactionsEnabled); + script.setAttribute('data-emit-metadata', '0'); + script.setAttribute('data-input-position', inputPosition); + script.setAttribute('data-lang', lang); + script.setAttribute('crossorigin', 'anonymous'); + + // Set the mapping if it is provided. + const mapping = commentsDiv.getAttribute('data-mapping'); + if (mapping) { + script.setAttribute('data-mapping', mapping); + } + + // Choose the correct theme based on the current theme of the document. + const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; + const selectedTheme = currentTheme === 'dark' ? darkTheme : lightTheme; + script.setAttribute('data-theme', selectedTheme); + + // Set the loading attribute if lazy loading is enabled. + if (lazyLoading === 'true') { + script.setAttribute('data-loading', 'lazy'); + } + + // Add the script tag to the div. + commentsDiv.appendChild(script); + + // Listen for theme changes and update the Giscus theme when they occur. + window.addEventListener('themeChanged', (event) => { + const selectedTheme = event.detail.theme === 'dark' ? darkTheme : lightTheme; + setGiscusTheme(selectedTheme); + }); + } +} + +// Initialize Giscus when the window loads. +window.addEventListener('load', () => { + initGiscus(); +}); diff --git a/static/js/giscus_min.js b/static/js/giscus_min.js new file mode 100644 index 0000000..0696a8c --- /dev/null +++ b/static/js/giscus_min.js @@ -0,0 +1 @@ +function setGiscusTheme(t){var e=document.querySelector("iframe.giscus-frame");e&&e.contentWindow.postMessage({giscus:{setConfig:{theme:t}}},"https://giscus.app")}function initGiscus(){var t=document.querySelector(".comments");if(t){var e=t.getAttribute("data-repo"),a=t.getAttribute("data-repo-id"),i=t.getAttribute("data-category"),r=t.getAttribute("data-category-id"),d=t.getAttribute("data-strict"),s=t.getAttribute("data-term"),u=t.getAttribute("data-reactions-enabled"),n=t.getAttribute("data-input-position");const b=t.getAttribute("data-light-theme"),A=t.getAttribute("data-dark-theme");var o=t.getAttribute("data-lang"),c=t.getAttribute("data-lazy-loading"),g=document.createElement("script"),e=(g.src="https://giscus.app/client.js",g.async=!0,g.setAttribute("data-repo",e),g.setAttribute("data-repo-id",a),g.setAttribute("data-category",i),g.setAttribute("data-category-id",r),g.setAttribute("data-term",s),g.setAttribute("data-strict",d),g.setAttribute("data-reactions-enabled",u),g.setAttribute("data-emit-metadata","0"),g.setAttribute("data-input-position",n),g.setAttribute("data-lang",o),g.setAttribute("crossorigin","anonymous"),t.getAttribute("data-mapping")),a=(e&&g.setAttribute("data-mapping",e),document.documentElement.getAttribute("data-theme")||"light"),i="dark"===a?A:b;g.setAttribute("data-theme",i),"true"===c&&g.setAttribute("data-loading","lazy"),t.appendChild(g),window.addEventListener("themeChanged",t=>{setGiscusTheme("dark"===t.detail.theme?A:b)})}}window.addEventListener("load",()=>{initGiscus()}); diff --git a/static/js/initialize_theme.js b/static/js/initializeTheme.js similarity index 100% rename from static/js/initialize_theme.js rename to static/js/initializeTheme.js diff --git a/static/js/initialize_theme_min.js b/static/js/initializeTheme_min.js similarity index 100% rename from static/js/initialize_theme_min.js rename to static/js/initializeTheme_min.js diff --git a/static/js/main.js b/static/js/main.js deleted file mode 100644 index f74df37..0000000 --- a/static/js/main.js +++ /dev/null @@ -1,37 +0,0 @@ -// Get the theme switcher button element. -const themeSwitcher = document.querySelector(".theme-switcher"); - -// Retrieve theme from localStorage. -let currentTheme = localStorage.getItem("theme"); - -// Function to switch between dark and light themes. -function switchTheme() { - // Set the new theme based on the current theme. - currentTheme = currentTheme === "dark" ? "light" : "dark"; - document.documentElement.setAttribute("data-theme", currentTheme); - localStorage.setItem("theme", currentTheme); - - // Send a message to the Utterances iframe to change its theme. - document.querySelectorAll(".utterances-frame").forEach((frame) => { - frame.contentWindow.postMessage( - { type: 'set-theme', theme: `github-${currentTheme}` }, - '*' - ); - }); -} - -// Initialize the theme switcher button. -themeSwitcher.addEventListener("click", switchTheme, false); - -// Update the theme based on system preference if the user hasn't manually changed the theme. -window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", e => { - if (!localStorage.getItem("theme")) { - currentTheme = e.matches ? "dark" : "light"; - document.documentElement.setAttribute("data-theme", currentTheme); - } -}); - -// Fix the theme switcher button not working on the first click by updating currentTheme. -if (!currentTheme) { - currentTheme = document.documentElement.getAttribute("data-theme"); -} diff --git a/static/js/main_min.js b/static/js/main_min.js deleted file mode 100644 index d444580..0000000 --- a/static/js/main_min.js +++ /dev/null @@ -1 +0,0 @@ -const themeSwitcher=document.querySelector(".theme-switcher");let currentTheme=localStorage.getItem("theme");function switchTheme(){currentTheme="dark"===currentTheme?"light":"dark",document.documentElement.setAttribute("data-theme",currentTheme),localStorage.setItem("theme",currentTheme),document.querySelectorAll(".utterances-frame").forEach(e=>{e.contentWindow.postMessage({type:"set-theme",theme:`github-${currentTheme}`},"*")})}themeSwitcher.addEventListener("click",switchTheme,!1),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",e=>{localStorage.getItem("theme")||(currentTheme=e.matches?"dark":"light",document.documentElement.setAttribute("data-theme",currentTheme))}),currentTheme||(currentTheme=document.documentElement.getAttribute("data-theme")); diff --git a/static/js/themeSwitcher.js b/static/js/themeSwitcher.js new file mode 100644 index 0000000..e36ac8f --- /dev/null +++ b/static/js/themeSwitcher.js @@ -0,0 +1,33 @@ +// Get the theme switcher button element. +const themeSwitcher = document.querySelector(".theme-switcher"); + +// Retrieve theme from localStorage. +let currentTheme = localStorage.getItem("theme"); + +// Function to switch between dark and light themes. +function setTheme(theme) { + document.documentElement.setAttribute("data-theme", theme); + localStorage.setItem("theme", theme); + + // Dispatch a custom event for utterances and giscus. + const event = new CustomEvent("themeChanged", { + detail: { theme: theme } + }); + window.dispatchEvent(event); +} + +function switchTheme() { + // Set the new theme based on the current theme. + const newTheme = currentTheme === "dark" ? "light" : "dark"; + setTheme(newTheme); +} + +themeSwitcher.addEventListener("click", switchTheme, false); + +// Update the theme based on system preference if the user hasn't manually changed the theme. +window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", e => { + if (!localStorage.getItem("theme")) { + const newTheme = e.matches ? "dark" : "light"; + setTheme(newTheme); + } +}); diff --git a/static/js/themeSwitcher_min.js b/static/js/themeSwitcher_min.js new file mode 100644 index 0000000..bd58b9d --- /dev/null +++ b/static/js/themeSwitcher_min.js @@ -0,0 +1,37 @@ +// Get the theme switcher button element. +const themeSwitcher = document.querySelector(".theme-switcher"); + +// Retrieve theme from localStorage. +let currentTheme = localStorage.getItem("theme"); + +// Function to set theme +function setTheme(theme, saveToLocalStorage = false) { + document.documentElement.setAttribute("data-theme", theme); + currentTheme = theme; + + if (saveToLocalStorage) { + localStorage.setItem("theme", theme); + } + + // Dispatch a custom event for utterances and giscus. + const event = new CustomEvent("themeChanged", { + detail: { theme: theme } + }); + window.dispatchEvent(event); +} + +// Function to switch between dark and light themes. +function switchTheme() { + // Set the new theme based on the current theme. + const newTheme = currentTheme === "dark" ? "light" : "dark"; + setTheme(newTheme, true); // Save the theme to localStorage when the user changes it. +} + +// Initialize the theme switcher button. +themeSwitcher.addEventListener("click", switchTheme, false); + +// Update the theme based on system preference if the user hasn't manually changed the theme. +window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", e => { + const newTheme = e.matches ? "dark" : "light"; + setTheme(newTheme); +}); diff --git a/static/js/utterances.js b/static/js/utterances.js new file mode 100644 index 0000000..d62fb31 --- /dev/null +++ b/static/js/utterances.js @@ -0,0 +1,64 @@ +function setUtterancesTheme(newTheme) { + // Get the frame with class "utterances-frame". + const frame = document.querySelector(".utterances-frame"); + + if (frame) { + // If the iframe exists, send a message to set the theme. + frame.contentWindow.postMessage( + { type: 'set-theme', theme: newTheme }, + 'https://utteranc.es' + ); + } +} + +function initUtterances() { + // Get the comments div. + const commentsDiv = document.querySelector('.comments'); + + // Check if the comments div exists. + if (commentsDiv) { + // Get all necessary attributes for initializing Utterances. + const repo = commentsDiv.getAttribute('data-repo'); + const issueTerm = commentsDiv.getAttribute('data-issue-term'); + const label = commentsDiv.getAttribute('data-label'); + const lightTheme = commentsDiv.getAttribute('data-light-theme'); + const darkTheme = commentsDiv.getAttribute('data-dark-theme'); + const lazyLoading = commentsDiv.getAttribute('data-lazy-loading'); + + // Create a new script element. + const script = document.createElement('script'); + script.src = 'https://utteranc.es/client.js'; + script.async = true; + script.setAttribute('repo', repo); + script.setAttribute('issue-term', issueTerm); + script.setAttribute('label', label); + + // Set the initial theme. + const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; + const selectedTheme = currentTheme === 'dark' ? darkTheme : lightTheme; + script.setAttribute('theme', selectedTheme); + + script.setAttribute('crossorigin', 'anonymous'); + + // Enable lazy loading if specified. + if (lazyLoading === 'true') { + script.setAttribute('data-loading', 'lazy'); + } + + // Append the script to the comments div. + commentsDiv.appendChild(script); + + // Listen for themeChanged event to update the theme. + window.addEventListener('themeChanged', (event) => { + // Determine the new theme based on the event detail. + const selectedTheme = event.detail.theme === 'dark' ? darkTheme : lightTheme; + // Set the new theme. + setUtterancesTheme(selectedTheme); + }); + } +} + +// Initialize Utterances once the window has loaded. +window.addEventListener('load', () => { + initUtterances(); +}); diff --git a/static/js/utterances_min.js b/static/js/utterances_min.js new file mode 100644 index 0000000..6a0f898 --- /dev/null +++ b/static/js/utterances_min.js @@ -0,0 +1 @@ +function setUtterancesTheme(t){const e=document.querySelector(".utterances-frame");e&&e.contentWindow.postMessage({type:"set-theme",theme:t},"https://utteranc.es")}function initUtterances(){const t=document.querySelector(".comments");if(t){var e=t.getAttribute("data-repo"),a=t.getAttribute("data-issue-term"),r=t.getAttribute("data-label");const s=t.getAttribute("data-light-theme"),d=t.getAttribute("data-dark-theme");var n=t.getAttribute("data-lazy-loading"),i=document.createElement("script"),e=(i.src="https://utteranc.es/client.js",i.async=!0,i.setAttribute("repo",e),i.setAttribute("issue-term",a),i.setAttribute("label",r),document.documentElement.getAttribute("data-theme")||"light"),a="dark"===e?d:s;i.setAttribute("theme",a),i.setAttribute("crossorigin","anonymous"),"true"===n&&i.setAttribute("data-loading","lazy"),t.appendChild(i),window.addEventListener("themeChanged",t=>{setUtterancesTheme("dark"===t.detail.theme?d:s)})}}window.addEventListener("load",()=>{initUtterances()}); diff --git a/templates/base.html b/templates/base.html index cd09f2f..94d3499 100644 --- a/templates/base.html +++ b/templates/base.html @@ -6,6 +6,7 @@ {% import "macros/set_title.html" as macros_set_title %} {% import "macros/paginate.html" as macros_paginate %} {% import "macros/format_date.html" as macros_format_date %} +{% import "macros/add_comments.html" as macros_add_comments %} diff --git a/templates/macros/add_comments.html b/templates/macros/add_comments.html new file mode 100644 index 0000000..24ee7c3 --- /dev/null +++ b/templates/macros/add_comments.html @@ -0,0 +1,64 @@ +{%- macro add_comments() -%} + +{% set giscus_enabled = config.extra.giscus.enabled_for_all_posts or page.extra.giscus %} +{% set utterances_enabled = config.extra.utterances.enabled_for_all_posts or page.extra.utterances %} + +{# Ensure only one comment system is enabled #} +{% if giscus_enabled and utterances_enabled %} + {{ throw(message="ERROR: Multiple comment systems have been enabled for the same page. Check your config.toml and individual page settings to ensure only one comment system is activated at a time.") }} + +{% elif giscus_enabled %} + {# Create a div to host giscus comments #} +
+
+ + {# giscus script to load the widget #} + + +{% elif utterances_enabled %} + {# Create a div to host utterances comments #} +
+
+ + {# utterances script to load the widget #} + +{% endif %} + +{%- endmacro add_comments -%} diff --git a/templates/macros/content.html b/templates/macros/content.html index d1b6598..f5f9528 100644 --- a/templates/macros/content.html +++ b/templates/macros/content.html @@ -80,26 +80,9 @@ {{ page.content | safe }} + {# Add comments if they are enabled #} + {{ macros_add_comments::add_comments() }} - - {# giscus / utterances comments #} - {% if config.extra.giscus.enabled_for_all_posts or page.extra.giscus %} - {# TODO: HERE GOES GISCUS SUPPORT #} - {% elif config.extra.utterances.enabled_for_all_posts or page.extra.utterances %} -
- -
- {% endif %} {% endmacro content %} diff --git a/templates/partials/header.html b/templates/partials/header.html index e484bb8..5aeb137 100644 --- a/templates/partials/header.html +++ b/templates/partials/header.html @@ -14,7 +14,7 @@ {% endif %} - {# RSS #} + {# Feed #} @@ -36,7 +36,16 @@ {% endfor %} {% endif %} - + + + {%- if page.description %} + + {%- elif page.summary %} + + {%- else %} + + {%- endif %} + {% if is_404 %} {% else %} @@ -61,11 +70,18 @@ content="default-src 'self' {%- if config.extra.allowed_domains -%} ; + {%- set utterances_enabled = config.extra.utterances.enabled_for_all_posts or page.extra.utterances -%} + {%- set giscus_enabled = config.extra.giscus.enabled_for_all_posts or page.extra.giscus -%} {%- for domain in config.extra.allowed_domains -%} - {{ domain.directive }} {{ domain.domains | join(sep=' ') }} + {{ domain.directive }} {{ domain.domains | join(sep=' ') -}} + + {% if utterances_enabled -%} + {%- if domain.directive == "style-src" %} 'unsafe-inline' + {%- endif -%} + {% endif -%} {# Automatically allow giscus/utterances, if enabled #} - {%- if domain.directive == "script-src" or domain.directive == "frame-src" or domain.directive == "style-src" -%} + {%- if domain.directive == "script-src" or domain.directive == "frame-src" -%} {%- if config.extra.giscus.enabled_for_all_posts or page.extra.giscus %} giscus.app {%- elif config.extra.utterances.enabled_for_all_posts or page.extra.utterances%} utteranc.es {%- endif %} @@ -77,8 +93,8 @@ {%- endif -%}"> {%- if config.extra.theme_switcher and config.extra.theme_switcher == true -%} - - + + {%- endif -%}