content: update frontend fetch UI

The frontend fetch UI is updated with a styled button, and loading dots
to indicate that a fetch request is in progress.

The Fetch API is now used instead of AJAX to make requests.

Updates golang/go#36811
Updates golang/go#37002

Change-Id: Ia37113dd9976f8e147875371c099f7b2bfd4bd85
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/240459
Reviewed-by: Andrew Bonventre <andybons@golang.org>
This commit is contained in:
Julie Qiu 2020-06-29 16:31:25 -04:00
Родитель 926e2322e4
Коммит 993436f73a
9 изменённых файлов: 171 добавлений и 61 удалений

Просмотреть файл

@ -516,6 +516,69 @@ code {
position: absolute;
top: 62rem;
}
.Error-gopher,
.EmptyContent-gopher,
.Fetch-gopher,
.SearchResults-emptyContentGopher {
display: block;
margin: auto;
padding: 1.25rem 0;
max-width: 15rem;
}
.Error-message,
.EmptyContent-message,
.SearchResults-emptyContentMessage {
text-align: center;
}
.Fetch-container {
align-items: center;
display: flex;
flex-flow: column;
}
@keyframes blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
.Fetch-loading {
display: none;
}
.Fetch-dot {
animation-duration: 1.4s;
animation-fill-mode: both;
animation-iteration-count: infinite;
animation-name: blink;
background-color: var(--turq-dark);
border-radius: 50%;
display: inline-block;
height: 0.5rem;
width: 0.5rem;
}
.Fetch-loading:nth-child(2) {
animation-delay: 0.2s;
}
.Fetch-loading:nth-child(3) {
animation-delay: 0.4s;
}
.Fetch-button {
background-color: var(--gray-10);
border-radius: 0.5rem;
border: 0.0625rem solid var(--gray-8);
color: var(--turq-dark);
font-size: 1rem;
height: 2.5rem;
margin: 1rem 0;
padding: 0rem 1rem;
width: 5rem;
}
.SearchResults {
margin: 0 auto;
max-width: 60em;

Просмотреть файл

@ -0,0 +1,33 @@
<!--
Copyright 2019 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
{{define "main_content"}}
<div class="Container">
<div class="Content">
<div class="Fetch-container">
<img class="Fetch-gopher" src="/static/img/gopher-airplane.svg" alt="The Go Gopher">
<h3 class="Fetch-message js-fetchMessage" aria-live="polite" data-path="{{.MessageData}}">
Oops! {{.MessageData}} does not exist.
</h3>
<div class="Fetch-loading js-fetchLoading" aria-live="polite">
<i class="Fetch-dot"></i>
<i class="Fetch-dot"></i>
<i class="Fetch-dot"></i>
</div>
<p class="Fetch-messageSecondary js-fetchMessageSecondary" aria-live="polite">
Check that you entered it correctly, or request to fetch it.
</p>
<button class="Fetch-button js-fetchButton" aria-live="polite">Fetch</button>
</div>
</div>
</div>
{{end}}
{{define "post_content"}}
<script>
loadScript("/static/js/fetch.min.js");
</script>
{{end}}

Просмотреть файл

@ -1,57 +0,0 @@
<!--
Copyright 2019 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
{{define "main_content"}}
<div class="Container">
<div class="Content">
<img class="NotFound-gopher" src="/static/img/gopher-airplane.svg" alt="The Go Gopher">
{{template "message" .MessageData}}
<div class="NotFound-container">
<button class="NotFound-button js-notFoundButton">Fetch</button>
</div>
</div>
</div>
<!-- TODO: update middleware.AcceptMethods so that the GET at the end of the
script can be a POST instead.
Do not add comments to the script below: they are stripped by html/template
(https://golang.org/issue/28628), which messes up the CSP hash.
-->
<script>
const fetchButton = document.querySelector('.js-notFoundButton');
if (fetchButton) {
fetchButton.addEventListener('click', e => {
e.preventDefault();
fetchPath()
});
}
function fetchPath() {
httpRequest = new XMLHttpRequest();
var btn = document.querySelector('.js-notFoundButton');
btn.disabled = true;
btn.className = 'NotFound-button-disabled';
if (!httpRequest) {
alert('Giving up :( Cannot create an XMLHTTP instance');
return false;
}
httpRequest.onreadystatechange = function(){
if (httpRequest.readyState === XMLHttpRequest.DONE) {
if (httpRequest.status === 200) {
location.reload();
} else {
document.querySelector('.js-notFoundMessage').innerHTML = httpRequest.responseText;
btn.innerHTML = 'Failed';
}
}
};
document.querySelector('.js-notFoundMessage').innerHTML = "Fetching... Feel free to navigate away and check back later, we'll keep working on it!";
btn.innerHTML = "Fetching...";
httpRequest.open('GET', "/fetch" + window.location.pathname);
httpRequest.send();
}
</script>
{{end}}

Просмотреть файл

@ -0,0 +1,33 @@
/**
* @license
* Copyright 2020 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
const fetchButton = document.querySelector('.js-fetchButton');
if (fetchButton) {
fetchButton.addEventListener('click', e => {
e.preventDefault();
fetchPath();
});
}
async function fetchPath() {
const fetchMessageEl = document.querySelector('.js-fetchMessage');
fetchMessageEl.textContent = `Fetching ${fetchMessageEl.dataset.path}`;
document.querySelector('.js-fetchMessageSecondary').textContent =
"Feel free to navigate away and check back later, well keep working on it!";
document.querySelector('.js-fetchButton').style.display = 'none';
document.querySelector('.js-fetchLoading').style.display = 'block';
const response = await fetch(`/fetch${window.location.pathname}`);
if (response.ok) {
window.location.reload();
return;
}
const responseText = await response.text();
document.querySelector('.js-fetchLoading').style.display = 'none';
document.querySelector('.js-fetchMessageSecondary').textContent = '';
document.querySelector('.js-fetchMessage').textContent = responseText;
}

37
content/static/js/fetch.min.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,37 @@
/*
Copyright 2020 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
*/
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}};$jscomp.arrayIterator=function(a){return{next:$jscomp.arrayIteratorImpl(a)}};$jscomp.makeIterator=function(a){var b="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return b?b.call(a):$jscomp.arrayIterator(a)};
$jscomp.getGlobal=function(a){a=["object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global,a];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}return globalThis};$jscomp.global=$jscomp.getGlobal(this);$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)};$jscomp.polyfill=function(a,b,c,d){if(b){c=$jscomp.global;a=a.split(".");for(d=0;d<a.length-1;d++){var e=a[d];e in c||(c[e]={});c=c[e]}a=a[a.length-1];d=c[a];b=b(d);b!=d&&null!=b&&$jscomp.defineProperty(c,a,{configurable:!0,writable:!0,value:b})}};$jscomp.FORCE_POLYFILL_PROMISE=!1;
$jscomp.polyfill("Promise",function(a){function b(){this.batch_=null}function c(a){return a instanceof e?a:new e(function(b,f){b(a)})}if(a&&!$jscomp.FORCE_POLYFILL_PROMISE)return a;b.prototype.asyncExecute=function(a){if(null==this.batch_){this.batch_=[];var b=this;this.asyncExecuteFunction(function(){b.executeBatch_()})}this.batch_.push(a)};var d=$jscomp.global.setTimeout;b.prototype.asyncExecuteFunction=function(a){d(a,0)};b.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var a=
this.batch_;this.batch_=[];for(var b=0;b<a.length;++b){var c=a[b];a[b]=null;try{c()}catch(k){this.asyncThrow_(k)}}}this.batch_=null};b.prototype.asyncThrow_=function(a){this.asyncExecuteFunction(function(){throw a;})};var e=function(a){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];var b=this.createResolveAndReject_();try{a(b.resolve,b.reject)}catch(h){b.reject(h)}};e.prototype.createResolveAndReject_=function(){function a(a){return function(f){c||(c=!0,a.call(b,f))}}var b=this,c=!1;
return{resolve:a(this.resolveTo_),reject:a(this.reject_)}};e.prototype.resolveTo_=function(a){if(a===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(a instanceof e)this.settleSameAsPromise_(a);else{a:switch(typeof a){case "object":var b=null!=a;break a;case "function":b=!0;break a;default:b=!1}b?this.resolveToNonPromiseObj_(a):this.fulfill_(a)}};e.prototype.resolveToNonPromiseObj_=function(a){var b=void 0;try{b=a.then}catch(h){this.reject_(h);return}"function"==typeof b?
this.settleSameAsThenable_(b,a):this.fulfill_(a)};e.prototype.reject_=function(a){this.settle_(2,a)};e.prototype.fulfill_=function(a){this.settle_(1,a)};e.prototype.settle_=function(a,b){if(0!=this.state_)throw Error("Cannot settle("+a+", "+b+"): Promise already settled in state"+this.state_);this.state_=a;this.result_=b;this.executeOnSettledCallbacks_()};e.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var a=0;a<this.onSettledCallbacks_.length;++a)g.asyncExecute(this.onSettledCallbacks_[a]);
this.onSettledCallbacks_=null}};var g=new b;e.prototype.settleSameAsPromise_=function(a){var b=this.createResolveAndReject_();a.callWhenSettled_(b.resolve,b.reject)};e.prototype.settleSameAsThenable_=function(a,b){var c=this.createResolveAndReject_();try{a.call(b,c.resolve,c.reject)}catch(k){c.reject(k)}};e.prototype.then=function(a,b){function c(a,b){return"function"==typeof a?function(b){try{d(a(b))}catch(m){f(m)}}:b}var d,f,l=new e(function(a,b){d=a;f=b});this.callWhenSettled_(c(a,d),c(b,f));return l};
e.prototype.catch=function(a){return this.then(void 0,a)};e.prototype.callWhenSettled_=function(a,b){function c(){switch(d.state_){case 1:a(d.result_);break;case 2:b(d.result_);break;default:throw Error("Unexpected state: "+d.state_);}}var d=this;null==this.onSettledCallbacks_?g.asyncExecute(c):this.onSettledCallbacks_.push(c)};e.resolve=c;e.reject=function(a){return new e(function(b,c){c(a)})};e.race=function(a){return new e(function(b,d){for(var e=$jscomp.makeIterator(a),f=e.next();!f.done;f=e.next())c(f.value).callWhenSettled_(b,
d)})};e.all=function(a){var b=$jscomp.makeIterator(a),d=b.next();return d.done?c([]):new e(function(a,e){function f(b){return function(c){g[b]=c;h--;0==h&&a(g)}}var g=[],h=0;do g.push(void 0),h++,c(d.value).callWhenSettled_(f(g.length-1),e),d=b.next();while(!d.done)})};return e},"es6","es3");$jscomp.SYMBOL_PREFIX="jscomp_symbol_";$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};
$jscomp.SymbolClass=function(a,b){this.$jscomp$symbol$id_=a;$jscomp.defineProperty(this,"description",{configurable:!0,writable:!0,value:b})};$jscomp.SymbolClass.prototype.toString=function(){return this.$jscomp$symbol$id_};$jscomp.Symbol=function(){function a(c){if(this instanceof a)throw new TypeError("Symbol is not a constructor");return new $jscomp.SymbolClass($jscomp.SYMBOL_PREFIX+(c||"")+"_"+b++,c)}var b=0;return a}();
$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var a=$jscomp.global.Symbol.iterator;a||(a=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("Symbol.iterator"));"function"!=typeof Array.prototype[a]&&$jscomp.defineProperty(Array.prototype,a,{configurable:!0,writable:!0,value:function(){return $jscomp.iteratorPrototype($jscomp.arrayIteratorImpl(this))}});$jscomp.initSymbolIterator=function(){}};
$jscomp.initSymbolAsyncIterator=function(){$jscomp.initSymbol();var a=$jscomp.global.Symbol.asyncIterator;a||(a=$jscomp.global.Symbol.asyncIterator=$jscomp.global.Symbol("Symbol.asyncIterator"));$jscomp.initSymbolAsyncIterator=function(){}};$jscomp.iteratorPrototype=function(a){$jscomp.initSymbolIterator();a={next:a};a[$jscomp.global.Symbol.iterator]=function(){return this};return a};$jscomp.underscoreProtoCanBeSet=function(){var a={a:!0},b={};try{return b.__proto__=a,b.a}catch(c){}return!1};
$jscomp.setPrototypeOf="function"==typeof Object.setPrototypeOf?Object.setPrototypeOf:$jscomp.underscoreProtoCanBeSet()?function(a,b){a.__proto__=b;if(a.__proto__!==b)throw new TypeError(a+" is not extensible");return a}:null;$jscomp.generator={};$jscomp.generator.ensureIteratorResultIsObject_=function(a){if(!(a instanceof Object))throw new TypeError("Iterator result "+a+" is not an object");};
$jscomp.generator.Context=function(){this.isRunning_=!1;this.yieldAllIterator_=null;this.yieldResult=void 0;this.nextAddress=1;this.finallyAddress_=this.catchAddress_=0;this.finallyContexts_=this.abruptCompletion_=null};$jscomp.generator.Context.prototype.start_=function(){if(this.isRunning_)throw new TypeError("Generator is already running");this.isRunning_=!0};$jscomp.generator.Context.prototype.stop_=function(){this.isRunning_=!1};
$jscomp.generator.Context.prototype.jumpToErrorHandler_=function(){this.nextAddress=this.catchAddress_||this.finallyAddress_};$jscomp.generator.Context.prototype.next_=function(a){this.yieldResult=a};$jscomp.generator.Context.prototype.throw_=function(a){this.abruptCompletion_={exception:a,isException:!0};this.jumpToErrorHandler_()};$jscomp.generator.Context.prototype.return=function(a){this.abruptCompletion_={return:a};this.nextAddress=this.finallyAddress_};
$jscomp.generator.Context.prototype.jumpThroughFinallyBlocks=function(a){this.abruptCompletion_={jumpTo:a};this.nextAddress=this.finallyAddress_};$jscomp.generator.Context.prototype.yield=function(a,b){this.nextAddress=b;return{value:a}};$jscomp.generator.Context.prototype.yieldAll=function(a,b){a=$jscomp.makeIterator(a);var c=a.next();$jscomp.generator.ensureIteratorResultIsObject_(c);if(c.done)this.yieldResult=c.value,this.nextAddress=b;else return this.yieldAllIterator_=a,this.yield(c.value,b)};
$jscomp.generator.Context.prototype.jumpTo=function(a){this.nextAddress=a};$jscomp.generator.Context.prototype.jumpToEnd=function(){this.nextAddress=0};$jscomp.generator.Context.prototype.setCatchFinallyBlocks=function(a,b){this.catchAddress_=a;void 0!=b&&(this.finallyAddress_=b)};$jscomp.generator.Context.prototype.setFinallyBlock=function(a){this.catchAddress_=0;this.finallyAddress_=a||0};$jscomp.generator.Context.prototype.leaveTryBlock=function(a,b){this.nextAddress=a;this.catchAddress_=b||0};
$jscomp.generator.Context.prototype.enterCatchBlock=function(a){this.catchAddress_=a||0;a=this.abruptCompletion_.exception;this.abruptCompletion_=null;return a};$jscomp.generator.Context.prototype.enterFinallyBlock=function(a,b,c){c?this.finallyContexts_[c]=this.abruptCompletion_:this.finallyContexts_=[this.abruptCompletion_];this.catchAddress_=a||0;this.finallyAddress_=b||0};
$jscomp.generator.Context.prototype.leaveFinallyBlock=function(a,b){b=this.finallyContexts_.splice(b||0)[0];if(b=this.abruptCompletion_=this.abruptCompletion_||b){if(b.isException)return this.jumpToErrorHandler_();void 0!=b.jumpTo&&this.finallyAddress_<b.jumpTo?(this.nextAddress=b.jumpTo,this.abruptCompletion_=null):this.nextAddress=this.finallyAddress_}else this.nextAddress=a};$jscomp.generator.Context.prototype.forIn=function(a){return new $jscomp.generator.Context.PropertyIterator(a)};
$jscomp.generator.Context.PropertyIterator=function(a){this.object_=a;this.properties_=[];for(var b in a)this.properties_.push(b);this.properties_.reverse()};$jscomp.generator.Context.PropertyIterator.prototype.getNext=function(){for(;0<this.properties_.length;){var a=this.properties_.pop();if(a in this.object_)return a}return null};$jscomp.generator.Engine_=function(a){this.context_=new $jscomp.generator.Context;this.program_=a};
$jscomp.generator.Engine_.prototype.next_=function(a){this.context_.start_();if(this.context_.yieldAllIterator_)return this.yieldAllStep_(this.context_.yieldAllIterator_.next,a,this.context_.next_);this.context_.next_(a);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.return_=function(a){this.context_.start_();var b=this.context_.yieldAllIterator_;if(b)return this.yieldAllStep_("return"in b?b["return"]:function(a){return{value:a,done:!0}},a,this.context_.return);this.context_.return(a);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.throw_=function(a){this.context_.start_();if(this.context_.yieldAllIterator_)return this.yieldAllStep_(this.context_.yieldAllIterator_["throw"],a,this.context_.next_);this.context_.throw_(a);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.yieldAllStep_=function(a,b,c){try{var d=a.call(this.context_.yieldAllIterator_,b);$jscomp.generator.ensureIteratorResultIsObject_(d);if(!d.done)return this.context_.stop_(),d;var e=d.value}catch(g){return this.context_.yieldAllIterator_=null,this.context_.throw_(g),this.nextStep_()}this.context_.yieldAllIterator_=null;c.call(this.context_,e);return this.nextStep_()};
$jscomp.generator.Engine_.prototype.nextStep_=function(){for(;this.context_.nextAddress;)try{var a=this.program_(this.context_);if(a)return this.context_.stop_(),{value:a.value,done:!1}}catch(b){this.context_.yieldResult=void 0,this.context_.throw_(b)}this.context_.stop_();if(this.context_.abruptCompletion_){a=this.context_.abruptCompletion_;this.context_.abruptCompletion_=null;if(a.isException)throw a.exception;return{value:a.return,done:!0}}return{value:void 0,done:!0}};
$jscomp.generator.Generator_=function(a){this.next=function(b){return a.next_(b)};this.throw=function(b){return a.throw_(b)};this.return=function(b){return a.return_(b)};$jscomp.initSymbolIterator();this[Symbol.iterator]=function(){return this}};$jscomp.generator.createGenerator=function(a,b){b=new $jscomp.generator.Generator_(new $jscomp.generator.Engine_(b));$jscomp.setPrototypeOf&&$jscomp.setPrototypeOf(b,a.prototype);return b};
$jscomp.asyncExecutePromiseGenerator=function(a){function b(b){return a.next(b)}function c(b){return a.throw(b)}return new Promise(function(d,e){function g(a){a.done?d(a.value):Promise.resolve(a.value).then(b,c).then(g,e)}g(a.next())})};$jscomp.asyncExecutePromiseGeneratorFunction=function(a){return $jscomp.asyncExecutePromiseGenerator(a())};$jscomp.asyncExecutePromiseGeneratorProgram=function(a){return $jscomp.asyncExecutePromiseGenerator(new $jscomp.generator.Generator_(new $jscomp.generator.Engine_(a)))};
$jscomp.polyfill("globalThis",function(a){return a||$jscomp.global},"es_next","es3");var fetchButton=document.querySelector(".js-fetchButton");fetchButton&&fetchButton.addEventListener("click",function(a){a.preventDefault();fetchPath()});
function fetchPath(){var a,b,c;return $jscomp.asyncExecutePromiseGeneratorProgram(function(d){if(1==d.nextAddress)return a=document.querySelector(".js-fetchMessage"),a.textContent="Fetching "+a.dataset.path,document.querySelector(".js-fetchMessageSecondary").textContent="Feel free to navigate away and check back later, we'll keep working on it!",document.querySelector(".js-fetchButton").style.display="none",document.querySelector(".js-fetchLoading").style.display="block",d.yield(fetch("/fetch"+window.location.pathname),
2);if(3!=d.nextAddress)return b=d.yieldResult,b.ok?(window.location.reload(),d.return()):d.yield(b.text(),3);c=d.yieldResult;document.querySelector(".js-fetchLoading").style.display="none";document.querySelector(".js-fetchMessageSecondary").textContent="";document.querySelector(".js-fetchMessage").textContent=c;d.jumpToEnd()})};

Просмотреть файл

@ -42,6 +42,7 @@ main() {
$cmd $JSDIR/base.min.js $JSDIR/{site,analytics}.js
# TODO: once this is not an experiment, add it to the line above.
$cmd $JSDIR/completion.min.js $JSDIR/completion.js
$cmd $JSDIR/fetch.min.js $JSDIR/fetch.js
$cmd $JSDIR/jump.min.js third_party/dialog-polyfill/dialog-polyfill.js $JSDIR/jump.js
}

Просмотреть файл

@ -277,7 +277,7 @@ func pathNotFoundErrorNew(fullPath, version string) error {
return &serverError{
status: http.StatusNotFound,
epage: &errorPage{
templateName: "notfound.tmpl",
templateName: "fetch.tmpl",
messageTemplate: `
<h3 class="NotFound-message">Oops! {{.}} does not exist.</h3>
<p class="NotFound-message js-notFoundMessage">

Просмотреть файл

@ -400,7 +400,7 @@ func parsePageTemplates(base string) (map[string]*template.Template, error) {
htmlSets := [][]string{
{"index.tmpl"},
{"error.tmpl"},
{"notfound.tmpl"},
{"fetch.tmpl"},
{"search.tmpl"},
{"search_help.tmpl"},
{"license_policy.tmpl"},

Просмотреть файл

@ -15,14 +15,14 @@ var scriptHashes = []string{
"'sha256-d6W7MwuGWbguTHRzQhf5QN1jXmNo9Ao218saZkWLWZI='",
"'sha256-CCu0fuIQFBHSCEpfR6ZRzzcczJIS/VGMGrez8LR49WY='",
"'sha256-qPGTOKPn+niRiNKQIEX0Ktwuj+D+iPQWIxnlhPicw58='",
// From content/static/html/pages/notfound.tmpl
"'sha256-h5L4TV5GzTaBQYCnA8tDw9+9/AIdK9dwgkwlqFjVqEI='",
// From content/static/html/pages/details.tmpl
"'sha256-s16e7aT7Gsajq5UH1DbaEFEnNx2VjvS5Xixcxwm4+F8='",
// From content/static/html/pages/pkg_doc.tmpl
"'sha256-AvMTqQ+22BA0Nsht+ajju4EQseFQsoG1RxW3Nh6M+wc='",
// From content/static/html/worker/index.tmpl
"'sha256-5EpitFYSzGNQNUsqi5gAaLqnI3ZWfcRo/6gLTO0oCoE='",
// From content/static/html/pages/fetch.tmpl
"'sha256-1J6DWwTWs/QDZ2+ORDuUQCibmFnXXaNXYOtc0Jk6VU4='",
}
// SecureHeaders adds a content-security-policy and other security-related