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>
diff --git a/content/static/css/stylesheet.css b/content/static/css/stylesheet.css
index 300c415..6647a3f 100644
--- a/content/static/css/stylesheet.css
+++ b/content/static/css/stylesheet.css
@@ -516,6 +516,69 @@
   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;
diff --git a/content/static/html/pages/fetch.tmpl b/content/static/html/pages/fetch.tmpl
new file mode 100644
index 0000000..d944d45
--- /dev/null
+++ b/content/static/html/pages/fetch.tmpl
@@ -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}}
diff --git a/content/static/html/pages/notfound.tmpl b/content/static/html/pages/notfound.tmpl
deleted file mode 100644
index b51399b..0000000
--- a/content/static/html/pages/notfound.tmpl
+++ /dev/null
@@ -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}}
diff --git a/content/static/js/fetch.js b/content/static/js/fetch.js
new file mode 100644
index 0000000..a6a9d45
--- /dev/null
+++ b/content/static/js/fetch.js
@@ -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, we’ll 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;
+}
diff --git a/content/static/js/fetch.min.js b/content/static/js/fetch.min.js
new file mode 100644
index 0000000..35ab3c0
--- /dev/null
+++ b/content/static/js/fetch.min.js
@@ -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()})};
diff --git a/devtools/compile_js.sh b/devtools/compile_js.sh
index 69191f9..e53cb4b 100755
--- a/devtools/compile_js.sh
+++ b/devtools/compile_js.sh
@@ -42,6 +42,7 @@
   $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
 }
 
diff --git a/internal/frontend/details.go b/internal/frontend/details.go
index f82658b..b7f058e 100644
--- a/internal/frontend/details.go
+++ b/internal/frontend/details.go
@@ -277,7 +277,7 @@
 	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">
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index a72ef01..1bbc34e 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -400,7 +400,7 @@
 	htmlSets := [][]string{
 		{"index.tmpl"},
 		{"error.tmpl"},
-		{"notfound.tmpl"},
+		{"fetch.tmpl"},
 		{"search.tmpl"},
 		{"search_help.tmpl"},
 		{"license_policy.tmpl"},
diff --git a/internal/middleware/secureheaders.go b/internal/middleware/secureheaders.go
index 7eee866..59c04fa 100644
--- a/internal/middleware/secureheaders.go
+++ b/internal/middleware/secureheaders.go
@@ -15,14 +15,14 @@
 	"'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