diff --git a/layouts/search.html b/layouts/search.html
index 723faf9..e1e1855 100644
--- a/layouts/search.html
+++ b/layouts/search.html
@@ -18,197 +18,41 @@
{{- end }}
-
+ {{ .Store.Set "en_article_in_fr_site" false }}
+ {{- partial "lang-detection.html" . -}}
+ {{- $.Store.Add "index" (dict "title" .Title "opensource" .Params.opensource "topics" .Params.topics "permalink" .Permalink "hasEnFlag" (.Store.Get "en_article_in_fr_site") "type" .Type "type_label" (i18n (print "type_" .Type)) "date" ($.Store.Get "date") "reading_time" ($.Store.Get "reading_time") "summary" (.Summary | plainify | htmlUnescape)) -}}
+ {{- end }}
+ {{- $data := dict "taxonomies" ($.Store.Get "taxonomies") "index" ($.Store.Get "index") "i18n" (dict "published_on" (i18n "published_on") "continue_reading" (i18n "continue_reading")) }}
+
+
{{ end }}
diff --git a/netlify.toml b/netlify.toml
index e61353f..e7c7052 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -16,6 +16,40 @@ command = "hugo --buildDrafts --buildFuture --baseURL $DEPLOY_PRIME_URL"
[context.deploy-preview.environment]
HUGO_ENABLEGITINFO = "true"
+# Security headers.
+#
+# HSTS is already set by Netlify (max-age=31536000). It does not include
+# the subdomains of itix.fr on purpose.
+#
+# Content Security Policy:
+# - no inline script (the search page uses a JSON data block + /js/search.js)
+# - inline styles are allowed (style attributes generated by Hugo)
+# - YouTube videos are embedded through iframes
+# - if Plausible is enabled again, add https://plausible.io to script-src
+# and connect-src
+[[headers]]
+ for = "/*"
+
+ [headers.values]
+ Content-Security-Policy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; media-src 'self'; frame-src https://www.youtube.com https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests"
+ X-Content-Type-Options = "nosniff"
+ X-Frame-Options = "DENY"
+ Referrer-Policy = "strict-origin-when-cross-origin"
+
+# The CSS file name contains a fingerprint of its content: it can be cached forever.
+[[headers]]
+ for = "/css/*"
+
+ [headers.values]
+ Cache-Control = "public, max-age=31536000, immutable"
+
+# The fonts are not fingerprinted but they almost never change.
+[[headers]]
+ for = "/fonts/*"
+
+ [headers.values]
+ Cache-Control = "public, max-age=2592000"
+
# Matrix homeserver discovery
[[headers]]
for = "/.well-known/matrix/*"
diff --git a/static/js/search.js b/static/js/search.js
new file mode 100644
index 0000000..3f706cc
--- /dev/null
+++ b/static/js/search.js
@@ -0,0 +1,167 @@
+//
+// Search engine of the "search" page.
+//
+// The search index, the taxonomies and the translated strings are generated
+// by Hugo in a JSON data block (see layouts/search.html), so that no inline
+// script is needed (Content Security Policy).
+//
+var searchData = JSON.parse(document.getElementById('search-data').textContent);
+var taxonomies = searchData.taxonomies;
+var searchDb = searchData.index;
+
+var searchResults = document.getElementById('searchResults'); // targets the
above
+var searchInput = document.getElementById('searchInput'); // input box for search
+var lastSearch = "";
+
+// Helper function to create an element with attributes
+function tag(name, attrs) {
+ var el = document.createElement(name.toString());
+
+ !!attrs && Object.keys(attrs).forEach(function(key) {
+ el.setAttribute(key, attrs[key]);
+ });
+
+ return el;
+}
+
+function printSearchResult(result) {
+ var section = tag("section");
+
+ //
+ // Article headline
+ //
+ var a = tag("a", { href: result.item.permalink });
+ a.textContent = result.item.title;
+ if (result.item.hasEnFlag) {
+ var uk_flag_svg = document.getElementById("embedded-svg-en");
+ if (uk_flag_svg != null) {
+ var img = uk_flag_svg.firstChild.cloneNode(true);
+ a.appendChild(img);
+ }
+ }
+ var h4 = tag("h4");
+ h4.textContent = result.item.type_label + " >> ";
+ h4.appendChild(a);
+ section.appendChild(h4);
+
+ //
+ // Metadata
+ //
+ if (result.item.type != "page") {
+ var div = tag("div", { 'class': 'post-metadata'});
+ var span_date = tag("span", { 'class': 'publication-date' });
+ span_date.textContent = searchData.i18n.published_on + " " + result.item.date;
+ div.appendChild(span_date);
+ if (result.item.topics != null) {
+ result.item.topics.forEach(function (v) {
+ var topic_detail = taxonomies.topics[v];
+ if (topic_detail == null) {
+ return;
+ }
+ var span = tag("span", { 'class': 'topic' });
+ var link_svg = document.getElementById("embedded-svg-link");
+ if (link_svg != null) {
+ var img = link_svg.firstChild.cloneNode(true);
+ span.appendChild(img);
+ }
+ var a = tag("a", { href: topic_detail.href });
+ a.textContent = topic_detail.title;
+ span.appendChild(a);
+ div.appendChild(span);
+ });
+ }
+ if (result.item.opensource != null) {
+ result.item.opensource.forEach(function (v) {
+ var opensource_detail = taxonomies.opensource[v];
+ if (opensource_detail == null) {
+ return;
+ }
+ var span = tag("span", { 'class': 'opensource' });
+ var label_svg = document.getElementById("embedded-svg-label");
+ if (label_svg != null) {
+ var img = label_svg.firstChild.cloneNode(true);
+ span.appendChild(img);
+ }
+ var a = tag("a", { href: opensource_detail.href });
+ a.textContent = opensource_detail.title;
+ span.appendChild(a);
+ div.appendChild(span);
+ });
+ }
+ if (result.item.reading_time != null) {
+ var span = tag("span", { 'class': 'reading-time' });
+ var schedule_svg = document.getElementById("embedded-svg-schedule");
+ if (schedule_svg != null) {
+ var img = schedule_svg.firstChild.cloneNode(true);
+ span.appendChild(img);
+ }
+ var readingTime = document.createTextNode(result.item.reading_time);
+ span.appendChild(readingTime);
+ div.appendChild(span);
+ }
+ section.appendChild(div);
+ }
+
+ var p = tag("p", {});
+ p.textContent = result.item.summary;
+ var a = tag("a", { href: result.item.permalink, 'class': 'continue-reading' });
+ a.textContent = searchData.i18n.continue_reading;
+ p.appendChild(a);
+ section.appendChild(p);
+
+ searchResults.appendChild(section);
+}
+
+function clearSearchResults() {
+ var child = searchResults.lastElementChild;
+ while (child) {
+ searchResults.removeChild(child);
+ child = searchResults.lastElementChild;
+ }
+}
+
+//
+// load the search index
+//
+function fuseInit(searchDb) {
+ var options = { // fuse.js options; check fuse.js website for details
+ shouldSort: true,
+ ignoreLocation: true,
+ threshold: 0.4,
+ minMatchCharLength: 3,
+ keys: [
+ 'title',
+ 'permalink',
+ 'summary',
+ 'topics',
+ 'opensource'
+ ]
+ };
+ fuse = new Fuse(searchDb, options); // build the index from the json file
+}
+
+//
+// using the index loaded previously, run a search query (for "term")
+// every time a letter is typed in the search box
+//
+function executeSearch(term) {
+ if (lastSearch == term) {
+ // Don't query Fuse if the search terms have not changed
+ return;
+ }
+
+ clearSearchResults();
+ let results = fuse.search(term);
+ results.slice(0, 20).forEach(printSearchResult);
+ lastSearch = term;
+}
+
+// Initialize the Fuse.io framework with the Search DB
+fuseInit(searchDb);
+
+//
+// execute search as each character is typed
+//
+searchInput.onkeyup = function(e) {
+ executeSearch(this.value);
+}